UNPKG

dino-core

Version:

A dependency injection framework for NodeJS applications

215 lines 11.2 kB
"use strict"; // Copyright 2018 Quirino Brizi [quirino.brizi@gmail.com] // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.ContextConfigurationProvider = void 0; const tslib_1 = require("tslib"); const app_root_path_1 = tslib_1.__importDefault(require("app-root-path")); const camel_case_1 = tslib_1.__importDefault(require("camel-case")); const fs_1 = tslib_1.__importDefault(require("fs")); const yamljs_1 = tslib_1.__importDefault(require("yamljs")); const awilix_1 = require("awilix"); const component_name_already_taken_exception_1 = require("../exceptions/component.name.already.taken.exception"); const component_name_not_provided_exception_1 = require("../exceptions/component.name.not.provided.exception"); const helper_1 = require("../helper/helper"); const object_helper_1 = require("../helper/object.helper"); const Logger_1 = require("../Logger"); const ComponentDescriptor_1 = require("../model/ComponentDescriptor"); const resolver_1 = require("../model/resolver"); const scope_1 = require("../model/scope"); /** * The provider of the application context configuration. * Configuration must have the following yaml format: * injectionMode: transient * components: * - name: (mandatory) the name of the component, will act as a reference for injection * - source: (mandatory) the source file of the component class relative to the root of the project or a module name * - scope: (optional) the resolution scope for the component, defaults to singleton * - resolver: (optional) the resolver to use available are class, value, function; if omitted class resolver wil be used * @typedef {ContextConfigurationProvider} * @public * @since 0.2.2 */ class ContextConfigurationProvider { /** * Constructor for ConfigurationProvider * @param {Any} conf the application context configuration * @param {array} activeProfiles the profiles that are considered active for the current run * * @private */ constructor(conf, activeProfiles = []) { // this.validateConfiguration(conf); this.conf = conf; this.activeProfiles = activeProfiles; this.contextRoots = helper_1.Helper.getContextRoot(conf) .split(',') .map((path) => { if (path.charAt(0) !== '/') { return `${app_root_path_1.default.path}/${path}`; } return path; }); // this.contextRoot = `${appRootPath.path}/${Helper.getContextRoot(conf)}`; // this.componentDescriptors = new Array(); this.selectedComponentNames = []; } getContextRoots() { return this.contextRoots; } /** * Provide the mode the injection will be performed, if mode is not defined "proxy" will be used. * Supported injection mode are: * - CLASSIC * - PROXY * * @returns {awilix.InjectionMode} the injection mode * @public */ mode() { let answer = awilix_1.InjectionMode.PROXY; if (this.conf.injectionMode !== undefined && this.conf.injectionMode.toLowerCase() === 'classic') { answer = awilix_1.InjectionMode.CLASSIC; } return answer; } /** * Normalize the source path * @param {String} source the configured source location * * @private */ normalizePath(source) { const path = `${app_root_path_1.default}/${source}`; return fs_1.default.existsSync(`${path}.js`) ? path : source; } /** * Create a new instance of ConfigurationProvider * @param {String} path the path of the configuration relative to the root of the project, * if not provided a scan will be performed to collect all entities defined. * @param {array} activeProfiles the profiles that are considered active for the current run * @returns {ContextConfigurationProvider} * * @public * @static */ static create(path, activeProfiles = []) { let conf = { injectionMode: 'proxy', contextScan: true, components: [], configurations: [] }; if (path !== undefined) { try { Logger_1.Logger.debug(`loading config from path [${path}]`); conf = yamljs_1.default.load(`${app_root_path_1.default}/${path}`); } catch (e) { Logger_1.Logger.error('unable to load config default to contextScan', e); } } return new ContextConfigurationProvider(conf, activeProfiles); } /** * Create the component descriptors based on the provided configuration or context * * @returns {Promise<Array<ComponentDescriptor>>} a promise resolved with an array of component descriptors * @public */ async createComponentDescriptors() { const componentDescriptors = []; Logger_1.Logger.debug('loading all configurations and components as a component descriptor'); for (let i = 0; i < (this.conf.configurations ?? []).length; i++) { const configuration = this.conf.configurations[i]; const name = (0, camel_case_1.default)(configuration.name); this.throwExceptionIfComponentNameIsNotProvidedOrNotAvailable(name); Logger_1.Logger.debug(`loading candidate configuration from ${configuration.source}`); componentDescriptors.push(ComponentDescriptor_1.ComponentDescriptor.create(name, this.normalizePath(configuration.source), helper_1.Helper.asLifetime(configuration.scope), helper_1.Helper.asResolver(configuration.resolver), false, object_helper_1.ObjectHelper.invoke(configuration, 'dependsOn', []))); } for (let i = 0; i < (this.conf.components ?? []).length; i++) { const component = this.conf.components[i]; const name = (0, camel_case_1.default)(component.name); this.throwExceptionIfComponentNameIsNotProvidedOrNotAvailable(name); Logger_1.Logger.debug(`loading candidate component from ${component.source}`); componentDescriptors.push(ComponentDescriptor_1.ComponentDescriptor.create(name, this.normalizePath(component.source), helper_1.Helper.asLifetime(component.scope), helper_1.Helper.asResolver(component.resolver), component.lazy ?? false, object_helper_1.ObjectHelper.invoke(component, 'dependsOn', []))); } // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (this.conf.contextScan) { Logger_1.Logger.debug('context scan is enabled, loading context based configurations and components as a component descriptor'); const promises = []; let componentsToLoad = 0; for (let i = 0; i < this.contextRoots.length; i++) { const contextRoot = this.contextRoots[i]; Logger_1.Logger.debug(`>>> inspecting ${contextRoot} for candidates`); const sources = helper_1.Helper.loadSources(contextRoot); componentsToLoad += sources.length; for (const source of sources) { promises.push(this.doBuildComponentDescriptorFromSource(source, componentDescriptors)); } } const fulfilled = (await Promise.allSettled(promises)).filter((value) => value.status === 'fulfilled'); if (fulfilled.length !== componentsToLoad) { throw new Error('unable to load all requested dependencies, stopping context loading!'); } } return componentDescriptors; } async doBuildComponentDescriptorFromSource(source, componentDescriptors) { // for (const source of sources) { Logger_1.Logger.debug(`loading candidate component or configuration from ${source}`); const injectables = await helper_1.Helper.dynamicallyImport(source); for (const injectable of injectables) { const autoLoad = object_helper_1.ObjectHelper.invoke(injectable, 'autoLoad', true); if (!autoLoad) { return; } const profile = object_helper_1.ObjectHelper.invoke(injectable, 'profile'); const profileIsActive = this.profileIsActive(profile); // const isInjectable = Helper.isInjectable(component); if (profileIsActive) { // const componentDescriptor = this.createComponentDefinitions(component); let componentDescriptor; const name = (0, camel_case_1.default)(injectable.name ?? injectable.constructor.name); this.throwExceptionIfComponentNameIsNotProvidedOrNotAvailable(name); if (helper_1.Helper.isConfiguration(injectable)) { componentDescriptor = ComponentDescriptor_1.ComponentDescriptor.create(name, injectable, scope_1.Scope.SINGLETON, resolver_1.Resolver.CLASS, false, [], true); } if (helper_1.Helper.isInjectable(injectable)) { componentDescriptor = ComponentDescriptor_1.ComponentDescriptor.create(name, injectable, helper_1.Helper.asLifetime(object_helper_1.ObjectHelper.invoke(injectable, 'scope', scope_1.Scope.SINGLETON)), resolver_1.Resolver.CLASS, object_helper_1.ObjectHelper.invoke(injectable, 'lazy', false), object_helper_1.ObjectHelper.invoke(injectable, 'dependsOn', []), false); } if (componentDescriptor !== undefined) { componentDescriptors.push(componentDescriptor); } } else { Logger_1.Logger.warn(`injectable candidate [${injectable.name}] defined at ${source} not loaded - profile is active ${profileIsActive}`); } } } /** * * @param {String} name the name of the component to validate * @private */ throwExceptionIfComponentNameIsNotProvidedOrNotAvailable(name) { if (name === undefined) { throw component_name_not_provided_exception_1.ComponentNameNotProvidedException.create(); } if (this.selectedComponentNames.includes(name)) { throw component_name_already_taken_exception_1.ComponentNameAlreadyTakenException.create(name); } } profileIsActive(profile) { return (object_helper_1.ObjectHelper.isNotDefined(profile) || (this.activeProfiles !== undefined && this.activeProfiles.length > 0 && this.activeProfiles.includes(profile))); } } exports.ContextConfigurationProvider = ContextConfigurationProvider; //# sourceMappingURL=ContextConfigurationProvider.js.map