@foal/core
Version:
Full-featured Node.js framework, with no complexity
197 lines (196 loc) • 7.88 kB
JavaScript
;
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;