dino-core
Version:
A dependency injection framework for NodeJS applications
215 lines • 11.2 kB
JavaScript
;
// 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