UNPKG

@athenna/ioc

Version:

Global Ioc helper for Athenna ecosystem. Built on top of awilix.

237 lines (236 loc) 7.35 kB
/** * @athenna/ioc * * (c) João Lenon <lenon@athenna.io> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { aliasTo, asClass, asValue, asFunction, InjectionMode, createContainer } from 'awilix'; import { sep } from 'node:path'; import { debug } from '#src/debug'; import { Annotation } from '#src/helpers/Annotation'; import { Is, Path, String, Module, Options, Macroable } from '@athenna/common'; import { NotFoundServiceException } from '#src/exceptions/NotFoundServiceException'; export class Ioc extends Macroable { /** * Hold all the services that are fakes. The fake * services will never be replaced if its alias * exists here. */ static { this.fakes = []; } /** * Creates a new instance of IoC. */ constructor(options) { super(); if (Ioc.container) { return this; } this.reconstruct(options); } /** * Reconstruct the awilix container and fakes. */ reconstruct(options) { options = Options.create(options, { injectionMode: InjectionMode.CLASSIC }); Ioc.fakes = []; Ioc.container = createContainer(options); debug('Reconstructing the container using the following options: %o.', options); return this; } /** * List all bindings of the Ioc. */ list() { return Ioc.container.registrations; } /** * Return the registration of the service. */ getRegistration(alias) { return Ioc.container.getRegistration(alias); } /** * Resolve a service from the container or * returns undefined if not found. */ use(alias) { return Ioc.container.resolve(alias, { allowUnregistered: true }); } /** * Resolve a service from the container or * throws exception if not found. */ safeUse(alias) { if (!this.has(alias)) { throw new NotFoundServiceException(alias); } return Ioc.container.resolve(alias); } /** * Register an alias to another service alias. */ alias(subAlias, originalAlias) { if (!this.has(originalAlias)) { throw new NotFoundServiceException(originalAlias); } debug('registering sub alias %s to %s original alias.', subAlias, originalAlias); Ioc.container.register(subAlias, aliasTo(originalAlias)); return this; } /** * Bind a transient service to the container. * Transient services will always resolve a new * instance of it every time you (or Athenna internally) * call ".use" or ".safeUse" method. */ bind(alias, service) { this.register(alias, service, { type: 'transient' }); return this; } /** * Bind a transient service to the container. * Transient services will always resolve a new * instance of it every time you (or Athenna internally) * call ".use" or ".safeUse" method. */ transient(alias, service) { this.register(alias, service, { type: 'transient' }); return this; } /** * Bind an instance service to the container. * Instance services have the same behavior of * singleton services, but you will have more control * on how you want to create your service constructor. */ instance(alias, service) { this.register(alias, service, { type: 'singleton' }); return this; } /** * Bind a singleton service to the container. * Singleton services will always resolve the same * instance of it every time you (or Athenna internally) * call ".use" or ".safeUse" method. */ singleton(alias, service) { this.register(alias, service, { type: 'singleton' }); return this; } /** * Bind a fake service to the container. * Fake services will not let the container * re-register the service alias until you call * "ioc.unfake()" method. */ fake(alias, service) { this.register(alias, service, { type: 'singleton' }); Ioc.fakes.push(alias); return this; } /** * Remove the fake service from fakes map. */ unfake(alias) { const index = Ioc.fakes.indexOf(alias); if (index > -1) { Ioc.fakes.splice(index, 1); } return this; } /** * Remove all fake services from fakes array. */ clearAllFakes() { Ioc.fakes = []; return this; } /** * Verify if service alias is fake or not. */ isFaked(alias) { return Ioc.fakes.includes(alias); } /** * Verify if the container has the service or not. */ has(alias) { return Ioc.container.hasRegistration(alias); } /** * Import and register a module found in the determined * path. */ async loadModule(path, options = {}) { options = Options.create(options, { addCamelAlias: true, parentURL: Path.toHref(Path.pwd() + sep) }); const Service = await Module.resolve(path, options.parentURL); const meta = Annotation.getMeta(Service); this[meta.type](meta.alias, Service); Annotation.defineAsRegistered(Service); if (meta.alias.includes('/') && options.addCamelAlias && !meta.camelAlias) { const subAlias = meta.alias.split('/').pop(); this.alias(String.toCamelCase(subAlias), meta.alias); } if (meta.name) { this.alias(meta.name, meta.alias); } if (meta.camelAlias) { this.alias(meta.camelAlias, meta.alias); } } /** * Import and register all the files found in all * the determined paths. */ async loadModules(paths, options = {}) { await paths.athenna.concurrently(async (path) => { await this.loadModule(path, options); }); } /** * Get the Awilix binder based on the type of the * service. */ getAwilixBinder(type, service) { if (Is.Class(service)) { return asClass(service)[type](); } if (Is.Function(service)) { return asFunction(service)[type](); } return asValue(service); } /** * Register the binder in the Awilix container. */ register(alias, service, options) { if (this.isFaked(alias)) { return; } options = Options.create(options, { type: 'transient' }); /** * Saving the logic inside the function to reuse the code * for promises and no promises services. */ const register = service => { const binder = this.getAwilixBinder(options.type, service); debug('registering service: %o', { name: service?.name, type: options.type, alias }); Ioc.container.register(alias, binder); }; if (service && service.then) { debug('service with alias %s is a promise. waiting for it to resolve to be registered.', alias); service.then(service => register(service)); return; } register(service); } }