dino-core
Version:
A dependency injection framework for NodeJS applications
309 lines • 14 kB
JavaScript
"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