UNPKG

@foal/core

Version:

Full-featured Node.js framework, with no complexity

197 lines (196 loc) 7.88 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceManager = void 0; exports.Dependency = Dependency; exports.dependency = dependency; exports.createService = createService; exports.createControllerOrService = createControllerOrService; // std const path_1 = require("path"); // 3p require("reflect-metadata"); const config_1 = require("./config"); /** * Decorator injecting a service inside a controller or another service. * * @param id {string} - The service ID. */ function Dependency(id) { return (target, propertyKey) => { const dependencies = [...(Reflect.getMetadata('dependencies', target) || [])]; dependencies.push({ propertyKey, serviceClassOrID: id }); Reflect.defineMetadata('dependencies', dependencies, target); }; } /** * Decorator injecting a service inside a controller or another service. * * @export */ function dependency(target, propertyKey) { const serviceClass = Reflect.getMetadata('design:type', target, propertyKey); const dependencies = [...(Reflect.getMetadata('dependencies', target) || [])]; dependencies.push({ propertyKey, serviceClassOrID: serviceClass }); Reflect.defineMetadata('dependencies', dependencies, target); } /** * Create a new service with its dependencies. * * @export * @template Service * @param {ClassOrAbstractClass<Service>} serviceClass - The service class. * @param {object} [dependencies] - An object which key/values are the service properties/instances. * @returns {Service} - The created service. */ function createService(serviceClass, dependencies) { return createControllerOrService(serviceClass, dependencies); } function createControllerOrService(serviceClass, dependencies) { const metadata = Reflect.getMetadata('dependencies', serviceClass.prototype) || []; const serviceManager = new ServiceManager(); if (dependencies) { metadata.forEach(dep => { const serviceMock = dependencies[dep.propertyKey]; if (serviceMock) { serviceManager.set(dep.serviceClassOrID, serviceMock); } }); } return serviceManager.get(serviceClass); } /** * Identity Mapper that instantiates and returns service singletons. * * @export * @class ServiceManager */ class ServiceManager { map = new Map(); /** * Boot all services : call the method "boot" of each service if it exists. * * If a service identifier is provided, only this service will be booted. * * Services are only booted once. * * @param {(string|ClassOrAbstractClass)} [identifier] - The service ID or the service class. * @returns {Promise<void>} * @memberof ServiceManager */ async boot(identifier) { if (typeof identifier !== 'undefined') { const value = this.map.get(identifier); if (!value) { throw new Error(`No service was found with the identifier "${identifier}".`); } return this.bootService(value); } const promises = []; for (const value of this.map.values()) { promises.push(this.bootService(value)); } await Promise.all(promises); } /** * Add manually a service to the identity mapper. * * @param {string|ClassOrAbstractClass} identifier - The service ID or the service class. * @param {*} service - The service object (or mock). * @param {{ boot: boolean }} [options={ boot: false }] If `boot` is true, the service method "boot" * will be executed when calling `ServiceManager.boot` is called. * @returns {this} The service manager. * @memberof ServiceManager */ set(identifier, service, options = { boot: false }) { this.map.set(identifier, { boot: options.boot, service, }); return this; } get(identifier) { // @ts-ignore : Type 'ServiceManager' is not assignable to type 'Service'. if (identifier === ServiceManager || identifier.isServiceManager === true) { // @ts-ignore : Type 'ServiceManager' is not assignable to type 'Service'. return this; } // Get the service if it exists. const value = this.map.get(identifier); if (value) { return value.service; } // Throw an error if the identifier is a string and no service was found in the map. if (typeof identifier === 'string') { throw new Error(`No service was found with the identifier "${identifier}".`); } if (identifier.hasOwnProperty('concreteClassConfigPath')) { const concreteClass = this.getConcreteClassFromConfig(identifier); return this.get(concreteClass); } // If the service has not been instantiated yet then do it. const dependencies = Reflect.getMetadata('dependencies', identifier.prototype) || []; // identifier is a class here. const service = new identifier(); for (const dependency of dependencies) { service[dependency.propertyKey] = this.get(dependency.serviceClassOrID); } // Save the service. this.map.set(identifier, { boot: true, service, }); return service; } async bootService(value) { if (value.boot && value.service.boot) { value.boot = false; await value.service.boot(); } } getConcreteClassFromConfig(cls) { const concreteClassConfigPath = this.getProperty(cls, 'concreteClassConfigPath', 'string'); const concreteClassName = this.getProperty(cls, 'concreteClassName', 'string'); let concreteClassPath; if (cls.hasOwnProperty('defaultConcreteClassPath')) { concreteClassPath = config_1.Config.get(concreteClassConfigPath, 'string', 'local'); } else { concreteClassPath = config_1.Config.getOrThrow(concreteClassConfigPath, 'string'); } let prettyConcreteClassPath; if (concreteClassPath === 'local') { concreteClassPath = this.getProperty(cls, 'defaultConcreteClassPath', 'string', `[CONFIG] ${cls.name} does not support the "local" option in ${concreteClassConfigPath}.`); } else if (concreteClassPath.startsWith('./')) { prettyConcreteClassPath = concreteClassPath; concreteClassPath = (0, path_1.join)(process.cwd(), 'build', concreteClassPath); } prettyConcreteClassPath = prettyConcreteClassPath || concreteClassPath; let pkg; try { pkg = require(concreteClassPath); } catch (err) { // TODO: test this line. if (err.code !== 'MODULE_NOT_FOUND') { throw err; } throw new Error(`[CONFIG] The package or file ${prettyConcreteClassPath} was not found.`); } const concreteClass = this.getProperty(pkg, concreteClassName, 'function', `[CONFIG] ${prettyConcreteClassPath} is not a valid package or file for ${cls.name}:` + ` class ${concreteClassName} not found.`, `[CONFIG] ${prettyConcreteClassPath} is not a valid package or file for ${cls.name}:` + ` ${concreteClassName} is not a class.`); return concreteClass; } getProperty(obj, propertyKey, type, notFoundMsg, typeMsg) { if (!obj.hasOwnProperty(propertyKey)) { throw new Error(notFoundMsg || `[CONFIG] ${obj.name}.${propertyKey} is missing.`); } const property = obj[propertyKey]; if (typeof property !== type) { throw new Error(typeMsg || `[CONFIG] ${obj.name}.${propertyKey} should be a ${type}.`); } return property; } } exports.ServiceManager = ServiceManager;