UNPKG

rsdi

Version:

TypeScript dependency injection container. Strong types without decorators.

120 lines (119 loc) 4.18 kB
import { DenyOverrideDependencyError, DependencyIsMissingError, ForbiddenNameError, IncorrectInvocationError, } from "./errors.js"; const containerMethods = ["add", "get", "extend", "update"]; /** * Dependency injection container */ export class DIContainer { resolvers = {}; resolvedDependencies = {}; context = {}; /** * Adds new dependency resolver to the container. If dependency with given name already exists it will throw an error. * Use update method instead. It will override existing dependency. * * @param name * @param resolver */ add(name, resolver) { if (containerMethods.includes(name)) { throw new ForbiddenNameError(name); } if (this.has(name)) { throw new DenyOverrideDependencyError(name); } return this.setValue(name, resolver); } /** * Updates existing dependency resolver. If dependency with given name does not exist it will throw an error. * In most cases you don't need to override dependencies and should use add method instead. This approach will * help you to avoid overriding dependencies by mistake. * * You may want to override dependency if you want to mock it in tests. * * @param name * @param resolver */ update(name, resolver) { if (containerMethods.includes(name)) { throw new ForbiddenNameError(name); } if (!this.has(name)) { throw new DependencyIsMissingError(name); } return this.setValue(name, resolver); } /** * Checks if dependency with given name exists * @param name */ has(name) { return this.resolvers.hasOwnProperty(name); } /** * Resolve dependency by name. Alternatively you can use property access to resolve dependency. * For example: const { a, b } = container; * @param dependencyName */ get(dependencyName) { if (this.resolvedDependencies[dependencyName] !== undefined) { return this.resolvedDependencies[dependencyName]; } const resolver = this.resolvers[dependencyName]; if (!resolver) { throw new DependencyIsMissingError(dependencyName); } this.resolvedDependencies[dependencyName] = resolver(this.context); return this.resolvedDependencies[dependencyName]; } /** * Extends container with given function. It will pass container as an argument to the function. * Function should return new container with extended resolvers. * It is useful when you want to split your container into multiple files. * You can create a file with resolvers and extend container with it. * You can also use it to create multiple containers with different resolvers. * * For example: * * const container = new DIContainer() * .extend(addValidators) * * export type DIWithValidators = ReturnType<typeof addValidators>; * export const addValidators = (container: DIWithDataAccessors) => { * return container * .add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c)) * .add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c)); * }; * * @param f */ extend(f) { return f(this.toContainer()); } setValue(name, resolver) { this.resolvers = { ...this.resolvers, [name]: resolver, }; let updatedObject = this; if (!this.hasOwnProperty(name)) { updatedObject = Object.defineProperty(this, name, { get() { return this.get(name); }, }); } this.context = new Proxy(this, { get(target, property) { if (containerMethods.includes(property.toString())) { throw new IncorrectInvocationError(); } // @ts-ignore return target[property]; }, }); return updatedObject; } toContainer() { return this; } }