UNPKG

@alphanova/builder

Version:

A fully fledged facade that facilitates object manipulation

322 lines (321 loc) 14.1 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { keyify } from './Models/Redux'; import { nanoid } from '@reduxjs/toolkit'; import axios from 'axios'; function headers() { return { headers: { 'Authorization': `Bearer ${config.token}`, 'Content-Type': 'application/json', // Set the content type as needed for your API } }; } let config; export function setConfig(conf) { config = conf; return conf; } export function getConfig() { return config; } export function actionBuilder(model_name, api, dispatch, override_url) { async function axiosPost(readyUrl, payload, extra) { stateSorter(model_name).builder.validateObject(payload ?? { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); return axios.post(readyUrl, { ...payload, ...extra }, headers()); } async function axiosPut(readyUrl, payload, extra) { stateSorter(model_name).builder.validateObject(payload ?? { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); return axios.put(readyUrl, { ...payload, ...extra }, headers()); } async function axiosGet(readyUrl) { return axios.get(readyUrl, headers()); } async function axiosDelete(readyUrl) { return axios.delete(readyUrl, headers()); } async function validateAndSend(mode, route, payload, extra) { const readyUrl = route.join('/'); let response; switch (mode) { case 'DELETE': response = await axiosDelete(readyUrl); break; case 'GET': response = await axiosGet(readyUrl); break; case 'POST': response = await axiosPost(readyUrl, payload, extra); break; case 'PUT': response = await axiosPut(readyUrl, payload, extra); break; } if (response.data.statusCode !== 200 && response.data.statusCode !== 201) throw response.data; const validated_response = validateResponse(response.data, readyUrl); if (!validated_response) throw new Error('Incorrect response'); return validated_response; } function checkIdIntegrity(response) { const id = response?.data.at(0)?.id; if (!id) throw new Error(`No id found in response: ${JSON.stringify(response)}`); return id; } async function validateSendAndCheckIdIntegrity(mode, route, payload, extra) { const result = await validateAndSend(mode, route, payload, extra); return { key: checkIdIntegrity(result), value: result.data.at(0) }; } async function validateSendCheckIdIntegrityAndReturn(mode, route, payload, extra) { const result = await validateSendAndCheckIdIntegrity(mode, route, payload, extra); return stateSorter(model_name).builder.instantiate(result.key, result.value); } function dispatchAndReturn(action, id, payload) { const completeModel = { ...payload, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const conf = stateSorter(model_name); const container = conf.builder.instantiate(id, completeModel); const allData = getConfig().queryClient.getQueryData([conf.action]) ?? []; switch (action) { case 'ADD': allData.push(container); getConfig().queryClient.setQueryData([conf.action], allData); break; case 'DELETE': getConfig().queryClient.setQueryData([conf.action], allData.filter(value => value.id !== id)); break; case 'UPDATE': getConfig().queryClient.setQueryData([conf.action], allData.map(value => value.id === id ? container : value)); break; } dispatch({ type: `${action}_${model_name}`, payload: keyify([id, completeModel]) }); return container; } async function changeKeyAndReturn(action, id, new_id, payload) { const completeModel = { ...payload, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; dispatch({ type: `${action}_${model_name}`, payload: keyify([id, new_id]) }); return stateSorter(action).builder.instantiate(id, completeModel); } const master_url = override_url ?? config.server; const ADD = async ({ payload, extra }) => { const { key } = await validateSendAndCheckIdIntegrity('POST', [master_url, api], payload, extra); return dispatchAndReturn('ADD', key, payload); }; const ASYNC_ADD = async ({ payload, extra, dummy_id }) => { const dummy = dummy_id ?? `ASYNC_${model_name}_${nanoid()}`; dispatchAndReturn('ADD', dummy, payload); const { key } = await validateSendAndCheckIdIntegrity('POST', [master_url, api], payload, extra); return changeKeyAndReturn('CHANGE_KEY', dummy, key, payload); }; const SERVER_ADD = async ({ payload, extra }) => { return await validateSendCheckIdIntegrityAndReturn('POST', [master_url, api], payload, extra); }; const LOCAL_ADD = (id, payload) => { return dispatchAndReturn('ADD', id, payload); }; const DELETE = async ({ id }) => { dispatchAndReturn('DELETE', id, {}); const { key: idToDelete } = await validateSendAndCheckIdIntegrity('DELETE', [master_url, api, id]); return dispatchAndReturn('DELETE', idToDelete, {}); }; const SERVER_DELETE = async ({ id }) => { return await validateSendCheckIdIntegrityAndReturn('DELETE', [master_url, api, id]); }; const LOCAL_DELETE = (id) => { return dispatchAndReturn('DELETE', id, {}); }; const UPDATE = async ({ id, object }) => { const builder = dispatchAndReturn('UPDATE', id, { ...object, updatedAt: new Date().toISOString() }); const result = await validateSendCheckIdIntegrityAndReturn('PUT', [master_url, api, builder.id], builder.extract); return result; }; const SERVER_UPDATE = async ({ id, object }) => { return await validateSendCheckIdIntegrityAndReturn('PUT', [master_url, api, id], object); }; const LOCAL_UPDATE = (id, object) => { return dispatchAndReturn('UPDATE', id, object); }; const FETCH_MANY = async () => { const response = await validateAndSend('GET', [master_url, api]); const data_to_dispatch = {}; response.data.forEach(data => peelLayer(data_to_dispatch, data, response.type)); response.other?.forEach(other => { other.data.forEach(data => peelLayer(data_to_dispatch, data, other.type)); }); Object.entries(data_to_dispatch).forEach((value) => { const conf = stateSorter(value[0]); if (value[0].toLowerCase() === 'ERRONEOUS'.toLowerCase()) { const erroneousImports = getConfig().queryClient.getQueryData(['ERRONEOUS']) ?? []; getConfig().queryClient.setQueryData(['ERRONEOUS'], [...erroneousImports, ...value[1].map(v => conf.builder.instantiate(v.key, v.value))]); } else { const queryData = getConfig().queryClient.getQueryData([conf.action]) ?? []; const erroneousImports = getConfig().queryClient.getQueryData([conf.action]) ?? []; getConfig().queryClient.setQueryData([conf.action], [...erroneousImports, ...value[1].map(v => conf.builder.instantiate(v.key, v.value)).filter(v => !queryData.find(q => q.id === v.id))]); } }); return data_to_dispatch[model_name.toLowerCase()]?.map(value => stateSorter(model_name).builder.instantiate(value.value.id, value.value)) ?? []; }; const FETCH_ONE = async ({ id: idToFetch }) => { const response = await validateAndSend('GET', [master_url, api, idToFetch]); const data_to_dispatch = {}; peelLayer(data_to_dispatch, response.data.at(0), response.type); response.other?.forEach(other => { other.data.forEach(data => peelLayer(data_to_dispatch, data, other.type)); }); Object.entries(data_to_dispatch).forEach((value) => { if (value[0].toLowerCase() === 'ERRONEOUS'.toLowerCase()) { dispatch({ type: 'FETCH_IDLE_ERRONEOUS', payload: value[1] }); } else { dispatch({ type: `FETCH_SUCCESS_${stateSorter(value[0]).action}`, payload: value[1] }); } }); return data_to_dispatch[model_name.toLowerCase()]?.map(value => stateSorter(model_name).builder.instantiate(value.value.id, value.value)) ?? []; }; const FETCH_ANY = async ({ route }) => { const response = await validateAndSend('GET', [master_url, api, ...route]); const data_to_dispatch = {}; response.data.forEach(data => peelLayer(data_to_dispatch, data, response.type)); response.other?.forEach(other => { other.data.forEach(data => peelLayer(data_to_dispatch, data, other.type)); }); Object.entries(data_to_dispatch).forEach((value) => { if (value[0].toLowerCase() === 'ERRONEOUS'.toLowerCase()) { dispatch({ type: 'FETCH_IDLE_ERRONEOUS', payload: value[1] }); } else { dispatch({ type: `FETCH_IDLE_${stateSorter(value[0]).action}`, payload: value[1] }); } }); return data_to_dispatch[model_name.toLowerCase()]?.map(value => stateSorter(model_name).builder.instantiate(value.value.id, value.value)) ?? []; }; return { ADD, ASYNC_ADD, DELETE, UPDATE, FETCH_MANY, FETCH_ONE, FETCH_ANY, LOCAL_ADD, LOCAL_DELETE, LOCAL_UPDATE, SERVER_ADD, SERVER_DELETE, SERVER_UPDATE }; } export function CreateBuilder(builder) { return builder; } function peelLayer(data, model, type) { if (!model) return; const { builder } = stateSorter(type); if (typeof model === 'string') return; const { fix, fixed } = builder.fixateExtra(model); const { extra, extracted } = builder.extractExtra(fixed); const object = builder.validateObject(extracted); if (object) { const valid = { ...builder.validateObject(extracted), id: model.id }; if (!data[type]) { data[type] = []; } data[type]?.push({ key: model.id, value: valid }); fix.forEach(fix_data => { fix_data.data && peelLayer(data, fix_data.data, fix_data.type); }); extra.forEach(extra_data => { extra_data.data.forEach(inner_extra => peelLayer(data, inner_extra, extra_data.type)); }); } else { if (!data['ERRONEOUS']) { data['ERRONEOUS'] = []; } data['ERRONEOUS'].push({ key: model.id, value: { ...extracted, id: model.id } }); } } export function stateSorter(type) { const result = config?.rules.find(config => config.action.toLowerCase() === type.toLowerCase()); if (!result) throw new Error(`Make sure to register the type ${type} in the stateSorter function`); return result; } function validateResponse(data, route) { const response = { data: [], error: '', other: [], statusCode: 0, type: '', message: '' }; const optional = ['other', 'error', 'message']; const objKeys = Object.keys(data || {}); const interfaceKeys = Object.keys(response); // Check if any extra attributes are present in the object const extraKeys = objKeys.filter((key) => !interfaceKeys.includes(key)); if (extraKeys.length > 0) { console.warn(`Extra attributes found from ${route}: ${JSON.stringify(extraKeys)} Consider modidying the API in case the attributes contain sensitive data`); } // Check if any required attributes are missing from the object const missingKeys = interfaceKeys.filter((key) => { if (objKeys.includes(key)) { return false; // Skip keys that exist in the object } if (optional.includes(key)) { return false; } return true; }); if (missingKeys.length > 0) { console.error(`Missing attributes from ${route}:`, missingKeys, 'Consider modidying the API to include these attributes. If this is intentional then consider adding these attributes to the optional array in the validateResponse function'); return undefined; } // Check if any attribute types do not match for (const key of interfaceKeys) { if (typeof (data[key]) !== 'undefined' && typeof (data[key]) !== typeof response[key]) { console.warn(`Mismatched attribute from route ${route}. Type '${typeof (data[key])}' for '${String(key)}'.Expected type: ${typeof response[key]}Consider modifying the API to fix this issue.Wrong data types will be ignored.`); return undefined; } } return data; } // export type returnType<T extends Rule<string, Structure, never>[], Action extends readonly string[]> = { // [K in T[number]['action']as K extends Action[number] ? Uppercase<K> : never]: MoreFunctions< // Extract<T[number], { action: K }>['builder']['getObj'], // Extract<T[number], { action: K }>['builder'] // > // } // export type returnType<T extends Rule<string, Structure, any>[], Action extends (T[number]['action'])[]> = { // [K in T[number]['action']as K extends Action[number] ? Uppercase<K> : never]: MoreFunctions< // Extract<T[number], { action: K }>['builder']['inst'], // Extract<T[number], { action: K }>['builder'] // > // }