@node-in-layers/core
Version:
The core library for the Node In Layers rapid web development framework.
334 lines • 11.6 kB
JavaScript
import z from 'zod';
import get from 'lodash/get.js';
import merge from 'lodash/merge.js';
import omit from 'lodash/omit.js';
import { wrap } from './utils.js';
import { CoreNamespace, LogLevel, LogLevelNames, } from './types.js';
const featurePassThrough = wrap;
const configHasKey = (key) => (config) => {
if (get(config, key) === undefined) {
throw new Error(`${key} was not found in config`);
}
};
const configItemIsArray = (key) => (config) => {
if (Array.isArray(get(config, key)) === false) {
throw new Error(`${key} must be an array`);
}
};
const configItemIsType = (key, type) => (config) => {
const theType = typeof get(config, key);
if (theType !== type) {
throw new Error(`${key} must be of type ${type}`);
}
};
const allAppsHaveAName = (config) => {
config[CoreNamespace.root]?.apps.find(app => {
if (app.name === undefined) {
throw new Error(`A configured app does not have a name.`);
}
return false;
});
return true;
};
const _getNamespaceProperty = (namespace, property) => {
return `${namespace}.${property}`;
};
const _configItemsToCheck = [
configHasKey('environment'),
configHasKey('systemName'),
configHasKey(_getNamespaceProperty(CoreNamespace.root, 'apps')),
configItemIsArray(_getNamespaceProperty(CoreNamespace.root, 'apps')),
configHasKey(_getNamespaceProperty(CoreNamespace.root, 'layerOrder')),
configItemIsArray(_getNamespaceProperty(CoreNamespace.root, 'layerOrder')),
allAppsHaveAName,
configItemIsType(_getNamespaceProperty(CoreNamespace.root, 'logging.logLevel'), 'string'),
configItemIsType(_getNamespaceProperty(CoreNamespace.root, 'logging.logFormat'), 'string'),
];
const validateConfig = (config) => {
_configItemsToCheck.forEach(x => x(config));
};
const getLogLevelName = (logLevel) => {
switch (logLevel) {
case LogLevel.TRACE:
return 'TRACE';
case LogLevel.DEBUG:
return 'DEBUG';
case LogLevel.INFO:
return 'INFO';
case LogLevel.WARN:
return 'WARN';
case LogLevel.ERROR:
return 'ERROR';
case LogLevel.SILENT:
return 'SILENT';
default:
throw new Error(`Unhandled log level ${logLevel}`);
}
};
const getLogLevelNumber = (logLevel) => {
switch (logLevel) {
case LogLevelNames.trace:
return LogLevel.TRACE;
case LogLevelNames.warn:
return LogLevel.WARN;
case LogLevelNames.debug:
return LogLevel.DEBUG;
case LogLevelNames.info:
return LogLevel.INFO;
case LogLevelNames.error:
return LogLevel.ERROR;
case LogLevelNames.silent:
return LogLevel.SILENT;
default:
throw new Error(`Unhandled log level ${logLevel}`);
}
};
const _getLayerKey = (layerDescription) => {
// Probably never going to have the first case.
/* c8 ignore next */
return Array.isArray(layerDescription)
? /* c8 ignore next */
layerDescription.join('-')
: layerDescription;
};
const getLayersUnavailable = (allLayers) => {
const layerToChoices = allLayers.reduce((acc, layer, index) => {
const antiLayers = allLayers.slice(index + 1);
// If we are dealing with a composite, we need to break it up
if (Array.isArray(layer)) {
const compositeAnti = layer.reduce((inner, compositeLayer, i) => {
// We don't want to give access to the composite layers further up ahead.
const nestedAntiLayers = layer.slice(i + 1);
return merge(inner, {
[compositeLayer]: antiLayers.concat(nestedAntiLayers),
});
}, acc);
return compositeAnti;
}
const key = _getLayerKey(layer);
return merge(acc, {
[key]: allLayers.slice(index + 1),
});
}, {});
return (layer) => {
const choices = layerToChoices[layer];
if (!choices) {
throw new Error(`${layer} is not a valid layer choice`);
}
return choices;
};
};
const isConfig = (obj) => {
if (typeof obj === 'string') {
return false;
}
return Boolean(get(obj, _getNamespaceProperty(CoreNamespace.root, 'layerOrder')));
};
const getNamespace = (packageName, app) => {
if (app) {
return `${packageName}/${app}`;
}
return packageName;
};
// @ts-ignore
const DoNothingFetcher = (model, primarykey) => Promise.resolve(primarykey);
/**
* Converts an Error object to a standard ErrorObject structure.
* This is an internal helper used by createErrorObject.
*
* @param error - The error to convert
* @param code - The error code to use
* @param message - The error message to use
* @returns An ErrorObject representation of the error
*/
const _convertErrorToCause = (error, code, message) => {
// Build the error details object
const errorObj = {
error: {
code,
message: message || error.message,
},
};
// Add details from the error
if (error.message) {
return merge({}, errorObj, {
error: {
details: error.message,
},
});
}
// Handle nested cause if available
if (error.cause) {
const causeObj = _convertErrorToCause(error.cause, 'NestedError', error.cause.message);
return merge({}, errorObj, {
error: {
cause: causeObj.error,
},
});
}
// Not likely to ever happen
/* c8 ignore next */
return errorObj;
};
/**
* Creates a standardized error object for consistent error handling across the application.
* This function handles all the logic for converting different error types to the standard format.
*
* @param code - A unique string code for the error
* @param message - A user-friendly error message
* @param error - Optional error object or details (can be any type - will be properly handled)
* @returns A standardized error object conforming to the ErrorObject type
*/
const createErrorObject = (code, message, error) => {
// Create base error details
const baseErrorObj = {
error: {
code,
message,
},
};
// Return early if no additional error information
if (!error) {
return baseErrorObj;
}
// Handle different types of error input
if (error instanceof Error) {
const errorDetails = {
error: {
details: error.message,
errorDetails: `${error}`,
},
};
// Add cause if available
if (error.cause) {
const causeObj = _convertErrorToCause(error.cause, 'CauseError', error.cause.message);
return merge({}, baseErrorObj, errorDetails, {
error: {
cause: causeObj.error,
},
});
}
return merge({}, baseErrorObj, errorDetails);
}
if (typeof error === 'string') {
return merge({}, baseErrorObj, {
error: {
details: error,
},
});
}
// For Record<string, JsonAble> or any object that can be serialized
if (error !== null && typeof error === 'object' && !Array.isArray(error)) {
// eslint-disable-next-line functional/no-try-statements
try {
// Test if it can be serialized
JSON.stringify(error);
return merge({}, baseErrorObj, {
error: {
data: error,
},
});
}
catch {
// If not serializable, convert to string
return merge({}, baseErrorObj, {
error: {
details: String(error),
},
});
}
}
// Handle arrays or any other types
return merge({}, baseErrorObj, {
error: {
details: String(error),
},
});
};
const isErrorObject = (value) => {
return typeof value === 'object' && value !== null && 'error' in value;
};
const combineCrossLayerProps = (crossLayerPropsA, crossLayerPropsB) => {
const loggingData = crossLayerPropsA.logging || {};
const ids = loggingData.ids || [];
const currentIds = crossLayerPropsB.logging?.ids || [];
//start with logger ids
const existingIds = ids.reduce((acc, obj) => {
return Object.entries(obj).reduce((accKeys, [key, value]) => {
return merge(accKeys, { [`${key}:${value}`]: key });
}, acc);
}, {});
//start with cross layer ids
const unique = currentIds.reduce((acc, passedIn) => {
const keys = Object.entries(passedIn);
const newKeys = keys
.filter(([key, value]) => !(`${key}:${value}` in existingIds))
.map(([key, value]) => ({ [key]: value }));
if (newKeys.length > 0) {
return acc.concat(newKeys);
}
return acc;
}, []);
const finalIds = ids.concat(unique);
return {
logging: merge({
ids: finalIds,
}, omit(loggingData, 'ids')),
};
};
/**
* Zod schema for ErrorObject (exported for external validation/unions)
*/
export const errorObjectSchema = () => z.object({
error: z.object({
code: z.string(),
message: z.string(),
details: z.string().optional(),
data: z.record(z.string(), z.any()).optional(),
trace: z.string().optional(),
cause: z
.object({ error: z.any() })
.transform(val => ({ error: val.error }))
.optional(),
}),
});
/**
* Creates a crossLayerProps available function that is also annotated with Zod.
* @param props - The arguments
* @param implementation - Your function
* @returns A function with a "schema" property
*/
const annotatedFunction = (props, implementation) => {
const base = z
.function()
.input([props.args, z.custom().optional()]);
const outputSchema = (() => {
// No returns schema: assume void
if (!props.returns) {
return z.union([z.void(), errorObjectSchema()]);
}
// Build Response<returns> = returns | ErrorObject
return z.union([props.returns, errorObjectSchema()]);
})();
const fn = base.output(outputSchema);
const schema = props.description ? fn.describe(props.description) : fn;
const isAsync = implementation.constructor?.name === 'AsyncFunction';
const implemented = isAsync
? schema.implementAsync(implementation)
: schema.implement(implementation);
// eslint-disable-next-line functional/immutable-data
implemented.schema = schema;
// eslint-disable-next-line functional/immutable-data
implemented.functionName = props.functionName;
// eslint-disable-next-line functional/immutable-data
implemented.domain = props.domain;
return implemented;
};
/**
* Creates standardized annotation function arguments. already typed.
* @param args - Arguments.
* @returns An AnnotatedFunctionProps object.
*/
const annotationFunctionProps = (args) => args;
export { createErrorObject, featurePassThrough, getLogLevelName, validateConfig, getLayersUnavailable, isConfig, getNamespace, DoNothingFetcher, getLogLevelNumber, isErrorObject, combineCrossLayerProps, annotatedFunction, annotationFunctionProps, };
//# sourceMappingURL=libs.js.map