From 1d10ac543136573651ce7b0568f8b11a6d1e09c7 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 6 Oct 2024 11:12:39 +0200 Subject: [PATCH 1/6] feat(di): add $injector/injector to create on the fly or get the injector every where --- .../common/decorators/autoInjectable.spec.ts | 18 ------------- .../src/common/decorators/autoInjectable.ts | 4 +-- packages/di/src/common/decorators/constant.ts | 4 +-- packages/di/src/common/decorators/inject.ts | 5 ++-- .../di/src/common/decorators/lazyInject.ts | 5 ++-- packages/di/src/common/decorators/value.ts | 6 ++--- packages/di/src/common/index.ts | 1 + .../di/src/common/services/InjectorService.ts | 3 ++- packages/di/src/common/utils/injector.ts | 25 +++++++++++++++++++ .../di/src/node/decorators/injectContext.ts | 6 ++--- .../common/src/utils/createInjector.ts | 4 +-- .../src/builder/PlatformServerless.ts | 4 +-- 12 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 packages/di/src/common/utils/injector.ts diff --git a/packages/di/src/common/decorators/autoInjectable.spec.ts b/packages/di/src/common/decorators/autoInjectable.spec.ts index 8b7e827e911..bb4d49c46c0 100644 --- a/packages/di/src/common/decorators/autoInjectable.spec.ts +++ b/packages/di/src/common/decorators/autoInjectable.spec.ts @@ -134,22 +134,4 @@ describe("AutoInjectable", () => { new Test(["item1", "item2", "item3"], "group"); }); }); - describe("when the instance is created outside of an injection context", () => { - it("should throw an error", () => { - @AutoInjectable() - class Test { - @Inject(Logger) - logger: Logger; - - foo() { - this.logger.info("test"); - } - } - - const error = catchError(() => new Test()); - - expect(error).toBeInstanceOf(Error); - expect(error?.message).toEqual("InjectorService instance is not created yet."); - }); - }); }); diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index 5473ff23eb2..69e107fb7e2 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -2,12 +2,12 @@ import {isArray, type Type} from "@tsed/core"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; -import {InjectorService} from "../services/InjectorService.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; +import {$injector} from "../utils/injector.js"; function resolveAutoInjectableArgs(token: Type, args: unknown[]) { + const injector = $injector(); const locals = new LocalsContainer(); - const injector = InjectorService.getInstance(); const deps: TokenProvider[] = getConstructorDependencies(token); const list: any[] = []; const length = Math.max(deps.length, args.length); diff --git a/packages/di/src/common/decorators/constant.ts b/packages/di/src/common/decorators/constant.ts index 28f151f95c3..b8489938a17 100644 --- a/packages/di/src/common/decorators/constant.ts +++ b/packages/di/src/common/decorators/constant.ts @@ -1,11 +1,11 @@ import {catchError, deepClone} from "@tsed/core"; -import {InjectorService} from "../services/InjectorService.js"; +import {injector as $injector} from "../utils/injector.js"; export function constant(expression: string): Type | undefined; export function constant(expression: string, defaultValue: Type | undefined): Type; export function constant(expression: string, defaultValue?: Type | undefined): Type | undefined { - return InjectorService.getInstance().settings.get(expression, defaultValue); + return $injector().settings.get(expression, defaultValue); } export function bindConstant(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { diff --git a/packages/di/src/common/decorators/inject.ts b/packages/di/src/common/decorators/inject.ts index 5bb8c1c7c17..6fa11b70031 100644 --- a/packages/di/src/common/decorators/inject.ts +++ b/packages/di/src/common/decorators/inject.ts @@ -6,13 +6,14 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; import {InjectorService} from "../services/InjectorService.js"; import {getConstructorDependencies, setConstructorDependencies} from "../utils/getConstructorDependencies.js"; +import {$injector} from "../utils/injector.js"; export function inject(token: TokenProvider, opts?: Partial>): T { - return InjectorService.getInstance().invoke(token, opts?.locals || InjectorService.getLocals(), opts); + return $injector().invoke(token, opts?.locals || InjectorService.getLocals(), opts); } export function injectMany(token: string | symbol, opts?: Partial>): T[] { - return InjectorService.getInstance().getMany(token, opts?.locals || InjectorService.getLocals(), opts); + return $injector().getMany(token, opts?.locals || InjectorService.getLocals(), opts); } function setToken( diff --git a/packages/di/src/common/decorators/lazyInject.ts b/packages/di/src/common/decorators/lazyInject.ts index 22ae5aacac2..2d6523dc383 100644 --- a/packages/di/src/common/decorators/lazyInject.ts +++ b/packages/di/src/common/decorators/lazyInject.ts @@ -1,6 +1,6 @@ import {catchError, importPackage} from "@tsed/core"; -import {InjectorService} from "../services/InjectorService.js"; +import {$injector} from "../utils/injector.js"; /** * Lazy load a provider from his package and invoke only when the provider is used @@ -34,7 +34,6 @@ export function LazyInject( Reflect.defineProperty(target, propertyKey, { async get() { if (!token || !bean) { - const injector = InjectorService.getInstance(); const exports = await importPackage(packageName, resolver, optional); token = exports[key]; if (!token) { @@ -43,7 +42,7 @@ export function LazyInject( } } - bean = token ? await injector.lazyInvoke(token) : {}; + bean = token ? await $injector().lazyInvoke(token) : {}; } return bean; diff --git a/packages/di/src/common/decorators/value.ts b/packages/di/src/common/decorators/value.ts index 2654cbb856b..dd3b001b389 100644 --- a/packages/di/src/common/decorators/value.ts +++ b/packages/di/src/common/decorators/value.ts @@ -1,14 +1,14 @@ import {catchError} from "@tsed/core"; -import {InjectorService} from "../services/InjectorService.js"; +import {$injector} from "../utils/injector.js"; export function bindValue(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { const descriptor = { get() { - return InjectorService.getInstance().settings.get(expression, defaultValue); + return $injector().settings.get(expression, defaultValue); }, set(value: unknown) { - InjectorService.getInstance().settings.set(expression, value); + $injector().settings.set(expression, value); }, enumerable: true, configurable: true diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index c3245ae1bb2..4ebc0e0e998 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -51,4 +51,5 @@ export * from "./utils/colors.js"; export * from "./utils/createContainer.js"; export * from "./utils/getConfiguration.js"; export * from "./utils/getConstructorDependencies.js"; +export * from "./utils/injector.js"; export * from "./utils/resolveControllers.js"; diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 9a17809c93f..7510cefd6cd 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -85,8 +85,9 @@ export class InjectorService extends Container { */ static getInstance() { if (!globalInjector) { - throw new Error("InjectorService instance is not created yet."); + return new InjectorService(); } + return globalInjector; } diff --git a/packages/di/src/common/utils/injector.ts b/packages/di/src/common/utils/injector.ts new file mode 100644 index 00000000000..2dedd2a82b5 --- /dev/null +++ b/packages/di/src/common/utils/injector.ts @@ -0,0 +1,25 @@ +import {InjectorService} from "../services/InjectorService.js"; + +/** + * Create or return the existing injector service. + * + * Example: + * + * ```typescript + * import {injector, Injectable} from "@tsed/di"; + * + * @Injectable() + * class MyService { + * injector = injector(); + * } + * ``` + */ +export function injector(): InjectorService { + return InjectorService.getInstance(); +} + +/** + * Alias of injector + * @alias injector + */ +export const $injector = injector; diff --git a/packages/di/src/node/decorators/injectContext.ts b/packages/di/src/node/decorators/injectContext.ts index d483ef0b2c8..90a08ecf9b7 100644 --- a/packages/di/src/node/decorators/injectContext.ts +++ b/packages/di/src/node/decorators/injectContext.ts @@ -1,6 +1,6 @@ import {catchError} from "@tsed/core"; -import {InjectorService} from "../../common/index.js"; +import {$injector} from "../../common/index.js"; import {DIContext} from "../domain/DIContext.js"; import {getContext} from "../utils/asyncHookContext.js"; @@ -27,8 +27,8 @@ export function InjectContext(transform: ($ctx: DIContext) => unknown = (o) => o getContext() || new DIContext({ id: "", - logger: InjectorService.getInstance().logger, - injector: InjectorService.getInstance(), + logger: $injector().logger, + injector: $injector(), maxStackSize: 0 }) ); diff --git a/packages/platform/common/src/utils/createInjector.ts b/packages/platform/common/src/utils/createInjector.ts index 6ad851187c4..ed6966025c4 100644 --- a/packages/platform/common/src/utils/createInjector.ts +++ b/packages/platform/common/src/utils/createInjector.ts @@ -1,5 +1,5 @@ import {toMap, Type} from "@tsed/core"; -import {InjectorService, ProviderOpts, setLoggerConfiguration} from "@tsed/di"; +import {$injector, ProviderOpts, setLoggerConfiguration} from "@tsed/di"; import {$log} from "@tsed/logger"; import {PlatformConfiguration} from "../config/services/PlatformConfiguration.js"; @@ -26,7 +26,7 @@ interface CreateInjectorOptions { } export function createInjector({adapter, settings = {}}: CreateInjectorOptions) { - const injector = new InjectorService(); + const injector = $injector(); injector.addProvider(PlatformConfiguration); injector.settings = injector.invoke(PlatformConfiguration); diff --git a/packages/platform/platform-serverless/src/builder/PlatformServerless.ts b/packages/platform/platform-serverless/src/builder/PlatformServerless.ts index e9f8fb6a4d9..ecd50f7bf9d 100644 --- a/packages/platform/platform-serverless/src/builder/PlatformServerless.ts +++ b/packages/platform/platform-serverless/src/builder/PlatformServerless.ts @@ -1,5 +1,5 @@ import {Env, Type} from "@tsed/core"; -import {createContainer, InjectorService, setLoggerConfiguration} from "@tsed/di"; +import {createContainer, injector, InjectorService, setLoggerConfiguration} from "@tsed/di"; import {$log, Logger} from "@tsed/logger"; import {getOperationsRoutes, JsonEntityStore} from "@tsed/schema"; import type {Handler} from "aws-lambda"; @@ -184,7 +184,7 @@ export class PlatformServerless { } protected createInjector(settings: any) { - this._injector = new InjectorService(); + this._injector = injector(); this.injector.logger = $log; this.injector.settings.set(settings); From 35cdff0a77739a71e5e13b525e7ac63fd5c41c28 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Mon, 7 Oct 2024 09:48:06 +0200 Subject: [PATCH 2/6] refactor(di): reorganise di tree directory to isolate new functional di API --- .../src/common/decorators/autoInjectable.ts | 2 +- .../di/src/common/decorators/constant.spec.ts | 108 ++--- packages/di/src/common/decorators/constant.ts | 8 +- .../di/src/common/decorators/inject.spec.ts | 399 ++++++++---------- packages/di/src/common/decorators/inject.ts | 12 +- .../di/src/common/decorators/injectable.ts | 4 +- .../di/src/common/decorators/intercept.ts | 2 +- .../di/src/common/decorators/lazyInject.ts | 2 +- packages/di/src/common/decorators/value.ts | 2 +- packages/di/src/common/fn/constant.spec.ts | 37 ++ packages/di/src/common/fn/constant.ts | 7 + packages/di/src/common/fn/inject.spec.ts | 29 ++ packages/di/src/common/fn/inject.ts | 8 + packages/di/src/common/fn/injectMany.ts | 7 + packages/di/src/common/fn/injectable.ts | 6 + .../di/src/common/{utils => fn}/injector.ts | 0 packages/di/src/common/index.ts | 6 +- .../di/src/node/decorators/injectContext.ts | 13 +- packages/di/src/node/fn/context.ts | 24 ++ packages/di/src/node/fn/logger.spec.ts | 46 ++ packages/di/src/node/fn/logger.ts | 17 + packages/di/src/node/index.ts | 2 + .../di/src/node/services/DILogger.spec.ts | 19 +- packages/di/src/node/services/DILogger.ts | 10 +- .../src/node/utils/asyncHookContext.spec.ts | 8 +- .../di/src/node/utils/asyncHookContext.ts | 4 +- 26 files changed, 446 insertions(+), 336 deletions(-) create mode 100644 packages/di/src/common/fn/constant.spec.ts create mode 100644 packages/di/src/common/fn/constant.ts create mode 100644 packages/di/src/common/fn/inject.spec.ts create mode 100644 packages/di/src/common/fn/inject.ts create mode 100644 packages/di/src/common/fn/injectMany.ts create mode 100644 packages/di/src/common/fn/injectable.ts rename packages/di/src/common/{utils => fn}/injector.ts (100%) create mode 100644 packages/di/src/node/fn/context.ts create mode 100644 packages/di/src/node/fn/logger.spec.ts create mode 100644 packages/di/src/node/fn/logger.ts diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index 69e107fb7e2..4fd7456ff8e 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -1,9 +1,9 @@ import {isArray, type Type} from "@tsed/core"; import {LocalsContainer} from "../domain/LocalsContainer.js"; +import {$injector} from "../fn/injector.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; -import {$injector} from "../utils/injector.js"; function resolveAutoInjectableArgs(token: Type, args: unknown[]) { const injector = $injector(); diff --git a/packages/di/src/common/decorators/constant.spec.ts b/packages/di/src/common/decorators/constant.spec.ts index ab8df90c51c..21cc5d55d7e 100644 --- a/packages/di/src/common/decorators/constant.spec.ts +++ b/packages/di/src/common/decorators/constant.spec.ts @@ -1,5 +1,5 @@ import {DITest} from "../../node/index.js"; -import {Constant, constant} from "./constant.js"; +import {Constant} from "./constant.js"; describe("@Constant()", () => { beforeEach(() => @@ -10,86 +10,58 @@ describe("@Constant()", () => { }) ); afterEach(() => DITest.reset()); - describe("when decorator is used as property decorator", () => { - it("should create a getter", async () => { - // WHEN - class Test { - @Constant("logger.level", "default value") - test: string; - } - - // THEN + it("should create a getter", async () => { + // WHEN + class Test { + @Constant("logger.level", "default value") + test: string; + } - const test = await DITest.invoke(Test); - - expect(test.test).toEqual("off"); - }); - it("should create a getter with default value", async () => { - // WHEN - class Test { - @Constant("logger.test", "default value") - test: string; - } + // THEN - // THEN + const test = await DITest.invoke(Test); - const test = await DITest.invoke(Test); - - expect(test.test).toEqual("default value"); - }); - it("shouldn't be possible to modify injected value from injector.settings", async () => { - // WHEN - class Test { - @Constant("logger.level") - test: string; - } + expect(test.test).toEqual("off"); + }); + it("should create a getter with default value", async () => { + // WHEN + class Test { + @Constant("logger.test", "default value") + test: string; + } - // THEN + // THEN - const test = await DITest.invoke(Test); + const test = await DITest.invoke(Test); - test.test = "new value"; + expect(test.test).toEqual("default value"); + }); + it("shouldn't be possible to modify injected value from injector.settings", async () => { + // WHEN + class Test { + @Constant("logger.level") + test: string; + } - expect(test.test).toEqual("off"); - }); - it("should create a getter with native default value", async () => { - // WHEN - class Test { - @Constant("logger.test") - test: string = "default prop"; - } + // THEN - // THEN + const test = await DITest.invoke(Test); - const test = await DITest.invoke(Test); + test.test = "new value"; - expect(test.test).toEqual("default prop"); - }); + expect(test.test).toEqual("off"); }); - describe("when constant is used as default value initializer", () => { - it("should inject constant to the property", async () => { - // WHEN - class Test { - test: string = constant("logger.level", "default value"); - } - - // THEN - - const test = await DITest.invoke(Test); - - expect(test.test).toEqual("off"); - }); - it("should return the default value if expression is undefined", async () => { - // WHEN - class Test { - test: string = constant("logger.test", "default value"); - } + it("should create a getter with native default value", async () => { + // WHEN + class Test { + @Constant("logger.test") + test: string = "default prop"; + } - // THEN + // THEN - const test = await DITest.invoke(Test); + const test = await DITest.invoke(Test); - expect(test.test).toEqual("default value"); - }); + expect(test.test).toEqual("default prop"); }); }); diff --git a/packages/di/src/common/decorators/constant.ts b/packages/di/src/common/decorators/constant.ts index b8489938a17..b1efa183448 100644 --- a/packages/di/src/common/decorators/constant.ts +++ b/packages/di/src/common/decorators/constant.ts @@ -1,12 +1,6 @@ import {catchError, deepClone} from "@tsed/core"; -import {injector as $injector} from "../utils/injector.js"; - -export function constant(expression: string): Type | undefined; -export function constant(expression: string, defaultValue: Type | undefined): Type; -export function constant(expression: string, defaultValue?: Type | undefined): Type | undefined { - return $injector().settings.get(expression, defaultValue); -} +import {constant} from "../fn/constant.js"; export function bindConstant(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { const symbol = Symbol(); diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index c720becbdb1..2b7b7445ccc 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -3,121 +3,210 @@ import {catchAsyncError} from "@tsed/core"; import {DITest} from "../../node/index.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "../services/InjectorService.js"; -import {Inject, inject} from "./inject.js"; +import {Inject} from "./inject.js"; import {Injectable} from "./injectable.js"; -describe("inject", () => { +describe("@Inject()", () => { beforeEach(() => DITest.create()); afterEach(() => DITest.reset()); - describe("inject()", () => { - describe("when inject function is used on a property", () => { - it("should inject the expected provider", async () => { - class Nested {} - class Test { - readonly injector = inject(InjectorService); - readonly nested = inject(Nested); + describe("when the decorator used on property", () => { + it("should inject service", async () => { + // GIVEN + @Injectable() + class Test { + @Inject() + test: InjectorService; + } - constructor() { - expect(this.injector).toBeInstanceOf(InjectorService); - expect(this.nested).not.toBeInstanceOf(Nested); - } - } + const injector = new InjectorService(); + const instance = await injector.invoke(Test); - await DITest.invoke(Test, [ - { - token: Nested, - use: {} - } - ]); - }); + expect(instance).toBeInstanceOf(Test); + expect(instance.test).toBeInstanceOf(InjectorService); }); - }); - describe("@Inject()", () => { - describe("when the decorator used on property", () => { - it("should inject service", async () => { - // GIVEN - @Injectable() - class Test { - @Inject() - test: InjectorService; + it("should inject service and async factory", async () => { + // GIVEN + class Test { + constructor(public type: string) {} + } + + const TokenAsync = Symbol.for("MyService"); + + registerProvider({ + provide: TokenAsync, + type: "test:async", + deps: [], + useAsyncFactory() { + return Promise.resolve(new Test("async")); } + }); - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + @Injectable() + class Parent1 { + @Inject(TokenAsync) + test: Test; + } - expect(instance).toBeInstanceOf(Test); - expect(instance.test).toBeInstanceOf(InjectorService); - }); - it("should inject service and async factory", async () => { - // GIVEN - class Test { - constructor(public type: string) {} - } + @Injectable() + class Parent2 { + @Inject(TokenAsync) + test: Test; + } - const TokenAsync = Symbol.for("MyService"); + const injector = new InjectorService(); - registerProvider({ - provide: TokenAsync, - type: "test:async", - deps: [], - useAsyncFactory() { - return Promise.resolve(new Test("async")); - } - }); + await injector.load(); - @Injectable() - class Parent1 { - @Inject(TokenAsync) - test: Test; - } + const parent1 = await injector.invoke(Parent1); + const parent2 = await injector.invoke(Parent2); - @Injectable() - class Parent2 { - @Inject(TokenAsync) - test: Test; + expect(parent1.test).toBeInstanceOf(Test); + expect(parent2.test).toBeInstanceOf(Test); + }); + it("should inject service and use onGet option to transform injected service", async () => { + // GIVEN + @Injectable() + class Test { + @Inject(InjectorService, {transform: (instance) => instance.get(InjectorService)}) + test: InjectorService; + } + + const injector = new InjectorService(); + const instance = await injector.invoke(Test); + + expect(instance).toBeInstanceOf(Test); + expect(instance.test).toBeInstanceOf(InjectorService); + }); + it("should inject service and use onGet option to transform injected service (legacy)", async () => { + // GIVEN + @Injectable() + class Test { + @Inject(InjectorService, (instance) => instance.get(InjectorService)) + test: InjectorService; + } + + const injector = new InjectorService(); + const instance = await injector.invoke(Test); + + expect(instance).toBeInstanceOf(Test); + expect(instance.test).toBeInstanceOf(InjectorService); + }); + it("should inject many services", async () => { + const TOKEN_GROUPS = Symbol.for("groups:1"); + + interface InterfaceGroup { + type: string; + } + + @Injectable({ + type: TOKEN_GROUPS + }) + class MyService1 implements InterfaceGroup { + readonly type: string = "service1"; + + constructor(@Inject(InjectorService) readonly injector: any) {} + } + + @Injectable({ + type: TOKEN_GROUPS + }) + class MyService2 implements InterfaceGroup { + readonly type: string = "service2"; + + constructor(@Inject(InjectorService) readonly injector: any) {} + } + + const TokenAsync = Symbol.for("MyService2"); + + registerProvider({ + provide: TokenAsync, + type: TOKEN_GROUPS, + deps: [], + useAsyncFactory() { + return Promise.resolve({ + type: "async" + }); } + }); - const injector = new InjectorService(); + @Injectable() + class MyInjectable { + @Inject(TOKEN_GROUPS) + instances: InterfaceGroup[]; + } - await injector.load(); + const injector = new InjectorService(); - const parent1 = await injector.invoke(Parent1); - const parent2 = await injector.invoke(Parent2); + await injector.load(); - expect(parent1.test).toBeInstanceOf(Test); - expect(parent2.test).toBeInstanceOf(Test); - }); - it("should inject service and use onGet option to transform injected service", async () => { + 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"); + }); + it("should throw error", () => { + try { // GIVEN @Injectable() class Test { - @Inject(InjectorService, {transform: (instance) => instance.get(InjectorService)}) - test: InjectorService; + @Inject() + test: Object; + } + } catch (er) { + expect(er.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); + } + }); + it("should inject service and use mock", async () => { + @Injectable() + class Nested { + get cache() { + return true; + } + } + + @Injectable() + class Test { + @Inject() + nested: Nested; + } + + const instance = await DITest.invoke(Test, [ + { + token: Nested, + use: { + cache: false + } } + ]); - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + expect(instance.nested.cache).toEqual(false); - expect(instance).toBeInstanceOf(Test); - expect(instance.test).toBeInstanceOf(InjectorService); - }); - it("should inject service and use onGet option to transform injected service (legacy)", async () => { - // GIVEN + const instance2 = await DITest.invoke(Test, []); + expect(instance2.nested.cache).toEqual(true); + }); + }); + describe("when the decorator is used on constructor parameter", () => { + describe("when token is given on constructor", () => { + it("should inject the expected provider", async () => { @Injectable() - class Test { - @Inject(InjectorService, (instance) => instance.get(InjectorService)) - test: InjectorService; + class MyInjectable { + constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } const injector = new InjectorService(); - const instance = await injector.invoke(Test); + const instance = await injector.invoke(MyInjectable); - expect(instance).toBeInstanceOf(Test); - expect(instance.test).toBeInstanceOf(InjectorService); + expect(instance.injector).toBeInstanceOf(InjectorService); }); - it("should inject many services", async () => { - const TOKEN_GROUPS = Symbol.for("groups:1"); + }); + + describe("when a group token is given on constructor", () => { + it("should inject the expected provider", async () => { + const TOKEN_GROUPS = Symbol.for("groups:2"); interface InterfaceGroup { type: string; @@ -129,7 +218,7 @@ describe("inject", () => { class MyService1 implements InterfaceGroup { readonly type: string = "service1"; - constructor(@Inject(InjectorService) readonly injector: any) {} + constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } @Injectable({ @@ -138,10 +227,10 @@ describe("inject", () => { class MyService2 implements InterfaceGroup { readonly type: string = "service2"; - constructor(@Inject(InjectorService) readonly injector: any) {} + constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } - const TokenAsync = Symbol.for("MyService2"); + const TokenAsync = Symbol.for("MyService1"); registerProvider({ provide: TokenAsync, @@ -156,8 +245,7 @@ describe("inject", () => { @Injectable() class MyInjectable { - @Inject(TOKEN_GROUPS) - instances: InterfaceGroup[]; + constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} } const injector = new InjectorService(); @@ -172,135 +260,22 @@ describe("inject", () => { expect(instance.instances[1].type).toEqual("service2"); expect(instance.instances[2].type).toEqual("async"); }); - it("should throw error", () => { - try { - // GIVEN - @Injectable() - class Test { - @Inject() - test: Object; - } - } catch (er) { - expect(er.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); - } - }); - it("should inject service and use mock", async () => { - @Injectable() - class Nested { - get cache() { - return true; - } - } - - @Injectable() - class Test { - @Inject() - nested: Nested; - } - - const instance = await DITest.invoke(Test, [ - { - token: Nested, - use: { - cache: false - } - } - ]); - - expect(instance.nested.cache).toEqual(false); - - const instance2 = await DITest.invoke(Test, []); - expect(instance2.nested.cache).toEqual(true); - }); }); - describe("when the decorator is used on constructor parameter", () => { - 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); - }); - }); - - describe("when a group token is given on constructor", () => { - it("should inject the expected provider", async () => { - const TOKEN_GROUPS = Symbol.for("groups:2"); - - interface InterfaceGroup { - type: 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: [], - useAsyncFactory() { - return Promise.resolve({ - type: "async" - }); - } - }); - - @Injectable() - class MyInjectable { - constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} - } - - const injector = new InjectorService(); - - await injector.load(); + }); + describe("when token is Object", () => { + it("should throw error", async () => { + class Test { + @Inject() + test: any; + } - const instance = await injector.invoke(MyInjectable); + const error = await catchAsyncError(async () => { + const instance = await DITest.invoke(Test); - 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"); - }); + return instance.test; }); - }); - describe("when token is Object", () => { - it("should throw error", async () => { - class Test { - @Inject() - test: any; - } - - const error = await catchAsyncError(async () => { - const instance = await DITest.invoke(Test); - return instance.test; - }); - - expect(error?.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); - }); + expect(error?.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); }); }); }); diff --git a/packages/di/src/common/decorators/inject.ts b/packages/di/src/common/decorators/inject.ts index 6fa11b70031..e8037352560 100644 --- a/packages/di/src/common/decorators/inject.ts +++ b/packages/di/src/common/decorators/inject.ts @@ -2,19 +2,13 @@ import {catchError, decoratorTypeOf, DecoratorTypes, isPromise, Metadata, Store, import {DI_INJECTABLE_PROPS, DI_INVOKE_OPTIONS, DI_USE_OPTIONS} from "../constants/constants.js"; import {InvalidPropertyTokenError} from "../errors/InvalidPropertyTokenError.js"; +import {inject} from "../fn/inject.js"; +import {injectMany} from "../fn/injectMany.js"; +import {$injector} from "../fn/injector.js"; import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; import {InjectorService} from "../services/InjectorService.js"; import {getConstructorDependencies, setConstructorDependencies} from "../utils/getConstructorDependencies.js"; -import {$injector} from "../utils/injector.js"; - -export function inject(token: TokenProvider, opts?: Partial>): T { - return $injector().invoke(token, opts?.locals || InjectorService.getLocals(), opts); -} - -export function injectMany(token: string | symbol, opts?: Partial>): T[] { - return $injector().getMany(token, opts?.locals || InjectorService.getLocals(), opts); -} function setToken( token: TokenProvider, diff --git a/packages/di/src/common/decorators/injectable.ts b/packages/di/src/common/decorators/injectable.ts index a9a462273e3..9ab97cf1566 100644 --- a/packages/di/src/common/decorators/injectable.ts +++ b/packages/di/src/common/decorators/injectable.ts @@ -1,5 +1,5 @@ +import {injectable} from "../fn/injectable.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; -import {registerProvider} from "../registries/ProviderRegistry.js"; /** * The decorators `@Injectable()` declare a new service can be injected in other service, controller, interceptor, etc.. on there `constructor`. @@ -21,7 +21,7 @@ import {registerProvider} from "../registries/ProviderRegistry.js"; */ export function Injectable(options: Partial = {}): ClassDecorator { return (target: any) => { - registerProvider({ + injectable({ ...options, ...(options.provide ? {useClass: target} : {provide: target}) }); diff --git a/packages/di/src/common/decorators/intercept.ts b/packages/di/src/common/decorators/intercept.ts index 76a7a7dc010..5be2b0790cd 100644 --- a/packages/di/src/common/decorators/intercept.ts +++ b/packages/di/src/common/decorators/intercept.ts @@ -1,11 +1,11 @@ import {classOf, decorateMethodsOf, DecoratorParameters, decoratorTypeOf, DecoratorTypes, Store, Type} from "@tsed/core"; import {DI_INTERCEPTOR_OPTIONS, DI_INVOKE_OPTIONS} from "../constants/constants.js"; +import {inject} from "../fn/inject.js"; import type {InterceptorContext} from "../interfaces/InterceptorContext.js"; import type {InterceptorMethods} from "../interfaces/InterceptorMethods.js"; import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; -import {inject} from "./inject.js"; export function getInterceptorOptions(target: Type, propertyKey: string | symbol) { return Store.fromMethod(target, propertyKey).get(DI_INTERCEPTOR_OPTIONS) as T; diff --git a/packages/di/src/common/decorators/lazyInject.ts b/packages/di/src/common/decorators/lazyInject.ts index 2d6523dc383..c5dcf5dfa7c 100644 --- a/packages/di/src/common/decorators/lazyInject.ts +++ b/packages/di/src/common/decorators/lazyInject.ts @@ -1,6 +1,6 @@ import {catchError, importPackage} from "@tsed/core"; -import {$injector} from "../utils/injector.js"; +import {$injector} from "../fn/injector.js"; /** * Lazy load a provider from his package and invoke only when the provider is used diff --git a/packages/di/src/common/decorators/value.ts b/packages/di/src/common/decorators/value.ts index dd3b001b389..5af0e784492 100644 --- a/packages/di/src/common/decorators/value.ts +++ b/packages/di/src/common/decorators/value.ts @@ -1,6 +1,6 @@ import {catchError} from "@tsed/core"; -import {$injector} from "../utils/injector.js"; +import {$injector} from "../fn/injector.js"; export function bindValue(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { const descriptor = { diff --git a/packages/di/src/common/fn/constant.spec.ts b/packages/di/src/common/fn/constant.spec.ts new file mode 100644 index 00000000000..476fa4116fa --- /dev/null +++ b/packages/di/src/common/fn/constant.spec.ts @@ -0,0 +1,37 @@ +import {DITest} from "../../node/index.js"; +import {constant} from "../fn/constant.js"; + +describe("constant()", () => { + beforeEach(() => + DITest.create({ + logger: { + level: "off" + } + }) + ); + afterEach(() => DITest.reset()); + it("should inject constant to the property", async () => { + // WHEN + class Test { + test: string = constant("logger.level", "default value"); + } + + // THEN + + const test = await DITest.invoke(Test); + + expect(test.test).toEqual("off"); + }); + it("should return the default value if expression is undefined", async () => { + // WHEN + class Test { + test: string = constant("logger.test", "default value"); + } + + // THEN + + const test = await DITest.invoke(Test); + + expect(test.test).toEqual("default value"); + }); +}); diff --git a/packages/di/src/common/fn/constant.ts b/packages/di/src/common/fn/constant.ts new file mode 100644 index 00000000000..22c042acd61 --- /dev/null +++ b/packages/di/src/common/fn/constant.ts @@ -0,0 +1,7 @@ +import {injector as $injector} from "./injector.js"; + +export function constant(expression: string): Type | undefined; +export function constant(expression: string, defaultValue: Type | undefined): Type; +export function constant(expression: string, defaultValue?: Type | undefined): Type | undefined { + return $injector().settings.get(expression, defaultValue); +} diff --git a/packages/di/src/common/fn/inject.spec.ts b/packages/di/src/common/fn/inject.spec.ts new file mode 100644 index 00000000000..46e7e29ab25 --- /dev/null +++ b/packages/di/src/common/fn/inject.spec.ts @@ -0,0 +1,29 @@ +import {DITest} from "../../node/index.js"; +import {InjectorService} from "../services/InjectorService.js"; +import {inject} from "./inject.js"; + +describe("inject()", () => { + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); + + it("should inject the expected provider", async () => { + class Nested {} + + class Test { + readonly injector = inject(InjectorService); + readonly nested = inject(Nested); + + constructor() { + expect(this.injector).toBeInstanceOf(InjectorService); + expect(this.nested).not.toBeInstanceOf(Nested); + } + } + + await DITest.invoke(Test, [ + { + token: Nested, + use: {} + } + ]); + }); +}); diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts new file mode 100644 index 00000000000..a5c5d9ad4e9 --- /dev/null +++ b/packages/di/src/common/fn/inject.ts @@ -0,0 +1,8 @@ +import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; +import {TokenProvider} from "../interfaces/TokenProvider.js"; +import {InjectorService} from "../services/InjectorService.js"; +import {$injector} from "./injector.js"; + +export function inject(token: TokenProvider, opts?: Partial>): T { + return $injector().invoke(token, opts?.locals || InjectorService.getLocals(), opts); +} diff --git a/packages/di/src/common/fn/injectMany.ts b/packages/di/src/common/fn/injectMany.ts new file mode 100644 index 00000000000..ba5cdfb95d9 --- /dev/null +++ b/packages/di/src/common/fn/injectMany.ts @@ -0,0 +1,7 @@ +import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; +import {InjectorService} from "../services/InjectorService.js"; +import {$injector} from "./injector.js"; + +export function injectMany(token: string | symbol, opts?: Partial>): T[] { + return $injector().getMany(token, opts?.locals || InjectorService.getLocals(), opts); +} diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts new file mode 100644 index 00000000000..51929f02ad3 --- /dev/null +++ b/packages/di/src/common/fn/injectable.ts @@ -0,0 +1,6 @@ +import {registerProvider} from "../registries/ProviderRegistry.js"; + +/** + * @alias {registerProvider} Alias of registerProvider + */ +export const injectable = registerProvider; diff --git a/packages/di/src/common/utils/injector.ts b/packages/di/src/common/fn/injector.ts similarity index 100% rename from packages/di/src/common/utils/injector.ts rename to packages/di/src/common/fn/injector.ts diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index 4ebc0e0e998..473d95df7cc 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -27,6 +27,11 @@ export * from "./domain/ProviderScope.js"; export * from "./domain/ProviderType.js"; export * from "./errors/InjectionError.js"; export * from "./errors/InvalidPropertyTokenError.js"; +export * from "./fn/constant.js"; +export * from "./fn/inject.js"; +export * from "./fn/injectable.js"; +export * from "./fn/injectMany.js"; +export * from "./fn/injector.js"; export * from "./interfaces/DIConfigurationOptions.js"; export * from "./interfaces/DILogger.js"; export * from "./interfaces/DILoggerOptions.js"; @@ -51,5 +56,4 @@ export * from "./utils/colors.js"; export * from "./utils/createContainer.js"; export * from "./utils/getConfiguration.js"; export * from "./utils/getConstructorDependencies.js"; -export * from "./utils/injector.js"; export * from "./utils/resolveControllers.js"; diff --git a/packages/di/src/node/decorators/injectContext.ts b/packages/di/src/node/decorators/injectContext.ts index 90a08ecf9b7..cad9eaf1fd5 100644 --- a/packages/di/src/node/decorators/injectContext.ts +++ b/packages/di/src/node/decorators/injectContext.ts @@ -1,8 +1,7 @@ import {catchError} from "@tsed/core"; -import {$injector} from "../../common/index.js"; import {DIContext} from "../domain/DIContext.js"; -import {getContext} from "../utils/asyncHookContext.js"; +import {context} from "../fn/context.js"; /** * Inject a context like PlatformContext or any BaseContext. @@ -23,15 +22,7 @@ export function InjectContext(transform: ($ctx: DIContext) => unknown = (o) => o catchError(() => Reflect.deleteProperty(target, propertyKey)); Reflect.defineProperty(target, propertyKey, { get() { - return transform( - getContext() || - new DIContext({ - id: "", - logger: $injector().logger, - injector: $injector(), - maxStackSize: 0 - }) - ); + return transform(context()); } }); }; diff --git a/packages/di/src/node/fn/context.ts b/packages/di/src/node/fn/context.ts new file mode 100644 index 00000000000..59844ad9266 --- /dev/null +++ b/packages/di/src/node/fn/context.ts @@ -0,0 +1,24 @@ +import {v4} from "uuid"; + +import {injector} from "../../common/index.js"; +import {DIContext} from "../domain/DIContext.js"; +import {getContext} from "../utils/asyncHookContext.js"; + +/** + * Get the current DIContext instance using async hook node.js api. + * + * ::: warning + * This function isn't available in the browser context. + * ::: + */ +export function context(): Ctx { + return ( + getContext() || + (new DIContext({ + id: v4(), + logger: injector().logger, + injector: injector(), + maxStackSize: 0 + }) as Ctx) + ); +} diff --git a/packages/di/src/node/fn/logger.spec.ts b/packages/di/src/node/fn/logger.spec.ts new file mode 100644 index 00000000000..2b85119ecf2 --- /dev/null +++ b/packages/di/src/node/fn/logger.spec.ts @@ -0,0 +1,46 @@ +import {Logger} from "@tsed/logger"; +import {beforeEach} from "vitest"; + +import {inject, Injectable} from "../../common/index.js"; +import {ContextLogger} from "../domain/ContextLogger.js"; +import {context} from "../fn/context.js"; +import {DITest} from "../services/DITest.js"; +import {runInContext} from "../utils/asyncHookContext.js"; +import {contextLogger, logger} from "./logger.js"; + +describe("DILogger", () => { + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); + + it("should inject logger in another service (decorator less)", async () => { + @Injectable() + class MyService { + logger = logger(); + } + + const service = inject(MyService, {rebuild: true}); + + expect(service.logger).toBeInstanceOf(Logger); + }); + + it("should inject context logger in another service", async () => { + @Injectable() + class MyService { + get logger() { + return contextLogger(); + } + } + + const service = inject(MyService, {rebuild: true}); + + expect(service.logger).toBeInstanceOf(ContextLogger); + + const $ctx = context(); + + await runInContext($ctx, () => { + expect(service.logger).toEqual(context().logger); + }); + + expect(service.logger).not.toEqual(context().logger); + }); +}); diff --git a/packages/di/src/node/fn/logger.ts b/packages/di/src/node/fn/logger.ts new file mode 100644 index 00000000000..1310d496af4 --- /dev/null +++ b/packages/di/src/node/fn/logger.ts @@ -0,0 +1,17 @@ +import {injector} from "../../common/index.js"; +import {context} from "./context.js"; + +/** + * Get the current `injector.logger` instance. + */ +export function logger() { + return injector().logger; +} + +/** + * Get the current logger instance. If the logger() function is invoked in Request context, it will return the logger instance attached to the request context. + * Otherwise, it will return the logger instance attached to the DI context. + */ +export function contextLogger() { + return context().logger; +} diff --git a/packages/di/src/node/index.ts b/packages/di/src/node/index.ts index 37bbb281d5b..3b1042f412e 100644 --- a/packages/di/src/node/index.ts +++ b/packages/di/src/node/index.ts @@ -4,6 +4,8 @@ export * from "./decorators/injectContext.js"; export * from "./domain/ContextLogger.js"; export * from "./domain/DIContext.js"; +export * from "./fn/context.js"; +export * from "./fn/logger.js"; export * from "./interfaces/AlterRunInContext.js"; export * from "./services/DILogger.js"; export * from "./services/DITest.js"; diff --git a/packages/di/src/node/services/DILogger.spec.ts b/packages/di/src/node/services/DILogger.spec.ts index 06f522ccbf1..de7dfc87282 100644 --- a/packages/di/src/node/services/DILogger.spec.ts +++ b/packages/di/src/node/services/DILogger.spec.ts @@ -1,8 +1,15 @@ +import "./DILogger.js"; + import {Logger} from "@tsed/logger"; +import {beforeEach} from "vitest"; -import {Container, Inject, Injectable, InjectorService} from "../../common/index.js"; +import {Inject, inject, Injectable, injector} from "../../common/index.js"; +import {DITest} from "./DITest.js"; describe("DILogger", () => { + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); + it("should inject logger in another service", async () => { @Injectable() class MyService { @@ -10,14 +17,8 @@ describe("DILogger", () => { logger: Logger; } - const injector = new InjectorService(); - injector.logger = new Logger(); - const container = new Container(); - container.add(MyService); - - await injector.load(container); - const logger = injector.get(MyService)!.logger; + const service = inject(MyService, {rebuild: true}); - expect(logger).toEqual(injector.logger); + expect(service.logger).toEqual(injector().logger); }); }); diff --git a/packages/di/src/node/services/DILogger.ts b/packages/di/src/node/services/DILogger.ts index 8ca72f3c3fa..2a4304116fa 100644 --- a/packages/di/src/node/services/DILogger.ts +++ b/packages/di/src/node/services/DILogger.ts @@ -1,11 +1,9 @@ import {Logger} from "@tsed/logger"; -import {InjectorService, registerProvider} from "../../common/index.js"; +import {injectable} from "../../common/fn/injectable.js"; +import {logger} from "../fn/logger.js"; -registerProvider({ +injectable({ provide: Logger, - deps: [InjectorService], - useFactory(injector: InjectorService) { - return injector.logger; - } + useFactory: logger }); diff --git a/packages/di/src/node/utils/asyncHookContext.spec.ts b/packages/di/src/node/utils/asyncHookContext.spec.ts index cc6f7f44fbb..3816e96c7e9 100644 --- a/packages/di/src/node/utils/asyncHookContext.spec.ts +++ b/packages/di/src/node/utils/asyncHookContext.spec.ts @@ -1,5 +1,5 @@ import {DITest} from "../services/DITest.js"; -import {getContext, runInContext, setContext, useContext} from "./asyncHookContext.js"; +import {getContext, runInContext, setContext} from "./asyncHookContext.js"; describe("asyncHookContext", () => { beforeEach(() => DITest.create()); @@ -9,7 +9,7 @@ describe("asyncHookContext", () => { const res = {type: "res"}; function next(res: any, req: any) { - return Promise.resolve(useContext()); + return Promise.resolve(getContext()); } function nextContext(res: any, req: any, next: any) { @@ -45,7 +45,7 @@ describe("asyncHookContext", () => { const res = {type: "res"}; function next(res: any, req: any) { - return Promise.resolve(useContext()); + return Promise.resolve(getContext()); } function nextContext(res: any, req: any, next: any) { @@ -79,7 +79,7 @@ describe("asyncHookContext", () => { const res = {type: "res"}; function next(res: any, req: any) { - return Promise.resolve(useContext({id: "id2"} as never)); + return Promise.resolve(getContext({id: "id2"} as never)); } function nextContext(res: any, req: any, next: any) { diff --git a/packages/di/src/node/utils/asyncHookContext.ts b/packages/di/src/node/utils/asyncHookContext.ts index 104087a335d..8d69bada202 100644 --- a/packages/di/src/node/utils/asyncHookContext.ts +++ b/packages/di/src/node/utils/asyncHookContext.ts @@ -13,12 +13,10 @@ export function useContextRef() { return getAsyncStore().getStore(); } -export function useContext(initialValue?: DIContext): Context | undefined { +export function getContext(initialValue?: DIContext): Context | undefined { return initialValue || (useContextRef()?.current as any); } -export const getContext = useContext; - export async function runInContext( ctx: DIContext | undefined, cb: (...args: unknown[]) => Result, From 6c43b1a3a05a97bd79e3a980c8aba0c108fe5e78 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Mon, 7 Oct 2024 10:24:44 +0200 Subject: [PATCH 3/6] feat(di): add configuration fn api --- .../di/src/common/fn/configuration.spec.ts | 28 +++++++++++++++++++ packages/di/src/common/fn/configuration.ts | 6 ++++ packages/di/src/common/index.ts | 1 + .../apollo/src/services/ApolloService.ts | 10 +++---- .../src/middlewares/ContextMiddleware.ts | 4 +-- .../src/builder/PlatformBuilder.spec.ts | 5 +++- .../components-scan/src/isTsEnv.ts | 5 ---- 7 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 packages/di/src/common/fn/configuration.spec.ts create mode 100644 packages/di/src/common/fn/configuration.ts diff --git a/packages/di/src/common/fn/configuration.spec.ts b/packages/di/src/common/fn/configuration.spec.ts new file mode 100644 index 00000000000..a94e68da2ea --- /dev/null +++ b/packages/di/src/common/fn/configuration.spec.ts @@ -0,0 +1,28 @@ +import {beforeEach} from "vitest"; + +import {DITest} from "../../node/index.js"; +import {Injectable} from "../decorators/injectable.js"; +import {configuration} from "./configuration.js"; +import {inject} from "./inject.js"; +import {injector} from "./injector.js"; + +@Injectable() +class Test { + public config = configuration(); +} + +describe("configuration()", () => { + beforeEach(() => + DITest.create({ + feature: "feature" + }) + ); + afterEach(() => DITest.reset()); + + it("should inject configuration", async () => { + const instance = inject(Test); + + expect(instance.config).toEqual(injector().settings); + expect(instance.config.get("feature")).toEqual("feature"); + }); +}); diff --git a/packages/di/src/common/fn/configuration.ts b/packages/di/src/common/fn/configuration.ts new file mode 100644 index 00000000000..37ffe9b50ea --- /dev/null +++ b/packages/di/src/common/fn/configuration.ts @@ -0,0 +1,6 @@ +import {DIConfiguration} from "../services/DIConfiguration.js"; +import {injector} from "./injector.js"; + +export function configuration() { + return injector().settings as TsED.Configuration & DIConfiguration; +} diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index 473d95df7cc..b198bdbd67a 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -27,6 +27,7 @@ export * from "./domain/ProviderScope.js"; export * from "./domain/ProviderType.js"; export * from "./errors/InjectionError.js"; export * from "./errors/InvalidPropertyTokenError.js"; +export * from "./fn/configuration.js"; export * from "./fn/constant.js"; export * from "./fn/inject.js"; export * from "./fn/injectable.js"; diff --git a/packages/graphql/apollo/src/services/ApolloService.ts b/packages/graphql/apollo/src/services/ApolloService.ts index 6787f9026e7..19f3dc840f5 100644 --- a/packages/graphql/apollo/src/services/ApolloService.ts +++ b/packages/graphql/apollo/src/services/ApolloService.ts @@ -3,8 +3,8 @@ import {ApolloServerPluginLandingPageDisabled} from "@apollo/server/plugin/disab import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer"; import {ApolloServerPluginLandingPageLocalDefault} from "@apollo/server/plugin/landingPage/default"; import type {IExecutableSchemaDefinition} from "@graphql-tools/schema"; -import {InjectorService, LocalsContainer, PlatformApplication, PlatformContext, Provider, useContext} from "@tsed/common"; -import {Constant, Inject, Service} from "@tsed/di"; +import {PlatformApplication, PlatformContext} from "@tsed/common"; +import {Constant, context, Inject, InjectorService, LocalsContainer, Provider, Service} from "@tsed/di"; import {Logger} from "@tsed/logger"; import type {GraphQLSchema} from "graphql"; import Http from "http"; @@ -177,14 +177,14 @@ export class ApolloService { }, new Map()); return async () => { - const $ctx = useContext(); - const context: ApolloContext = { + const $ctx = context(); + const apolloContext: ApolloContext = { dataSources: { ...(settings.dataSources?.() || {}) } }; - const alteredContext = await this.injector.alterAsync("$alterApolloContext", context, $ctx); + const alteredContext = await this.injector.alterAsync("$alterApolloContext", apolloContext, $ctx); $ctx!.set(APOLLO_CONTEXT, alteredContext); diff --git a/packages/graphql/typegraphql/src/middlewares/ContextMiddleware.ts b/packages/graphql/typegraphql/src/middlewares/ContextMiddleware.ts index 2de5635e25b..4e096e4e274 100644 --- a/packages/graphql/typegraphql/src/middlewares/ContextMiddleware.ts +++ b/packages/graphql/typegraphql/src/middlewares/ContextMiddleware.ts @@ -1,7 +1,7 @@ -import {type DIContext, runInContext, useContext} from "@tsed/di"; +import {type DIContext, getContext, runInContext} from "@tsed/di"; import {MiddlewareFn} from "type-graphql"; export const ContextMiddleware: MiddlewareFn<{req: {$ctx: DIContext}}> = (action, next) => { - const $ctx = useContext(action.context?.req?.$ctx); + const $ctx = getContext(action.context?.req?.$ctx); return runInContext($ctx, next); }; diff --git a/packages/platform/common/src/builder/PlatformBuilder.spec.ts b/packages/platform/common/src/builder/PlatformBuilder.spec.ts index cfa80f0489e..469f61add51 100644 --- a/packages/platform/common/src/builder/PlatformBuilder.spec.ts +++ b/packages/platform/common/src/builder/PlatformBuilder.spec.ts @@ -1,5 +1,5 @@ import {catchAsyncError, Type} from "@tsed/core"; -import {Configuration, Controller, Injectable, InjectorService, Module} from "@tsed/di"; +import {Configuration, Controller, Injectable, injector, InjectorService, Module} from "@tsed/di"; import {AfterInit} from "../interfaces/AfterInit.js"; import {AfterListen} from "../interfaces/AfterListen.js"; @@ -105,6 +105,9 @@ class ServerModule implements BeforeInit, AfterInit, BeforeRoutesInit, AfterRout } describe("PlatformBuilder", () => { + beforeEach(() => { + injector().destroy(); + }); describe("loadStatics()", () => { it("should loadStatics", async () => { // WHEN diff --git a/packages/third-parties/components-scan/src/isTsEnv.ts b/packages/third-parties/components-scan/src/isTsEnv.ts index 2fb440d5482..6822b7c76ea 100644 --- a/packages/third-parties/components-scan/src/isTsEnv.ts +++ b/packages/third-parties/components-scan/src/isTsEnv.ts @@ -1,9 +1,4 @@ export function isTsEnv() { - try { - if (require && require.extensions && require.extensions[".ts"]) { - return true; - } - } catch (er) {} return ( process.env["TS_NODE_DEV"] || process.env["_"]?.includes("ts-node") || From c2a779db26acc606d87864d8d62f5168b699b456 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Mon, 7 Oct 2024 10:00:16 +0200 Subject: [PATCH 4/6] refactor(oidc-provider): use functional DI Api to inject services --- packages/di/src/common/fn/refValue.spec.ts | 42 +++++++++++++ packages/di/src/common/fn/refValue.ts | 33 ++++++++++ packages/di/src/common/index.ts | 1 + .../platform-params/vitest.config.mts | 4 +- .../src/OidcWildcardRedirectUriModule.ts | 11 +--- .../security/oidc-provider/src/OidcModule.ts | 27 +++------ .../src/services/OidcAdapters.ts | 12 ++-- .../src/services/OidcInteractionContext.ts | 27 ++++----- .../src/services/OidcInteractions.ts | 17 ++---- .../oidc-provider/src/services/OidcJwks.ts | 12 ++-- .../oidc-provider/src/services/OidcPolicy.ts | 9 +-- .../src/services/OidcProvider.spec.ts | 2 +- .../src/services/OidcProvider.ts | 60 ++++++------------- 13 files changed, 136 insertions(+), 121 deletions(-) create mode 100644 packages/di/src/common/fn/refValue.spec.ts create mode 100644 packages/di/src/common/fn/refValue.ts diff --git a/packages/di/src/common/fn/refValue.spec.ts b/packages/di/src/common/fn/refValue.spec.ts new file mode 100644 index 00000000000..71f6faf3e09 --- /dev/null +++ b/packages/di/src/common/fn/refValue.spec.ts @@ -0,0 +1,42 @@ +import {DITest} from "../../node/index.js"; +import {refValue} from "./refValue.js"; + +describe("refValue()", () => { + beforeEach(() => + DITest.create({ + logger: { + level: "off" + } + }) + ); + afterEach(() => DITest.reset()); + describe("when decorator is used as property decorator", () => { + it("should create a getter", async () => { + // WHEN + class Test { + test = refValue("logger.level", "default value"); + } + + // THEN + + const test = await DITest.invoke(Test); + + expect(test.test.value).toEqual("off"); + }); + it("should create a getter with default value", async () => { + expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + + // WHEN + class Test { + test = refValue("logger.test", "default value"); + } + + // THEN + + const test = await DITest.invoke(Test); + + expect(test.test.value).toEqual("default value"); + expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + }); + }); +}); diff --git a/packages/di/src/common/fn/refValue.ts b/packages/di/src/common/fn/refValue.ts new file mode 100644 index 00000000000..63dd1091f34 --- /dev/null +++ b/packages/di/src/common/fn/refValue.ts @@ -0,0 +1,33 @@ +import {injector} from "./injector.js"; + +/** + * Get a value from the `injector.settings`. + * + * ## Example + * + * ```ts + * import {refValue, Injectable} from "@tsed/di"; + * + * @Injectable() + * class Test { + * test = refValue("logger.level", "default value"); + * + * constructor() { + * console.log(this.test.value); // "off" + * } + * } + * + * @param expression The expression to get the value from the `injector.settings`. + */ +export function refValue(expression: string): {value: Type | undefined}; +export function refValue(expression: string, defaultValue: Type | undefined): {value: Type}; +export function refValue(expression: string, defaultValue?: Type | undefined): {value: Type | undefined} { + return Object.freeze({ + get value() { + return injector().settings.get(expression, defaultValue); + }, + set value(value: Type) { + injector().settings.set(expression, value); + } + }); +} diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index b198bdbd67a..ab620d5c40f 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -33,6 +33,7 @@ export * from "./fn/inject.js"; export * from "./fn/injectable.js"; export * from "./fn/injectMany.js"; export * from "./fn/injector.js"; +export * from "./fn/refValue.js"; export * from "./interfaces/DIConfigurationOptions.js"; export * from "./interfaces/DILogger.js"; export * from "./interfaces/DILoggerOptions.js"; diff --git a/packages/platform/platform-params/vitest.config.mts b/packages/platform/platform-params/vitest.config.mts index f9d941dcb43..947a6a0a2a9 100644 --- a/packages/platform/platform-params/vitest.config.mts +++ b/packages/platform/platform-params/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 99.2, - branches: 90.69, + branches: 90.62, functions: 100, lines: 99.2 } } } } -); \ No newline at end of file +); diff --git a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/src/OidcWildcardRedirectUriModule.ts b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/src/OidcWildcardRedirectUriModule.ts index 5b803fed4f6..8d92547e247 100644 --- a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/src/OidcWildcardRedirectUriModule.ts +++ b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/src/OidcWildcardRedirectUriModule.ts @@ -1,5 +1,4 @@ -import {Constant, Inject, Module} from "@tsed/di"; -import {Logger} from "@tsed/logger"; +import {constant, logger, Module} from "@tsed/di"; import {OidcSettings} from "@tsed/oidc-provider"; import Provider, {errors, type KoaContextWithOIDC} from "oidc-provider"; // @ts-ignore @@ -19,11 +18,7 @@ declare global { @Module() export class OidcWildcardRedirectUriModule { - @Constant("oidc.plugins.wildcard.enabled", false) - readonly enabled: boolean; - - @Inject(Logger) - protected logger: Logger; + readonly enabled = constant("oidc.plugins.wildcard.enabled", false); $onCreateOIDC(provider: Provider) { if (this.enabled) { @@ -34,7 +29,7 @@ export class OidcWildcardRedirectUriModule { "postLogoutRedirectUris" ); - this.logger.warn("⚠️⚠️⚠️ OIDC Wildcard Uris plugin is ENABLED ⚠️⚠️⚠️"); + logger().warn("⚠️⚠️⚠️ OIDC Wildcard Uris plugin is ENABLED ⚠️⚠️⚠️"); } } diff --git a/packages/security/oidc-provider/src/OidcModule.ts b/packages/security/oidc-provider/src/OidcModule.ts index a6510617980..2f79f2c7075 100644 --- a/packages/security/oidc-provider/src/OidcModule.ts +++ b/packages/security/oidc-provider/src/OidcModule.ts @@ -1,5 +1,5 @@ -import {Inject, InjectorService, PlatformApplication} from "@tsed/common"; -import {Constant, Module} from "@tsed/di"; +import {PlatformApplication} from "@tsed/common"; +import {$injector, constant, inject, Module} from "@tsed/di"; import koaMount from "koa-mount"; import {OidcAdapters} from "./services/OidcAdapters.js"; @@ -10,20 +10,10 @@ import {OidcProvider} from "./services/OidcProvider.js"; imports: [OidcProvider, OidcAdapters, OidcJwks] }) export class OidcModule { - @Inject() - protected app: PlatformApplication; - - @Constant("PLATFORM_NAME") - protected platformName: string; - - @Constant("oidc.path", "/oidc") - protected basePath: string; - - @Inject() - protected oidcProvider: OidcProvider; - - @Inject() - protected injector: InjectorService; + protected app: PlatformApplication = inject(PlatformApplication); + protected platformName = constant("PLATFORM_NAME"); + protected basePath = constant("oidc.path", "/oidc"); + protected oidcProvider = inject(OidcProvider); async $onInit() { if (this.oidcProvider.hasConfiguration()) { @@ -54,8 +44,9 @@ export class OidcModule { } $onReady() { - if (this.oidcProvider.hasConfiguration() && "getBestHost" in this.injector.settings) { - const {injector} = this; + const injector = $injector(); + + if (this.oidcProvider.hasConfiguration() && "getBestHost" in injector.settings) { // @ts-ignore const host = injector.settings.getBestHost(); const url = host.toString(); diff --git a/packages/security/oidc-provider/src/services/OidcAdapters.ts b/packages/security/oidc-provider/src/services/OidcAdapters.ts index de84bbb7b96..d811c6eefa7 100644 --- a/packages/security/oidc-provider/src/services/OidcAdapters.ts +++ b/packages/security/oidc-provider/src/services/OidcAdapters.ts @@ -1,21 +1,17 @@ import {Adapter, Adapters} from "@tsed/adapters"; -import {Configuration, Inject, Injectable} from "@tsed/di"; +import {constant, inject, Injectable} from "@tsed/di"; import type {Adapter as OidcAdapter, AdapterConstructor} from "oidc-provider"; export type OidcAdapterMethods = Adapter & Partial>; @Injectable() export class OidcAdapters { - @Inject() - protected adapters: Adapters; - - @Configuration() - protected settings: Configuration; + protected adapters = inject(Adapters); createAdapterClass(): AdapterConstructor { const self = this; - const adapterBase = this.settings.get("oidc.Adapter", this.settings.get("adapters.Adapter")); - const connectionName = this.settings.get("oidc.connectionName", "default"); + const adapterBase = constant("oidc.Adapter", constant("adapters.Adapter")); + const connectionName = constant("oidc.connectionName", "default"); return class CustomAdapter implements OidcAdapter { adapter: OidcAdapterMethods; diff --git a/packages/security/oidc-provider/src/services/OidcInteractionContext.ts b/packages/security/oidc-provider/src/services/OidcInteractionContext.ts index 8aaf2c72440..b38e4adb7bf 100644 --- a/packages/security/oidc-provider/src/services/OidcInteractionContext.ts +++ b/packages/security/oidc-provider/src/services/OidcInteractionContext.ts @@ -1,6 +1,6 @@ -import {Constant, InjectContext, PlatformContext} from "@tsed/common"; +import {PlatformContext} from "@tsed/common"; import {Env} from "@tsed/core"; -import {Inject, Injectable} from "@tsed/di"; +import {constant, context, inject, Injectable} from "@tsed/di"; import {Unauthorized} from "@tsed/exceptions"; import omit from "lodash/omit.js"; import type {Account, default as Provider, InteractionResults, PromptDetail} from "oidc-provider"; @@ -24,20 +24,13 @@ import {OidcProvider} from "./OidcProvider.js"; @Injectable() export class OidcInteractionContext { - @Constant("env") - protected env: Env; + protected env = constant("env"); + protected oidcProvider = inject(OidcProvider); + protected oidcInteractions = inject(OidcInteractions); - @Constant("oidc.render.omitClientProps", []) - protected omitClientProps: string[]; - - @Inject() - protected oidcProvider: OidcProvider; - - @Inject() - protected oidcInteractions: OidcInteractions; - - @InjectContext() - protected $ctx: PlatformContext; + get $ctx() { + return context(); + } get raw(): OidcInteraction { return this.$ctx.get(INTERACTION_DETAILS)!; @@ -115,8 +108,10 @@ export class OidcInteractionContext { async interactionPrompt({client, ...options}: Record): Promise { client = client || (await this.findClient()); + const omitClientProps = constant("oidc.render.omitClientProps", []); + return { - client: omit(client, ["clientSecret", ...this.omitClientProps]), + client: omit(client, ["clientSecret", ...omitClientProps]), uid: this.uid, grantId: this.grantId, details: this.prompt.details, diff --git a/packages/security/oidc-provider/src/services/OidcInteractions.ts b/packages/security/oidc-provider/src/services/OidcInteractions.ts index 47cd2ca8084..1c7aba354ce 100644 --- a/packages/security/oidc-provider/src/services/OidcInteractions.ts +++ b/packages/security/oidc-provider/src/services/OidcInteractions.ts @@ -1,22 +1,15 @@ -import {Constant, EndpointMetadata, PlatformContext, PlatformHandler, Provider, TokenProvider} from "@tsed/common"; +import {PlatformContext, PlatformHandler} from "@tsed/common"; import {Env} from "@tsed/core"; -import {Inject, Injectable, InjectorService} from "@tsed/di"; +import {constant, Injectable, injector, Provider, TokenProvider} from "@tsed/di"; +import {EndpointMetadata} from "@tsed/schema"; import {INTERACTION, INTERACTION_OPTIONS, INTERACTIONS} from "../constants/constants.js"; import {OidcInteractionOptions} from "../domain/OidcInteractionOptions.js"; -import {OidcSettings} from "../domain/OidcSettings.js"; @Injectable() export class OidcInteractions { - @Inject() - protected injector: InjectorService; - - @Constant("env") - protected env: Env; - - @Constant("oidc") - protected oidcSettings: OidcSettings; - + protected injector = injector(); + protected env = constant("env"); protected interactions: Map = new Map(); $onInit(): void { diff --git a/packages/security/oidc-provider/src/services/OidcJwks.ts b/packages/security/oidc-provider/src/services/OidcJwks.ts index 270b5d37037..5f9f9f0c648 100644 --- a/packages/security/oidc-provider/src/services/OidcJwks.ts +++ b/packages/security/oidc-provider/src/services/OidcJwks.ts @@ -1,16 +1,12 @@ -import {Constant, Injectable} from "@tsed/di"; +import {constant, Injectable} from "@tsed/di"; import {getJwks, JwksKeyParameters} from "@tsed/jwks"; import {join} from "path"; @Injectable() export class OidcJwks { - @Constant("oidc.jwksPath", join(process.cwd(), "keys", "jwks.json")) - jwksPath: string; - - @Constant("oidc.certificates") - certificates?: JwksKeyParameters[]; - - keys: string; + public jwksPath: string = constant("oidc.jwksPath", join(process.cwd(), "keys", "jwks.json")); + public certificates?: JwksKeyParameters[] = constant("oidc.certificates"); + public keys: string; $onInit() { return this.getJwks(); diff --git a/packages/security/oidc-provider/src/services/OidcPolicy.ts b/packages/security/oidc-provider/src/services/OidcPolicy.ts index 8ebe88779e2..e7e385fcc9e 100644 --- a/packages/security/oidc-provider/src/services/OidcPolicy.ts +++ b/packages/security/oidc-provider/src/services/OidcPolicy.ts @@ -1,4 +1,4 @@ -import {Inject, Injectable, InjectorService, Provider} from "@tsed/di"; +import {inject, Injectable, injector, Provider} from "@tsed/di"; import {interactionPolicy} from "oidc-provider"; import {InteractionMethods} from "../domain/InteractionMethods.js"; @@ -8,11 +8,8 @@ import Prompt = interactionPolicy.Prompt; @Injectable() export class OidcPolicy { - @Inject() - protected injector: InjectorService; - - @Inject() - protected oidcInteractions: OidcInteractions; + protected injector = injector(); + protected oidcInteractions = inject(OidcInteractions); public getPolicy() { let policy = interactionPolicy.base(); diff --git a/packages/security/oidc-provider/src/services/OidcProvider.spec.ts b/packages/security/oidc-provider/src/services/OidcProvider.spec.ts index 5857c267bff..6da709775be 100644 --- a/packages/security/oidc-provider/src/services/OidcProvider.spec.ts +++ b/packages/security/oidc-provider/src/services/OidcProvider.spec.ts @@ -60,7 +60,7 @@ describe("OidcProvider", () => { expect((oidcProvider as any).injector.logger.error).toHaveBeenCalledWith({ duration: expect.any(Number), - reqId: "", + reqId: expect.any(String), account_id: "account_id", error: {error_description: "error_description", error_detail: "error_detail", error: "error"}, event: "OIDC_ERROR", diff --git a/packages/security/oidc-provider/src/services/OidcProvider.ts b/packages/security/oidc-provider/src/services/OidcProvider.ts index 4a151ec64d5..bc66ba3ef88 100644 --- a/packages/security/oidc-provider/src/services/OidcProvider.ts +++ b/packages/security/oidc-provider/src/services/OidcProvider.ts @@ -1,6 +1,6 @@ -import {InjectContext, PlatformApplication, PlatformContext} from "@tsed/common"; +import {PlatformApplication, PlatformContext} from "@tsed/common"; import {Env, setValue} from "@tsed/core"; -import {Constant, Inject, Injectable, InjectorService} from "@tsed/di"; +import {constant, context, inject, Injectable, InjectorService} from "@tsed/di"; import Provider, {type Configuration, type KoaContextWithOIDC} from "oidc-provider"; import {INTERACTIONS} from "../constants/constants.js"; @@ -8,7 +8,6 @@ import {OidcAccountsMethods} from "../domain/OidcAccountsMethods.js"; import {OidcSettings} from "../domain/OidcSettings.js"; import {OIDC_ERROR_EVENTS} from "../utils/events.js"; import {OidcAdapters} from "./OidcAdapters.js"; -import {OidcInteractions} from "./OidcInteractions.js"; import {OidcJwks} from "./OidcJwks.js"; import {OidcPolicy} from "./OidcPolicy.js"; @@ -25,47 +24,24 @@ function mapError(error: any) { export class OidcProvider { raw: Provider; - @Constant("env") - protected env: Env; - - @Constant("httpPort") - protected httpPort: number | string; - - @Constant("httpsPort") - protected httpsPort: number | string; - - @Constant("oidc.issuer", "") - protected issuer: string; - - @Constant("oidc") - protected oidc: OidcSettings; - - @Constant("PLATFORM_NAME") - protected platformName: string; - - @Inject() - protected oidcJwks: OidcJwks; - - @Inject() - protected oidcInteractions: OidcInteractions; - - @Inject() - protected oidcPolicy: OidcPolicy; - - @Inject() - protected adapters: OidcAdapters; - - @Inject() - protected injector: InjectorService; - - @Inject() - protected app: PlatformApplication; - - @InjectContext() - protected $ctx?: PlatformContext; + protected env = constant("env"); + protected httpPort = constant("httpPort"); + protected httpsPort = constant("httpsPort"); + protected issuer = constant("oidc.issuer", ""); + protected oidc = constant("oidc")!; + protected platformName = constant("PLATFORM_NAME"); + protected oidcJwks = inject(OidcJwks); + protected oidcPolicy = inject(OidcPolicy); + protected adapters = inject(OidcAdapters); + protected injector = inject(InjectorService); + protected app = inject(PlatformApplication); get logger() { - return this.$ctx?.logger || this.injector.logger; + return this.$ctx.logger; + } + + protected get $ctx() { + return context(); } hasConfiguration() { From bea2874c667e5f2826b1695cf0bd9e715e787a80 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 8 Oct 2024 11:12:30 +0200 Subject: [PATCH 5/6] feat(di): add events function to emit event --- packages/di/.barrelsby.json | 2 +- .../common/decorators/autoInjectable.spec.ts | 4 +- .../src/common/decorators/autoInjectable.ts | 8 +- .../di/src/common/decorators/inject.spec.ts | 37 +++---- packages/di/src/common/decorators/inject.ts | 2 - .../src/common/decorators/intercept.spec.ts | 4 - .../src/common/decorators/lazyInject.spec.ts | 30 +++--- .../di/src/common/decorators/lazyInject.ts | 4 +- .../di/src/common/decorators/value.spec.ts | 7 +- packages/di/src/common/decorators/value.ts | 6 +- packages/di/src/common/fn/events.spec.ts | 102 ++++++++++++++++++ packages/di/src/common/fn/events.ts | 33 ++++++ packages/di/src/common/fn/inject.spec.ts | 1 + packages/di/src/common/fn/inject.ts | 23 +++- packages/di/src/common/fn/injectMany.ts | 6 +- packages/di/src/common/fn/injector.ts | 32 ++++-- packages/di/src/common/fn/localsContainer.ts | 39 +++++++ packages/di/src/common/fn/refValue.spec.ts | 5 +- packages/di/src/common/index.ts | 2 + .../di/src/common/services/InjectorService.ts | 40 +------ packages/di/src/node/services/DITest.ts | 88 +++++---------- .../orm/ioredis/src/domain/IORedisTest.ts | 1 - .../utils/registerConnectionProvider.spec.ts | 8 +- .../mongoose/test/buffer.integration.spec.ts | 42 ++++---- .../mongoose/test/user.integration.spec.ts | 70 ++++++------ .../common/src/services/PlatformTest.ts | 26 ++--- .../common/src/utils/createInjector.ts | 28 ++--- .../security/oidc-provider/src/OidcModule.ts | 8 +- 28 files changed, 392 insertions(+), 266 deletions(-) create mode 100644 packages/di/src/common/fn/events.spec.ts create mode 100644 packages/di/src/common/fn/events.ts create mode 100644 packages/di/src/common/fn/localsContainer.ts diff --git a/packages/di/.barrelsby.json b/packages/di/.barrelsby.json index d217b1efeb9..71a24c70e5a 100644 --- a/packages/di/.barrelsby.json +++ b/packages/di/.barrelsby.json @@ -1,5 +1,5 @@ { "directory": ["./src/common", "./src/node"], - "exclude": ["**/__mock__", "**/__mocks__", "**/*.spec.ts"], + "exclude": ["**/__mock__", "**/__mocks__", "**/*.spec.ts", "localsContainer.ts"], "delete": true } diff --git a/packages/di/src/common/decorators/autoInjectable.spec.ts b/packages/di/src/common/decorators/autoInjectable.spec.ts index bb4d49c46c0..ca91a76cf8e 100644 --- a/packages/di/src/common/decorators/autoInjectable.spec.ts +++ b/packages/di/src/common/decorators/autoInjectable.spec.ts @@ -1,4 +1,3 @@ -import {catchError} from "@tsed/core"; import {Logger} from "@tsed/logger"; import {beforeEach} from "vitest"; @@ -105,9 +104,8 @@ describe("AutoInjectable", () => { class Test { @Inject(Logger) logger: Logger; - - private value: string; instances?: InterfaceGroup[]; + private value: string; constructor(initialValue: string, @Inject(TOKEN_GROUPS) instances?: InterfaceGroup[]) { this.value = initialValue; diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index 4fd7456ff8e..3a2285be8e9 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -1,12 +1,12 @@ import {isArray, type Type} from "@tsed/core"; import {LocalsContainer} from "../domain/LocalsContainer.js"; -import {$injector} from "../fn/injector.js"; +import {injector} from "../fn/injector.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; function resolveAutoInjectableArgs(token: Type, args: unknown[]) { - const injector = $injector(); + const inj = injector(); const locals = new LocalsContainer(); const deps: TokenProvider[] = getConstructorDependencies(token); const list: any[] = []; @@ -17,9 +17,7 @@ function resolveAutoInjectableArgs(token: Type, args: unknown[]) { list.push(args[i]); } else { const value = deps[i]; - const instance = isArray(value) - ? injector!.getMany(value[0], locals, {parent: token}) - : injector!.invoke(value, locals, {parent: token}); + const instance = isArray(value) ? inj!.getMany(value[0], locals, {parent: token}) : inj!.invoke(value, locals, {parent: token}); list.push(instance); } diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index 2b7b7445ccc..dcd64dc2864 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -1,6 +1,7 @@ import {catchAsyncError} from "@tsed/core"; import {DITest} from "../../node/index.js"; +import {injector} from "../fn/injector.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "../services/InjectorService.js"; import {Inject} from "./inject.js"; @@ -19,8 +20,8 @@ describe("@Inject()", () => { test: InjectorService; } - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + const inj = injector({rebuild: true}); + const instance = await inj.invoke(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -54,12 +55,12 @@ describe("@Inject()", () => { test: Test; } - const injector = new InjectorService(); + const inj = injector({rebuild: true}); - await injector.load(); + await inj.load(); - const parent1 = await injector.invoke(Parent1); - const parent2 = await injector.invoke(Parent2); + const parent1 = await inj.invoke(Parent1); + const parent2 = await inj.invoke(Parent2); expect(parent1.test).toBeInstanceOf(Test); expect(parent2.test).toBeInstanceOf(Test); @@ -72,8 +73,8 @@ describe("@Inject()", () => { test: InjectorService; } - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + const inj = injector({rebuild: true}); + const instance = await inj.invoke(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -86,8 +87,8 @@ describe("@Inject()", () => { test: InjectorService; } - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + const inj = injector({rebuild: true}); + const instance = await inj.invoke(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -136,11 +137,11 @@ describe("@Inject()", () => { instances: InterfaceGroup[]; } - const injector = new InjectorService(); + const inj = injector({rebuild: true}); - await injector.load(); + await inj.load(); - const instance = await injector.invoke(MyInjectable); + const instance = await inj.invoke(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); @@ -197,8 +198,8 @@ describe("@Inject()", () => { constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } - const injector = new InjectorService(); - const instance = await injector.invoke(MyInjectable); + const inj = injector({rebuild: true}); + const instance = await inj.invoke(MyInjectable); expect(instance.injector).toBeInstanceOf(InjectorService); }); @@ -248,11 +249,11 @@ describe("@Inject()", () => { constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} } - const injector = new InjectorService(); + const inj = injector({rebuild: true}); - await injector.load(); + await inj.load(); - const instance = await injector.invoke(MyInjectable); + const instance = await inj.invoke(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); diff --git a/packages/di/src/common/decorators/inject.ts b/packages/di/src/common/decorators/inject.ts index e8037352560..c4209723b08 100644 --- a/packages/di/src/common/decorators/inject.ts +++ b/packages/di/src/common/decorators/inject.ts @@ -4,10 +4,8 @@ import {DI_INJECTABLE_PROPS, DI_INVOKE_OPTIONS, DI_USE_OPTIONS} from "../constan import {InvalidPropertyTokenError} from "../errors/InvalidPropertyTokenError.js"; import {inject} from "../fn/inject.js"; import {injectMany} from "../fn/injectMany.js"; -import {$injector} from "../fn/injector.js"; import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; -import {InjectorService} from "../services/InjectorService.js"; import {getConstructorDependencies, setConstructorDependencies} from "../utils/getConstructorDependencies.js"; function setToken( diff --git a/packages/di/src/common/decorators/intercept.spec.ts b/packages/di/src/common/decorators/intercept.spec.ts index 278364432e2..4870d6db0da 100644 --- a/packages/di/src/common/decorators/intercept.spec.ts +++ b/packages/di/src/common/decorators/intercept.spec.ts @@ -10,10 +10,6 @@ import {Service} from "./service.js"; @Interceptor() class MyInterceptor implements InterceptorMethods { - constructor(injSrv: InjectorService) { - // do some logic - } - intercept(context: InterceptorContext) { const r = typeof context.args[0] === "string" ? undefined : new Error(`Error message`); const retValue = context.next(r); diff --git a/packages/di/src/common/decorators/lazyInject.spec.ts b/packages/di/src/common/decorators/lazyInject.spec.ts index cd73520611a..7fb257754af 100644 --- a/packages/di/src/common/decorators/lazyInject.spec.ts +++ b/packages/di/src/common/decorators/lazyInject.spec.ts @@ -1,6 +1,6 @@ import {catchAsyncError, classOf, nameOf} from "@tsed/core"; -import {InjectorService} from "../services/InjectorService.js"; +import {injector} from "../fn/injector.js"; import type {MyLazyModule} from "./__mock__/lazy.module.js"; import {Injectable} from "./injectable.js"; import {LazyInject, OptionalLazyInject} from "./lazyInject.js"; @@ -13,14 +13,14 @@ describe("LazyInject", () => { lazy: Promise; } - const injector = new InjectorService(); - const service = await injector.invoke(MyInjectable); - const nbProviders = injector.getProviders().length; + const inj = injector({rebuild: true}); + const service = await inj.invoke(MyInjectable); + const nbProviders = inj.getProviders().length; const lazyService = await service.lazy; expect(nameOf(classOf(lazyService))).toEqual("MyLazyModule"); - expect(nbProviders).not.toEqual(injector.getProviders().length); + expect(nbProviders).not.toEqual(inj.getProviders().length); }); it("should throw an error when token isn't a valid provider", async () => { @@ -30,8 +30,8 @@ describe("LazyInject", () => { lazy?: Promise; } - const injector = new InjectorService(); - const service = await injector.invoke(MyInjectable); + const inj = injector({rebuild: true}); + const service = await inj.invoke(MyInjectable); const error = await catchAsyncError(() => service.lazy); expect(error?.message).toEqual('Unable to lazy load the "TKO". The token isn\'t a valid token provider.'); @@ -45,8 +45,8 @@ describe("LazyInject", () => { lazy?: Promise; } - const injector = new InjectorService(); - const service = await injector.invoke(MyInjectable); + const inj = injector({rebuild: true}); + const service = await inj.invoke(MyInjectable); const error = await catchAsyncError(() => service.lazy); expect(error?.message).toContain("Failed to load url lazy-module"); @@ -60,8 +60,8 @@ describe("LazyInject", () => { lazy?: Promise; } - const injector = new InjectorService(); - const service = await injector.invoke(MyInjectable); + const inj = injector({rebuild: true}); + const service = await inj.invoke(MyInjectable); const lazyService = await service.lazy; expect(lazyService).toEqual({}); @@ -74,13 +74,13 @@ describe("LazyInject", () => { lazy: Promise; } - const injector = new InjectorService(); - const service = await injector.invoke(MyInjectable); - const originalLazyInvoke = injector.lazyInvoke.bind(injector); + const inj = injector({rebuild: true}); + const service = await inj.invoke(MyInjectable); + const originalLazyInvoke = inj.lazyInvoke.bind(inj); const promise1 = service.lazy; let promise2: Promise | undefined; - vi.spyOn(injector, "lazyInvoke").mockImplementationOnce((token) => { + vi.spyOn(inj, "lazyInvoke").mockImplementationOnce((token) => { promise2 = service.lazy; return originalLazyInvoke(token); }); diff --git a/packages/di/src/common/decorators/lazyInject.ts b/packages/di/src/common/decorators/lazyInject.ts index c5dcf5dfa7c..c64ac387469 100644 --- a/packages/di/src/common/decorators/lazyInject.ts +++ b/packages/di/src/common/decorators/lazyInject.ts @@ -1,6 +1,6 @@ import {catchError, importPackage} from "@tsed/core"; -import {$injector} from "../fn/injector.js"; +import {injector} from "../fn/injector.js"; /** * Lazy load a provider from his package and invoke only when the provider is used @@ -42,7 +42,7 @@ export function LazyInject( } } - bean = token ? await $injector().lazyInvoke(token) : {}; + bean = token ? await injector().lazyInvoke(token) : {}; } return bean; diff --git a/packages/di/src/common/decorators/value.spec.ts b/packages/di/src/common/decorators/value.spec.ts index 43c1072ac87..2813df4098c 100644 --- a/packages/di/src/common/decorators/value.spec.ts +++ b/packages/di/src/common/decorators/value.spec.ts @@ -1,4 +1,5 @@ import {DITest} from "../../node/index.js"; +import {configuration} from "../fn/configuration.js"; import {Value} from "./value.js"; describe("@Value()", () => { @@ -25,7 +26,7 @@ describe("@Value()", () => { expect(test.test).toEqual("off"); }); it("should create a getter with default value", async () => { - expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + expect(configuration().get("logger.test")).toEqual(undefined); // WHEN class Test { @@ -38,7 +39,7 @@ describe("@Value()", () => { const test = await DITest.invoke(Test); expect(test.test).toEqual("default value"); - expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + expect(configuration().get("logger.test")).toEqual(undefined); }); it("should create a getter with native default value", async () => { // WHEN @@ -52,7 +53,7 @@ describe("@Value()", () => { const test = await DITest.invoke(Test); expect(test.test).toEqual("default prop"); - expect(DITest.injector.settings.get("logger.test")).toEqual("default prop"); + expect(configuration().get("logger.test")).toEqual("default prop"); }); }); }); diff --git a/packages/di/src/common/decorators/value.ts b/packages/di/src/common/decorators/value.ts index 5af0e784492..cfd7f6971e4 100644 --- a/packages/di/src/common/decorators/value.ts +++ b/packages/di/src/common/decorators/value.ts @@ -1,14 +1,14 @@ import {catchError} from "@tsed/core"; -import {$injector} from "../fn/injector.js"; +import {injector} from "../fn/injector.js"; export function bindValue(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { const descriptor = { get() { - return $injector().settings.get(expression, defaultValue); + return injector().settings.get(expression, defaultValue); }, set(value: unknown) { - $injector().settings.set(expression, value); + injector().settings.set(expression, value); }, enumerable: true, configurable: true diff --git a/packages/di/src/common/fn/events.spec.ts b/packages/di/src/common/fn/events.spec.ts new file mode 100644 index 00000000000..efc44aea7df --- /dev/null +++ b/packages/di/src/common/fn/events.spec.ts @@ -0,0 +1,102 @@ +import {beforeEach} from "vitest"; + +import {DITest} from "../../node/index.js"; +import {Injectable} from "../decorators/injectable.js"; +import {registerProvider} from "../registries/ProviderRegistry.js"; +import {$alter, $alterAsync, $emit} from "./events.js"; +import {injector} from "./injector.js"; + +@Injectable() +class Test { + $event(value: any) {} + + $alterValue(value: any) { + return "alteredValue"; + } + + $alterAsyncValue(value: any) { + return Promise.resolve("alteredValue"); + } +} + +describe("events", () => { + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); + + describe("$emit()", () => { + it("should alter value", async () => { + // GIVEN + const service = DITest.get(Test); + + vi.spyOn(service, "$event"); + + await $emit("$event", "value"); + + expect(service.$event).toHaveBeenCalledWith("value"); + }); + it("should alter value (factory)", () => { + registerProvider({ + provide: "TOKEN", + useFactory: () => { + return {}; + }, + hooks: { + $alterValue(instance: any, value: any) { + return "alteredValue"; + } + } + }); + + // GIVEN + injector().invoke("TOKEN"); + + const value = $alter("$alterValue", "value"); + + expect(value).toEqual("alteredValue"); + }); + }); + describe("$alter()", () => { + it("should alter value", async () => { + // GIVEN + const service = await DITest.invoke(Test); + vi.spyOn(service, "$alterValue"); + + const value = $alter("$alterValue", "value"); + + expect(service.$alterValue).toHaveBeenCalledWith("value"); + expect(value).toEqual("alteredValue"); + }); + it("should alter value (factory)", () => { + registerProvider({ + provide: "TOKEN", + useFactory: () => { + return {}; + }, + hooks: { + $alterValue(instance: any, value: any) { + return "alteredValue"; + } + } + }); + + // GIVEN + injector().invoke("TOKEN"); + + const value = $alter("$alterValue", "value"); + + expect(value).toEqual("alteredValue"); + }); + }); + describe("$alterAsync()", () => { + it("should alter value", async () => { + const service = await DITest.invoke(Test)!; + + vi.spyOn(service, "$alterAsyncValue"); + + const value = await $alterAsync("$alterAsyncValue", "value"); + + expect(service.$alterAsyncValue).toHaveBeenCalledWith("value"); + expect(value).toEqual("alteredValue"); + }); + }); +}); diff --git a/packages/di/src/common/fn/events.ts b/packages/di/src/common/fn/events.ts new file mode 100644 index 00000000000..c35154f81d7 --- /dev/null +++ b/packages/di/src/common/fn/events.ts @@ -0,0 +1,33 @@ +import {injector} from "./injector.js"; + +/** + * Alter value attached to an event asynchronously. + * @param eventName + * @param value + * @param args + * @param callThis + */ +export function $alterAsync(eventName: string, value: any, ...args: unknown[]) { + return injector().hooks.asyncAlter(eventName, value, args); +} + +/** + * Emit an event to all service. See service [lifecycle hooks](/docs/services.md#lifecycle-hooks). + * @param eventName The event name to emit at all services. + * @param args List of the parameters to give to each service. + * @returns A list of promises. + */ +export function $emit(eventName: string, ...args: unknown[]): Promise { + return injector().hooks.asyncEmit(eventName, args); +} + +/** + * Alter value attached to an event. + * @param eventName + * @param value + * @param args + * @param callThis + */ +export function $alter(eventName: string, value: unknown, ...args: unknown[]): T { + return injector().hooks.alter(eventName, value, args); +} diff --git a/packages/di/src/common/fn/inject.spec.ts b/packages/di/src/common/fn/inject.spec.ts index 46e7e29ab25..532bb89728d 100644 --- a/packages/di/src/common/fn/inject.spec.ts +++ b/packages/di/src/common/fn/inject.spec.ts @@ -26,4 +26,5 @@ describe("inject()", () => { } ]); }); + it("should rebuild all dependencies using invoke", async () => {}); }); diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts index a5c5d9ad4e9..e248569c9f6 100644 --- a/packages/di/src/common/fn/inject.ts +++ b/packages/di/src/common/fn/inject.ts @@ -1,8 +1,25 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; -import {InjectorService} from "../services/InjectorService.js"; -import {$injector} from "./injector.js"; +import {injector} from "./injector.js"; +import {localsContainer} from "./localsContainer.js"; +/** + * Inject a provider to another provider. + * + * Use this function to inject a custom provider on constructor parameter or property. + * + * ```typescript + * @Injectable() + * export class MyService { + * connection = inject(CONNECTION); + * } + * ``` + * + * @param token A token provider or token provider group + * @param opts + * @returns {Function} + * @decorator + */ export function inject(token: TokenProvider, opts?: Partial>): T { - return $injector().invoke(token, opts?.locals || InjectorService.getLocals(), opts); + return injector().invoke(token, opts?.locals || localsContainer(), opts); } diff --git a/packages/di/src/common/fn/injectMany.ts b/packages/di/src/common/fn/injectMany.ts index ba5cdfb95d9..c0358dad2af 100644 --- a/packages/di/src/common/fn/injectMany.ts +++ b/packages/di/src/common/fn/injectMany.ts @@ -1,7 +1,7 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; -import {InjectorService} from "../services/InjectorService.js"; -import {$injector} from "./injector.js"; +import {injector} from "./injector.js"; +import {localsContainer} from "./localsContainer.js"; export function injectMany(token: string | symbol, opts?: Partial>): T[] { - return $injector().getMany(token, opts?.locals || InjectorService.getLocals(), opts); + return injector().getMany(token, opts?.locals || localsContainer(), opts); } diff --git a/packages/di/src/common/fn/injector.ts b/packages/di/src/common/fn/injector.ts index 2dedd2a82b5..7cb80e16de7 100644 --- a/packages/di/src/common/fn/injector.ts +++ b/packages/di/src/common/fn/injector.ts @@ -1,5 +1,8 @@ import {InjectorService} from "../services/InjectorService.js"; +let globalInjector: InjectorService | undefined; + +type InjectorFnOpts = {rebuild?: boolean; logger?: any; settings?: Partial}; /** * Create or return the existing injector service. * @@ -14,12 +17,27 @@ import {InjectorService} from "../services/InjectorService.js"; * } * ``` */ -export function injector(): InjectorService { - return InjectorService.getInstance(); +export function injector(opts?: InjectorFnOpts): InjectorService { + if (!globalInjector || opts?.rebuild) { + globalInjector = new InjectorService(); + + if (opts && opts.logger) { + globalInjector.logger = opts.logger; + } + + if (opts?.settings) { + globalInjector.settings.set(opts.settings); + } + } + + return globalInjector; } -/** - * Alias of injector - * @alias injector - */ -export const $injector = injector; +export function hasInjector() { + return !!globalInjector; +} + +export async function destroyInjector() { + await globalInjector?.destroy(); + globalInjector = undefined; +} diff --git a/packages/di/src/common/fn/localsContainer.ts b/packages/di/src/common/fn/localsContainer.ts new file mode 100644 index 00000000000..c594f2a624b --- /dev/null +++ b/packages/di/src/common/fn/localsContainer.ts @@ -0,0 +1,39 @@ +import {LocalsContainer} from "../domain/LocalsContainer.js"; +import type {UseImportTokenProviderOpts} from "../interfaces/ImportTokenProviderOpts.js"; +import {InjectorService} from "../services/InjectorService.js"; +import {injector} from "./injector.js"; + +let globalLocals: LocalsContainer | undefined; +const stagedLocals: LocalsContainer[] = []; + +/** + * Get the locals container initiated by DITest or .bootstrap() method. + */ +export function localsContainer({providers}: {providers?: UseImportTokenProviderOpts[]; rebuild?: boolean} = {}) { + if (!globalLocals || providers) { + globalLocals = new LocalsContainer(); + + if (providers) { + providers.forEach((p) => { + globalLocals!.set(p.token, p.use); + }); + + globalLocals.set(InjectorService, injector()); + } + } + + return globalLocals; +} + +/** + * Reset the locals container. + */ +export function detachLocalsContainer() { + globalLocals && stagedLocals.push(globalLocals); + globalLocals = undefined; +} + +export function cleanAllLocalsContainer() { + detachLocalsContainer(); + stagedLocals.map((item) => item.clear()); +} diff --git a/packages/di/src/common/fn/refValue.spec.ts b/packages/di/src/common/fn/refValue.spec.ts index 71f6faf3e09..cccaba62fb4 100644 --- a/packages/di/src/common/fn/refValue.spec.ts +++ b/packages/di/src/common/fn/refValue.spec.ts @@ -1,4 +1,5 @@ import {DITest} from "../../node/index.js"; +import {configuration} from "./configuration.js"; import {refValue} from "./refValue.js"; describe("refValue()", () => { @@ -24,7 +25,7 @@ describe("refValue()", () => { expect(test.test.value).toEqual("off"); }); it("should create a getter with default value", async () => { - expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + expect(configuration().get("logger.test")).toEqual(undefined); // WHEN class Test { @@ -36,7 +37,7 @@ describe("refValue()", () => { const test = await DITest.invoke(Test); expect(test.test.value).toEqual("default value"); - expect(DITest.injector.settings.get("logger.test")).toEqual(undefined); + expect(configuration().get("logger.test")).toEqual(undefined); }); }); }); diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index ab620d5c40f..2c084734ce7 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -29,10 +29,12 @@ export * from "./errors/InjectionError.js"; export * from "./errors/InvalidPropertyTokenError.js"; export * from "./fn/configuration.js"; export * from "./fn/constant.js"; +export * from "./fn/events.js"; export * from "./fn/inject.js"; export * from "./fn/injectable.js"; export * from "./fn/injectMany.js"; export * from "./fn/injector.js"; +export * from "./fn/localsContainer.js"; export * from "./fn/refValue.js"; export * from "./interfaces/DIConfigurationOptions.js"; export * from "./interfaces/DILogger.js"; diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 7510cefd6cd..1b9780abed9 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -32,9 +32,6 @@ import {getConstructorDependencies} from "../utils/getConstructorDependencies.js import {resolveControllers} from "../utils/resolveControllers.js"; import {DIConfiguration} from "./DIConfiguration.js"; -let globalInjector: InjectorService | undefined; -let globalLocals: LocalsContainer | undefined; - /** * This service contain all services collected by `@Service` or services declared manually with `InjectorService.factory()` or `InjectorService.service()`. * @@ -64,12 +61,11 @@ export class InjectorService extends Container { public logger: DILogger = console; private resolvedConfiguration: boolean = false; #cache = new LocalsContainer(); - #hooks = new Hooks(); + readonly hooks = new Hooks(); constructor() { super(); this.#cache.set(InjectorService, this); - globalInjector = this; } get resolvers() { @@ -80,31 +76,6 @@ export class InjectorService extends Container { return this.settings.scopes || {}; } - /** - * Return the current injector service. - */ - static getInstance() { - if (!globalInjector) { - return new InjectorService(); - } - - return globalInjector; - } - - /** - * Get the locals container initiated by DITest or .bootstrap() method. - */ - static getLocals() { - return globalLocals || (globalLocals = new LocalsContainer()); - } - - /** - * Reset the locals container. - */ - static unsetLocals() { - globalLocals = undefined; - } - /** * Retrieve default scope for a given provider. * @param provider @@ -411,7 +382,7 @@ export class InjectorService extends Container { * @returns A list of promises. */ public emit(eventName: string, ...args: any[]): Promise { - return this.#hooks.asyncEmit(eventName, args); + return this.hooks.asyncEmit(eventName, args); } /** @@ -421,7 +392,7 @@ export class InjectorService extends Container { * @param args */ public alter(eventName: string, value: any, ...args: any[]): T { - return this.#hooks.alter(eventName, value, args); + return this.hooks.alter(eventName, value, args); } /** @@ -431,7 +402,7 @@ export class InjectorService extends Container { * @param args */ public alterAsync(eventName: string, value: any, ...args: any[]): Promise { - return this.#hooks.asyncAlter(eventName, value, args); + return this.hooks.asyncAlter(eventName, value, args); } /** @@ -439,7 +410,6 @@ export class InjectorService extends Container { */ async destroy() { await this.emit("$onDestroy"); - globalInjector = undefined; } /** @@ -656,7 +626,7 @@ export class InjectorService extends Container { Object.entries(provider.hooks).forEach(([event, cb]) => { const callback = (...args: any[]) => cb(this.get(provider.token) || instance, ...args); - this.#hooks.on(event, callback); + this.hooks.on(event, callback); }); } } diff --git a/packages/di/src/node/services/DITest.ts b/packages/di/src/node/services/DITest.ts index 3a577d8ba01..0258ff5b431 100644 --- a/packages/di/src/node/services/DITest.ts +++ b/packages/di/src/node/services/DITest.ts @@ -1,90 +1,62 @@ -import {Env, getValue, isClass, isObject, isPromise, setValue} from "@tsed/core"; +import {Env, getValue, isClass, isObject} from "@tsed/core"; import {$log} from "@tsed/logger"; +import {cleanAllLocalsContainer, detachLocalsContainer, localsContainer} from "../../common/fn/localsContainer.js"; import { createContainer, + destroyInjector, DI_INJECTABLE_PROPS, + hasInjector, + inject, + injector, InjectorService, type OnInit, TokenProvider, type UseImportTokenProviderOpts } from "../../common/index.js"; import {DIContext} from "../domain/DIContext.js"; +import {logger} from "../fn/logger.js"; import {setLoggerConfiguration} from "../utils/setLoggerConfiguration.js"; /** * Tool to run test with lightweight DI sandbox. */ export class DITest { - static options: Partial = {}; - protected static _injector: InjectorService | null = null; - - static get injector(): InjectorService { - if (DITest._injector) { - return DITest._injector!; - } - - /* istanbul ignore next */ - throw new Error( - "PlatformTest.injector is not initialized. Use PlatformTest.create(): Promise before PlatformTest.invoke() or PlatformTest.injector.\n" + - "Example:\n" + - "before(async () => {\n" + - " await PlatformTest.create()\n" + - " await PlatformTest.invoke(MyService, [])\n" + - "})" - ); - } - - static set injector(injector: InjectorService) { - DITest._injector = injector; - } - - static set(key: string, value: any) { - setValue(DITest.options, key, value); - } - - static hasInjector() { - return !!DITest._injector; + static get injector() { + return injector(); } static async create(settings: Partial = {}) { - settings = { - ...DITest.options, - ...settings - }; - - DITest.injector = DITest.createInjector(settings); - + DITest.createInjector(settings); await DITest.createContainer(); } static async createContainer() { - await DITest.injector.load(createContainer()); + await injector().load(createContainer()); } /** * Create a new injector with the right default services */ static createInjector(settings: any = {}): InjectorService { - const injector = new InjectorService(); - injector.logger = $log; - - // @ts-ignore - injector.settings.set(DITest.configure(settings)); + const inj = injector({ + rebuild: true, + logger: $log, + settings: DITest.configure(settings) + }); - setLoggerConfiguration(injector); + setLoggerConfiguration(inj); - return injector; + return inj; } /** * Resets the test injector of the test context, so it won't pollute your next test. Call this in your `tearDown` logic. */ static async reset() { - if (DITest.hasInjector()) { - await DITest.injector.destroy(); - InjectorService.unsetLocals(); - DITest._injector = null; + if (hasInjector()) { + await destroyInjector(); + cleanAllLocalsContainer(); } } @@ -94,15 +66,9 @@ export class DITest { * @param providers */ static async invoke(target: TokenProvider, providers: UseImportTokenProviderOpts[] = []): Promise { - const locals = InjectorService.getLocals(); - - providers.forEach((p) => { - locals.set(p.token, p.use); - }); - - locals.set(InjectorService, DITest.injector); + const locals = localsContainer({providers, rebuild: true}); - const instance: T & OnInit = DITest.injector.invoke(target, locals, {rebuild: true}); + const instance: T & OnInit = inject(target, {locals, rebuild: true}); if (instance && isObject(instance) && "$onInit" in instance) { const result = instance.$onInit(); @@ -124,7 +90,7 @@ export class DITest { } } - InjectorService.unsetLocals(); + detachLocalsContainer(); return instance as any; } @@ -135,14 +101,14 @@ export class DITest { * @param options */ static get(target: TokenProvider, options: any = {}): T { - return DITest.injector.get(target, options)!; + return injector().get(target, options)!; } static createDIContext() { return new DIContext({ id: "id", - injector: DITest.injector, - logger: DITest.injector.logger + injector: injector(), + logger: logger() }); } diff --git a/packages/orm/ioredis/src/domain/IORedisTest.ts b/packages/orm/ioredis/src/domain/IORedisTest.ts index e0998aa15f9..d286ab9ac4a 100644 --- a/packages/orm/ioredis/src/domain/IORedisTest.ts +++ b/packages/orm/ioredis/src/domain/IORedisTest.ts @@ -9,7 +9,6 @@ export class IORedisTest extends DITest { await Promise.all(imports.map(({use}) => use.flushall())); return DITest.create({ - ...IORedisTest.options, ...options, imports: [...(options?.imports || []), ...imports] }); diff --git a/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts b/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts index 2d3f471129b..b33a15db853 100644 --- a/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts +++ b/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts @@ -1,4 +1,4 @@ -import {DITest} from "@tsed/di"; +import {configuration, DITest} from "@tsed/di"; import {Redis} from "ioredis"; import {registerConnectionProvider} from "./registerConnectionProvider.js"; @@ -64,7 +64,7 @@ describe("RedisConnection", () => { it("should create redis connection", () => { const connection = DITest.get(REDIS_CONNECTION); - const cacheSettings = DITest.injector.settings.get("cache"); + const cacheSettings = configuration().get("cache"); expect((connection as any).options).toMatchObject({ host: "localhost", @@ -93,7 +93,7 @@ describe("RedisConnection", () => { it("should create redis connection", () => { const connection = DITest.get(REDIS_CONNECTION); - const cacheSettings = DITest.injector.settings.get("cache"); + const cacheSettings = configuration().get("cache"); expect((connection as any).options).toMatchObject({ clusterRetryStrategy: expect.any(Function), @@ -130,7 +130,7 @@ describe("RedisConnection", () => { it("should create redis connection", () => { const connection = DITest.get(REDIS_CONNECTION); - const cacheSettings = DITest.injector.settings.get("cache"); + const cacheSettings = configuration().get("cache"); expect((connection as any).options).toMatchObject({ value: "value", diff --git a/packages/orm/mongoose/test/buffer.integration.spec.ts b/packages/orm/mongoose/test/buffer.integration.spec.ts index d094d981797..17272874e41 100644 --- a/packages/orm/mongoose/test/buffer.integration.spec.ts +++ b/packages/orm/mongoose/test/buffer.integration.spec.ts @@ -11,30 +11,28 @@ describe("Mongoose", () => { beforeEach(() => TestContainersMongo.create()); afterEach(() => TestContainersMongo.reset()); - it( - "Should save and load buffer", - PlatformTest.inject([TestAvatar], async (avatarModel: MongooseModel) => { - const imageBuffer = await axios - .get(faker.image.avatarGitHub(), { - responseType: "arraybuffer" - }) - .then((response) => Buffer.from(response.data, "binary")); + it("Should save and load buffer", async () => { + const avatarModel = PlatformTest.get>(TestAvatar); + const imageBuffer = await axios + .get(faker.image.avatarGitHub(), { + responseType: "arraybuffer" + }) + .then((response) => Buffer.from(response.data, "binary")); - // GIVEN - const newAvatar = new avatarModel({ - image: imageBuffer - }); + // GIVEN + const newAvatar = new avatarModel({ + image: imageBuffer + }); - // WHEN - await newAvatar.save(); - const savedAvatar = await avatarModel.findById(newAvatar.id); + // WHEN + await newAvatar.save(); + const savedAvatar = await avatarModel.findById(newAvatar.id); - // THEN - expect(savedAvatar).not.toBeNull(); - if (savedAvatar) { - expect(savedAvatar.image).toBeInstanceOf(Buffer); - } - }) - ); + // THEN + expect(savedAvatar).not.toBeNull(); + if (savedAvatar) { + expect(savedAvatar.image).toBeInstanceOf(Buffer); + } + }); }); }); diff --git a/packages/orm/mongoose/test/user.integration.spec.ts b/packages/orm/mongoose/test/user.integration.spec.ts index b60465ec13d..f9a38017a5b 100644 --- a/packages/orm/mongoose/test/user.integration.spec.ts +++ b/packages/orm/mongoose/test/user.integration.spec.ts @@ -10,48 +10,44 @@ describe("Mongoose", () => { beforeEach(() => TestContainersMongo.create()); afterEach(() => TestContainersMongo.reset()); - it( - "should run pre and post hook", - PlatformTest.inject([TestUser], async (userModel: MongooseModel) => { - // GIVEN - const user = new userModel({ - email: "test@test.fr", - password: faker.internet.password({length: 12}) - }); - - // WHEN - await user.save(); - - // THEN - expect(user.email).toBe("test@test.fr"); - expect(user.password).toBe(user.password); - - expect(user.pre).toBe("hello pre"); - expect(user.post).toBe("hello post"); - }) - ); + it("should run pre and post hook", async () => { + const userModel = PlatformTest.get>(TestUser); + // GIVEN + const user = new userModel({ + email: "test@test.fr", + password: faker.internet.password({length: 12}) + }); + + // WHEN + await user.save(); + + // THEN + expect(user.email).toBe("test@test.fr"); + expect(user.password).toBe(user.password); + + expect(user.pre).toBe("hello pre"); + expect(user.post).toBe("hello post"); + }); }); describe("UserModel", () => { beforeEach(() => TestContainersMongo.create()); afterEach(() => TestContainersMongo.reset()); - it( - "should run pre and post hook", - PlatformTest.inject([TestUser], async (userModel: MongooseModel) => { - // GIVEN - const user = new userModel({ - email: "test@test.fr", - password: faker.internet.password({length: 12}) - }); - - // WHEN - await user.save(); - - // THEN - expect(user.pre).toBe("hello pre"); - expect(user.post).toBe("hello post"); - }) - ); + it("should run pre and post hook", async () => { + const userModel = PlatformTest.get>(TestUser); + // GIVEN + const user = new userModel({ + email: "test@test.fr", + password: faker.internet.password({length: 12}) + }); + + // WHEN + await user.save(); + + // THEN + expect(user.pre).toBe("hello pre"); + expect(user.post).toBe("hello post"); + }); }); }); diff --git a/packages/platform/common/src/services/PlatformTest.ts b/packages/platform/common/src/services/PlatformTest.ts index b127e3ed173..eda1bf6dc7d 100644 --- a/packages/platform/common/src/services/PlatformTest.ts +++ b/packages/platform/common/src/services/PlatformTest.ts @@ -1,5 +1,5 @@ import {Type} from "@tsed/core"; -import {DITest, InjectorService} from "@tsed/di"; +import {DITest, hasInjector, injector, InjectorService} from "@tsed/di"; import accepts from "accepts"; import type {IncomingMessage, RequestListener, ServerResponse} from "http"; @@ -18,7 +18,7 @@ export class PlatformTest extends DITest { public static adapter: Type; static async create(settings: Partial = {}) { - DITest.injector = PlatformTest.createInjector(getConfiguration(settings)); + PlatformTest.createInjector(getConfiguration(settings)); await DITest.createContainer(); } @@ -56,18 +56,9 @@ export class PlatformTest extends DITest { settings.adapter = adapter as any; const configuration = getConfiguration(settings, mod); - const disableComponentsScan = configuration.disableComponentsScan || !!process.env.WEBPACK; - - if (!disableComponentsScan) { - const {importProviders} = await import("@tsed/components-scan"); - await importProviders(configuration); - } instance = await PlatformBuilder.build(mod, configuration).bootstrap(); await instance.listen(!!listen); - - // used by inject method - DITest.injector = instance.injector; }; } @@ -79,20 +70,21 @@ export class PlatformTest extends DITest { * * an array of Service dependency injection tokens, * * a test function whose parameters correspond exactly to each item in the injection token array. * + * @deprecated use PlatformTest.injector.invoke instead * @param targets * @param func */ static inject(targets: any[], func: (...args: any[]) => Promise | T): () => Promise { return async (): Promise => { - if (!DITest.hasInjector()) { + if (!hasInjector()) { await PlatformTest.create(); } - const injector: InjectorService = DITest.injector; + const inj: InjectorService = injector(); const deps = []; for (const target of targets) { - deps.push(injector.has(target) ? injector.get(target) : await injector.invoke(target)); + deps.push(inj.has(target) ? inj.get(target) : await inj.invoke(target)); } return func(...deps); @@ -116,7 +108,7 @@ export class PlatformTest extends DITest { * ``` */ static callback(): RequestListener { - return DITest.injector.get(PlatformApplication)?.callback() as any; + return injector().get(PlatformApplication)?.callback() as any; } static createRequest(options: any = {}): any { @@ -148,8 +140,8 @@ export class PlatformTest extends DITest { const $ctx = new PlatformContext({ id: "id", - injector: DITest.injector, - logger: DITest.injector.logger, + injector: injector(), + logger: injector().logger, url: "/", ...options, event diff --git a/packages/platform/common/src/utils/createInjector.ts b/packages/platform/common/src/utils/createInjector.ts index ed6966025c4..6b4524ba304 100644 --- a/packages/platform/common/src/utils/createInjector.ts +++ b/packages/platform/common/src/utils/createInjector.ts @@ -1,5 +1,5 @@ import {toMap, Type} from "@tsed/core"; -import {$injector, ProviderOpts, setLoggerConfiguration} from "@tsed/di"; +import {injector, ProviderOpts, setLoggerConfiguration} from "@tsed/di"; import {$log} from "@tsed/logger"; import {PlatformConfiguration} from "../config/services/PlatformConfiguration.js"; @@ -26,33 +26,33 @@ interface CreateInjectorOptions { } export function createInjector({adapter, settings = {}}: CreateInjectorOptions) { - const injector = $injector(); - injector.addProvider(PlatformConfiguration); + const inj = injector(); + inj.addProvider(PlatformConfiguration); - injector.settings = injector.invoke(PlatformConfiguration); - injector.logger = $log; - injector.settings.set(settings); + inj.settings = inj.invoke(PlatformConfiguration); + inj.logger = $log; + inj.settings.set(settings); if (adapter) { - injector.addProvider(PlatformAdapter, { + inj.addProvider(PlatformAdapter, { useClass: adapter }); } - injector.invoke(PlatformAdapter); - injector.alias(PlatformAdapter, "PlatformAdapter"); + inj.invoke(PlatformAdapter); + inj.alias(PlatformAdapter, "PlatformAdapter"); - setLoggerConfiguration(injector); + setLoggerConfiguration(inj); - const instance = injector.get(PlatformAdapter)!; + const instance = inj.get(PlatformAdapter)!; instance.providers = [...DEFAULT_PROVIDERS, ...instance.providers]; toMap(instance.providers, "provide").forEach((provider, token) => { - injector.addProvider(token, provider); + inj.addProvider(token, provider); }); - injector.invoke(PlatformApplication); + inj.invoke(PlatformApplication); - return injector; + return inj; } diff --git a/packages/security/oidc-provider/src/OidcModule.ts b/packages/security/oidc-provider/src/OidcModule.ts index 2f79f2c7075..da1126b5825 100644 --- a/packages/security/oidc-provider/src/OidcModule.ts +++ b/packages/security/oidc-provider/src/OidcModule.ts @@ -1,5 +1,5 @@ import {PlatformApplication} from "@tsed/common"; -import {$injector, constant, inject, Module} from "@tsed/di"; +import {constant, inject, injector, Module} from "@tsed/di"; import koaMount from "koa-mount"; import {OidcAdapters} from "./services/OidcAdapters.js"; @@ -44,14 +44,14 @@ export class OidcModule { } $onReady() { - const injector = $injector(); + const inj = injector(); - if (this.oidcProvider.hasConfiguration() && "getBestHost" in injector.settings) { + if (this.oidcProvider.hasConfiguration() && "getBestHost" in inj.settings) { // @ts-ignore const host = injector.settings.getBestHost(); const url = host.toString(); - injector.logger.info(`WellKnown is available on ${url}/.well-known/openid-configuration`); + inj.logger.info(`WellKnown is available on ${url}/.well-known/openid-configuration`); } } From 82019db45d474bf83af0d480137e7b9047f202df Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Wed, 9 Oct 2024 08:08:24 +0200 Subject: [PATCH 6/6] fix(di): fix DI.invoke when inject() is used on nested class --- .../di/src/common/decorators/inject.spec.ts | 26 +++ packages/di/src/common/fn/inject.spec.ts | 26 ++- packages/di/src/common/fn/inject.ts | 7 +- packages/di/src/common/fn/localsContainer.ts | 18 +- .../di/src/common/services/InjectorService.ts | 1 - .../src/builder/PlatformBuilder.spec.ts | 164 +++++++----------- .../platform-router/vitest.config.mts | 4 +- .../src/PlatformServerlessTest.ts | 13 +- .../security/oidc-provider/src/OidcModule.ts | 2 +- 9 files changed, 148 insertions(+), 113 deletions(-) diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index dcd64dc2864..a1c3f9f9c9d 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -1,12 +1,26 @@ import {catchAsyncError} from "@tsed/core"; import {DITest} from "../../node/index.js"; +import {inject} from "../fn/inject.js"; import {injector} from "../fn/injector.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "../services/InjectorService.js"; import {Inject} from "./inject.js"; import {Injectable} from "./injectable.js"; +@Injectable() +class ProvidersList extends Map {} + +@Injectable() +class MyService { + @Inject(ProvidersList) + providersList: ProvidersList; + + getValue() { + return this.providersList.get("key"); + } +} + describe("@Inject()", () => { beforeEach(() => DITest.create()); afterEach(() => DITest.reset()); @@ -279,4 +293,16 @@ describe("@Inject()", () => { expect(error?.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); }); }); + it("should rebuild all dependencies using invoke", async () => { + const providersList = inject(ProvidersList); + const myService = inject(MyService); + providersList.set("key", "value"); + + expect(inject(ProvidersList).get("key")).toEqual("value"); + expect(myService.getValue()).toEqual("value"); + + const newMyService = await DITest.invoke(MyService, []); + expect(newMyService.getValue()).toEqual(undefined); + expect(myService.getValue()).toEqual("value"); + }); }); diff --git a/packages/di/src/common/fn/inject.spec.ts b/packages/di/src/common/fn/inject.spec.ts index 532bb89728d..7dfff62187f 100644 --- a/packages/di/src/common/fn/inject.spec.ts +++ b/packages/di/src/common/fn/inject.spec.ts @@ -1,7 +1,20 @@ import {DITest} from "../../node/index.js"; +import {Injectable} from "../decorators/injectable.js"; import {InjectorService} from "../services/InjectorService.js"; import {inject} from "./inject.js"; +@Injectable() +class ProvidersList extends Map {} + +@Injectable() +class MyService { + readonly providersList = inject(ProvidersList); + + getValue() { + return this.providersList.get("key"); + } +} + describe("inject()", () => { beforeEach(() => DITest.create()); afterEach(() => DITest.reset()); @@ -26,5 +39,16 @@ describe("inject()", () => { } ]); }); - it("should rebuild all dependencies using invoke", async () => {}); + it("should rebuild all dependencies using invoke", async () => { + const providersList = inject(ProvidersList); + const myService = inject(MyService); + providersList.set("key", "value"); + + expect(inject(ProvidersList).get("key")).toEqual("value"); + expect(myService.getValue()).toEqual("value"); + + const newMyService = await DITest.invoke(MyService, []); + expect(newMyService.getValue()).toEqual(undefined); + expect(myService.getValue()).toEqual("value"); + }); }); diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts index e248569c9f6..6394c16d855 100644 --- a/packages/di/src/common/fn/inject.ts +++ b/packages/di/src/common/fn/inject.ts @@ -1,7 +1,7 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; import {injector} from "./injector.js"; -import {localsContainer} from "./localsContainer.js"; +import {invokeOptions, localsContainer} from "./localsContainer.js"; /** * Inject a provider to another provider. @@ -21,5 +21,8 @@ import {localsContainer} from "./localsContainer.js"; * @decorator */ export function inject(token: TokenProvider, opts?: Partial>): T { - return injector().invoke(token, opts?.locals || localsContainer(), opts); + return injector().invoke(token, opts?.locals || localsContainer(), { + ...opts, + ...invokeOptions() + }); } diff --git a/packages/di/src/common/fn/localsContainer.ts b/packages/di/src/common/fn/localsContainer.ts index c594f2a624b..9640a3719c8 100644 --- a/packages/di/src/common/fn/localsContainer.ts +++ b/packages/di/src/common/fn/localsContainer.ts @@ -4,12 +4,19 @@ import {InjectorService} from "../services/InjectorService.js"; import {injector} from "./injector.js"; let globalLocals: LocalsContainer | undefined; +let globalInvOpts: any = {}; const stagedLocals: LocalsContainer[] = []; /** * Get the locals container initiated by DITest or .bootstrap() method. */ -export function localsContainer({providers}: {providers?: UseImportTokenProviderOpts[]; rebuild?: boolean} = {}) { +export function localsContainer({ + providers, + rebuild +}: { + providers?: UseImportTokenProviderOpts[]; + rebuild?: boolean; +} = {}) { if (!globalLocals || providers) { globalLocals = new LocalsContainer(); @@ -20,17 +27,26 @@ export function localsContainer({providers}: {providers?: UseImportTokenProvider globalLocals.set(InjectorService, injector()); } + + if (rebuild) { + globalInvOpts.rebuild = rebuild; + } } return globalLocals; } +export function invokeOptions() { + return {...globalInvOpts}; +} + /** * Reset the locals container. */ export function detachLocalsContainer() { globalLocals && stagedLocals.push(globalLocals); globalLocals = undefined; + globalInvOpts = {}; } export function cleanAllLocalsContainer() { diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 1b9780abed9..f619fe2df78 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -507,7 +507,6 @@ export class InjectorService extends Container { Reflect.defineProperty(instance, DI_INVOKE_OPTIONS, { get: () => ({rebuild: options.rebuild, locals}) }); - // TODO add a way to notify DI consumer when a class instance is build } return instance; diff --git a/packages/platform/common/src/builder/PlatformBuilder.spec.ts b/packages/platform/common/src/builder/PlatformBuilder.spec.ts index 469f61add51..24a83d37762 100644 --- a/packages/platform/common/src/builder/PlatformBuilder.spec.ts +++ b/packages/platform/common/src/builder/PlatformBuilder.spec.ts @@ -1,5 +1,5 @@ import {catchAsyncError, Type} from "@tsed/core"; -import {Configuration, Controller, Injectable, injector, InjectorService, Module} from "@tsed/di"; +import {Configuration, configuration, Controller, destroyInjector, Injectable, injector, Module} from "@tsed/di"; import {AfterInit} from "../interfaces/AfterInit.js"; import {AfterListen} from "../interfaces/AfterListen.js"; @@ -105,9 +105,6 @@ class ServerModule implements BeforeInit, AfterInit, BeforeRoutesInit, AfterRout } describe("PlatformBuilder", () => { - beforeEach(() => { - injector().destroy(); - }); describe("loadStatics()", () => { it("should loadStatics", async () => { // WHEN @@ -219,31 +216,10 @@ describe("PlatformBuilder", () => { ); }); }); - describe("static boostrap()", () => { - beforeAll(() => { - vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterListen").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined); - vi.spyOn(PlatformBuilder.prototype, "loadStatics"); - // @ts-ignore - vi.spyOn(PlatformBuilder.prototype, "listenServers"); - vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); - vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined); - }); - it("should boostrap a custom platform", async () => { - const result = await PlatformBuilder.bootstrap(ServerModule, { - adapter: FakeAdapter - }); - - expect(result).toBeInstanceOf(PlatformBuilder); - }); - }); - describe("static create()", () => { + describe("boostrap", () => { beforeEach(() => { + destroyInjector(); + const inj = injector(); vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined); vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined); vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined); @@ -254,89 +230,82 @@ describe("PlatformBuilder", () => { vi.spyOn(PlatformBuilder.prototype, "loadStatics"); // @ts-ignore vi.spyOn(PlatformBuilder.prototype, "listenServers"); - vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined); }); - afterAll(() => { - vi.resetAllMocks(); + + describe("static boostrap()", () => { + it("should boostrap a custom platform", async () => { + const result = await PlatformBuilder.bootstrap(ServerModule, { + adapter: FakeAdapter + }); + + expect(result).toBeInstanceOf(PlatformBuilder); + }); }); - it("should boostrap a custom platform", () => { - const platform = PlatformBuilder.create(ServerModule, { - adapter: FakeAdapter + describe("static create()", () => { + it("should boostrap a custom platform", () => { + PlatformBuilder.create(ServerModule, { + adapter: FakeAdapter + }); + + expect(configuration().get("httpPort")).toEqual(false); + expect(configuration().get("httpsPort")).toEqual(false); }); - - expect(platform.settings.get("httpPort")).toEqual(false); - expect(platform.settings.get("httpsPort")).toEqual(false); }); - }); - describe("bootstrap()", () => { - it("should bootstrap platform", async () => { - // WHEN - const stub = ServerModule.prototype.$beforeRoutesInit; - const server = await PlatformCustom.bootstrap(ServerModule, { - httpPort: false, - httpsPort: false + describe("bootstrap()", () => { + it("should bootstrap platform", async () => { + // WHEN + const spyOn = vi.spyOn(injector().hooks, "asyncEmit").mockResolvedValue(undefined); + const stub = ServerModule.prototype.$beforeRoutesInit; + const server = await PlatformCustom.bootstrap(ServerModule, { + httpPort: false, + httpsPort: false + }); + // THEN + await server.listen(); + + // THEN + // @ts-ignore + expect(server.listenServers).toHaveBeenCalledWith(); + expect(server.loadStatics).toHaveBeenCalledWith("$beforeRoutesInit"); + expect(server.loadStatics).toHaveBeenCalledWith("$afterRoutesInit"); + expect(spyOn).toHaveBeenCalledWith("$afterInit", []); + expect(spyOn).toHaveBeenCalledWith("$beforeRoutesInit", []); + expect(spyOn).toHaveBeenCalledWith("$afterRoutesInit", []); + expect(spyOn).toHaveBeenCalledWith("$afterListen", []); + expect(spyOn).toHaveBeenCalledWith("$beforeListen", []); + expect(spyOn).toHaveBeenCalledWith("$onServerReady", []); + expect(spyOn).toHaveBeenCalledWith("$onReady", []); + + // THEN + expect(server.rootModule).toBeInstanceOf(ServerModule); + expect(stub).toHaveBeenCalled(); + expect(server.name).toEqual("custom"); + + await server.stop(); + expect(spyOn).toHaveBeenCalledWith("$onDestroy", []); }); + }); + describe("adapter()", () => { + it("should boostrap a custom platform", async () => { + const platformBuilder = await PlatformBuilder.bootstrap(ServerModule, { + adapter: FakeAdapter + }); - // THEN - await server.listen(); + expect(platformBuilder.callback()).toBeInstanceOf(Function); - // THEN - // @ts-ignore - expect(server.listenServers).toHaveBeenCalledWith(); - expect(server.loadStatics).toHaveBeenCalledWith("$beforeRoutesInit"); - expect(server.loadStatics).toHaveBeenCalledWith("$afterRoutesInit"); - expect(server.injector.emit).toHaveBeenCalledWith("$afterInit"); - expect(server.injector.emit).toHaveBeenCalledWith("$beforeRoutesInit"); - expect(server.injector.emit).toHaveBeenCalledWith("$afterRoutesInit"); - expect(server.injector.emit).toHaveBeenCalledWith("$afterListen"); - expect(server.injector.emit).toHaveBeenCalledWith("$beforeListen"); - expect(server.injector.emit).toHaveBeenCalledWith("$onServerReady"); - expect(server.injector.emit).toHaveBeenCalledWith("$onReady"); - - // THEN - expect(server.rootModule).toBeInstanceOf(ServerModule); - expect(stub).toHaveBeenCalled(); - expect(server.name).toEqual("custom"); - - await server.stop(); - expect(server.injector.emit).toHaveBeenCalledWith("$onDestroy"); - }); - }); - describe("adapter()", () => { - beforeAll(() => { - vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$afterListen").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined); - vi.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined); - vi.spyOn(PlatformBuilder.prototype, "loadStatics").mockResolvedValue(undefined as never); - // @ts-ignore - vi.spyOn(PlatformBuilder.prototype, "listenServers"); - vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined); - vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined); - }); - it("should boostrap a custom platform", async () => { - const platformBuilder = await PlatformBuilder.bootstrap(ServerModule, { - adapter: FakeAdapter + expect(platformBuilder.adapter).toBeInstanceOf(FakeAdapter); }); - expect(platformBuilder.callback()).toBeInstanceOf(Function); - - expect(platformBuilder.adapter).toBeInstanceOf(FakeAdapter); - }); + it("should listen a custom platform", async () => { + const platform = await PlatformBuilder.create(ServerModule, { + adapter: FakeAdapter + }); - it("should listen a custom platform", async () => { - const platform = await PlatformBuilder.create(ServerModule, { - adapter: FakeAdapter + await platform.listen(); }); - - await platform.listen(); }); }); - describe("useProvider()", () => { it("should add provider", async () => { // WHEN @@ -356,6 +325,7 @@ describe("PlatformBuilder", () => { }); }); describe("addControllers", () => { + beforeEach(() => destroyInjector()); it("should add controllers", async () => { // GIVEN const server = await PlatformCustom.bootstrap(ServerModule, {}); diff --git a/packages/platform/platform-router/vitest.config.mts b/packages/platform/platform-router/vitest.config.mts index ff3e1c550f8..f8d5e5aa841 100644 --- a/packages/platform/platform-router/vitest.config.mts +++ b/packages/platform/platform-router/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 94.92, + branches: 94.89, functions: 100, lines: 100 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts index d33203121ea..14aaab58df9 100644 --- a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts +++ b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts @@ -1,6 +1,6 @@ -import {PlatformBuilder, PlatformBuilderSettings} from "@tsed/common"; +import type {PlatformBuilder, PlatformBuilderSettings} from "@tsed/common"; import {nameOf, Type} from "@tsed/core"; -import {DITest} from "@tsed/di"; +import {destroyInjector, DITest, hasInjector} from "@tsed/di"; import {APIGatewayEventDefaultAuthorizerContext, APIGatewayProxyEventBase, APIGatewayProxyHandler} from "aws-lambda"; import {APIGatewayProxyResult} from "aws-lambda/trigger/api-gateway-proxy.js"; @@ -153,7 +153,7 @@ export class PlatformServerlessTest extends DITest { static request = LambdaClientRequest; static bootstrap( - serverless: {bootstrap: (server: Type, settings: PlatformBuilderSettings) => PlatformBuilder}, + serverless: {bootstrap: (server: Type, settings: TsED.Configuration) => PlatformBuilder}, {server, ...settings}: PlatformBuilderSettings & {server: Type} ): () => Promise; static bootstrap( @@ -177,8 +177,6 @@ export class PlatformServerlessTest extends DITest { } PlatformServerlessTest.callbacks.handler = instance.handler(); - // used by inject method - DITest.injector = instance.injector; return instance.promise; }; @@ -191,9 +189,8 @@ export class PlatformServerlessTest extends DITest { if (PlatformServerlessTest.instance) { await PlatformServerlessTest.instance.stop(); } - if (DITest.hasInjector()) { - await DITest.injector.destroy(); - DITest._injector = null; + if (hasInjector()) { + await destroyInjector(); } } } diff --git a/packages/security/oidc-provider/src/OidcModule.ts b/packages/security/oidc-provider/src/OidcModule.ts index da1126b5825..443e05babc5 100644 --- a/packages/security/oidc-provider/src/OidcModule.ts +++ b/packages/security/oidc-provider/src/OidcModule.ts @@ -48,7 +48,7 @@ export class OidcModule { if (this.oidcProvider.hasConfiguration() && "getBestHost" in inj.settings) { // @ts-ignore - const host = injector.settings.getBestHost(); + const host = inj.settings.getBestHost(); const url = host.toString(); inj.logger.info(`WellKnown is available on ${url}/.well-known/openid-configuration`);