UNPKG

@mdzzohrabi/container

Version:

Dependency Injection Container

353 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@mdzzohrabi/common"); let { isObject, isArray, isFunction, isEmpty, isString, isClass, isClassObject } = common_1.is; function isFactory(object) { return isObject(object) && 'factory' in object; } function isService(object) { return object instanceof Function; } exports.isService = isService; function isIDefinition(object) { return (isObject(object) && ('target' in object || 'factory' in object)); } exports.isIDefinition = isIDefinition; function isInjectable(object) { return '$inject' in object; } exports.isInjectable = isInjectable; exports.Name = { isService: (name) => name[0] !== '$', isParameter: (name) => name[0] == '$' && name[1] !== '$', isTag: (name) => name[0] == '$' && name[1] == '$' }; let defaultContainer; var ServiceType; (function (ServiceType) { ServiceType[ServiceType["DefinitionClass"] = 0] = "DefinitionClass"; ServiceType[ServiceType["DefinitionObject"] = 1] = "DefinitionObject"; ServiceType[ServiceType["Service"] = 2] = "Service"; ServiceType[ServiceType["FactoryFunction"] = 3] = "FactoryFunction"; ServiceType[ServiceType["FactoryClass"] = 4] = "FactoryClass"; })(ServiceType || (ServiceType = {})); /** * Dependency Injection Container * @author Masoud Zohrabi <mdzzohrabi@gmail.com> */ class Container { constructor(services = {}, parameters = {}, global = false) { this.services = {}; this.parameters = {}; // Loaded services cache this.loaded = {}; this.setAll(services); this.setParams(parameters); if (global) this.setAsMain(); } /** Clean loaded services cache */ fresh() { this.loaded = {}; return this; } /** Reset container */ reset() { this.services = {}; this.loaded = {}; this.parameters = {}; return this; } setParams(parameters) { common_1.forEach(parameters, (value, name) => { this.setParam(name, value); }); return this; } /** Declare multiple services */ setAll(services) { common_1.forEach(services, (service, name) => { this.set(name, service); }); return this; } /** Get service definition */ getDefinition(serviceName) { if (!this.has(serviceName)) throw Error(`Service "${serviceName}" not found`); return this.services[`$${serviceName}`]; } /** Set a service */ set(serviceName, service) { // Service definition let def = { shared: true, tags: [], factory: undefined, target: undefined, parameters: [], calls: {} }; if (isFunction(service)) { let parsed = common_1.parseFunction(service); if (parsed.arrow || parsed.name.endsWith('Factory')) def.factory = service; else def.target = service; def.parameters = this.getDependencies(service); } else if (isIDefinition(service)) { def = Object.assign(def, service); } else { throw Error(`Invalid service definition`); } this.loaded[`$${serviceName}`] = undefined; this.services[`$${serviceName}`] = def; return this; } /** Set a service factory */ setFactory(serviceName, factory, shared = true) { return this.set(serviceName, { factory: factory, shared: shared }); } /** Set container parameter */ setParam(name, value) { this.parameters['$' + name] = value; return this; } /** Check if a service has factory */ isFactory(name) { return !!this.services['$' + name].factory; } /** Get a parameter */ getParam(name) { return this.parameters['$' + name]; } /** Check if a service exists */ has(serviceName) { return this.services['$' + serviceName] !== undefined; } /** Build and get a service */ get(serviceName) { // Check if service cached if (this.loaded['$' + serviceName]) { return this.loaded['$' + serviceName]; } // Get service definition let service = this.getDefinition(serviceName); // Build service let instance = this.buildService(service); // Private instance if (service.shared === false) { return instance; } // Cache for next fetch return this.loaded['$' + serviceName] = instance; } /** * Resolve dependencies * There are three type of dependencies : * - Tag dependency [prefixed with $$] * - Parameter dependency [prefixed with $] * - Service dependency * @example * resolve([ '$dbName', '$$loggers', 'odm' ]) * @param deps Dependencies */ resolve(deps) { let resolveDependency = (dep) => { // Tag if (exports.Name.isTag(dep)) { let services = []; let tagName = dep.substr(2); common_1.forEach(this.services, (service, name) => { if (service.tags.indexOf(tagName) >= 0) { services.push(this.get(name.substr(1))); } }); return services; } // Parameter if (exports.Name.isParameter(dep)) return this.parameters[dep]; // Service return this.get(dep); }; if (isString(deps)) { return resolveDependency(deps); } return deps.map(resolveDependency); } invoke(func) { if (func === undefined || !func) return undefined; if (typeof func === 'string') { return this.resolve(func); } return this.buildService(func); } prepareDefinition(service) { let def; let parsedParams = []; if (isString(service)) def = this.get(service); else if (isFunction(service)) { parsedParams = this.getDependencies(service); def = { target: service, parameters: [], calls: {}, factory: undefined, tags: [], properties: {} }; } else { def = service; } let { target, parameters, properties, calls } = def; // Inject decorator if (target && isInjectable(target)) { let { $inject } = target; if ($inject.arguments) def.parameters = parameters.length > 0 ? parameters : $inject.arguments; if ($inject.properties) def.properties = Object.assign({}, properties, $inject.properties); if ($inject.calls) def.calls = Object.assign({}, calls, $inject.calls); } // Use parsed parameters if no parameters injected by user if (def.parameters.length == 0) def.parameters = parsedParams; return def; } /** * Build a service * @param service Service definition or function */ buildService(service) { let { parameters, calls, factory, target, properties } = this.prepareDefinition(service); // Service factory if (factory) { let resolvedParams = this.resolve(parameters); let parsed = common_1.parseFunction(factory); if (target) resolvedParams = [target].concat(resolvedParams); if (parsed.arrow) return factory(...resolvedParams); else return factory.apply(this, resolvedParams); // Service build using ServiceDefinition } else if (target) { let parsed = common_1.parseFunction(target); let resolvedParams = this.resolve(parameters); if (parsed.arrow) return target(...resolvedParams); let instance = new target(...resolvedParams); // Properties common_1.forEach(properties, (service, name) => { instance[name] = this.resolve(service); }); // Calls common_1.forEach(calls, (params, method) => { instance[method].call(instance, this.resolve(params)); }); return instance; } throw Error(`Invalid service`); } invokeLater(func, method) { if (isFunction(func)) { let instance; return common_1.createFunction(func.name + '$' + method, (...params) => { return (instance || (instance = this.invoke(func)))[method](...params); }); } if (isObject(func)) { return (...params) => { return func[method](...params); }; } } getDependencies(target) { return common_1.parseFunction(target).args; } setAsMain() { defaultContainer = this; return this; } get isGlobal() { return defaultContainer === this; } static get main() { return defaultContainer; } } exports.Container = Container; defaultContainer = new Container; //#region Decorators function Service(name, options = {}) { return (target) => { let definition = { target: target }; definition.shared = options.shared || true; definition.parameters = options.arguments || []; definition.calls = options.calls || {}; definition.tags = options.tags || []; let container = options.container || defaultContainer; container.set(name, definition); }; } exports.Service = Service; function $getInject(target) { if (isClassObject(target)) { target = target.constructor; } target.$inject = target.$inject || { properties: {}, calls: {}, arguments: [] }; return target; } function classDecorator(injects, target) { let { $inject } = $getInject(target); let def = {}; if (isArray(injects)) { def.arguments = injects; } $inject = Object.assign($inject, def); } function propertyDecorator(injects, target, propertyKey) { let { $inject } = $getInject(target); $inject.properties[propertyKey] = injects; } function Call(...params) { return (target, key, descriptor) => { let { $inject } = $getInject(target); $inject.calls[key] = params; }; } exports.Call = Call; exports.Decorator = { isClass: (args) => args.length == 1, isProperty: (args) => args.length == 2 || (args.length == 3 && args[2] === undefined), isMethod: (args) => args.length == 3 && typeof args[2] === 'object', isParameter: (args) => args.length == 3 && typeof args[2] === 'number' }; function Inject(injects) { return function (...params) { let { isClass, isMethod, isParameter, isProperty } = exports.Decorator; if (isClass(arguments)) { let [target] = params; classDecorator(injects, target); } else if (isProperty(arguments)) { let [target, key] = params; propertyDecorator(injects, target, key); } else if (isParameter(arguments)) { let [target, method, index] = arguments; let { $inject } = $getInject(target); // Constructor if (method == undefined) { $inject.arguments = Array.isArray($inject.arguments) ? $inject.arguments : []; $inject.arguments[index] = injects; } else { throw Error(`Method parameter decorator not allowed`); } } }; } exports.Inject = Inject; //#endregion //# sourceMappingURL=container.js.map