const MOCK_ROUTES = {}; import { user, cache_reset } from "./stores.js"; import { log } from "$/client/logging.js"; import Validator from 'Validator'; export const validate = (form, extra) => { if(form._rules) { let validation = Validator.make(form, form._rules); form._valid = validation.passes(); if(extra) extra(form); form._errors = validation.getErrors(); return form; } else { return form; } } export const can_submit = (form) => { return form._rules === undefined || form._valid; } export const clean_form = (form, extras=[]) => { form._errors = {}; // errors is accessed so needs to exist delete form._valid; delete form._rules; for(let field of extras) delete form[field]; } // use these when you do your own fetch const fetch_opts = { credentials: 'same-origin',}; export const mock = (config) => { for(let [route, value] of Object.entries(config)) { MOCK_ROUTES[route] = value; } } export const raw_mock = (url, raw_method, body, unauthed_action) => { let error; const method = raw_method.toLowerCase(); const config = MOCK_ROUTES[url]; const data = config[method]; if(data === undefined) { error = `Mock ${url}:${method} is not in your mocks list. Did you forget to add it to the api.mock call?`; } else if(data.length !== 2) { error = `Mock ${url}:${method} does not have the correct config. Must be [status (int), {data}], like [200, {message: 'OK'}].` } if(error) { log.error(error, MOCK_ROUTES); return [500, {message: error}]; } else { // this is the same as res.status below if(unauthed_action && (data[0] === 401 || data[0] === 403)) unauthed_action(); return data; } } const parse_url_because_js_is_stupid = (url) => { try { // javascript is dumb as hell and thinks a typical /this/that is not a URL return new URL(url); } catch(error) { // so fake it for the mock by tacking on ... localhost then ignoring it return new URL(`http://localhost${url}`); } } export const raw = async (url, method, body, unauthed_action) => { const parsed = parse_url_because_js_is_stupid(url); // if there is a rules and valid is exactly false we should just run // the validation here and avoid the request if(body && body._rules && body._valid === false) { const res = validate(body); if(!can_submit(res)) { log.debug("Form invalid, won't HTTP submit."); return [400, res]; } else { clean_form(body); } } if(parsed.pathname in MOCK_ROUTES) { return raw_mock(parsed.pathname, method, body); } else { let options = { method, credentials: 'same-origin', headers: { 'Content-Type': 'application/json', }, } if(body) options.body = JSON.stringify(body); /* this handy little gem exists because fetch likes to go: * "the string did not match the expected pattern” * when the response body is not json...or at least that's * what I think it means. Now I get the text, and use it * for logging. */ let res = await fetch(url, options); let text = await res.text(); try { if(unauthed_action && (res.status === 401 || res.status === 403)) unauthed_action(); return [res.status, JSON.parse(text)]; } catch(error) { log.error(error, "Failed to parse reply body as JSON. Text is:", text, "error", error, "URL", url); return [500, {"message": "Exception processing request. See log.debug."}]; } } } export const get = async (url, data, unauthed_action) => { const params = new URLSearchParams(data || {}); const param_url = `${url}?${params.toString()}`; return await raw(param_url, 'GET', undefined, unauthed_action); } export const post = async (url, data, unauthed_action) => { return await raw(url, 'POST', data, unauthed_action); } export const put = async (url, data, unauthed_action) => { return await raw(url, 'PUT', data, unauthed_action); } export const del = async (url, unauthed_action) => { return await raw(url, 'DELETE', undefined, unauthed_action); } export const options = async (url, unauthed_action) => { return await raw(url, 'OPTIONS', undefined, unauthed_action); } export const logout_user = async () => { user.update(() => { return {authenticated: undefined} }); // really only a problem for multiple people // using the same browser, but clear it out cache_reset(); let [status, data] = await get('/api/logout'); if(status !== 200) log.error("Invalid status from logout", status, data); window.location.replace("/client/#/login"); } export const schema = async (table) => { let [status, tables] = await get('/api/admin/schema'); if(status == 200) { for(let t of tables) { if(t.name === table) { // this exits the function with the schema return t._columns; } } // this happens if the table isn't in the schema return undefined; } else { return undefined; } } export default { post, get, put, del, mock, options, fetch_opts, logout_user, validate, can_submit, clean_form, schema }