@athenna/ioc
Version:
Global Ioc helper for Athenna ecosystem. Built on top of awilix.
237 lines (236 loc) • 7.35 kB
JavaScript
/**
* @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);
}
}