From 9babaf64e4e3ceb3ddff92783dc4f36639534145 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Tue, 3 Sep 2024 08:19:15 +0200 Subject: [PATCH] feat(di): add inject() function to inject provider on prop using angular style --- packages/core/src/domain/Type.ts | 6 +- .../core/src/utils/objects/descriptorOf.ts | 2 +- packages/di/package.json | 3 +- packages/di/src/common/constants/constants.ts | 6 +- .../common/decorators/autoInjectable.spec.ts | 5 +- .../src/common/decorators/autoInjectable.ts | 30 +- packages/di/src/common/decorators/constant.ts | 2 +- .../di/src/common/decorators/inject.spec.ts | 362 ++++++++++-------- packages/di/src/common/decorators/inject.ts | 185 +++++---- .../src/common/decorators/intercept.spec.ts | 4 +- .../di/src/common/decorators/intercept.ts | 14 +- .../di/src/common/decorators/lazyInject.ts | 34 +- .../decorators/overrideProvider.spec.ts | 3 +- packages/di/src/common/decorators/useOpts.ts | 8 +- packages/di/src/common/domain/Container.ts | 12 +- packages/di/src/common/domain/Provider.ts | 12 +- .../src/common/errors/UndefinedTokenError.ts | 9 - packages/di/src/common/index.ts | 2 - .../interfaces/ImportTokenProviderOpts.ts | 4 +- .../common/interfaces/InjectableProperties.ts | 27 -- .../di/src/common/interfaces/InvokeOptions.ts | 11 +- .../di/src/common/interfaces/ProviderOpts.ts | 2 +- .../src/common/interfaces/RegistrySettings.ts | 3 +- .../interfaces/ResolvedInvokeOptions.ts | 2 +- .../di/src/common/interfaces/TokenProvider.ts | 13 +- .../common/registries/GlobalProviders.spec.ts | 4 +- .../src/common/registries/GlobalProviders.ts | 4 +- .../di/src/common/services/DILogger.spec.ts | 2 +- .../common/services/InjectorService.spec.ts | 162 +------- .../di/src/common/services/InjectorService.ts | 279 +++----------- .../src/node/decorators/injectContext.spec.ts | 36 ++ .../di/src/node/decorators/injectContext.ts | 19 +- .../di/src/node/services/DILogger.spec.ts | 2 +- packages/di/src/node/services/DITest.ts | 25 +- packages/di/vitest.config.mts | 10 +- .../src/services/TypeGraphQLService.ts | 2 +- .../orm/adapters/src/services/Adapters.ts | 8 +- .../orm/ioredis/src/utils/mockConnections.ts | 2 +- .../src/utils/registerConnectionProvider.ts | 2 +- packages/orm/mikro-orm/src/MikroOrmModule.ts | 2 - .../src/decorators/entityManager.spec.ts | 4 +- .../orm/mikro-orm/src/decorators/orm.spec.ts | 4 +- .../src/decorators/transactional.spec.ts | 4 +- packages/orm/typeorm/src/TypeORMModule.ts | 4 +- .../common/src/builder/PlatformBuilder.ts | 2 +- .../common/src/utils/createHttpServer.spec.ts | 10 +- .../src/utils/createHttpsServer.spec.ts | 10 +- packages/platform/platform-cache/src/index.ts | 1 - .../src/utils/getInterceptorOptions.ts | 7 - .../platform-cache/src/utils/getPrefix.ts | 5 +- .../platform/platform-koa/vitest.config.mts | 4 +- .../src/decorators/responseFilter.ts | 2 +- .../oidc-provider/src/OidcModule.spec.ts | 8 +- .../swagger/src/services/SwaggerService.ts | 3 +- yarn.lock | 1 + 55 files changed, 605 insertions(+), 784 deletions(-) delete mode 100644 packages/di/src/common/errors/UndefinedTokenError.ts delete mode 100644 packages/di/src/common/interfaces/InjectableProperties.ts create mode 100644 packages/di/src/node/decorators/injectContext.spec.ts delete mode 100644 packages/platform/platform-cache/src/utils/getInterceptorOptions.ts diff --git a/packages/core/src/domain/Type.ts b/packages/core/src/domain/Type.ts index b587826a6df..280547df030 100644 --- a/packages/core/src/domain/Type.ts +++ b/packages/core/src/domain/Type.ts @@ -1,5 +1,5 @@ /** - * An example of a `Type` is `MyCustomComponent` filters, which in JavaScript is be represented by + * An example of a `Type` is `MyCustomComponent` filters, which in JavaScript is represented by * the `MyCustomComponent` constructor function. */ // tslint:disable-next-line: variable-name @@ -14,3 +14,7 @@ export const Type = Function; // @ts-ignore global.Type = Type; + +export interface AbstractType extends Function { + prototype: T; +} diff --git a/packages/core/src/utils/objects/descriptorOf.ts b/packages/core/src/utils/objects/descriptorOf.ts index fc2755b3513..83dcd65372d 100644 --- a/packages/core/src/utils/objects/descriptorOf.ts +++ b/packages/core/src/utils/objects/descriptorOf.ts @@ -5,7 +5,7 @@ * @returns {PropertyDescriptor} */ export function descriptorOf(target: any, propertyKey: string | symbol): PropertyDescriptor { - return Object.getOwnPropertyDescriptor((target && target.prototype) || target, propertyKey)!; + return Reflect.getOwnPropertyDescriptor((target && target.prototype) || target, propertyKey)!; } export function isMethodDescriptor(target: any, propertyKey: string | symbol) { diff --git a/packages/di/package.json b/packages/di/package.json index 21c1bc178c2..7dce8904da7 100644 --- a/packages/di/package.json +++ b/packages/di/package.json @@ -26,7 +26,8 @@ "test:ci": "vitest run --coverage.thresholds.autoUpdate=true" }, "dependencies": { - "tslib": "2.6.2" + "tslib": "2.6.2", + "uuid": "9.0.1" }, "devDependencies": { "@tsed/barrels": "workspace:*", diff --git a/packages/di/src/common/constants/constants.ts b/packages/di/src/common/constants/constants.ts index b0732aaea65..48e5522d9e3 100644 --- a/packages/di/src/common/constants/constants.ts +++ b/packages/di/src/common/constants/constants.ts @@ -1,3 +1,5 @@ -export const INJECTABLE_PROP = "DI:INJECTABLE_PROP"; -export const DI_PARAMS = "DI:PARAMS"; +export const DI_INVOKE_OPTIONS = Symbol("DI:INVOKE_OPTIONS"); +export const DI_INJECTABLE_PROPS = Symbol("DI_INJECTABLE_PROPS"); +export const DI_INJECTABLE_PROP = "DI:DI_INJECTABLE_PROP"; export const DI_PARAM_OPTIONS = "DI:PARAM:OPTIONS"; +export const DI_INTERCEPTOR_OPTIONS = "DI:INTERCEPTOR:OPTIONS"; diff --git a/packages/di/src/common/decorators/autoInjectable.spec.ts b/packages/di/src/common/decorators/autoInjectable.spec.ts index 8bd1f8ba032..21cb69697bd 100644 --- a/packages/di/src/common/decorators/autoInjectable.spec.ts +++ b/packages/di/src/common/decorators/autoInjectable.spec.ts @@ -106,14 +106,15 @@ describe("AutoInjectable", () => { logger: Logger; private value: string; + instances?: InterfaceGroup[]; constructor(initialValue: string, @Inject(TOKEN_GROUPS) instances?: InterfaceGroup[]) { this.value = initialValue; - expect(instances).toHaveLength(3); + this.instances = instances; } } - new Test("test"); + expect(new Test("test").instances).toHaveLength(3); }); it("should return a class that extends the original class (with 3 arguments)", () => { @AutoInjectable() diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index 519e6f55a52..5363f2f6955 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -1,13 +1,37 @@ +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"; + +function resolveAutoInjectableArgs(token: Type, args: unknown[]) { + const locals = new LocalsContainer(); + const injector = InjectorService.getInstance(); + const deps: TokenProvider[] = getConstructorDependencies(token); + const list: any[] = []; + const length = Math.max(deps.length, args.length); + + for (let i = 0; i < length; i++) { + if (args[i] !== undefined) { + 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}); + + list.push(instance); + } + } + + return list; +} export function AutoInjectable() { return }>(constr: T): T => { return class AutoInjectable extends constr { constructor(...args: any[]) { - const locals = new LocalsContainer(); - super(...InjectorService.resolveAutoInjectableArgs(constr, locals, args)); - InjectorService.bind(this, locals); + super(...resolveAutoInjectableArgs(constr, args)); } } as unknown as T; }; diff --git a/packages/di/src/common/decorators/constant.ts b/packages/di/src/common/decorators/constant.ts index f595795f2c8..0e1022d5aa3 100644 --- a/packages/di/src/common/decorators/constant.ts +++ b/packages/di/src/common/decorators/constant.ts @@ -8,7 +8,7 @@ export function constant(expression: string, defaultValue?: Type | undefin } export function bindConstant(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) { - let symbol = Symbol(); + const symbol = Symbol(); catchError(() => Reflect.deleteProperty(target, propertyKey)); Reflect.defineProperty(target, propertyKey, { diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index 358fa4bba71..cf33242b15a 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -1,186 +1,122 @@ -import {descriptorOf} from "@tsed/core"; +import {DITest} from "../../node/index.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "../services/InjectorService.js"; -import {Inject} from "./inject.js"; +import {inject, Inject} from "./inject.js"; import {Injectable} from "./injectable.js"; -describe("@Inject()", () => { - describe("used on unsupported decorator type", () => { - it("should store metadata", () => { - // GIVEN - class Test { - test() {} - } - - // WHEN - let actualError; - try { - Inject()(Test, "test", descriptorOf(Test, "test")); - } catch (er) { - actualError = er; - } - - // THEN - expect(actualError.message).toEqual("Inject cannot be used as method.static decorator on Test.test"); - }); - }); - - describe("@property", () => { - it("should inject service", async () => { - // GIVEN - @Injectable() - class Test { - @Inject() - test: InjectorService; - } +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 {} - const injector = new InjectorService(); - const instance = await injector.invoke(Test); + class Test { + readonly injector = inject(InjectorService); + readonly nested = inject(Nested); - expect(instance).toBeInstanceOf(Test); - expect(instance.test).toBeInstanceOf(InjectorService); - }); - it("should inject service w/ 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")); + constructor() { + expect(this.injector).toBeInstanceOf(InjectorService); + expect(this.nested).not.toBeInstanceOf(Nested); + } } + + await DITest.invoke(Test, [ + { + token: Nested, + use: {} + } + ]); }); + }); + }); - @Injectable() - class Parent1 { - @Inject(TokenAsync) - test: Test; - } + describe("@Inject()", () => { + describe("when the decorator used on property", () => { + it("should inject service", async () => { + // GIVEN + @Injectable() + class Test { + @Inject() + test: InjectorService; + } - @Injectable() - class Parent2 { - @Inject(TokenAsync) - test: Test; - } + const injector = new InjectorService(); + const instance = await injector.invoke(Test); - const injector = new InjectorService(); + expect(instance).toBeInstanceOf(Test); + expect(instance.test).toBeInstanceOf(InjectorService); + }); + it("should inject service and async factory", async () => { + // GIVEN + class Test { + constructor(public type: string) {} + } - await injector.load(); + const TokenAsync = Symbol.for("MyService"); - const parent1 = await injector.invoke(Parent1); - const parent2 = await injector.invoke(Parent2); + registerProvider({ + provide: TokenAsync, + type: "test:async", + deps: [], + useAsyncFactory() { + return Promise.resolve(new Test("async")); + } + }); - expect(parent1.test).toBeInstanceOf(Test); - expect(parent2.test).toBeInstanceOf(Test); - }); - it("should inject service with the given type", async () => { - // GIVEN - @Injectable() - class Test { - @Inject(InjectorService, (bean: any) => bean.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" - }); + @Injectable() + class Parent1 { + @Inject(TokenAsync) + test: Test; } - }); - @Injectable() - class MyInjectable { - @Inject(TOKEN_GROUPS) - instances: InterfaceGroup[]; - } + @Injectable() + class Parent2 { + @Inject(TokenAsync) + test: Test; + } - const injector = new InjectorService(); + const injector = new InjectorService(); - await injector.load(); + await injector.load(); - const instance = await injector.invoke(MyInjectable); + const parent1 = await injector.invoke(Parent1); + const parent2 = await injector.invoke(Parent2); - 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 { + 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() - test: Object; + @Inject(InjectorService, {transform: (instance) => instance.get(InjectorService)}) + test: InjectorService; } - } catch (er) { - expect(er.message).toContain("Object isn't a valid token. Please check the token set on Test.test"); - } - }); - }); - describe("@constructorParameters", () => { - describe("when token is given on constructor", () => { - it("should inject the expected provider", async () => { + 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 MyInjectable { - constructor(@Inject(InjectorService) readonly injector: InjectorService) {} + class Test { + @Inject(InjectorService, (instance) => instance.get(InjectorService)) + test: InjectorService; } const injector = new InjectorService(); - const instance = await injector.invoke(MyInjectable); + const instance = await injector.invoke(Test); - expect(instance.injector).toBeInstanceOf(InjectorService); + expect(instance).toBeInstanceOf(Test); + expect(instance.test).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"); + it("should inject many services", async () => { + const TOKEN_GROUPS = Symbol.for("groups:1"); interface InterfaceGroup { type: string; @@ -192,7 +128,7 @@ describe("@Inject()", () => { class MyService1 implements InterfaceGroup { readonly type: string = "service1"; - constructor(@Inject(InjectorService) readonly injector: InjectorService) {} + constructor(@Inject(InjectorService) readonly injector: any) {} } @Injectable({ @@ -201,10 +137,10 @@ describe("@Inject()", () => { class MyService2 implements InterfaceGroup { readonly type: string = "service2"; - constructor(@Inject(InjectorService) readonly injector: InjectorService) {} + constructor(@Inject(InjectorService) readonly injector: any) {} } - const TokenAsync = Symbol.for("MyService1"); + const TokenAsync = Symbol.for("MyService2"); registerProvider({ provide: TokenAsync, @@ -219,7 +155,8 @@ describe("@Inject()", () => { @Injectable() class MyInjectable { - constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} + @Inject(TOKEN_GROUPS) + instances: InterfaceGroup[]; } const injector = new InjectorService(); @@ -234,6 +171,119 @@ 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(); + + const instance = await injector.invoke(MyInjectable); + + expect(instance.instances).toBeInstanceOf(Array); + expect(instance.instances).toHaveLength(3); + expect(instance.instances[0].type).toEqual("service1"); + expect(instance.instances[1].type).toEqual("service2"); + expect(instance.instances[2].type).toEqual("async"); + }); + }); }); }); }); diff --git a/packages/di/src/common/decorators/inject.ts b/packages/di/src/common/decorators/inject.ts index e0d95487526..783c04e685f 100644 --- a/packages/di/src/common/decorators/inject.ts +++ b/packages/di/src/common/decorators/inject.ts @@ -1,22 +1,107 @@ -import {decoratorTypeOf, DecoratorTypes, isPromise, Metadata, Store, UnsupportedDecoratorType} from "@tsed/core"; -import {DI_PARAM_OPTIONS, INJECTABLE_PROP} from "../constants/constants.js"; +import {catchError, decoratorTypeOf, DecoratorTypes, isPromise, Metadata, Store} from "@tsed/core"; +import {DI_INJECTABLE_PROP, DI_INJECTABLE_PROPS, DI_INVOKE_OPTIONS} from "../constants/constants.js"; import {InvalidPropertyTokenError} from "../errors/InvalidPropertyTokenError.js"; -import type {InjectablePropertyOptions} from "../interfaces/InjectableProperties.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"; -export function injectProperty(target: any, propertyKey: string, options: Partial) { - Store.from(target).merge(INJECTABLE_PROP, { - [propertyKey]: { - bindingType: DecoratorTypes.PROP, - propertyKey, - ...options +export function inject(token: TokenProvider, opts?: Partial>): T { + return InjectorService.getInstance().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); +} + +function setToken( + token: TokenProvider, + { + target, + propertyKey, + parameterIndex + }: { + target: any; + propertyKey: string | symbol | undefined; + parameterIndex: number; + } +) { + const paramTypes = getConstructorDependencies(target, propertyKey); + const type = paramTypes[parameterIndex]; + + paramTypes[parameterIndex] = type === Array ? [token] : token; + + Metadata.setParamTypes(target, propertyKey!, paramTypes); + setConstructorDependencies(target, paramTypes); +} + +function getTokenType(token: TokenProvider | (() => TokenProvider) | undefined, target: any, propertyKey: string | symbol) { + const useType = token || Metadata.getType(target, propertyKey); + + if (useType === Object) { + throw new InvalidPropertyTokenError(target, String(propertyKey)); + } + + return useType; +} + +export type TransformInjectedProviderCB = (instance: T) => unknown; + +export interface BindInjectablePropertyOpts { + token?: TokenProvider; + useOpts?: Record; + transform?: TransformInjectedProviderCB; +} + +function bindInjectableProperty( + target: any, + propertyKey: string | symbol, + {token, transform = (o) => o, useOpts}: BindInjectablePropertyOpts +) { + const symbol = Symbol(); + + if (!target[DI_INJECTABLE_PROPS]) { + Reflect.defineProperty(target, DI_INJECTABLE_PROPS, { + value: new Set(), + enumerable: false, + configurable: false + }); + } + + target[DI_INJECTABLE_PROPS].add(propertyKey); + + catchError(() => Reflect.deleteProperty(target, propertyKey)); + Reflect.defineProperty(target, propertyKey, { + get() { + const useType = getTokenType(token, target, propertyKey!); + const originalType = Metadata.getType(target, propertyKey); + + const invokeOpts: Partial = { + rebuild: !!this[DI_INVOKE_OPTIONS]?.rebuild, + locals: this[DI_INVOKE_OPTIONS]?.locals, + useOpts: useOpts || Store.from(target, propertyKey).get(DI_INJECTABLE_PROP) + }; + + if (this[symbol] === undefined) { + this[symbol] = originalType === Array ? injectMany(token as string, invokeOpts) : inject(useType, invokeOpts); + } + + [].concat(this[symbol]).forEach((instance: any, index) => { + if (isPromise(this[symbol])) { + instance.then((result: any) => { + this[symbol]![index] = result; + if (originalType !== Array) { + this[symbol] = result; + } + }); + } + }); + + return transform(this[symbol]); } }); } -export const AWAITABLE_PROPERTIES = Symbol("INIT_PROMISES"); - /** * Inject a provider to another provider. * @@ -31,78 +116,32 @@ export const AWAITABLE_PROPERTIES = Symbol("INIT_PROMISES"); * ``` * * @param token A token provider or token provider group - * @param onGet Use the given name method to inject + * @param transform * @returns {Function} * @decorator */ -export function Inject(token?: TokenProvider | (() => TokenProvider), onGet = (bean: any) => bean): Function { - return (target: any, propertyKey: string | symbol | undefined, descriptor: TypedPropertyDescriptor | number): any | void => { - const bindingType = decoratorTypeOf([target, propertyKey, descriptor]); +export function Inject(token?: TokenProvider | (() => TokenProvider), transform?: TransformInjectedProviderCB): any; +export function Inject( + token?: TokenProvider | (() => TokenProvider), + opts?: Partial, "token">> +): any; +export function Inject( + token?: TokenProvider | (() => TokenProvider), + opts: TransformInjectedProviderCB | Partial, "token">> = {} +) { + opts = typeof opts === "function" ? {transform: opts} : opts; + + return (target: any, propertyKey: string | symbol | undefined, index?: number) => { + const bindingType = decoratorTypeOf([target, propertyKey, index]); switch (bindingType) { case DecoratorTypes.PARAM_CTOR: if (token) { - const paramTypes = getConstructorDependencies(target, propertyKey); - const type = paramTypes[descriptor as number]; - - paramTypes[descriptor as number] = type === Array ? [token] : token; - - Metadata.setParamTypes(target, propertyKey!, paramTypes); - setConstructorDependencies(target, paramTypes); + setToken(token, {target, propertyKey, parameterIndex: index!}); } break; - case DecoratorTypes.PROP: - const useType = token || Metadata.getType(target, propertyKey); - - if (useType === Object) { - throw new InvalidPropertyTokenError(target, String(propertyKey)); - } - - injectProperty(target, String(propertyKey), { - resolver(injector, locals, {options, ...invokeOptions}) { - const originalType = Metadata.getType(target, propertyKey); - locals.set(DI_PARAM_OPTIONS, {...options}); - - if (originalType === Array) { - let bean: any[] | undefined; - - if (!bean) { - bean = injector.getMany(token, locals, invokeOptions); - locals.delete(DI_PARAM_OPTIONS); - } - - bean.forEach((instance: any, index) => { - if (isPromise(bean)) { - instance.then((result: any) => { - bean![index] = result; - }); - } - }); - - return () => onGet(bean); - } - - let bean: any; - - if (!bean) { - bean = injector.invoke(useType, locals, invokeOptions); - locals.delete(DI_PARAM_OPTIONS); - } - - if (isPromise(bean)) { - bean.then((result: any) => { - bean = result; - }); - } - - return () => onGet(bean); - } - }); - break; - - default: - throw new UnsupportedDecoratorType(Inject, [target, propertyKey, descriptor]); + bindInjectableProperty(target, propertyKey!, {...opts, token}); } }; } diff --git a/packages/di/src/common/decorators/intercept.spec.ts b/packages/di/src/common/decorators/intercept.spec.ts index 861d8f58437..dd732585951 100644 --- a/packages/di/src/common/decorators/intercept.spec.ts +++ b/packages/di/src/common/decorators/intercept.spec.ts @@ -3,7 +3,7 @@ import {DITest} from "../../node/index.js"; import {InterceptorContext} from "../interfaces/InterceptorContext.js"; import {InterceptorMethods} from "../interfaces/InterceptorMethods.js"; import {InjectorService} from "../services/InjectorService.js"; -import {Intercept} from "./intercept.js"; +import {getInterceptorOptions, Intercept} from "./intercept.js"; import {Interceptor} from "./interceptor.js"; import {Service} from "./service.js"; @@ -49,6 +49,8 @@ describe("@Intercept", () => { // WHEN const result = serviceTest.exec("param data"); + expect(getInterceptorOptions(ServiceTest, "exec")).toEqual("options data"); + // THEN expect(result).toEqual("Original data - param data - options data - intercepted"); }); diff --git a/packages/di/src/common/decorators/intercept.ts b/packages/di/src/common/decorators/intercept.ts index 1999521465d..9734327d9e1 100644 --- a/packages/di/src/common/decorators/intercept.ts +++ b/packages/di/src/common/decorators/intercept.ts @@ -1,19 +1,21 @@ -import {classOf, decorateMethodsOf, DecoratorParameters, decoratorTypeOf, DecoratorTypes, Metadata, Store, Type} from "@tsed/core"; -import {AST} from "eslint"; -import {INJECTABLE_PROP} from "../constants/constants.js"; -import {InjectablePropertyType} from "../domain/InjectablePropertyType.js"; +import {classOf, decorateMethodsOf, DecoratorParameters, decoratorTypeOf, DecoratorTypes, Store, Type} from "@tsed/core"; +import {DI_INTERCEPTOR_OPTIONS} from "../constants/constants.js"; import type {InterceptorContext} from "../interfaces/InterceptorContext.js"; import type {InterceptorMethods} from "../interfaces/InterceptorMethods.js"; -import {type InjectableProperties, InjectablePropertyOptions} from "../interfaces/InjectableProperties.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {InjectorService} from "../services/InjectorService.js"; -import Token = AST.Token; + +export function getInterceptorOptions(target: Type, propertyKey: string | symbol) { + return Store.fromMethod(target, propertyKey).get(DI_INTERCEPTOR_OPTIONS) as T; +} export function bindIntercept(target: any, propertyKey: string | symbol, token: TokenProvider, options: Record) { const klass = classOf(target); const descriptor = Reflect.getOwnPropertyDescriptor(klass.prototype, propertyKey); const originalMethod = descriptor!.value; + Store.fromMethod(klass, propertyKey).set(DI_INTERCEPTOR_OPTIONS, options); + descriptor!.value = function (...args: any[]) { const next = (err?: Error) => { if (!err) { diff --git a/packages/di/src/common/decorators/lazyInject.ts b/packages/di/src/common/decorators/lazyInject.ts index fe768abad50..ec58efc5081 100644 --- a/packages/di/src/common/decorators/lazyInject.ts +++ b/packages/di/src/common/decorators/lazyInject.ts @@ -1,5 +1,5 @@ -import {importPackage} from "@tsed/core"; -import {injectProperty} from "./inject.js"; +import {catchError, importPackage} from "@tsed/core"; +import {InjectorService} from "../services/InjectorService.js"; /** * Lazy load a provider from his package and invoke only when the provider is used @@ -26,26 +26,26 @@ export function LazyInject( resolver: () => any, {optional = false, packageName = resolver.toString()}: {optional?: boolean; packageName?: string} = {} ): PropertyDecorator { - return (target: any, propertyKey: string): any | void => { + return (target: any, propertyKey: string | symbol): any | void => { let bean: any, token: any; - injectProperty(target, propertyKey, { - resolver(injector) { - return async () => { - if (!token) { - const exports = await importPackage(packageName, resolver, optional); - token = exports[key]; - if (!token) { - if (!optional) { - throw new Error(`Unable to lazy load the "${key}". The token isn\'t a valid token provider.`); - } + catchError(() => Reflect.deleteProperty(target, propertyKey)); + Reflect.defineProperty(target, propertyKey, { + async get() { + if (!token) { + const injector = InjectorService.getInstance(); + const exports = await importPackage(packageName, resolver, optional); + token = exports[key]; + if (!token) { + if (!optional) { + throw new Error(`Unable to lazy load the "${key}". The token isn\'t a valid token provider.`); } - - bean = token ? await injector.lazyInvoke(token) : {}; } - return bean; - }; + bean = token ? await injector.lazyInvoke(token) : {}; + } + + return bean; } }); }; diff --git a/packages/di/src/common/decorators/overrideProvider.spec.ts b/packages/di/src/common/decorators/overrideProvider.spec.ts index 0091ef34991..b17d8a60843 100644 --- a/packages/di/src/common/decorators/overrideProvider.spec.ts +++ b/packages/di/src/common/decorators/overrideProvider.spec.ts @@ -1,4 +1,5 @@ import {Provider} from "../domain/Provider.js"; +import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {OverrideProvider} from "./overrideProvider.js"; @@ -14,7 +15,7 @@ describe("OverrideProvider", () => { // GIVEN const provider = new Provider(Test); - vi.mocked(GlobalProviders.get).mockImplementation((token: object) => { + vi.mocked(GlobalProviders.get).mockImplementation((token: TokenProvider) => { if (token === Test) { return provider; } diff --git a/packages/di/src/common/decorators/useOpts.ts b/packages/di/src/common/decorators/useOpts.ts index ffd823a7b47..9837ba05b11 100644 --- a/packages/di/src/common/decorators/useOpts.ts +++ b/packages/di/src/common/decorators/useOpts.ts @@ -1,5 +1,5 @@ import {decoratorTypeOf, DecoratorTypes, Store, UnsupportedDecoratorType} from "@tsed/core"; -import {DI_PARAM_OPTIONS, INJECTABLE_PROP} from "../constants/constants.js"; +import {DI_PARAM_OPTIONS, DI_INJECTABLE_PROP} from "../constants/constants.js"; /** * Add options to invoke the Service. @@ -50,11 +50,7 @@ export function UseOpts(options: {[key: string]: any}): Function { break; case DecoratorTypes.PROP: - Store.from(target).merge(INJECTABLE_PROP, { - [propertyKey as string]: { - options - } - }); + Store.from(target, propertyKey).set(DI_INJECTABLE_PROP, options); break; default: diff --git a/packages/di/src/common/domain/Container.ts b/packages/di/src/common/domain/Container.ts index 7ee6ee3659c..ffbb7e7e89d 100644 --- a/packages/di/src/common/domain/Container.ts +++ b/packages/di/src/common/domain/Container.ts @@ -49,21 +49,21 @@ export class Container extends Map { * @returns {T} Returns the element associated with the specified key or undefined if the key can't be found in the Map object. * @param token */ - public getProvider(token: TokenProvider): T | undefined { - return super.get(token) as T; + public getProvider(token: TokenProvider | undefined): T | undefined { + return super.get(token!) as T; } /** * Get all providers registered in the injector container. * - * @param {ProviderType} type Filter the list by the given ProviderType. + * @param type Filter the list by the given ProviderType. * @returns {[TokenProvider , Provider][]} */ - public getProviders(type?: ProviderType | string | string[]): Provider[] { - const types = ([] as (string | ProviderType)[]).concat(type as never); + public getProviders(type?: TokenProvider | ProviderType | string | string[]): Provider[] { + const types = ([] as (string | ProviderType)[]).concat(type as never).map(String); return [...this].reduce((providers, [_, provider]) => { - if (types.includes(provider.type) || !type) { + if (types.includes(String(provider.type)) || !type) { return [...providers, provider]; } return providers; diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index d684231b2f0..ee4f55ce9f9 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -1,4 +1,4 @@ -import {classOf, getClassOrSymbol, isClass, methodsOf, nameOf, Store, Type} from "@tsed/core"; +import {type AbstractType, classOf, getClassOrSymbol, isClass, methodsOf, nameOf, Store, Type} from "@tsed/core"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {ProviderScope} from "./ProviderScope.js"; @@ -10,7 +10,7 @@ export class Provider implements ProviderOpts { /** * Token group provider to retrieve all provider from the same type */ - public type: TokenProvider | ProviderType = ProviderType.PROVIDER; + public type: ProviderType | string = ProviderType.PROVIDER; public deps: TokenProvider[]; public imports: (TokenProvider | [TokenProvider])[]; public alias?: string; @@ -25,9 +25,9 @@ export class Provider implements ProviderOpts { [key: string]: any; - constructor(token: TokenProvider, options: Partial = {}) { + constructor(token: TokenProvider, options: Partial = {}) { this.provide = token; - this.useClass = token; + this.useClass = token as Type; Object.assign(this, options); } @@ -52,10 +52,10 @@ export class Provider implements ProviderOpts { } /** - * Create a new store if the given value is a class. Otherwise the value is ignored. + * Create a new store if the given value is a class. Otherwise, the value is ignored. * @param value */ - set useClass(value: Type) { + set useClass(value: Type | AbstractType) { if (isClass(value)) { this._useClass = classOf(value); this._store = Store.from(value); diff --git a/packages/di/src/common/errors/UndefinedTokenError.ts b/packages/di/src/common/errors/UndefinedTokenError.ts deleted file mode 100644 index 8ae8646d1f4..00000000000 --- a/packages/di/src/common/errors/UndefinedTokenError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class UndefinedTokenError extends Error { - name = "UNDEFINED_TOKEN_ERROR"; - - constructor() { - super( - "Given token is undefined. Have you enabled emitDecoratorMetadata in your tsconfig.json or decorated your class with @Injectable, @Service, ... decorator ?" - ); - } -} diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index 227b5ba9cc1..c3245ae1bb2 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -27,13 +27,11 @@ export * from "./domain/ProviderScope.js"; export * from "./domain/ProviderType.js"; export * from "./errors/InjectionError.js"; export * from "./errors/InvalidPropertyTokenError.js"; -export * from "./errors/UndefinedTokenError.js"; export * from "./interfaces/DIConfigurationOptions.js"; export * from "./interfaces/DILogger.js"; export * from "./interfaces/DILoggerOptions.js"; export * from "./interfaces/DIResolver.js"; export * from "./interfaces/ImportTokenProviderOpts.js"; -export * from "./interfaces/InjectableProperties.js"; export * from "./interfaces/InterceptorContext.js"; export * from "./interfaces/InterceptorMethods.js"; export * from "./interfaces/InvokeOptions.js"; diff --git a/packages/di/src/common/interfaces/ImportTokenProviderOpts.ts b/packages/di/src/common/interfaces/ImportTokenProviderOpts.ts index b267f0ba9ce..2374a7c1e68 100644 --- a/packages/di/src/common/interfaces/ImportTokenProviderOpts.ts +++ b/packages/di/src/common/interfaces/ImportTokenProviderOpts.ts @@ -1,4 +1,4 @@ -import type {Type} from "@tsed/core"; +import type {AbstractType, Type} from "@tsed/core"; import type {TokenProvider} from "./TokenProvider.js"; export type UseImportTokenProviderOpts = { @@ -8,7 +8,7 @@ export type UseImportTokenProviderOpts = { export type UseClassImportTokenProviderOpts = { token: TokenProvider; - useClass: Type | Function; + useClass: Type | AbstractType; }; export type UseFactoryImportTokenProviderOpts = { diff --git a/packages/di/src/common/interfaces/InjectableProperties.ts b/packages/di/src/common/interfaces/InjectableProperties.ts deleted file mode 100644 index 93bfbbc33ee..00000000000 --- a/packages/di/src/common/interfaces/InjectableProperties.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type {InjectablePropertyType} from "../domain/InjectablePropertyType.js"; -import type {LocalsContainer} from "../domain/LocalsContainer.js"; -import type {TokenProvider} from "./TokenProvider.js"; -import type {InjectorService} from "../services/InjectorService.js"; -import type {InvokeOptions} from "./InvokeOptions.js"; - -export interface InjectableProperty { - propertyKey: string | symbol; -} - -export interface InjectablePropertyOptions extends InjectableProperty { - bindingType: InjectablePropertyType.METHOD | InjectablePropertyType.PROPERTY | InjectablePropertyType.INTERCEPTOR; - propertyType: string; - useType?: TokenProvider; - resolver?: (injector: InjectorService, locals: LocalsContainer, options: Partial & {options: any}) => () => any; - options?: any; -} - -export interface InjectablePropertyValue extends InjectableProperty { - bindingType: InjectablePropertyType.CONSTANT | InjectablePropertyType.VALUE; - expression: string; - defaultValue?: any; -} - -export interface InjectableProperties { - [key: string]: InjectablePropertyOptions | InjectablePropertyValue; -} diff --git a/packages/di/src/common/interfaces/InvokeOptions.ts b/packages/di/src/common/interfaces/InvokeOptions.ts index 9c0ca320e1e..f292e5644fd 100644 --- a/packages/di/src/common/interfaces/InvokeOptions.ts +++ b/packages/di/src/common/interfaces/InvokeOptions.ts @@ -1,3 +1,4 @@ +import type {LocalsContainer} from "../domain/LocalsContainer.js"; import {ProviderScope} from "../domain/ProviderScope.js"; import {TokenProvider} from "./TokenProvider.js"; @@ -5,11 +6,11 @@ export interface InvokeOptions { /** * Define dependencies to build the provider and inject them in the constructor. */ - deps: unknown[]; + deps: TokenProvider[]; /** * List of imports to be created before the provider. Imports list aren't injected directly in the provider constructor. */ - imports: unknown[]; + imports: TokenProvider[]; /** * Parent provider. */ @@ -22,4 +23,10 @@ export interface InvokeOptions { * If true, the injector will rebuild the instance. */ rebuild?: boolean; + /** + * Option given to injectable props or parameter constructor (UseOpts). + */ + useOpts?: Record; + + locals?: LocalsContainer; } diff --git a/packages/di/src/common/interfaces/ProviderOpts.ts b/packages/di/src/common/interfaces/ProviderOpts.ts index b4cfb048f0e..9ea743878d3 100644 --- a/packages/di/src/common/interfaces/ProviderOpts.ts +++ b/packages/di/src/common/interfaces/ProviderOpts.ts @@ -8,7 +8,7 @@ export interface ProviderOpts { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ - provide: TokenProvider; + provide: TokenProvider; /** * Create alias token to retrieve the instance provider. */ diff --git a/packages/di/src/common/interfaces/RegistrySettings.ts b/packages/di/src/common/interfaces/RegistrySettings.ts index ed79865caa8..975e064432b 100644 --- a/packages/di/src/common/interfaces/RegistrySettings.ts +++ b/packages/di/src/common/interfaces/RegistrySettings.ts @@ -1,4 +1,5 @@ import {Type} from "@tsed/core"; +import type {LocalsContainer} from "../domain/LocalsContainer.js"; import type {Provider} from "../domain/Provider.js"; import type {InjectorService} from "../services/InjectorService.js"; import type {ResolvedInvokeOptions} from "./ResolvedInvokeOptions.js"; @@ -16,5 +17,5 @@ export interface RegistrySettings { * @param {Map} locals * @param options */ - onInvoke?(provider: Provider, locals: Map, options: ResolvedInvokeOptions & {injector: InjectorService}): void; + onInvoke?(provider: Provider, locals: LocalsContainer, options: ResolvedInvokeOptions & {injector: InjectorService}): void; } diff --git a/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts b/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts index 1bd069c2aa1..93181c0f961 100644 --- a/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts +++ b/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts @@ -7,7 +7,7 @@ export interface ResolvedInvokeOptions { parent?: TokenProvider; scope: ProviderScope; deps: TokenProvider[]; - imports: TokenProvider[]; + imports: (TokenProvider | [TokenProvider])[]; provider: Provider; construct(deps: TokenProvider[]): any; diff --git a/packages/di/src/common/interfaces/TokenProvider.ts b/packages/di/src/common/interfaces/TokenProvider.ts index a0594843f72..7e651fd5b6c 100644 --- a/packages/di/src/common/interfaces/TokenProvider.ts +++ b/packages/di/src/common/interfaces/TokenProvider.ts @@ -1,12 +1,3 @@ -import type {Type} from "@tsed/core"; +import type {Type, AbstractType} from "@tsed/core"; -export interface AbstractType extends Function { - prototype: T; -} - -export type TokenProvider = string | symbol | Type | AbstractType | Function | any; - -export interface TokenProviderOpts { - token?: TokenProvider; - use: any; -} +export type TokenProvider = string | symbol | Type | AbstractType | Function; diff --git a/packages/di/src/common/registries/GlobalProviders.spec.ts b/packages/di/src/common/registries/GlobalProviders.spec.ts index 615fbeacc28..b83e79e3952 100644 --- a/packages/di/src/common/registries/GlobalProviders.spec.ts +++ b/packages/di/src/common/registries/GlobalProviders.spec.ts @@ -1,3 +1,5 @@ +import {s} from "vite/dist/node/types.d-aGj9QkWt.js"; +import {a} from "vitest/dist/suite-CRLAhsm0.js"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; import {ProviderType} from "../domain/ProviderType.js"; @@ -45,7 +47,7 @@ describe("GlobalProviderRegistry", () => { const provider = new Provider("token"); provider.type = ProviderType.PROVIDER; // WHEN - const settings = providers.getRegistrySettings(provider); + const settings = providers.getRegistrySettings(provider as never); // THEN expect(settings).toEqual({ diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index 6359e0eba28..aaf688ab1b0 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -9,7 +9,7 @@ import {TokenProvider} from "../interfaces/TokenProvider.js"; import type {InjectorService} from "../services/InjectorService.js"; export class GlobalProviderRegistry extends Map { - #settings: Map = new Map(); + #settings: Map = new Map(); /** * The get() method returns a specified element from a Map object. @@ -119,7 +119,7 @@ export class GlobalProviderRegistry extends Map { * @param options */ protected createIfNotExists(key: TokenProvider, options: Partial): Provider { - const type = options.type || ProviderType.PROVIDER; + const type = String(options.type || ProviderType.PROVIDER); if (!this.has(key)) { const {model = Provider} = this.#settings.get(type) || {}; diff --git a/packages/di/src/common/services/DILogger.spec.ts b/packages/di/src/common/services/DILogger.spec.ts index 5a9daf1ad6e..2eddde47253 100644 --- a/packages/di/src/common/services/DILogger.spec.ts +++ b/packages/di/src/common/services/DILogger.spec.ts @@ -15,7 +15,7 @@ describe("DILogger", () => { container.add(MyService); await injector.load(container); - const logger = injector.get(MyService).logger; + const logger = injector.get(MyService)!.logger; expect(logger).toEqual(injector.logger); }); diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index 6a74910a46e..f85e2ff636f 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -1,5 +1,5 @@ import {Store} from "@tsed/core"; -import {INJECTABLE_PROP} from "../constants/constants.js"; +import {DI_INJECTABLE_PROP} from "../constants/constants.js"; import {Configuration} from "../decorators/configuration.js"; import {Inject} from "../decorators/inject.js"; import {Injectable} from "../decorators/injectable.js"; @@ -423,7 +423,7 @@ describe("InjectorService", () => { const provider3 = new Provider(token3); provider3.scope = ProviderScope.SINGLETON; - provider3.deps = [undefined]; + provider3.deps = [undefined] as never; const injector = new InjectorService(); injector.set(token2, provider2); @@ -439,7 +439,7 @@ describe("InjectorService", () => { // THEN expect(actualError.message).toContain( - "Injection failed on Test\nOrigin: Unable to inject dependency. Given token is undefined. Have you enabled emitDecoratorMetadata in your tsconfig.json or decorated your class with @Injectable, @Service, ... decorator ?" + "Injection failed on Test\nOrigin: Unable to inject dependency. Given token is undefined. Could mean a circular dependency problem. Try to use @Inject(() => Token) to solve it." ); }); it("should throw InjectionError > Object", () => { @@ -605,167 +605,13 @@ describe("InjectorService", () => { describe("bindInjectableProperties()", () => { class TestBind {} - it("should bind all properties", () => { + it("should do nothing", () => { // GIVEN const injector = new InjectorService(); const instance = new TestBind(); - vi.spyOn(injector as any, "bindProperty").mockReturnValue(undefined); - vi.spyOn(injector as any, "bindConstant").mockReturnValue(undefined); - vi.spyOn(injector as any, "bindValue").mockReturnValue(undefined); - vi.spyOn(injector as any, "bindInterceptor").mockReturnValue(undefined); - - const injectableProperties = { - testMethod: { - bindingType: "method" - }, - testProp: { - bindingType: "property" - }, - testConst: { - bindingType: "constant" - }, - testValue: { - bindingType: "value" - }, - testInterceptor: { - bindingType: "interceptor" - } - }; - - Store.from(TestBind).set(INJECTABLE_PROP, injectableProperties); - // WHEN injector.bindInjectableProperties(instance, new LocalsContainer(), {}); - - // THEN - expect(injector.bindProperty).toBeCalledWith(instance, injectableProperties.testProp, new LocalsContainer(), {}); - expect(injector.bindConstant).toBeCalledWith(instance, injectableProperties.testConst); - expect(injector.bindValue).toBeCalledWith(instance, injectableProperties.testValue); - expect(injector.bindInterceptor).toBeCalledWith(instance, injectableProperties.testInterceptor); - }); - }); - - describe("bindProperty()", () => { - it("should bind the method", () => { - // GIVEN - const injector = new InjectorService(); - const instance = new Test(); - - // WHEN - injector.bindProperty( - instance, - { - bindingType: "property", - propertyKey: "prop", - resolver: (injector: InjectorService) => () => injector.get(InjectorService) - } as any, - new LocalsContainer(), - {} - ); - - // THEN - expect(instance.prop).toEqual(injector); - }); - }); - - describe("bindValue()", () => { - it("should bind a property with a value (1)", () => { - // GIVEN - const injector = new InjectorService(); - const instance = new Test(); - - // WHEN - injector.bindValue(instance, {propertyKey: "value", expression: "expression"} as any); - - instance.value = "test"; - // THEN - expect(instance.value).toEqual("test"); - }); - - it("should bind a property with a value (2)", () => { - // GIVEN - const injector = new InjectorService(); - const instance = new Test(); - - // WHEN - injector.bindValue(instance, {propertyKey: "value", expression: "UNKNOW", defaultValue: "test2"} as any); - - // THEN - expect(instance.value).toEqual("test2"); - }); - }); - - describe("bindConstant()", () => { - it("should bind a property with a value (1)", () => { - // GIVEN - const injector = new InjectorService(); - const instance = new Test(); - - injector.settings.set("expression", "constant"); - - // WHEN - injector.bindConstant(instance, {propertyKey: "constant", expression: "expression"} as any); - - // THEN - expect(instance.constant).toEqual("constant"); - // should be the same - expect(instance.constant).toEqual("constant"); - - let actualError: any; - try { - instance.constant = "test"; - } catch (er) { - actualError = er; - } - expect(!!actualError).toEqual(true); - }); - - it("should bind a property with a value (2)", () => { - // GIVEN - const injector = new InjectorService(); - const instance = new Test(); - - // WHEN - injector.bindConstant(instance, {propertyKey: "constant", expression: "UNKNOW", defaultValue: "test"} as any); - - // THEN - expect(instance.constant).toEqual("test"); - }); - }); - - describe("bindInterceptor()", () => { - it("should bind the method with intercept", async () => { - // GIVEN - class InterceptorTest { - intercept(ctx: any) { - return ctx.next() + " intercepted"; - } - } - - const injector = new InjectorService(); - const container = new Container(); - container.addProvider(InterceptorTest); - - await injector.load(container); - - const instance = new Test(); - - vi.spyOn(injector, "get"); - - // WHEN - injector.bindInterceptor(instance, { - bindingType: "interceptor", - propertyKey: "test3", - useType: InterceptorTest - } as any); - - const result = (instance as any).test3("test"); - - // THEN - expect(injector.get).toBeCalledWith(InterceptorTest); - - expect(result).toEqual("test called intercepted"); }); }); diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 8a152ebfe87..6eeb7e37dec 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -1,6 +1,4 @@ import { - ancestorsOf, - catchError, classOf, deepClone, deepMerge, @@ -9,25 +7,21 @@ import { isClass, isFunction, isInheritedFrom, + isObject, isPromise, nameOf, - Store, - type Type + Store } from "@tsed/core"; -import {DI_PARAM_OPTIONS, INJECTABLE_PROP} from "../constants/constants.js"; +import {DI_INVOKE_OPTIONS, DI_PARAM_OPTIONS} from "../constants/constants.js"; import {Configuration} from "../decorators/configuration.js"; import {Injectable} from "../decorators/injectable.js"; import {Container} from "../domain/Container.js"; -import {InjectablePropertyType} from "../domain/InjectablePropertyType.js"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; import {ProviderScope} from "../domain/ProviderScope.js"; import {InjectionError} from "../errors/InjectionError.js"; -import {UndefinedTokenError} from "../errors/UndefinedTokenError.js"; import type {DILogger} from "../interfaces/DILogger.js"; -import {InjectableProperties, InjectablePropertyOptions, InjectablePropertyValue} from "../interfaces/InjectableProperties.js"; -import type {InterceptorContext} from "../interfaces/InterceptorContext.js"; -import type {InterceptorMethods} from "../interfaces/InterceptorMethods.js"; +import type {ImportTokenProviderOpts} from "../interfaces/ImportTokenProviderOpts.js"; import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import type {ResolvedInvokeOptions} from "../interfaces/ResolvedInvokeOptions.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; @@ -38,6 +32,7 @@ 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()`. @@ -84,6 +79,9 @@ export class InjectorService extends Container { return this.settings.scopes || {}; } + /** + * Return the current injector service. + */ static getInstance() { if (!globalInjector) { throw new Error("InjectorService instance is not created yet."); @@ -91,31 +89,18 @@ export class InjectorService extends Container { return globalInjector; } - static resolveAutoInjectableArgs(token: Type, locals: LocalsContainer, args: unknown[]) { - const injector = InjectorService.getInstance(); - const deps: unknown[] = getConstructorDependencies(token); - const list: any[] = []; - const length = Math.max(deps.length, args.length); - - for (let i = 0; i < length; i++) { - if (args[i] !== undefined) { - 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}); - - list.push(instance); - } - } - - return list; + /** + * Get the locals container initiated by DITest or .bootstrap() method. + */ + static getLocals() { + return globalLocals || (globalLocals = new LocalsContainer()); } - static bind(instance: any, locals: LocalsContainer) { - InjectorService.getInstance().bindInjectableProperties(instance, locals, {}); + /** + * Reset the locals container. + */ + static unsetLocals() { + globalLocals = undefined; } /** @@ -123,7 +108,7 @@ export class InjectorService extends Container { * @param provider */ public scopeOf(provider: Provider) { - return provider.scope || this.scopes[provider.type] || ProviderScope.SINGLETON; + return provider.scope || this.scopes[String(provider.type)] || ProviderScope.SINGLETON; } /** @@ -151,9 +136,8 @@ export class InjectorService extends Container { * * @param token The class or symbol registered in InjectorService. * @param options - * @returns {boolean} */ - get(token: TokenProvider, options: any = {}): T | undefined { + get(token: TokenProvider, options: Record = {}): T | undefined { const instance = this.getInstance(token); if (instance !== undefined) { @@ -173,23 +157,23 @@ export class InjectorService extends Container { /** * Return all instance of the same provider type - * @param type - * @param locals - * @param options */ getMany(type: any, locals?: LocalsContainer, options?: Partial): Type[] { - return this.getProviders(type).map((provider) => this.invoke(provider.token, locals, options)!); + return this.getProviders(type).map((provider) => { + return this.invoke(provider.token, locals, options)!; + }); } /** * The has() method returns a boolean indicating whether an element with the specified key exists or not. - * @returns {boolean} - * @param token */ has(token: TokenProvider): boolean { return this.#cache.get(token) !== undefined; } + /** + * Declare an alias for a given token. + */ alias(token: TokenProvider, alias: TokenProvider) { this.#cache.set(alias, this.#cache.get(token)); @@ -235,7 +219,7 @@ export class InjectorService extends Container { } if (token === DI_PARAM_OPTIONS) { - return {} as Type; + return options.useOpts as Type; } const provider = this.ensureProvider(token); @@ -265,6 +249,7 @@ export class InjectorService extends Container { if (!provider.isAsync() || !isPromise(instance)) { set(instance); + // locals?.delete(DI_PARAM_OPTIONS); return instance; } @@ -276,7 +261,7 @@ export class InjectorService extends Container { return instance; }); - + // locals?.delete(DI_PARAM_OPTIONS); return instance; case ProviderScope.REQUEST: @@ -288,6 +273,8 @@ export class InjectorService extends Container { } } + // locals?.delete(DI_PARAM_OPTIONS); + return instance; } @@ -305,6 +292,9 @@ export class InjectorService extends Container { } } + /** + * Build only providers which are synchronous. + */ loadSync() { for (const [, provider] of this) { if (!this.has(provider.token) && this.scopeOf(provider) === ProviderScope.SINGLETON) { @@ -399,161 +389,9 @@ export class InjectorService extends Container { } /** - * - * @param instance - * @param locals - * @param options - * @deprecated + * @deprecated This method will be removed. There is no replacement for this method. */ - public bindInjectableProperties(instance: any, locals: LocalsContainer, options: Partial) { - const properties: InjectableProperties = ancestorsOf(classOf(instance)).reduce((properties: any, target: any) => { - const store = Store.from(target); - - return { - ...properties, - ...(store.get(INJECTABLE_PROP) || {}) - }; - }, {}); - - Object.values(properties).forEach((definition) => { - switch (definition.bindingType) { - case InjectablePropertyType.PROPERTY: - this.bindProperty(instance, definition, locals, options); - break; - // case InjectablePropertyType.CONSTANT: - // this.bindConstant(instance, definition); - // break; - // case InjectablePropertyType.VALUE: - // this.bindValue(instance, definition); - // break; - // case InjectablePropertyType.INTERCEPTOR: - // this.bindInterceptor(instance, definition); - // break; - } - }); - } - - /** - * Create an injectable property. - * - * @param instance - * @param {string} propertyKey - * @param {any} useType - * @param resolver - * @param options - * @param locals - * @param invokeOptions - */ - public bindProperty( - instance: any, - {propertyKey, resolver, options = {}}: InjectablePropertyOptions, - locals: LocalsContainer, - invokeOptions: Partial - ) { - let get: () => any; - - get = resolver!(this, locals, {...invokeOptions, options}); - - catchError(() => Reflect.deleteProperty(instance, propertyKey)); - catchError(() => - Reflect.defineProperty(instance, propertyKey, { - get, - enumerable: true, - configurable: true - }) - ); - } - - /** - * - * @param instance - * @param {string} propertyKey - * @param {any} useType - * @deprecated - */ - public bindValue(instance: any, {propertyKey, expression, defaultValue}: InjectablePropertyValue) { - const descriptor = { - get: () => this.settings.get(expression) || defaultValue, - set: (value: any) => this.settings.set(expression, value), - enumerable: true, - configurable: true - }; - - catchError(() => Reflect.deleteProperty(instance, propertyKey)); - catchError(() => Reflect.defineProperty(instance, propertyKey, descriptor)); - } - - /** - * - * @param instance - * @param {string} propertyKey - * @param {any} useType - * @deprecated - */ - public bindConstant(instance: any, {propertyKey, expression, defaultValue}: InjectablePropertyValue) { - let bean: any; - - const get = () => { - if (bean !== undefined) { - return bean; - } - - const value = this.settings.get(expression, defaultValue); - bean = Object.freeze(deepClone(value)); - - return bean; - }; - - const descriptor = { - get, - enumerable: true, - configurable: true - }; - - catchError(() => Reflect.deleteProperty(instance, propertyKey)); - catchError(() => Object.defineProperty(instance, propertyKey, descriptor)); - } - - /** - * - * @param instance - * @param propertyKey - * @param useType - * @param options - * @deprecated - */ - public bindInterceptor(instance: any, {propertyKey, useType, options}: InjectablePropertyOptions) { - const target = classOf(instance); - const originalMethod = instance[propertyKey]; - - instance[propertyKey] = (...args: any[]) => { - const next = (err?: Error) => { - if (!err) { - return originalMethod.apply(instance, args); - } - - throw err; - }; - - const context: InterceptorContext = { - target, - propertyKey, - args, - options, - next - }; - - const interceptor = this.get(useType)!; - - return interceptor.intercept!( - { - ...context, - options - }, - next - ); - }; - } + public bindInjectableProperties(instance: any, locals: LocalsContainer, options: Partial) {} async lazyInvoke(token: TokenProvider) { let instance = this.getInstance(token); @@ -573,13 +411,14 @@ export class InjectorService extends Container { * 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 {Promise} A list of promises. + * @returns A list of promises. */ - public emit(eventName: string, ...args: any[]) { + public emit(eventName: string, ...args: any[]): Promise { return this.#hooks.asyncEmit(eventName, args); } /** + * Alter value attached to an event. * @param eventName * @param value * @param args @@ -589,6 +428,7 @@ export class InjectorService extends Container { } /** + * Alter value attached to an event asynchronously. * @param eventName * @param value * @param args @@ -597,17 +437,21 @@ export class InjectorService extends Container { return this.#hooks.asyncAlter(eventName, value, args); } + /** + * Destroy the injector and all services. + */ async destroy() { await this.emit("$onDestroy"); globalInjector = undefined; } + /** + * Ensure that a provider is added to the container. + * @protected + */ protected ensureProvider(token: TokenProvider, force: true): Provider; - protected ensureProvider(token: TokenProvider, force: false): Provider | undefined; - protected ensureProvider(token: TokenProvider): Provider | undefined; - protected ensureProvider(token: TokenProvider, force = false): Provider | undefined { if (!this.hasProvider(token) && (GlobalProviders.has(token) || force)) { this.addProvider(token); @@ -663,17 +507,13 @@ export class InjectorService extends Container { (token: TokenProvider | [TokenProvider], index: number): any => { currentDependency = {token, index, deps}; - if (token !== DI_PARAM_OPTIONS) { - const options = provider?.store?.get(`${DI_PARAM_OPTIONS}:${index}`); - - locals.set(DI_PARAM_OPTIONS, options || {}); - } - if (isArray(token)) { return this.getMany(token[0], locals, options); } - return isInheritedFrom(token, Provider, 1) ? provider : this.invoke(token, locals, {parent}); + const useOpts = provider?.store?.get(`${DI_PARAM_OPTIONS}:${index}`) || options.useOpts; + + return isInheritedFrom(token, Provider, 1) ? provider : this.invoke(token, locals, {parent, useOpts}); }; // Invoke manually imported providers @@ -697,7 +537,10 @@ export class InjectorService extends Container { } if (instance && isClass(classOf(instance))) { - this.bindInjectableProperties(instance, locals, options); + 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; @@ -705,9 +548,9 @@ export class InjectorService extends Container { private resolveImportsProviders() { this.settings.imports = this.settings.imports - ?.filter((meta) => meta.token !== InjectorService) + ?.filter((meta) => isObject(meta) && "token" in meta && meta.token !== InjectorService) .map((meta) => { - if ("token" in meta) { + if (isObject(meta) && "token" in meta) { const {token, ...props} = meta; const provider = this.ensureProvider(token, true); @@ -723,12 +566,12 @@ export class InjectorService extends Container { } if ("useFactory" in props) { - provider.useFactory = props.useFactory; + provider.useFactory = props.useFactory as never; return; } if ("useAsyncFactory" in props) { - provider.useAsyncFactory = props.useAsyncFactory; + provider.useAsyncFactory = props.useAsyncFactory as never; return; } @@ -741,7 +584,7 @@ export class InjectorService extends Container { return meta; }) - .filter(Boolean); + .filter(Boolean) as unknown as (TokenProvider | ImportTokenProviderOpts)[]; } /** @@ -755,13 +598,13 @@ export class InjectorService extends Container { locals: Map, options: Partial ): ResolvedInvokeOptions | false { - let imports: TokenProvider[] | undefined = options.imports; + let imports: (TokenProvider | [TokenProvider])[] | undefined = options.imports; let deps: TokenProvider[] | undefined = options.deps; let scope = options.scope; let construct; if (!token || token === Object) { - throw new UndefinedTokenError(); + throw new Error("Given token is undefined. Could mean a circular dependency problem. Try to use @Inject(() => Token) to solve it."); } let provider: Provider; diff --git a/packages/di/src/node/decorators/injectContext.spec.ts b/packages/di/src/node/decorators/injectContext.spec.ts new file mode 100644 index 00000000000..ae32c237e12 --- /dev/null +++ b/packages/di/src/node/decorators/injectContext.spec.ts @@ -0,0 +1,36 @@ +import {afterEach, beforeEach, expect} from "vitest"; +import {Injectable} from "../../common/index.js"; +import {DIContext} from "../domain/DIContext.js"; +import {DITest} from "../services/DITest.js"; +import {runInContext} from "../utils/asyncHookContext.js"; +import {InjectContext} from "./injectContext.js"; + +describe("InjectContext", () => { + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); + + it("should inject a context", async () => { + @Injectable() + class MyService { + @InjectContext() + ctx: DIContext; + } + + const $ctx = new DIContext({ + id: "test", + logger: DITest.injector.logger, + injector: DITest.injector, + maxStackSize: 0 + }); + + const myService = await DITest.invoke(MyService); + + await runInContext($ctx, () => { + expect(myService.ctx).toBeInstanceOf(DIContext); + expect(myService.ctx).toEqual($ctx); + }); + + expect(myService.ctx).toBeInstanceOf(DIContext); + expect(myService.ctx).not.toEqual($ctx); + }); +}); diff --git a/packages/di/src/node/decorators/injectContext.ts b/packages/di/src/node/decorators/injectContext.ts index 9bb7c6849d1..c8a02a8f161 100644 --- a/packages/di/src/node/decorators/injectContext.ts +++ b/packages/di/src/node/decorators/injectContext.ts @@ -1,4 +1,6 @@ -import {injectProperty} from "../../common/index.js"; +import {catchError} from "@tsed/core"; +import {InjectorService} from "../../common/index.js"; +import {DIContext} from "../domain/DIContext.js"; import {getContext} from "../utils/asyncHookContext.js"; /** @@ -17,9 +19,18 @@ import {getContext} from "../utils/asyncHookContext.js"; */ export function InjectContext(): PropertyDecorator { return (target: any, propertyKey: string): any | void => { - injectProperty(target, propertyKey, { - resolver() { - return () => getContext(); + catchError(() => Reflect.deleteProperty(target, propertyKey)); + Reflect.defineProperty(target, propertyKey, { + get() { + return ( + getContext() || + new DIContext({ + id: "", + logger: InjectorService.getInstance().logger, + injector: InjectorService.getInstance(), + maxStackSize: 0 + }) + ); } }); }; diff --git a/packages/di/src/node/services/DILogger.spec.ts b/packages/di/src/node/services/DILogger.spec.ts index 7504fdcecff..a847cbe7eb7 100644 --- a/packages/di/src/node/services/DILogger.spec.ts +++ b/packages/di/src/node/services/DILogger.spec.ts @@ -15,7 +15,7 @@ describe("DILogger", () => { container.add(MyService); await injector.load(container); - const logger = injector.get(MyService).logger; + const logger = injector.get(MyService)!.logger; expect(logger).toEqual(injector.logger); }); diff --git a/packages/di/src/node/services/DITest.ts b/packages/di/src/node/services/DITest.ts index d28940e52fd..144ed1fc9e2 100644 --- a/packages/di/src/node/services/DITest.ts +++ b/packages/di/src/node/services/DITest.ts @@ -1,10 +1,10 @@ -import {Env, getValue, isClass, isPromise, setValue} from "@tsed/core"; +import {Env, getValue, isClass, isObject, isPromise, setValue} from "@tsed/core"; import {$log} from "@tsed/logger"; import { createContainer, + DI_INJECTABLE_PROPS, InjectorService, - LocalsContainer, - OnInit, + type OnInit, TokenProvider, type UseImportTokenProviderOpts } from "../../common/index.js"; @@ -82,6 +82,7 @@ export class DITest { static async reset() { if (DITest.hasInjector()) { await DITest.injector.destroy(); + InjectorService.unsetLocals(); DITest._injector = null; } } @@ -91,18 +92,18 @@ export class DITest { * @param target * @param providers */ - static async invoke(target: TokenProvider, providers: UseImportTokenProviderOpts[] = []): Promise { - const locals = new LocalsContainer(); + 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 instance: OnInit = DITest.injector.invoke(target, locals, {rebuild: true}); - let promise; + const instance: T & OnInit = DITest.injector.invoke(target, locals, {rebuild: true}); - if (instance && instance.$onInit) { + if (isObject(instance) && "$onInit" in instance) { const result = instance.$onInit(); if (result instanceof Promise) { @@ -115,9 +116,15 @@ export class DITest { } if (isClass(instance)) { - await Promise.all(Object.values(instance).filter(isPromise)); + const keys = (instance as any)[DI_INJECTABLE_PROPS]; + + if (keys) { + await Promise.all([...keys.keys()].map((key: string) => (instance as any)[key])); + } } + InjectorService.unsetLocals(); + return instance as any; } diff --git a/packages/di/vitest.config.mts b/packages/di/vitest.config.mts index 5da402fb625..c5a894a9cb6 100644 --- a/packages/di/vitest.config.mts +++ b/packages/di/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 98.92, - branches: 97.36, - functions: 99.08, - lines: 98.92 + statements: 98.75, + branches: 97.2, + functions: 98.67, + lines: 98.75 } } } } -); \ No newline at end of file +); diff --git a/packages/graphql/typegraphql/src/services/TypeGraphQLService.ts b/packages/graphql/typegraphql/src/services/TypeGraphQLService.ts index 75d26a3bb71..bacc95c03ad 100644 --- a/packages/graphql/typegraphql/src/services/TypeGraphQLService.ts +++ b/packages/graphql/typegraphql/src/services/TypeGraphQLService.ts @@ -47,7 +47,7 @@ export class TypeGraphQLService { const resolvers: any = this.getResolvers(id, [...(initialResolvers as any[]), ...(buildSchemaOptions.resolvers || [])]); const schema = await this.createSchema({ - container: this.injector, + container: this.injector as never, ...buildSchemaOptions, resolvers, globalMiddlewares: [ContextMiddleware, ...(buildSchemaOptions.globalMiddlewares || [])] diff --git a/packages/orm/adapters/src/services/Adapters.ts b/packages/orm/adapters/src/services/Adapters.ts index ed4561a0822..6940764afa0 100644 --- a/packages/orm/adapters/src/services/Adapters.ts +++ b/packages/orm/adapters/src/services/Adapters.ts @@ -1,5 +1,5 @@ import {Type} from "@tsed/core"; -import {Constant, DI_PARAM_OPTIONS, Inject, Injectable, InjectorService} from "@tsed/di"; +import {Constant, Inject, Injectable, InjectorService} from "@tsed/di"; import {MemoryAdapter} from "../adapters/MemoryAdapter.js"; import {Adapter, AdapterConstructorOptions} from "../domain/Adapter.js"; @@ -17,9 +17,9 @@ export class Adapters { invokeAdapter(options: AdapterInvokeOptions): Adapter { const {adapter = this.adapter, ...props} = options; - const locals = options.locals || new Map(); - locals.set(DI_PARAM_OPTIONS, props); - return this.injector.invoke>(adapter, locals); + return this.injector.invoke>(adapter, options.locals, { + useOpts: props + }); } } diff --git a/packages/orm/ioredis/src/utils/mockConnections.ts b/packages/orm/ioredis/src/utils/mockConnections.ts index ed93b991385..efd0e99a177 100644 --- a/packages/orm/ioredis/src/utils/mockConnections.ts +++ b/packages/orm/ioredis/src/utils/mockConnections.ts @@ -18,7 +18,7 @@ export async function mockConnection(token: TokenProvider, name: string) { export function mockConnections() { return Promise.all( [...GlobalProviders.values()] - .filter((provider) => provider.type === IOREDIS_CONNECTIONS) + .filter((provider) => provider.type === String(IOREDIS_CONNECTIONS)) .map((provider) => mockConnection(provider.token, provider.connectionName)) ); } diff --git a/packages/orm/ioredis/src/utils/registerConnectionProvider.ts b/packages/orm/ioredis/src/utils/registerConnectionProvider.ts index a1aaa388a17..36a14af11b7 100644 --- a/packages/orm/ioredis/src/utils/registerConnectionProvider.ts +++ b/packages/orm/ioredis/src/utils/registerConnectionProvider.ts @@ -56,7 +56,7 @@ export function registerConnectionProvider({provide, name = "default"}: CreateCo setValue(sentinelsOptions, "redisOptions.reconnectOnError", reconnectOnError); connection = new Redis({ - name: sentinelName, + name: String(sentinelName), sentinels, ...sentinelsOptions, lazyConnect: true diff --git a/packages/orm/mikro-orm/src/MikroOrmModule.ts b/packages/orm/mikro-orm/src/MikroOrmModule.ts index 5ec1aeed109..3e6494c88cb 100644 --- a/packages/orm/mikro-orm/src/MikroOrmModule.ts +++ b/packages/orm/mikro-orm/src/MikroOrmModule.ts @@ -78,8 +78,6 @@ export class MikroOrmModule implements OnDestroy, OnInit, AlterRunInContext { return this.injector.invoke(subscriber, container, diOpts); } - this.injector.bindInjectableProperties(subscriber, container, diOpts); - return subscriber; }); } diff --git a/packages/orm/mikro-orm/src/decorators/entityManager.spec.ts b/packages/orm/mikro-orm/src/decorators/entityManager.spec.ts index d3d636dcc0e..ea17a2ca9a5 100644 --- a/packages/orm/mikro-orm/src/decorators/entityManager.spec.ts +++ b/packages/orm/mikro-orm/src/decorators/entityManager.spec.ts @@ -1,5 +1,5 @@ import {DecoratorTypes, Store} from "@tsed/core"; -import {Controller, INJECTABLE_PROP} from "@tsed/di"; +import {Controller, DI_INJECTABLE_PROP} from "@tsed/di"; import {MongoEntityManager} from "@mikro-orm/mongodb"; import {EntityManager} from "./entityManager.js"; @@ -11,7 +11,7 @@ export class UsersCtrl { describe("@Orm", () => { it("should decorate property", () => { - expect(Store.from(UsersCtrl).get(INJECTABLE_PROP)).toEqual({ + expect(Store.from(UsersCtrl).get(DI_INJECTABLE_PROP)).toEqual({ em: { propertyKey: "em", bindingType: DecoratorTypes.PROP, diff --git a/packages/orm/mikro-orm/src/decorators/orm.spec.ts b/packages/orm/mikro-orm/src/decorators/orm.spec.ts index e9c0a3b6f89..f6911acd149 100644 --- a/packages/orm/mikro-orm/src/decorators/orm.spec.ts +++ b/packages/orm/mikro-orm/src/decorators/orm.spec.ts @@ -1,5 +1,5 @@ import {DecoratorTypes, Store} from "@tsed/core"; -import {Controller, INJECTABLE_PROP} from "@tsed/di"; +import {Controller, DI_INJECTABLE_PROP} from "@tsed/di"; import {Orm} from "./orm.js"; import {MikroORM} from "@mikro-orm/core"; @@ -11,7 +11,7 @@ export class UsersCtrl { describe("@Orm", () => { it("should decorate property", () => { - expect(Store.from(UsersCtrl).get(INJECTABLE_PROP)).toEqual({ + expect(Store.from(UsersCtrl).get(DI_INJECTABLE_PROP)).toEqual({ orm: { propertyKey: "orm", bindingType: DecoratorTypes.PROP, diff --git a/packages/orm/mikro-orm/src/decorators/transactional.spec.ts b/packages/orm/mikro-orm/src/decorators/transactional.spec.ts index 5c5c1061845..7dcaf9a5999 100644 --- a/packages/orm/mikro-orm/src/decorators/transactional.spec.ts +++ b/packages/orm/mikro-orm/src/decorators/transactional.spec.ts @@ -1,6 +1,6 @@ import {Post} from "@tsed/common"; import {Store} from "@tsed/core"; -import {Controller, INJECTABLE_PROP} from "@tsed/di"; +import {Controller, DI_INJECTABLE_PROP} from "@tsed/di"; import {TransactionalInterceptor} from "../interceptors/TransactionalInterceptor.js"; import {Transactional} from "./transactional.js"; @@ -13,7 +13,7 @@ export class UsersCtrl { describe("@Transactional", () => { it("should decorate method", () => { - expect(Store.from(UsersCtrl).get(INJECTABLE_PROP)).toEqual({ + expect(Store.from(UsersCtrl).get(DI_INJECTABLE_PROP)).toEqual({ create: { bindingType: "interceptor", propertyKey: "create", diff --git a/packages/orm/typeorm/src/TypeORMModule.ts b/packages/orm/typeorm/src/TypeORMModule.ts index 9b8815a9339..90326eee0d0 100644 --- a/packages/orm/typeorm/src/TypeORMModule.ts +++ b/packages/orm/typeorm/src/TypeORMModule.ts @@ -44,9 +44,9 @@ registerProvider({ { deps: [TypeORMModule], get(type, options: any) { - if (isRepository(type)) { + if (isRepository(type as any)) { try { - return getCustomRepository(type, options.connection || "default"); + return getCustomRepository(type as any, options.connection || "default"); } catch (er) { if (process.env.NODE_ENV !== "test") { throw er; diff --git a/packages/platform/common/src/builder/PlatformBuilder.ts b/packages/platform/common/src/builder/PlatformBuilder.ts index 1e917b1f7e1..a910967638d 100644 --- a/packages/platform/common/src/builder/PlatformBuilder.ts +++ b/packages/platform/common/src/builder/PlatformBuilder.ts @@ -172,7 +172,7 @@ export class PlatformBuilder { * @param {any[]} controllers */ public addControllers(endpoint: string, controllers: TokenProvider | TokenProvider[]) { - [].concat(controllers).forEach((token) => { + [].concat(controllers as never[]).forEach((token: TokenProvider) => { this.settings.routes.push({token, route: endpoint}); }); } diff --git a/packages/platform/common/src/utils/createHttpServer.spec.ts b/packages/platform/common/src/utils/createHttpServer.spec.ts index 5f677887b50..474178155fb 100644 --- a/packages/platform/common/src/utils/createHttpServer.spec.ts +++ b/packages/platform/common/src/utils/createHttpServer.spec.ts @@ -18,17 +18,17 @@ describe("createHttpServer", () => { expect(listener).toBeInstanceOf(Function); - const server = injector.get(Http.Server); + const server = injector.get(Http.Server)!; vi.spyOn(injector.logger, "info").mockReturnValue(undefined); vi.spyOn(injector.logger, "debug").mockReturnValue(undefined); - vi.spyOn(server, "listen").mockReturnValue(undefined); - vi.spyOn(server, "address").mockReturnValue({port: 8089, address: "0.0.0.0"}); - vi.spyOn(server, "on").mockImplementation((event: string, cb: any) => { + vi.spyOn(server, "listen").mockReturnValue(undefined as never); + vi.spyOn(server, "address").mockReturnValue({port: 8089, address: "0.0.0.0"} as never); + vi.spyOn(server, "on").mockImplementation(((event: string, cb: any) => { if (event === "listening") { cb(); } - }); + }) as never); await listener(); diff --git a/packages/platform/common/src/utils/createHttpsServer.spec.ts b/packages/platform/common/src/utils/createHttpsServer.spec.ts index 3d7e98194ff..acdc6c37e35 100644 --- a/packages/platform/common/src/utils/createHttpsServer.spec.ts +++ b/packages/platform/common/src/utils/createHttpsServer.spec.ts @@ -17,17 +17,17 @@ describe("createHttpsServer", () => { expect(!!injector.get(Https.Server)).toEqual(true); expect(listener).toBeInstanceOf(Function); - const server = injector.get(Https.Server); + const server = injector.get(Https.Server)!; vi.spyOn(injector.logger, "info").mockReturnValue(undefined); vi.spyOn(injector.logger, "debug").mockReturnValue(undefined); - vi.spyOn(server, "listen").mockReturnValue(undefined); - vi.spyOn(server, "address").mockReturnValue({port: 8089, address: "0.0.0.0"}); - vi.spyOn(server, "on").mockImplementation((event: string, cb: any) => { + vi.spyOn(server, "listen").mockReturnValue(undefined as never); + vi.spyOn(server, "address").mockReturnValue({port: 8089, address: "0.0.0.0"} as never); + vi.spyOn(server, "on").mockImplementation(((event: string, cb: any) => { if (event === "listening") { cb(); } - }); + }) as never); await listener(); diff --git a/packages/platform/platform-cache/src/index.ts b/packages/platform/platform-cache/src/index.ts index 6a0a760e751..49f6d8e2c2c 100644 --- a/packages/platform/platform-cache/src/index.ts +++ b/packages/platform/platform-cache/src/index.ts @@ -7,6 +7,5 @@ export * from "./interfaces/interfaces.js"; export * from "./interfaces/PlatformCachedObject.js"; export * from "./interfaces/PlatformCacheOptions.js"; export * from "./services/PlatformCache.js"; -export * from "./utils/getInterceptorOptions.js"; export * from "./utils/getPrefix.js"; export * from "./utils/isEndpoint.js"; diff --git a/packages/platform/platform-cache/src/utils/getInterceptorOptions.ts b/packages/platform/platform-cache/src/utils/getInterceptorOptions.ts deleted file mode 100644 index 8918dfe670b..00000000000 --- a/packages/platform/platform-cache/src/utils/getInterceptorOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Store, Type} from "@tsed/core"; -import {INJECTABLE_PROP} from "@tsed/di"; -import {PlatformCacheOptions} from "../interfaces/PlatformCacheOptions.js"; - -export function getInterceptorOptions(target: Type, propertyKey: string | symbol): PlatformCacheOptions { - return Store.from(target).get(INJECTABLE_PROP)?.[propertyKey]?.options; -} diff --git a/packages/platform/platform-cache/src/utils/getPrefix.ts b/packages/platform/platform-cache/src/utils/getPrefix.ts index df49f9d8a9c..f1170388c77 100644 --- a/packages/platform/platform-cache/src/utils/getPrefix.ts +++ b/packages/platform/platform-cache/src/utils/getPrefix.ts @@ -1,8 +1,9 @@ import {nameOf, Type} from "@tsed/core"; -import {getInterceptorOptions} from "./getInterceptorOptions.js"; +import {getInterceptorOptions} from "@tsed/di"; +import type {PlatformCacheOptions} from "../interfaces/PlatformCacheOptions.js"; export function getPrefix(target: Type, propertyKey: string | symbol) { - const {prefix} = getInterceptorOptions(target, propertyKey); + const {prefix} = getInterceptorOptions(target, propertyKey); if (prefix) { return [prefix]; } diff --git a/packages/platform/platform-koa/vitest.config.mts b/packages/platform/platform-koa/vitest.config.mts index ca838a14781..510b341b8f9 100644 --- a/packages/platform/platform-koa/vitest.config.mts +++ b/packages/platform/platform-koa/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 99.15, - branches: 95.65, + branches: 95.6, functions: 100, lines: 99.15 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-response-filter/src/decorators/responseFilter.ts b/packages/platform/platform-response-filter/src/decorators/responseFilter.ts index 6688cf66cbf..f5aa9f46777 100644 --- a/packages/platform/platform-response-filter/src/decorators/responseFilter.ts +++ b/packages/platform/platform-response-filter/src/decorators/responseFilter.ts @@ -8,7 +8,7 @@ import {registerResponseFilter, ResponseFilterKey} from "../domain/ResponseFilte * @decorator */ export function ResponseFilter(...contentTypes: ResponseFilterKey[]): ClassDecorator { - return (target: T): void | T => { + return (target: any) => { contentTypes.forEach((contentType) => { registerResponseFilter(contentType, target as any); }); diff --git a/packages/security/oidc-provider/src/OidcModule.spec.ts b/packages/security/oidc-provider/src/OidcModule.spec.ts index fbed983208a..9b5f4b8a3d3 100644 --- a/packages/security/oidc-provider/src/OidcModule.spec.ts +++ b/packages/security/oidc-provider/src/OidcModule.spec.ts @@ -30,7 +30,7 @@ describe("OidcModule", () => { afterEach(() => PlatformTest.reset()); describe('when path "/oidc"', () => { it("should register the appropriate rewrite middleware", async () => { - const mdl = await PlatformTest.invoke(OidcModule); + const mdl = await PlatformTest.invoke(OidcModule); vi.spyOn(mdl.app, "use").mockReturnValue(undefined); @@ -49,7 +49,7 @@ describe("OidcModule", () => { get: vi.fn().mockReturnValue(provider), create: vi.fn() }; - const mdl = await PlatformTest.invoke(OidcModule, [ + const mdl = await PlatformTest.invoke(OidcModule, [ { token: OidcProvider, use: oidcProvider @@ -77,7 +77,7 @@ describe("OidcModule", () => { afterEach(() => PlatformTest.reset()); describe('when path "/oidc"', () => { it("should register the appropriate rewrite middleware", async () => { - const mdl = await PlatformTest.invoke(OidcModule); + const mdl = await PlatformTest.invoke(OidcModule); vi.spyOn(mdl.app, "use").mockReturnValue(undefined); @@ -96,7 +96,7 @@ describe("OidcModule", () => { get: vi.fn().mockReturnValue(provider), create: vi.fn() }; - const mdl = await PlatformTest.invoke(OidcModule, [ + const mdl = await PlatformTest.invoke(OidcModule, [ { token: OidcProvider, use: oidcProvider diff --git a/packages/specs/swagger/src/services/SwaggerService.ts b/packages/specs/swagger/src/services/SwaggerService.ts index 1fa53740675..72f04fecb19 100644 --- a/packages/specs/swagger/src/services/SwaggerService.ts +++ b/packages/specs/swagger/src/services/SwaggerService.ts @@ -1,4 +1,5 @@ import {Configuration, Injectable, InjectorService, Platform} from "@tsed/common"; +import type {Type} from "@tsed/core"; import {OpenSpec2, OpenSpec3} from "@tsed/openspec"; import {generateSpec} from "@tsed/schema"; import {SwaggerOS2Settings, SwaggerOS3Settings, SwaggerSettings} from "../interfaces/SwaggerSettings.js"; @@ -29,7 +30,7 @@ export class SwaggerService { const tokens = this.platform .getMountedControllers() .filter(({routes, provider}) => [...routes.values()].some((route) => includeRoute(route, provider, conf))) - .map(({route, provider}) => ({token: provider.token, rootPath: route})); + .map(({route, provider}) => ({token: provider.token as Type, rootPath: route})); const spec = await generateSpec({ tokens, diff --git a/yarn.lock b/yarn.lock index 5ad0f6e7496..69054491486 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6568,6 +6568,7 @@ __metadata: eslint: "npm:^8.57.0" tslib: "npm:2.6.2" typescript: "npm:4.9.5" + uuid: "npm:9.0.1" vitest: "npm:2.0.4" webpack: "npm:^5.75.0" peerDependencies: