UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

279 lines 14.5 kB
import { constant, name, location, autoService, singleton, alsoInject, } from 'knifecycle'; import { noop } from '../libs/utils.js'; import initRoutesHandlers from './ROUTES_HANDLERS.js'; import initCronsHandlers from './CRONS_HANDLERS.js'; import initConsumersHandlers from './CRONS_HANDLERS.js'; import initTransformersHandlers from './TRANSFORMERS_HANDLERS.js'; import initRoutesWrappers, { ROUTES_WRAPPERS_REG_EXP, } from './ROUTES_WRAPPERS.js'; import initCronsWrappers, { CRONS_WRAPPERS_REG_EXP, } from './CRONS_WRAPPERS.js'; import initConsumersWrappers, { CONSUMERS_WRAPPERS_REG_EXP, } from './CONSUMERS_WRAPPERS.js'; import initTransformersWrappers, { TRANSFORMERS_WRAPPERS_REG_EXP, } from './TRANSFORMERS_WRAPPERS.js'; import { extname, join as pathJoin } from 'node:path'; import { access as _access, constants } from 'node:fs/promises'; import { YError, printStackTrace } from 'yerror'; import { WHOOK_DEFAULT_PLUGINS, WHOOK_PROJECT_PLUGIN_NAME, } from './WHOOK_RESOLVED_PLUGINS.js'; import { DEFAULT_CRONS_DEFINITIONS_OPTIONS, } from './CRONS_DEFINITIONS.js'; import { DEFAULT_ROUTES_DEFINITIONS_OPTIONS, } from './ROUTES_DEFINITIONS.js'; import { DEFAULT_CONSUMERS_DEFINITIONS_OPTIONS, } from './CONSUMERS_DEFINITIONS.js'; import { DEFAULT_TRANSFORMERS_DEFINITIONS_OPTIONS, } from './TRANSFORMERS_DEFINITIONS.js'; const DEFAULT_INITIALIZER_PATH_MAP = {}; /* Architecture Note #2.9: the `$autoload` service Whook provides a simple way to load the constants, services and handlers of a project automatically though several strategies. It is done by implementing the `knifecycle` auto loading interface. */ export default singleton(name('$autoload', autoService(initAutoload))); /** * Initialize the Whook default DI autoloader * @param {Object} services * The services `$autoload` depends on * @param {Array} [services.APP_CONFIG] * Optional APP_CONFIG object to inject * @param {Object} [services.INITIALIZER_PATH_MAP] * An optional map of paths mapping initializers * @param {Array<String>} [services.WHOOK_PLUGINS] * The activated plugins * @param {Array} services.WHOOK_RESOLVED_PLUGINS * The resolved plugins * @param {Array} [services.ROUTES_WRAPPERS_NAMES] * The global wrappers names to wrap the handlers with * @param {Object} services.$injector * An optional object to map services names to other names * @param {Object} services.importer * A service allowing to dynamically import ES modules * @param {Object} services.resolve * A service allowing to dynamically resolve ES modules * @param {Object} services.access * A service allowing to verify access to a file * @param {Object} [services.log=noop] * An optional logging service * @return {Promise<Function>} * A promise of the autoload function. */ async function initAutoload({ APP_CONFIG = undefined, INITIALIZER_PATH_MAP = DEFAULT_INITIALIZER_PATH_MAP, WHOOK_PLUGINS = WHOOK_DEFAULT_PLUGINS, WHOOK_RESOLVED_PLUGINS, ROUTES_WRAPPERS_NAMES = [], CRONS_WRAPPERS_NAMES = [], CONSUMERS_WRAPPERS_NAMES = [], TRANSFORMERS_WRAPPERS_NAMES = [], ROUTES_DEFINITIONS_OPTIONS = DEFAULT_ROUTES_DEFINITIONS_OPTIONS, CRONS_DEFINITIONS_OPTIONS = DEFAULT_CRONS_DEFINITIONS_OPTIONS, CONSUMERS_DEFINITIONS_OPTIONS = DEFAULT_CONSUMERS_DEFINITIONS_OPTIONS, TRANSFORMERS_DEFINITIONS_OPTIONS = DEFAULT_TRANSFORMERS_DEFINITIONS_OPTIONS, args, importer, $injector, resolve, access = _access, log = noop, }) { log('warning', '🤖 - Initializing the `$autoload` service.'); /* Architecture Note #2.9.2: the `API` auto loading We cannot inject the `API` in the auto loader since it is dynamically loaded so doing this during the auto loader initialization. */ let ROUTES_DEFINITIONS; const getRoutesDefinitions = (() => { return async () => { if (!ROUTES_DEFINITIONS) { ROUTES_DEFINITIONS = (await $injector(['ROUTES_DEFINITIONS'])) .ROUTES_DEFINITIONS; } return ROUTES_DEFINITIONS; }; })(); let CRONS_DEFINITIONS; const getCronsDefinitions = (() => { return async () => { if (!CRONS_DEFINITIONS) { CRONS_DEFINITIONS = (await $injector(['CRONS_DEFINITIONS'])) .CRONS_DEFINITIONS; } return CRONS_DEFINITIONS; }; })(); let CONSUMERS_DEFINITIONS; const getConsumersDefinitions = (() => { return async () => { if (!CONSUMERS_DEFINITIONS) { CONSUMERS_DEFINITIONS = (await $injector(['CONSUMERS_DEFINITIONS'])) .CONSUMERS_DEFINITIONS; } return CONSUMERS_DEFINITIONS; }; })(); let TRANSFORMERS_DEFINITIONS; const getTransformersDefinitions = (() => { return async () => { if (!TRANSFORMERS_DEFINITIONS) { TRANSFORMERS_DEFINITIONS = (await $injector(['TRANSFORMERS_DEFINITIONS'])).TRANSFORMERS_DEFINITIONS; } return TRANSFORMERS_DEFINITIONS; }; })(); let commandModule; const getCommandModule = (() => { return async () => { if (!commandModule) { const commandName = args.rest[0]; // This is not supposed to happen if (!commandName) { log('warning', `❌ - No command given in argument.`); throw new YError('E_NO_COMMAND_NAME'); } for (const pluginName of WHOOK_PLUGINS) { const resolvedPlugin = WHOOK_RESOLVED_PLUGINS[pluginName]; const finalPath = new URL(pathJoin('.', 'commands', commandName + extname(resolvedPlugin.mainURL)), resolvedPlugin.mainURL).toString(); try { commandModule = await importer(finalPath); break; } catch (err) { log('debug', `❌ - Command "${commandName}" not found in "${finalPath}".`); log('debug-stack', printStackTrace(err)); } } if (!commandModule) { throw new YError('E_BAD_COMMAND_NAME', commandName); } if (!commandModule.default) { throw new YError('E_NO_COMMAND_HANDLER', commandName); } if (!commandModule.definition) { throw new YError('E_NO_COMMAND_DEFINITION', commandName); } } return commandModule; }; })(); return $autoload; /** * Autoload an initializer from its name * @param {String} The dependency name * @return {Promise<Object>} * A promise resolving with the actual autoloader definition. * An Object containing the `path`, `name` and the `initializer` * in its properties. */ async function $autoload(injectedName) { /* Architecture Note #2.9.1: the `APP_CONFIG` mapper First of all the autoloader looks for constants in the previously loaded `APP_CONFIG` configurations hash. */ if (APP_CONFIG && APP_CONFIG[injectedName]) { log('debug', `📖 - Picking the "${injectedName}" constant in the "APP_CONFIG" service properties.`); return constant(injectedName, APP_CONFIG[injectedName]); } const isRouteHandler = ROUTES_DEFINITIONS_OPTIONS.serviceNamePatterns.some((pattern) => new RegExp(pattern).test(injectedName)); const isRouteWrapper = ROUTES_WRAPPERS_REG_EXP.test(injectedName); const isCronHandler = CRONS_DEFINITIONS_OPTIONS.serviceNamePatterns.some((pattern) => new RegExp(pattern).test(injectedName)); const isConsumerHandler = CONSUMERS_DEFINITIONS_OPTIONS.serviceNamePatterns.some((pattern) => new RegExp(pattern).test(injectedName)); const isTransformerHandler = TRANSFORMERS_DEFINITIONS_OPTIONS.serviceNamePatterns.some((pattern) => new RegExp(pattern).test(injectedName)); const isCronWrapper = CRONS_WRAPPERS_REG_EXP.test(injectedName); const isConsumerWrapper = CONSUMERS_WRAPPERS_REG_EXP.test(injectedName); const isTransformerWrapper = TRANSFORMERS_WRAPPERS_REG_EXP.test(injectedName); /* Architecture Note #2.9.4: the `ROUTES_HANDLERS` mapper Here, we build the handlers map needed by the router by injecting every handler required by the API. */ if ('ROUTES_HANDLERS' === injectedName) { const handlerNames = Object.keys(await getRoutesDefinitions()); return location(alsoInject(handlerNames, initRoutesHandlers), '@whook/whook/dist/services/ROUTES_HANDLERS.js'); } if ('CRONS_HANDLERS' === injectedName) { const handlerNames = Object.keys(await getCronsDefinitions()); return location(alsoInject(handlerNames, initCronsHandlers), '@whook/whook/dist/services/CRONS_HANDLERS.js'); } if ('CONSUMERS_HANDLERS' === injectedName) { const handlerNames = Object.keys(await getConsumersDefinitions()); return location(alsoInject(handlerNames, initConsumersHandlers), '@whook/whook/dist/services/CONSUMERS_HANDLERS.js'); } if ('TRANSFORMERS_HANDLERS' === injectedName) { const handlerNames = Object.keys(await getTransformersDefinitions()); return location(alsoInject(handlerNames, initTransformersHandlers), '@whook/whook/dist/services/TRANSFORMERS_HANDLERS.js'); } /* Architecture Note #2.9.5: the `ROUTES_WRAPPERS` auto loading We inject the `ROUTES_WRAPPERS_NAMES` in the `ROUTES_WRAPPERS` service so that they can be dynamically applied. */ if ('ROUTES_WRAPPERS' === injectedName) { return location(alsoInject(ROUTES_WRAPPERS_NAMES, initRoutesWrappers), '@whook/whook/dist/services/ROUTES_WRAPPERS.js'); } if ('CRONS_WRAPPERS' === injectedName) { return location(alsoInject(CRONS_WRAPPERS_NAMES, initCronsWrappers), '@whook/whook/dist/services/CRONS_WRAPPERS.js'); } if ('CONSUMERS_WRAPPERS' === injectedName) { return location(alsoInject(CONSUMERS_WRAPPERS_NAMES, initConsumersWrappers), '@whook/whook/dist/services/CONSUMERS_WRAPPERS.js'); } if ('TRANSFORMERS_WRAPPERS' === injectedName) { return location(alsoInject(TRANSFORMERS_WRAPPERS_NAMES, initTransformersWrappers), '@whook/whook/dist/services/TRANSFORMERS_WRAPPERS.js'); } if (injectedName === 'COMMAND_DEFINITION') { return constant(injectedName, (await getCommandModule()).definition); } if (injectedName === 'commandHandler') { return name('commandHandler', (await getCommandModule()).default); } /* Architecture Note #2.9.6: Service/handler/wrapper loading Finally, we either load the handler/service/wrapper module if none of the previous strategies applied. */ let modulePath = ''; /* Architecture Note #2.9.3.1: Initializer path mapping In order to be able to load a service from a given path map one can directly specify a path to use for its resolution. */ if (INITIALIZER_PATH_MAP?.[injectedName]) { log('debug', `📖 - Using "INITIALIZER_PATH_MAP" to resolve the "${injectedName}" module path.`); modulePath = INITIALIZER_PATH_MAP[injectedName].startsWith('.') ? resolve(INITIALIZER_PATH_MAP[injectedName]) : INITIALIZER_PATH_MAP[injectedName]; } /* Architecture Note #2.9.6.2: Plugins/project paths Trying to load services from plugins/project paths. */ for (const pluginName of WHOOK_PLUGINS) { if (modulePath) { continue; } const resolvedPlugin = WHOOK_RESOLVED_PLUGINS[pluginName]; const finalPath = new URL(pathJoin(isCronHandler ? 'crons' : isConsumerHandler ? 'consumers' : isTransformerHandler ? 'transformers' : isRouteHandler ? 'routes' : isRouteWrapper || isCronWrapper || isConsumerWrapper || isTransformerWrapper ? 'wrappers' : 'services', '.', injectedName + extname(resolvedPlugin.mainURL)), resolvedPlugin.mainURL); log('debug', `🍀 - Trying to find "${injectedName}" module path in "${pluginName}".`); modulePath = (await checkAccess({ log, access }, finalPath)).toString(); if (pluginName !== WHOOK_PROJECT_PLUGIN_NAME) { // TODO: This assumes a `dist`/`src` folder which // is not necessarily true (`..`) const prefix = new URL('..', resolvedPlugin.mainURL).toString(); if (modulePath.startsWith(new URL('..', resolvedPlugin.mainURL).toString())) { modulePath = modulePath.replace(prefix, pluginName + '/'); } } } if (modulePath) { log('debug', `✅ - Module path of "${injectedName}" found at "${modulePath}".`); } else { log('debug', `🚫 - Module path of "${injectedName}" not found.`); throw new YError('E_UNMATCHED_DEPENDENCY', injectedName); } log('debug', `💿 - Service "${injectedName}" found in "${modulePath}".`); const resolvedInitializer = (await importer(modulePath)) .default; log('debug', `💿 - Loading "${injectedName}" initializer from "${modulePath}".`); const renamedInitializer = injectedName !== injectedName ? name(injectedName, resolvedInitializer) : resolvedInitializer; return location(renamedInitializer, modulePath, 'default'); } } export async function checkAccess({ log, access = _access, }, url) { try { await access(url, constants.R_OK); return url; } catch (err) { log('debug', `🚫 - File doesn't exist at "${url}".`); log('debug-stack', printStackTrace(err)); return ''; } } //# sourceMappingURL=_autoload.js.map