1.1.1 • Published 3 years ago

injector-next v1.1.1

Weekly downloads
-
License
LGPL-3.0
Repository
gitlab
Last release
3 years ago

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.

npm

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] }
}