UNPKG

dino-core

Version:

A dependency injection framework for NodeJS applications

309 lines 14 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.ApplicationContext = void 0; const tslib_1 = require("tslib"); const awilix_1 = require("awilix"); const camel_case_1 = tslib_1.__importDefault(require("camel-case")); const Logger_1 = require("../Logger"); const environment_configuration_builder_1 = require("../environment/environment.configuration.builder"); const helper_1 = require("../helper/helper"); const object_helper_1 = require("../helper/object.helper"); const promise_helper_1 = require("../helper/promise.helper"); const ComponentDescriptor_1 = require("../model/ComponentDescriptor"); const component_1 = require("../model/component"); const configuration_1 = require("../model/configuration"); const error_handler_1 = require("../model/error.handler"); const injectable_1 = require("../model/injectable"); const resolver_1 = require("../model/resolver"); const scope_1 = require("../model/scope"); const ApplicationContextBuilder_1 = require("./ApplicationContextBuilder"); const ContextConfigurationProvider_1 = require("./ContextConfigurationProvider"); const ContextScope_1 = require("./ContextScope"); /** * Define the application context * * @public */ class ApplicationContext { /** * Create a new application context. * * @constructor */ constructor() { this.loaded = false; this.contextConfigurationProvider = undefined; this.executedLifecycles = []; } createScope() { return new ContextScope_1.ContextScope(this.container.createScope(), this); } /** * Try resolve the requested dependency, if not resolved try require the dependency and if found * registers it on the application context otherwise throw error. * * > **Will not take dependency graph into account!** * * @param {String} name the name of the dependency to require * @throws {Error} when not able to resolve the dependency * * @public */ require(name) { if (this.contextConfigurationProvider === undefined) { throw Error('context configuration provider is not defined!!!'); } const _name = helper_1.Helper.isPath(name) ? helper_1.Helper.fileNameFromPath(name) : name; let answer = this.container.resolve(_name, { allowUnregistered: true }); if (answer === undefined) { let dependency; try { dependency = require(name); } catch (error) { const contextRoots = this.contextConfigurationProvider.getContextRoots(); for (let i = 0; i < contextRoots.length; i++) { const source = helper_1.Helper.findSource(contextRoots[i], name); if (source !== undefined) { dependency = require(source); break; } } } let scope = scope_1.Scope.SINGLETON; let resolver = resolver_1.Resolver.VALUE; let lazy = false; const dependsOn = []; if (object_helper_1.ObjectHelper.instanceOf(dependency, injectable_1.Injectable)) { Logger_1.Logger.debug('injecting new component from require'); scope = helper_1.Helper.asLifetime(dependency.scope()); resolver = resolver_1.Resolver.CLASS; lazy = dependency.lazy() ?? false; } // eslint-disable-next-line @typescript-eslint/ban-types answer = this.register(_name, ComponentDescriptor_1.ComponentDescriptor.create(name, dependency, scope, resolver, lazy, dependsOn)); } return answer; } /** * Resolve a component from this context and return a reference. * @param {String} component the name of the component to resolve * * @public` */ resolve(component) { const name = (0, camel_case_1.default)(component); const instance = this.container.resolve(name, { allowUnregistered: false }); const lifetime = this.container.registrations[name] !== undefined ? this.container.registrations[name].lifetime : 'UNKNOWN'; this.executeLifecycleIfNeeded(instance, lifetime === 'TRANSIENT'); return instance; } /** * Resolves all components of a particular type. This is a fairly expensive method * as such it should be used with caution. * The current implementation checks the context cache and the registrations skipping already cached * instances, the drawback is that unwanted registration are resolved potentially ahead of time. * * While work is ongoing to resolve the drawback a suitable workaround might be to instantiate a factory via * configuration, this creates a manual overhead but might avoid issues. * * @param {Function} type the type to resolve * @param [onlyCached=false] allows to define is only cached injectables will be resolved, defaults to false meaning that * all registration will be scanned and forcibly loaded. * @returns {Array<any>} * * @public */ resolveAll(type, onlyCached = false) { const answer = []; const cacheEntries = Array.from(this.container.cache.keys()); const injectablesFilter = (key) => !key.toString().startsWith('dinoConcreteInstanceForLazy_') && !['applicationContext', 'environment'].includes(key.toString()); cacheEntries.filter(injectablesFilter).forEach((key) => { const obj = this.container.cache.get(key); if (obj !== undefined) { const target = obj.value; if (object_helper_1.ObjectHelper.instanceOf(target, type)) { this.executeLifecycleIfNeeded(target, obj.resolver.lifetime === 'TRANSIENT'); answer.push(target); } } }); if (!onlyCached) { Object.keys(this.container.registrations) .filter(injectablesFilter) .forEach((key) => { if (!cacheEntries.includes(key)) { // try resolve only if a cached instance has not been found const resolver = this.container.registrations[key]; const obj = resolver.resolve(this.container); if (object_helper_1.ObjectHelper.instanceOf(obj, type) || object_helper_1.ObjectHelper.instanceOf(obj.constructor, type)) { this.executeLifecycleIfNeeded(obj, resolver.lifetime === 'TRANSIENT'); answer.push(obj); } } }); } return answer; } /** * Add, register, a component on this context. This is a replacement of the register function * that will be deprecated on future releases. * * The main difference is that it uses the name provided via ComponentDescriptor avoiding the possibility * of double naming for injectables. * * @param {ComponentDescriptor} componentDescriptor the component descriptor able to resolve this component * @returns {any} the registered instance * * @public */ add(componentDescriptor) { return this.register(componentDescriptor.getName(), componentDescriptor); } /** * Register a component on this context * @param {String} name the component name * @param {ComponentDescriptor} resolver the component descriptor able to resolve this component * @returns {any} the registered instance * * @public * @deprecated use `ApplicationContext#add` instead */ register(name, resolver) { const _name = (0, camel_case_1.default)(name); Logger_1.Logger.debug(`** register ${_name} on context`); let instance = this.container.resolve(_name, { allowUnregistered: true }); if (instance === undefined) { // need to register and resolve instead of build as this will respect awilix // internal structures related to lifecycle. instance = this.container.register(_name, resolver.resolve(this)).resolve(_name); } if (this.executedLifecycles[_name] === undefined) { this.executeLifecycleIfNeeded(instance, true); this.executedLifecycles.push(_name); } return instance; } /** * Try to resolve an injectable instance * * @param {String} name the instance name * @param {Boolean} doExecuteLifecycle a flag indicating if the lifecycle should be executed * @returns {Injectable} the resolved instance * * @private */ tryResolveInstance(name, doExecuteLifecycle) { try { const instance = this.container.resolve(name); this.executeLifecycleIfNeeded(instance, doExecuteLifecycle); return instance; } catch (e) { // } return undefined; } /** * Execute an component lifecycle if required * @param {Injectable} instance the instance to execute the lifecycle for * @param {Boolean} doExecuteLifecycle a flag indicating fif the lifecycle should be executed * @returns {void} */ // eslint-disable-next-line @typescript-eslint/ban-types executeLifecycleIfNeeded(instance, doExecuteLifecycle) { if (!doExecuteLifecycle) { return; } const typeName = instance.getTypeName !== undefined ? instance.getTypeName() : undefined; if (['ErrorHandler', 'Component', 'Service', 'Repository'].includes(typeName) || instance instanceof component_1.Component || instance instanceof error_handler_1.ErrorHandler) { if (instance.postConstruct !== undefined) { const answer = instance.postConstruct(); promise_helper_1.PromiseHelper.handleAsPromiseIfRequired(answer, () => { }); } } if (typeName === 'Configuration' || instance instanceof configuration_1.Configuration) { const componentDescriptor = instance.configure(); promise_helper_1.PromiseHelper.handleAsPromiseIfRequired(componentDescriptor, (cd) => cd !== undefined && this.add(cd)); } } /** * Load the application context * @param {String} config path, relative to the root directory of the project * of the application context configuration file * @returns {ApplicationContext} the application context instance * * @public */ async load(config) { const start = Date.now(); try { if (this.loaded) { throw new Error('application context is already loaded'); } const environment = environment_configuration_builder_1.EnvironmentConfigurationBuilder.load(); this.contextConfigurationProvider = ContextConfigurationProvider_1.ContextConfigurationProvider.create(config, this.getActiveProfiles(environment)); this.container = (0, awilix_1.createContainer)({ injectionMode: this.contextConfigurationProvider.mode() }); this.container.register('applicationContext', (0, awilix_1.asValue)(this)); this.container.register('environment', (0, awilix_1.asValue)(environment)); const applicationContextBuilder = new ApplicationContextBuilder_1.ApplicationContextBuilder(); const componentDescriptors = await this.contextConfigurationProvider.createComponentDescriptors(); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument await applicationContextBuilder.build(componentDescriptors, this); } catch (e) { Logger_1.Logger.error(`unable to load context ${e.message}`); process.exit(1); } finally { this.registerShutdown(); this.loaded = true; Logger_1.Logger.info(`Context loaded in ${(Date.now() - start) / 1000} s`); } } getActiveProfiles(environment) { const profiles = environment.getOrDefault('dino:active:profiles', []); return Array.isArray(profiles) ? profiles : profiles.split(','); } async destroy() { this.loaded = false; const cacheValues = Array.from(this.container.cache.values()); for await (const cacheValue of cacheValues) { const target = cacheValue.value; if (object_helper_1.ObjectHelper.instanceOf(target, component_1.Component)) { await target.preDestroy(); } } await this.container.dispose(); } /** * Register the shutdown handler, the handler will be responsible for invoking the preDestroy method * on all components and dispose the awilix container. */ registerShutdown() { const terminator = () => { promise_helper_1.PromiseHelper.handleAsPromiseIfRequired(this.destroy.bind(this)(), () => process.exit(0)); }; process.on('SIGINT', terminator); process.on('SIGTERM', terminator); process.on('SIGUSR1', terminator); process.on('SIGUSR2', terminator); } } exports.ApplicationContext = ApplicationContext; //# sourceMappingURL=ApplicationContext.js.map