UNPKG

@node-in-layers/core

Version:

The core library for the Node In Layers rapid web development framework.

334 lines 11.6 kB
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