injector-next
v1.1.1
Published
--- Next gen Dependency Injector. Very similar to Angular injector. Extremely small ~1Kb
Downloads
2
Maintainers
Readme
Injector Next
Next gen Dependency Injector. Very similar to Angular injector. Extremely small ~1Kb
WARNING: It was meant to be used with Typescript. While it is possible to use with plain JS it is not advised and will be hard to use.
For Typescript, you need to have these two options turned on in tsconfig.json
:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
Depends on reflect-metadata
.
WARNING: API is not yet final
Requirements
Technically it should have no requirements.
Installation
For yarn:
yarn add injector-next
For npm:
npm i injector-next
Docs
Imports:
// ES6 JS/Typescript style
import { Injector } from 'injector-next';
// require
const { Injector } = require('injector-next');
Class Decorators:
Put this before the class you wish to be automatically injected as singleton
@Injectable()
// Alias to Injectable, does the same thing
@Service()
Parameter decorators:
If you wish to provide your own factory, put this before the parameter, this is mainly used for injecting dynamic values, however can be used to inject.
const dateFactory = () => { return new Map(); };
dateFactory.diFactory = true;
@Injectable()
class MyService {
constructor(
@Token({ factory: dateFactory }) protected createdAt: Date
) {}
}
Importing named tokens:
// Somwhere before in your application
Injector.register('appStartTime', new Date());
@Injectable()
class MyService {
constructor(
@Token('appStartTime') protected appStartTime: Date
) {}
}
Basic usage:
@Injectable()
class ServiceA {
a = 1;
}
@Injectable()
class ServiceB {
constructor(
// injector automatically figures out the class
// based on parameter type
protected sa: ServiceA
) {}
}
const sb = Injector.resolve(ServiceB);
Manual registration:
class ExternalClass {
a = 1;
}
// Notice that registration requires actual instance
Injector.register('unique-name', new ExternalClass());
// Now you can get it directly by name:
Injector.get('unique-name');
// Or by token itself, beware that it will return the first registered instance
Injector.resolve(ExternalClass);
// You can also override it, calling register twice will result in error
// IMPORTANT: this will get rid of original instance
Injector.override('unique-name', new ExternalClass());
Custom resolver:
// External class that you cannot modify
class ServiceExternal {
a = 1;
}
@Injectable()
class ServiceB {
constructor(
protected sa: any,
protected cons: typeof console,
protected dt: Date,
) {}
}
const customResolve = (token: any, idx: string) => {
// Either check by index of parameter
if (+idx === 0) {
// this will make it basically factory that would spawn instance
// each time is resolved
return new ServiceExternal();
}
if (+idx === 1) {
return console;
}
// Or by class itself
if (token === Date) {
return new Date();
}
return null;
};
const sb = Injector.resolve(ServiceB, customResolve);
Result:
ServiceB {
sa: ServiceExternal { a: 1 },
cons: Object [console] {
log: [Function: log],
warn: [Function: warn],
dir: [Function: dir],
time: [Function: time],
timeEnd: [Function: timeEnd],
timeLog: [Function: timeLog],
trace: [Function: trace],
assert: [Function: assert],
clear: [Function: clear],
count: [Function: count],
countReset: [Function: countReset],
group: [Function: group],
groupEnd: [Function: groupEnd],
table: [Function: table],
debug: [Function: debug],
info: [Function: info],
dirxml: [Function: dirxml],
error: [Function: error],
groupCollapsed: [Function: groupCollapsed],
Console: [Function: Console],
profile: [Function: profile],
profileEnd: [Function: profileEnd],
timeStamp: [Function: timeStamp],
context: [Function: context]
},
dt: 2021-07-07T18:35:05.968Z
}
Custom tokens:
// create a factory class
const mapFactory = () => { return new Map(); }
// mark it as di factory (otherwise injector cannot distinguish
// between actual class and factory
mapFactory.diFactory = true;
@Injectable()
class ServiceB {
constructor(
// Mark parameter directly
@Token({ factory: mapFactory }) protected map: Map<any, any>
) {}
}
const sb = Injector.resolve(ServiceB);
Result (map is a factory):
ServiceB { map: Map(0) {} }
Custom token to provide non classes:
// create a factory class
const singletonInstance = {
unnamed: 'object',
that: 'has no constructor'
}
const configFactory = () => { return singletonInstance; }
configFactory.diFactory = true;
@Injectable()
class ServiceB {
constructor(
// Mark parameter directly
@Token({ factory: configFactory })
protected config: any
) {}
}
const sb = Injector.resolve(ServiceB);
Result (config is singleton):
ServiceB { config: { unnamed: 'object', that: 'has no constructor' } }
Injector as factory:
// create a factory class
const randFactory = () => { return Math.random(); }
randFactory.diFactory = true;
@Injectable()
class ServiceX {
stable = Math.random();
}
@Injectable()
class Entity {
constructor(
protected serv: ServiceX,
@Token({ factory: randFactory })
protected rand: number
) {}
}
// Those will be just instanciated but not kept in registry
const e1 = Injector.resolve(Entity, null, true);
const e2 = Injector.resolve(Entity, null, true);
const e3 = Injector.resolve(Entity, null, true);
Result (serv is still a singleton but factory spawned random numbers each time):
Entity {
serv: ServiceX { stable: 0.5692771742563438 },
rand: 0.7034761836358194
}
Entity {
serv: ServiceX { stable: 0.5692771742563438 },
rand: 0.20460451477948371
}
Entity {
serv: ServiceX { stable: 0.5692771742563438 },
rand: 0.22173878210817932
}
Advanced usage:
WARNING modifying design:paramtypes
directly may result in some odd behaviour
if not done right.
interface CustomOptions {
min: number;
max: number;
amount: number;
}
const CustomToken = (options: CustomOptions): ParameterDecorator => {
return (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
// collect existing param types
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
// make new factory
const factory = () => {
const out = [];
for (let i = 0; i < options.amount; i++) {
out.push(Math.random() * (options.max - options.min) + options.min)
}
return out;
};
// mark it as factory
factory.diFactory = true;
tokens[parameterIndex] = factory;
// redefine param types so we can check where is our namespace suppose to be injected
Reflect.defineMetadata('design:paramtypes', tokens, target);
};
};
@Injectable()
class ServiceB {
constructor(
@CustomToken({ min: 1, max: 10, amount: 3 })
protected arr: number[],
@CustomToken({ min: 10, max: 20, amount: 5 })
protected arr2: number[],
@CustomToken({ min: 100, max: 200, amount: 7 })
protected arr3: number[],
) {}
}
const sb = Injector.resolve(ServiceB);
Result:
ServiceB {
arr: [ 6.682922113497945, 2.1919589707056892, 3.3588555893813377 ],
arr2: [
15.002033575190106,
11.684026606562002,
16.84351565375917,
16.820905407384693,
16.210885789832872
],
arr3: [
166.39310836720702,
130.8052358138108,
185.8191399183811,
164.22233809832989,
131.44972544841204,
186.65161324868083,
110.1801704311695
]
}
Registering custom resolvers
Sometimes you wish to have a custom resolver entire application.
// Dummy generic class
class PostgresRepository<T> {
constructor(public type: any) {
}
}
// Dummy model
class SomeModel {}
export class OmniRepositoryToken {
constructor(
public repoClass: any,
public modelClass?: any,
public ormName: string = 'orm',
) {
}
}
export class OmniOrmToken {
constructor(
public ormName: string = 'orm',
) {
}
}
export function OmniRepository(repoClass: any, modelClass?: any, ormName?: string) {
return (target: any, propertyKey: string, parameterIndex: number) =>{
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
tokens[parameterIndex] = new OmniRepositoryToken(repoClass, modelClass, ormName);
// redefine param types so we can check where is our namespace suppose to be injected
Reflect.defineMetadata('design:paramtypes', tokens, target);
}
}
export function Omni(ormName?: string) {
return (target: any, propertyKey: string, parameterIndex: number) =>{
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
tokens[parameterIndex] = new OmniOrmToken(ormName);
// redefine param types so we can check where is our namespace suppose to be injected
Reflect.defineMetadata('design:paramtypes', tokens, target);
}
}
const customResolve = (token: any, idx: string) => {
if (token instanceof OmniRepositoryToken) {
return new token.repoClass(token.modelClass);
}
if (token instanceof OmniOrmToken) {
return Injector.get(token.ormName);
}
// If your resolver cannot resolve token, just return null
// Injector will try to resolve token by itself
return null;
};
// Registering custom global resolver
Injector.registerResolver(customResolve);
@Injectable()
class ServiceB {
constructor(
@Omni() public orm: any,
@OmniRepository(PostgresRepository, SomeModel) protected rep: PostgresRepository<SomeModel>,
) {}
}
const sb = Injector.resolve(ServiceB);
console.log(sb);
Result
ServiceB {
orm: OmniORM {},
rep: PostgresRepository { type: [class SomeModel] }
}