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`); } }