diff --git a/docs/docs/providers.md b/docs/docs/providers.md
index b2f73ce92f6..a9d9871ef74 100644
--- a/docs/docs/providers.md
+++ b/docs/docs/providers.md
@@ -133,6 +133,61 @@ This is possible with the combination of @@Opts@@ and @@UseOpts@@ decorators.
to `ProviderScope.INSTANCE`.
:::
+## Inject many provider
+
+This feature lets to simplify the dependency management while working on multiple implementations of the same interface with type code.
+
+If users use the same token when registering providers, the IoC container should exchange a token for a list of instances. Let's consider the following real example:
+
+```typescript
+interface Bar {
+ type: string;
+}
+
+const Bar: unique symbol = Symbol("Bar");
+
+@Injectable({type: Bar})
+class Foo implements Bar {
+ private readonly type = "foo";
+}
+
+@Injectable({type: Bar})
+class Baz implements Bar {
+ private readonly type = "baz";
+}
+```
+
+Now as a user, I would like to create a [registry](https://www.martinfowler.com/eaaCatalog/registry.html) and retrieve an appropriate instance by type:
+
+```typescript
+@Controller("/some")
+export class SomeController {
+ constructor(@Inject(Bar) private readonly bars: Bar[]) {}
+
+ @Post()
+ async create(@Body("type") type: "baz" | "foo") {
+ const bar: Bar | undefined = this.bars.find((x) => x.type === type);
+ }
+}
+```
+
+or in the following way as well:
+
+```typescript
+@Controller("/some")
+export class SomeController {
+ constructor(private readonly injector: InjectorService) {}
+
+ @Post()
+ async create(@Body("type") type: "baz" | "foo") {
+ const bars: Bar[] = this.injector.getAll(Bar);
+ const bar: Bar | undefined = bars.find((x) => x.type === type);
+
+ // your code
+ }
+}
+```
+
## Override an injection token
By default, the `@Injectable()` are register a class provider with an injection token obtained from the metadata generated by Typescript.
diff --git a/packages/di/coverage.json b/packages/di/coverage.json
index acd2c2746ea..fc6b48084a6 100644
--- a/packages/di/coverage.json
+++ b/packages/di/coverage.json
@@ -1,6 +1,6 @@
{
- "statements": 99.37,
- "branches": 93.54,
- "functions": 99.22,
- "lines": 99.48
+ "statements": 99,
+ "branches": 93.12,
+ "lines": 99.1,
+ "functions": 98.46
}
diff --git a/packages/di/src/decorators/inject.spec.ts b/packages/di/src/decorators/inject.spec.ts
index 00fcbe9bc2c..d857725f466 100644
--- a/packages/di/src/decorators/inject.spec.ts
+++ b/packages/di/src/decorators/inject.spec.ts
@@ -1,7 +1,5 @@
-import {catchError, descriptorOf, Metadata, Store} from "@tsed/core";
-import {Required} from "@tsed/schema";
-import {Inject, Injectable, InjectorService} from "../../src";
-import {INJECTABLE_PROP} from "../constants/constants";
+import {descriptorOf} from "@tsed/core";
+import {Inject, Injectable, InjectorService, registerProvider} from "../../src";
describe("@Inject()", () => {
describe("used on unsupported decorator type", () => {
@@ -24,29 +22,8 @@ describe("@Inject()", () => {
});
});
- describe("used on method", () => {
- it("should store metadata", () => {
- // GIVEN
- class Test {
- test() {}
- }
-
- // WHEN
- Inject()(Test.prototype, "test", descriptorOf(Test, "test"));
-
- // THEN
- const store = Store.from(Test).get(INJECTABLE_PROP);
- expect(store).toEqual({
- test: {
- bindingType: "method",
- propertyKey: "test"
- }
- });
- });
- });
-
- describe("used on property", () => {
- it("should store metadata from inferred type", async () => {
+ describe("@property", () => {
+ it("should inject service", async () => {
// GIVEN
@Injectable()
class Test {
@@ -60,7 +37,7 @@ describe("@Inject()", () => {
expect(instance).toBeInstanceOf(Test);
expect(instance.test).toBeInstanceOf(InjectorService);
});
- it("should store metadata from given type", async () => {
+ it("should inject service with the given type", async () => {
// GIVEN
@Injectable()
class Test {
@@ -74,67 +51,135 @@ describe("@Inject()", () => {
expect(instance).toBeInstanceOf(Test);
expect(instance.test).toBeInstanceOf(InjectorService);
});
- it("should catch error when an object is given as token provider", async () => {
- // GIVEN
- @Injectable()
- class Test {
- @Required()
- test: Object;
+ it("should inject many services", async () => {
+ const TOKEN_GROUPS = Symbol.for("groups:1");
+
+ interface InterfaceGroup {
+ type: string;
}
- const error = catchError(() => {
- Inject()(Test.prototype, "test");
- });
+ @Injectable({
+ type: TOKEN_GROUPS
+ })
+ class MyService1 implements InterfaceGroup {
+ readonly type: string = "service1";
- expect(error?.message).toMatchSnapshot();
- });
- });
+ constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
+ }
- describe("used on constructor/params", () => {
- beforeAll(() => {
- jest.spyOn(Metadata, "getParamTypes").mockReturnValue([]);
- jest.spyOn(Metadata, "setParamTypes").mockReturnValue(undefined);
- });
- afterAll(() => {
- jest.resetAllMocks();
- });
+ @Injectable({
+ type: TOKEN_GROUPS
+ })
+ class MyService2 implements InterfaceGroup {
+ readonly type: string = "service2";
- it("should call Metadata.getParamTypes()", () => {
- // GIVEN
- class Test {
- test() {}
+ constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
}
- // WHEN
- Inject(String)(Test.prototype, undefined, 0);
+ const TokenAsync = Symbol.for("MyService2");
- // THEN
- expect(Metadata.getParamTypes).toBeCalledWith(Test.prototype, undefined);
- expect(Metadata.setParamTypes).toBeCalledWith(Test.prototype, undefined, [String]);
+ registerProvider({
+ provide: TokenAsync,
+ type: TOKEN_GROUPS,
+ deps: [],
+ async useAsyncFactory() {
+ return {
+ type: "async"
+ };
+ }
+ });
+
+ @Injectable()
+ class MyInjectable {
+ @Inject(TOKEN_GROUPS)
+ instances: InterfaceGroup[];
+ }
+
+ const injector = new InjectorService();
+
+ await injector.load();
+
+ const instance = await injector.invoke(MyInjectable);
+
+ expect(instance.instances).toBeInstanceOf(Array);
+ expect(instance.instances).toHaveLength(3);
+ expect(instance.instances[0].type).toEqual("service1");
+ expect(instance.instances[1].type).toEqual("service2");
+ expect(instance.instances[2].type).toEqual("async");
});
});
- describe("used on method/params", () => {
- beforeAll(() => {
- jest.spyOn(Metadata, "getParamTypes").mockReturnValue([]);
- jest.spyOn(Metadata, "setParamTypes").mockReturnValue(undefined);
- });
- afterAll(() => {
- jest.resetAllMocks();
+ describe("@constructorParameters", () => {
+ describe("when token is given on constructor", () => {
+ it("should inject the expected provider", async () => {
+ @Injectable()
+ class MyInjectable {
+ constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
+ }
+
+ const injector = new InjectorService();
+ const instance = await injector.invoke(MyInjectable);
+
+ expect(instance.injector).toBeInstanceOf(InjectorService);
+ });
});
- it("should call Metadata.getParamTypes()", () => {
- // GIVEN
- class Test {
- test() {}
- }
+ describe("when a group token is given on constructor", () => {
+ it("should inject the expected provider", async () => {
+ const TOKEN_GROUPS = Symbol.for("groups:2");
- // WHEN
- Inject(String)(Test.prototype, "propertyKey", 0);
+ interface InterfaceGroup {
+ type: string;
+ }
- // THEN
- expect(Metadata.getParamTypes).toBeCalledWith(Test.prototype, "propertyKey");
- expect(Metadata.setParamTypes).toBeCalledWith(Test.prototype, "propertyKey", [String]);
+ @Injectable({
+ type: TOKEN_GROUPS
+ })
+ class MyService1 implements InterfaceGroup {
+ readonly type: string = "service1";
+
+ constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
+ }
+
+ @Injectable({
+ type: TOKEN_GROUPS
+ })
+ class MyService2 implements InterfaceGroup {
+ readonly type: string = "service2";
+
+ constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
+ }
+
+ const TokenAsync = Symbol.for("MyService1");
+
+ registerProvider({
+ provide: TokenAsync,
+ type: TOKEN_GROUPS,
+ deps: [],
+ async useAsyncFactory() {
+ return {
+ type: "async"
+ };
+ }
+ });
+
+ @Injectable()
+ class MyInjectable {
+ constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {}
+ }
+
+ const injector = new InjectorService();
+
+ await injector.load();
+
+ const instance = await injector.invoke(MyInjectable);
+
+ expect(instance.instances).toBeInstanceOf(Array);
+ expect(instance.instances).toHaveLength(3);
+ expect(instance.instances[0].type).toEqual("service1");
+ expect(instance.instances[1].type).toEqual("service2");
+ expect(instance.instances[2].type).toEqual("async");
+ });
});
});
});
diff --git a/packages/di/src/decorators/inject.ts b/packages/di/src/decorators/inject.ts
index d0f9738e3a7..438a9a0d93b 100644
--- a/packages/di/src/decorators/inject.ts
+++ b/packages/di/src/decorators/inject.ts
@@ -2,6 +2,7 @@ import {decoratorTypeOf, DecoratorTypes, isPromise, Metadata, Store, Unsupported
import {DI_PARAM_OPTIONS, INJECTABLE_PROP} from "../constants/constants";
import {InvalidPropertyTokenError} from "../errors/InvalidPropertyTokenError";
import type {InjectablePropertyOptions} from "../interfaces/InjectableProperties";
+import {TokenProvider} from "../interfaces/TokenProvider";
import {getContext} from "../utils/asyncHookContext";
export function injectProperty(target: any, propertyKey: string, options: Partial) {
@@ -27,28 +28,30 @@ export function injectProperty(target: any, propertyKey: string, options: Partia
* }
* ```
*
- * @param symbol
+ * @param token A token provider or token provider group
* @param onGet Use the given name method to inject
* @returns {Function}
* @decorator
*/
-export function Inject(symbol?: any, onGet = (bean: any) => bean): Function {
+export function Inject(token?: TokenProvider | (() => TokenProvider), onGet = (bean: any) => bean): Function {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor | number): any | void => {
const bindingType = decoratorTypeOf([target, propertyKey, descriptor]);
switch (bindingType) {
- case DecoratorTypes.PARAM:
case DecoratorTypes.PARAM_CTOR:
- if (symbol) {
+ if (token) {
const paramTypes = Metadata.getParamTypes(target, propertyKey);
+ const type = paramTypes[descriptor as number];
+
+ paramTypes[descriptor as number] = type === Array ? [token] : token;
- paramTypes[descriptor as number] = symbol;
Metadata.setParamTypes(target, propertyKey, paramTypes);
}
break;
case DecoratorTypes.PROP:
- const useType = symbol || Metadata.getType(target, propertyKey);
+ const useType = token || Metadata.getType(target, propertyKey);
+ const originalType = Metadata.getType(target, propertyKey);
if (useType === Object) {
throw new InvalidPropertyTokenError(target, propertyKey);
@@ -58,6 +61,25 @@ export function Inject(symbol?: any, onGet = (bean: any) => bean): Function {
resolver(injector, locals, {options, ...invokeOptions}) {
locals.set(DI_PARAM_OPTIONS, {...options});
+ if (originalType === Array) {
+ let bean: any[] | undefined;
+
+ if (!bean) {
+ bean = injector.getMany(token, locals, invokeOptions);
+ locals.delete(DI_PARAM_OPTIONS);
+ }
+
+ bean.forEach((instance: any, index) => {
+ if (isPromise(bean)) {
+ instance.then((result: any) => {
+ bean![index] = result;
+ });
+ }
+ });
+
+ return () => onGet(bean);
+ }
+
let bean: any;
if (!bean) {
@@ -76,16 +98,6 @@ export function Inject(symbol?: any, onGet = (bean: any) => bean): Function {
});
break;
- case DecoratorTypes.METHOD:
- Store.from(target).merge(INJECTABLE_PROP, {
- [propertyKey]: {
- bindingType,
- propertyKey
- }
- });
-
- return descriptor;
-
default:
throw new UnsupportedDecoratorType(Inject, [target, propertyKey, descriptor]);
}
diff --git a/packages/di/src/domain/Provider.ts b/packages/di/src/domain/Provider.ts
index 6b96536ce02..2299d998a75 100644
--- a/packages/di/src/domain/Provider.ts
+++ b/packages/di/src/domain/Provider.ts
@@ -7,7 +7,10 @@ import {ProviderType} from "./ProviderType";
export type ProviderHookCallback = (instance: T, ...args: any[]) => Promise | void;
export class Provider implements ProviderOpts {
- public type: ProviderType | any = ProviderType.PROVIDER;
+ /**
+ * Token group provider to retrieve all provider from the same type
+ */
+ public type: TokenProvider | ProviderType = ProviderType.PROVIDER;
public deps: TokenProvider[];
public imports: any[];
public useFactory: Function;
diff --git a/packages/di/src/interfaces/ProviderOpts.ts b/packages/di/src/interfaces/ProviderOpts.ts
index 7abb551b7b9..3a475708dd9 100644
--- a/packages/di/src/interfaces/ProviderOpts.ts
+++ b/packages/di/src/interfaces/ProviderOpts.ts
@@ -1,7 +1,7 @@
import type {Type} from "@tsed/core";
+import type {ProviderType} from "../domain/ProviderType";
import type {DIResolver} from "./DIResolver";
import type {ProviderScope} from "../domain/ProviderScope";
-import type {ProviderType} from "../domain/ProviderType";
import type {TokenProvider} from "./TokenProvider";
export interface ProviderOpts {
@@ -12,7 +12,7 @@ export interface ProviderOpts {
/**
* Provider type
*/
- type?: ProviderType | string;
+ type?: TokenProvider | ProviderType;
/**
* Instance build by the injector
*/
diff --git a/packages/di/src/services/InjectorService.spec.ts b/packages/di/src/services/InjectorService.spec.ts
index 59c90095582..70fddbfbeab 100644
--- a/packages/di/src/services/InjectorService.spec.ts
+++ b/packages/di/src/services/InjectorService.spec.ts
@@ -23,15 +23,6 @@ class Test {
constructor() {}
- @Inject()
- test(injectorService: InjectorService) {
- return injectorService;
- }
-
- test2(@Inject() injectorService: InjectorService) {
- return injectorService;
- }
-
test3(o: any) {
return o + " called ";
}
@@ -57,7 +48,7 @@ describe("InjectorService", () => {
expect(new InjectorService().get(Test)).toBeUndefined();
});
});
- describe("getAll()", () => {
+ describe("getMany()", () => {
it("should return all instance", () => {
const injector = new InjectorService();
injector.addProvider("token", {
@@ -65,8 +56,8 @@ describe("InjectorService", () => {
useValue: 1
});
- expect(!!injector.getAll(ProviderType.VALUE).length).toEqual(true);
- expect(!!injector.getAll(ProviderType.FACTORY).length).toEqual(false);
+ expect(!!injector.getMany(ProviderType.VALUE).length).toEqual(true);
+ expect(!!injector.getMany(ProviderType.FACTORY).length).toEqual(false);
});
});
@@ -609,7 +600,6 @@ describe("InjectorService", () => {
const injector = new InjectorService();
const instance = new TestBind();
- jest.spyOn(injector as any, "bindMethod").mockReturnValue(undefined);
jest.spyOn(injector as any, "bindProperty").mockReturnValue(undefined);
jest.spyOn(injector as any, "bindConstant").mockReturnValue(undefined);
jest.spyOn(injector as any, "bindValue").mockReturnValue(undefined);
@@ -639,7 +629,6 @@ describe("InjectorService", () => {
injector.bindInjectableProperties(instance, new LocalsContainer(), {});
// THEN
- expect(injector.bindMethod).toBeCalledWith(instance, injectableProperties.testMethod);
expect(injector.bindProperty).toBeCalledWith(instance, injectableProperties.testProp, new LocalsContainer(), {});
expect(injector.bindConstant).toBeCalledWith(instance, injectableProperties.testConst);
expect(injector.bindValue).toBeCalledWith(instance, injectableProperties.testValue);
@@ -647,26 +636,6 @@ describe("InjectorService", () => {
});
});
- describe("bindMethod()", () => {
- it("should bind the method", () => {
- // GIVEN
- const injector = new InjectorService();
- const instance = new Test();
-
- const spyTest2 = jest.spyOn(instance, "test2");
- jest.spyOn(injector, "get");
-
- // WHEN
- injector.bindMethod(instance, {bindingType: "method", propertyKey: "test2"} as any);
- const result = (instance as any).test2();
-
- // THEN
- expect(spyTest2).toBeCalledWith(injector);
- expect(injector.get).toBeCalledWith(InjectorService);
- expect(result).toEqual(injector);
- });
- });
-
describe("bindProperty()", () => {
it("should bind the method", () => {
// GIVEN
@@ -771,7 +740,6 @@ describe("InjectorService", () => {
await injector.load(container);
const instance = new Test();
- const originalMethod = instance["test"];
jest.spyOn(injector, "get");
@@ -785,7 +753,6 @@ describe("InjectorService", () => {
const result = (instance as any).test3("test");
// THEN
- expect(expect(originalMethod)).not.toEqual(instance.test3);
expect(injector.get).toBeCalledWith(InterceptorTest);
expect(result).toEqual("test called intercepted");
diff --git a/packages/di/src/services/InjectorService.ts b/packages/di/src/services/InjectorService.ts
index 17eedcb1eea..9e347781aaa 100644
--- a/packages/di/src/services/InjectorService.ts
+++ b/packages/di/src/services/InjectorService.ts
@@ -11,7 +11,8 @@ import {
Metadata,
nameOf,
prototypeOf,
- Store
+ Store,
+ isArray
} from "@tsed/core";
import {DI_PARAM_OPTIONS, INJECTABLE_PROP} from "../constants/constants";
import {Configuration} from "../decorators/configuration";
@@ -138,11 +139,11 @@ export class InjectorService extends Container {
/**
* Return all instance of the same provider type
* @param type
+ * @param locals
+ * @param options
*/
- getAll(type: string) {
- return this.getProviders(type).map((provider) => {
- return this.get(provider.token);
- });
+ getMany(type: string, locals?: LocalsContainer, options?: Partial>): Type[] {
+ return this.getProviders(type).map((provider) => this.invoke(provider.token, locals, options)!);
}
/**
@@ -366,9 +367,6 @@ export class InjectorService extends Container {
Object.values(properties).forEach((definition) => {
switch (definition.bindingType) {
- case InjectablePropertyType.METHOD:
- this.bindMethod(instance, definition);
- break;
case InjectablePropertyType.PROPERTY:
this.bindProperty(instance, definition, locals, options);
break;
@@ -385,22 +383,6 @@ export class InjectorService extends Container {
});
}
- /**
- * @param instance
- * @param {string} propertyKey
- */
- public bindMethod(instance: any, {propertyKey}: InjectablePropertyOptions) {
- const target = classOf(instance);
- const originalMethod = instance[propertyKey];
- const deps = Metadata.getParamTypes(prototypeOf(target), propertyKey);
-
- instance[propertyKey] = () => {
- const services = deps.map((dependency: any) => this.get(dependency));
-
- return originalMethod.call(instance, ...services);
- };
- }
-
/**
* Create an injectable property.
*
@@ -608,7 +590,7 @@ export class InjectorService extends Container {
try {
const invokeDependency =
(parent?: any) =>
- (token: any, index: number): any => {
+ (token: TokenProvider | [TokenProvider], index: number): any => {
currentDependency = {token, index, deps};
if (token !== DI_PARAM_OPTIONS) {
@@ -617,6 +599,10 @@ export class InjectorService extends Container {
locals.set(DI_PARAM_OPTIONS, options || {});
}
+ if (isArray(token)) {
+ return this.getMany(token[0], locals, options);
+ }
+
return isInheritedFrom(token, Provider, 1) ? provider : this.invoke(token, locals, {parent});
};
@@ -686,9 +672,9 @@ export class InjectorService extends Container {
if (provider.useValue !== undefined) {
construct = () => (isFunction(provider.useValue) ? provider.useValue() : provider.useValue);
} else if (provider.useFactory) {
- construct = (deps: TokenProvider[]) => provider.useFactory(...deps);
+ construct = (deps: any[]) => provider.useFactory(...deps);
} else if (provider.useAsyncFactory) {
- construct = async (deps: TokenProvider[]) => {
+ construct = async (deps: any[]) => {
deps = await Promise.all(deps);
return provider.useAsyncFactory(...deps);
};