@alphanova/builder
Version:
A fully fledged facade that facilitates object manipulation
322 lines (321 loc) • 14.1 kB
JavaScript
/* 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']
// >
// }