@node-in-layers/core
Version:
The core library for the Node In Layers rapid web development framework.
458 lines • 23.7 kB
JavaScript
import get from 'lodash/get.js';
import flatten from 'lodash/flatten.js';
import omit from 'lodash/omit.js';
import merge from 'lodash/merge.js';
import cloneDeep from 'lodash/cloneDeep.js';
import { Model } from 'functional-models';
import { extractCrossLayerProps } from './globals/libs.js';
import { CoreNamespace, } from './types.js';
import { DoNothingFetcher, getLayersUnavailable } from './libs.js';
import { memoizeValueSync } from './utils.js';
import { createModelCruds } from './models/libs.js';
const CONTEXT_TO_SKIP = {
_logging: true,
rootLogger: true,
log: true,
constants: true,
config: true,
models: true,
getModels: true,
cruds: true,
};
const name = CoreNamespace.layers;
const modelGetter = (context, apps, modelProps) => {
const memoized = {};
// We have to create a self reference, so we have to set this to null, and then overwrite it.
// @ts-ignore
// eslint-disable-next-line functional/no-let
let getModel = null;
getModel = (namespace, modelName) => {
const app = apps.find(a => a.name === namespace);
if (!app || !app.models) {
throw new Error(`An app with models does not exist for namespace ${namespace}`);
}
const models = app.models;
const modelConstructor = models[modelName];
if (!modelConstructor) {
throw new Error(`A model named ${modelName} does not exist for namespace ${namespace}`);
}
if (!(namespace in memoized)) {
// We are doing a memoized state so we need this
// eslint-disable-next-line functional/immutable-data
memoized[namespace] = {};
}
if (!(modelName in memoized)) {
const func = memoizeValueSync(() => modelConstructor.create({
context,
...modelProps,
getModel,
}));
// We are doing a memoized state so we need this
// eslint-disable-next-line functional/immutable-data
memoized[namespace][modelName] = func;
}
return memoized[namespace][modelName];
};
return getModel;
};
const services = {
create: () => {
const getModelProps = (context) => {
const fetcher = DoNothingFetcher;
const modelGetterInstance = modelGetter(context, context.config[CoreNamespace.root].apps, { Model, fetcher });
return {
context,
Model,
fetcher,
getModel: modelGetterInstance,
};
};
const loadLayer = (app, layer, context) => {
const constructor = get(app, `${layer}`);
if (!constructor?.create) {
return undefined;
}
const instance = constructor.create(context);
if (!instance) {
throw new Error(`App ${app.name} did not return an instance layer ${layer}`);
}
return instance;
};
return {
getModelProps,
loadLayer,
};
},
};
const isPromise = (t) => {
if (!t) {
return false;
}
return Boolean(t.then);
};
const features = {
create: (context) => {
const _getLayerContext = (commonContext, layer) => {
if (layer) {
return merge({}, commonContext, layer);
}
return commonContext;
};
const _getModelLoadedContext = (app, currentLayer, layerContext) => {
if (app.models) {
// If this is services, we need to load models first if they exist
if (currentLayer === 'services') {
const mfNamespace = context.config['@node-in-layers/core'].modelFactory ||
CoreNamespace.layers;
const customMf = context.config['@node-in-layers/core'].customModelFactory || {};
const defaultMf =
// @ts-ignore
layerContext.services[mfNamespace] || context.services[mfNamespace];
if (!defaultMf) {
throw new Error(`Namespace ${mfNamespace} does not have a services object`);
}
if (!defaultMf.getModelProps) {
throw new Error(`Namespace ${mfNamespace} does not have a services object with a getModelProps(context: ServicesContext) function`);
}
const models = app.models;
// This function is added to the services context.
const getModels = memoizeValueSync(() => {
const defaultModelProps = defaultMf.getModelProps(layerContext);
const modelsObj = Object.entries(models).reduce((acc, [modelName, constructor]) => {
// Do we have a custom model props for this?
const custom = get(customMf, `${app.name}.${modelName}`);
const isCustomArray = Array.isArray(custom);
const customArgs = isCustomArray ? custom.slice(1) : [];
const customModelProps = custom
? isCustomArray
? get(layerContext, `services[${custom[0]}].getModelProps`)
: get(layerContext, `services[${custom}].getModelProps`)
: undefined;
if (custom && !customModelProps) {
throw new Error(`Configuration requires that Model named ${modelName} receive a model props from ${custom}`);
}
const modelProps = customModelProps
? customModelProps(layerContext, ...customArgs)
: defaultModelProps;
if (!constructor.create) {
throw new Error('Model constructor must have a create function');
}
const getModel = modelGetter(context, context.config['@node-in-layers/core'].apps, modelProps);
const instance = constructor.create({
...modelProps,
getModel,
});
return merge(acc, {
[modelName]: instance,
});
}, {});
return modelsObj;
});
const serviceCruds = context.config['@node-in-layers/core'].modelCruds
? Object.keys(models).reduce((acc, name) => {
return merge(acc, {
[name]: createModelCruds(() => getModels()[name]),
});
}, {})
: undefined;
return merge({}, layerContext, serviceCruds
? {
services: {
[app.name]: {
cruds: serviceCruds,
},
},
}
: {}, {
models: {
[app.name]: {
getModels,
},
},
});
}
else if (currentLayer === 'features' &&
context.config['@node-in-layers/core'].modelCruds) {
// We need to add the feature wrappers over service level wrappers.
const serviceWrappers =
// @ts-ignore
Object.entries(get(layerContext, `services.${app.name}.cruds`, {}));
// @ts-ignore
const featureWrappers = serviceWrappers.reduce((acc, [name, cruds]) => {
return merge(acc, {
[name]: createModelCruds(() => cruds.getModel(), {
overrides: cruds,
}),
});
}, {});
return merge({}, layerContext, {
features: {
[app.name]: {
cruds: featureWrappers,
},
},
});
}
}
return layerContext;
};
const _loadLayer = async (app, currentLayer, commonContext, previousLayer) => {
const layerContext1 = _getModelLoadedContext(app, currentLayer, _getLayerContext(commonContext, previousLayer));
const layerLogger = context.rootLogger
.getLogger(layerContext1)
.getAppLogger(app.name)
.getLayerLogger(currentLayer);
const layerContext = cloneDeep(
// eslint-disable-next-line functional/immutable-data
Object.assign(layerContext1, {
log: layerLogger,
}));
const loggerIds = layerLogger.getIds();
const ignoreLayerFunctions = commonContext.config[CoreNamespace.root].logging
?.ignoreLayerFunctions || {};
const wrappedContext = Object.entries(layerContext).reduce((acc, [layerKey, layerData]) => {
const layerType = typeof layerData;
if (layerKey in CONTEXT_TO_SKIP || layerType !== 'object') {
return merge(acc, { [layerKey]: layerData });
}
const finalLayerData = Object.entries(layerData).reduce((acc2, [domainKey, domainValue]) => {
const theType = typeof domainValue;
// We are only looking for objects with functions
if (theType !== 'object') {
return merge(acc2, { [domainKey]: domainValue });
}
// Are we going to ignore any log wrapping for this domain's whole layer??
const layerLevelKey = `${domainKey}.${layerKey}`;
if (get(ignoreLayerFunctions, layerLevelKey)) {
return merge(acc2, { [domainKey]: domainValue });
}
const domainData = Object.entries(domainValue).reduce((acc3, [propertyName, func]) => {
const funcType = typeof func;
// We are only looking for objects with functions
if (funcType !== 'function') {
return merge(acc3, { [propertyName]: func });
}
// Are we going to ignore this function from wrapping
const functionLevelKey = `${domainKey}.${layerKey}.${propertyName}`;
if (get(ignoreLayerFunctions, functionLevelKey)) {
return merge(acc3, { [propertyName]: func });
}
// WE HAVE TO MERGE the function on top. If we are wrapping, we can loose annotated information.
const newFunc = merge((...args2) => {
const [argsNoCrossLayer, crossLayer] = extractCrossLayerProps(args2);
// Automatically create the crossLayerProps
// @ts-ignore
return func(...argsNoCrossLayer, crossLayer || {
logging: {
ids: loggerIds,
},
});
}, func);
return merge(acc3, { [propertyName]: newFunc });
}, {});
return merge(acc2, { [domainKey]: domainData });
}, {});
return merge(acc, {
[layerKey]: finalLayerData,
});
}, {});
const layer = context.services[CoreNamespace.layers].loadLayer(app, currentLayer,
// @ts-ignore
//layerContext
wrappedContext);
// We need to wrap all the layer functions so that they automatically pass trace information
const theLayer = isPromise(layer) ? await layer : layer;
if (!theLayer) {
return {};
}
// Are we going to ignore any log wrapping for this domain's whole layer??
const layerLevelKey = `${app.name}.${currentLayer}`;
const shouldIgnore = get(ignoreLayerFunctions, layerLevelKey);
const finalLayer = shouldIgnore
? theLayer
: Object.entries(theLayer).reduce((acc, [propertyName, func]) => {
const funcType = typeof func;
// We are only looking for objects with functions
if (funcType !== 'function') {
return merge(acc, { [propertyName]: func });
}
// Are we going to ignore this function from wrapping
const functionLevelKey = `${app.name}.${currentLayer}.${propertyName}`;
if (get(ignoreLayerFunctions, functionLevelKey)) {
return merge(acc, { [propertyName]: func });
}
const newFunc = merge(layerLogger._logWrap(propertyName, merge((log, ...args2) => {
const [argsNoCrossLayer, crossLayer] = extractCrossLayerProps(args2);
// Automatically create the crossLayerProps
// @ts-ignore
return func(...argsNoCrossLayer, crossLayer || {
// create cross layer args.
logging: {
ids: log.getIds(),
},
});
}, func)), func);
return merge(acc, { [propertyName]: newFunc });
}, {});
return merge({
[currentLayer]: {
[app.name]: finalLayer,
},
}, layerContext);
};
const _loadCompositeLayer = async (app, currentLayer, commonContext, previousLayer, antiLayers) => {
return currentLayer.reduce(async (previousSubLayersP, layer) => {
const previousSubLayers = isPromise(previousSubLayersP)
? await previousSubLayersP
: previousSubLayersP;
const layersToRemove = antiLayers(layer);
// We need common context PLUS the previous layers.
const theContext1 = omit(merge({}, commonContext, previousSubLayers), layersToRemove);
const layerLogger = context.rootLogger
// @ts-ignore
.getLogger(theContext1)
.getAppLogger(app.name)
.getLayerLogger(layer);
// eslint-disable-next-line
const theContext = Object.assign(theContext1, {
log: layerLogger,
});
// @ts-ignore
const layerContext = _getLayerContext(theContext, previousLayer);
const loggerIds = layerLogger.getIds();
const ignoreLayerFunctions = commonContext.config[CoreNamespace.root].logging
?.ignoreLayerFunctions || {};
const wrappedContext = Object.entries(layerContext).reduce((acc, [layerKey, layerData]) => {
const layerType = typeof layerData;
if (layerKey in CONTEXT_TO_SKIP || layerType !== 'object') {
return merge(acc, { [layerKey]: layerData });
}
const finalLayerData = Object.entries(layerData).reduce((acc2, [domainKey, domainValue]) => {
const theType = typeof domainValue;
// We are only looking for objects with functions
if (theType !== 'object') {
return merge(acc2, { [domainKey]: domainValue });
}
// Are we going to ignore any log wrapping for this domain's whole layer??
const layerLevelKey = `${domainKey}.${layerKey}`;
if (get(ignoreLayerFunctions, layerLevelKey)) {
return merge(acc2, { [domainKey]: domainValue });
}
const domainData = Object.entries(domainValue).reduce((acc3, [propertyName, func]) => {
const funcType = typeof func;
// We are only looking for objects with functions
if (funcType !== 'function') {
return merge(acc3, { [propertyName]: func });
}
// Are we going to ignore this function from wrapping
const functionLevelKey = `${domainKey}.${layerKey}.${propertyName}`;
if (get(ignoreLayerFunctions, functionLevelKey)) {
return merge(acc3, { [propertyName]: func });
}
const newFunc = merge((...args2) => {
const [argsNoCrossLayer, crossLayer] = extractCrossLayerProps(args2);
// Automatically create the crossLayerProps
// @ts-ignore
return func(...argsNoCrossLayer, crossLayer || {
logging: {
ids: loggerIds,
},
});
}, func);
return merge(acc3, { [propertyName]: newFunc });
}, {});
return merge(acc2, { [domainKey]: domainData });
}, {});
return merge(acc, {
[layerKey]: finalLayerData,
});
}, {});
const loadedLayer = context.services[CoreNamespace.layers].loadLayer(app, layer,
// @ts-ignore
wrappedContext);
if (!loadedLayer) {
return previousSubLayers;
}
const theLayer = isPromise(loadedLayer)
? await loadedLayer
: loadedLayer;
// Are we going to ignore any log wrapping for this domain's whole layer??
const layerLevelKey = `${app.name}.${layer}`;
const shouldIgnore = get(ignoreLayerFunctions, layerLevelKey);
const finalLayer = shouldIgnore
? theLayer
: // @ts-ignore
Object.entries(theLayer).reduce((acc, [propertyName, func]) => {
const funcType = typeof func;
// We are only looking for objects with functions
if (funcType !== 'function') {
return merge(acc, { [propertyName]: func });
}
// Are we going to ignore this function from wrapping
const functionLevelKey = `${app.name}.${layer}.${propertyName}`;
if (get(ignoreLayerFunctions, functionLevelKey)) {
return merge(acc, { [propertyName]: func });
}
const newFunc = merge(layerLogger._logWrap(propertyName, merge((log, ...args2) => {
const [argsNoCrossLayer, crossLayer] = extractCrossLayerProps(args2);
// Automatically create the crossLayerProps
// @ts-ignore
return func(...argsNoCrossLayer, crossLayer || {
// create cross layer args.
logging: {
ids: log.getIds(),
},
});
}, func)), func);
return merge(acc, { [propertyName]: newFunc });
}, {});
// We have to create a NEW context to be passed along each time. If we put acc as the first arg, all the other sub-layers will magically get things they can't have.
const result = merge({}, previousSubLayers, {
[layer]: {
[app.name]: finalLayer,
},
});
return result;
}, {});
};
const loadLayers = () => {
const layersInOrder = context.config[CoreNamespace.root].layerOrder;
const antiLayers = getLayersUnavailable(layersInOrder);
const coreLayersToIgnore = [CoreNamespace.layers, CoreNamespace.globals]
.map(l => `services.${l}`)
.concat([CoreNamespace.layers, CoreNamespace.globals].map(l => `features.${l}`));
const startingContext = omit(context, coreLayersToIgnore);
// @ts-ignore
return context.config[CoreNamespace.root].apps.reduce(async (existingLayersP, app) => {
const existingLayers = await existingLayersP;
const result = await layersInOrder.reduce(async (accP, layer) => {
const acc = await accP;
const [existingLayers2, previousLayer] = acc;
const layersToRemove = Array.isArray(layer)
? // Remove the composite layers from the anti-layers, this will be handled in the composite layer
flatten(layer.map(antiLayers)).filter(x => layer.find(y => x === y) === false)
: antiLayers(layer);
// We have to remove existing layers that we don't want to be exposed.
const correctContext = omit(existingLayers, layersToRemove.concat('log'));
const layerInstance = await (Array.isArray(layer)
? _loadCompositeLayer(app, layer, correctContext, previousLayer, antiLayers)
: _loadLayer(app, layer, correctContext, previousLayer));
if (!layerInstance) {
return [existingLayers2, {}];
}
const newContext = merge({}, existingLayers2, layerInstance);
// @ts-ignore
// eslint-disable-next-line
delete newContext.log;
return [newContext, layerInstance];
}, Promise.resolve([existingLayers, {}]));
return result[0];
}, Promise.resolve(startingContext));
};
return {
loadLayers,
};
},
};
export { name, services, features };
//# sourceMappingURL=layers.js.map