From 1880c6290e898b77e19d95a420ad7f8d4160d8ec Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 11 Oct 2024 11:20:23 +0200 Subject: [PATCH] feat(di): export injectable,controller,interceptor functions BREAKING CHANGE: registerValue and registerController are removed in favor of injectable/controller functions --- commitlint.config.mjs | 3 +- .../di/src/common/decorators/controller.ts | 14 +-- .../di/src/common/decorators/injectable.ts | 5 +- .../di/src/common/decorators/interceptor.ts | 4 +- .../di/src/common/domain/Container.spec.ts | 2 +- .../common/domain/ControllerProvider.spec.ts | 6 +- .../src/common/domain/ControllerProvider.ts | 11 +- packages/di/src/common/domain/Provider.ts | 8 +- packages/di/src/common/domain/ProviderType.ts | 2 - packages/di/src/common/fn/inject.ts | 2 +- packages/di/src/common/fn/injectable.spec.ts | 80 ++++++++++++++ packages/di/src/common/fn/injectable.ts | 102 +++++++++++++++++- .../src/common/registries/GlobalProviders.ts | 7 +- .../registries/ProviderRegistry.spec.ts | 64 +---------- .../src/common/registries/ProviderRegistry.ts | 83 +------------- .../common/services/InjectorService.spec.ts | 1 - packages/di/src/node/services/DILogger.ts | 5 +- .../platform-params/vitest.config.mts | 4 +- .../components-scan/vitest.config.mts | 4 +- 19 files changed, 217 insertions(+), 190 deletions(-) create mode 100644 packages/di/src/common/fn/injectable.spec.ts diff --git a/commitlint.config.mjs b/commitlint.config.mjs index 57232d2cfa8..e87f790a31e 100644 --- a/commitlint.config.mjs +++ b/commitlint.config.mjs @@ -21,7 +21,8 @@ export default { extends: ["@commitlint/config-conventional"], rules: { "scope-enum": [RuleConfigSeverity.Error, "always", findPackages()], - "header-max-length": [0, "always", 120] + "header-max-length": [0, "always", 120], + "footer-max-line-length": [0, "always", 200], }, ignores: [ (message) => diff --git a/packages/di/src/common/decorators/controller.ts b/packages/di/src/common/decorators/controller.ts index 04e47d01f1c..ccd336ec24d 100644 --- a/packages/di/src/common/decorators/controller.ts +++ b/packages/di/src/common/decorators/controller.ts @@ -1,17 +1,12 @@ import {isArrayOrArrayClass, Type, useDecorators} from "@tsed/core"; import {Children, Path} from "@tsed/schema"; +import type {ControllerMiddlewares} from "../domain/ControllerProvider.js"; +import {controller} from "../fn/injectable.js"; import {ProviderOpts} from "../interfaces/ProviderOpts.js"; -import {registerController} from "../registries/ProviderRegistry.js"; export type PathType = string | RegExp | (string | RegExp)[]; -export interface ControllerMiddlewares { - useBefore: any[]; - use: any[]; - useAfter: any[]; -} - export interface ControllerOptions extends Partial> { path?: PathType; children?: Type[]; @@ -61,10 +56,7 @@ export function Controller(options: PathType | ControllerOptions): ClassDecorato return useDecorators( (target: Type) => { - registerController({ - provide: target, - ...opts - }); + controller(target, opts); }, path && Path(path as any), Children(...children) diff --git a/packages/di/src/common/decorators/injectable.ts b/packages/di/src/common/decorators/injectable.ts index 9ab97cf1566..22979518692 100644 --- a/packages/di/src/common/decorators/injectable.ts +++ b/packages/di/src/common/decorators/injectable.ts @@ -21,9 +21,10 @@ import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; */ export function Injectable(options: Partial = {}): ClassDecorator { return (target: any) => { - injectable({ + const opts = { ...options, ...(options.provide ? {useClass: target} : {provide: target}) - }); + }; + injectable(opts.provide, opts); }; } diff --git a/packages/di/src/common/decorators/interceptor.ts b/packages/di/src/common/decorators/interceptor.ts index 9b7fee411e9..c335d95f7b1 100644 --- a/packages/di/src/common/decorators/interceptor.ts +++ b/packages/di/src/common/decorators/interceptor.ts @@ -2,8 +2,8 @@ import {ProviderType} from "../domain/ProviderType.js"; import {Injectable} from "./injectable.js"; /** - * The decorators `@Service()` declare a new service can be injected in other service or controller on there `constructor`. - * All services annotated with `@Service()` are constructed one time. + * The decorators `@Interceptor()` declare a new service can be injected in other service or controller on there `constructor`. + * All services annotated with `@Interceptor()` are constructed one time. * * > `@Service()` use the `reflect-metadata` to collect and inject service on controllers or other services. * diff --git a/packages/di/src/common/domain/Container.spec.ts b/packages/di/src/common/domain/Container.spec.ts index e5fdd49d83f..e5d9c868edd 100644 --- a/packages/di/src/common/domain/Container.spec.ts +++ b/packages/di/src/common/domain/Container.spec.ts @@ -32,7 +32,7 @@ describe("Container", () => { container = new Container(); container.addProvider(MyMiddleware, {type: ProviderType.MIDDLEWARE}); - container.addProvider(MyService, {type: ProviderType.SERVICE}); + container.addProvider(MyService, {type: ProviderType.PROVIDER}); container.addProvider(MyController, {type: ProviderType.CONTROLLER}); // await container.load(); diff --git a/packages/di/src/common/domain/ControllerProvider.spec.ts b/packages/di/src/common/domain/ControllerProvider.spec.ts index 6f7d10ff3e1..3d7991c23e2 100644 --- a/packages/di/src/common/domain/ControllerProvider.spec.ts +++ b/packages/di/src/common/domain/ControllerProvider.spec.ts @@ -46,10 +46,10 @@ describe("ControllerProvider", () => { it("should have a middlewares", () => { expect(Array.isArray(controllerProvider.middlewares.use)).toBe(true); - expect(controllerProvider.middlewares.use[0]).toBeInstanceOf(Function); + expect(controllerProvider.middlewares.use![0]).toBeInstanceOf(Function); expect(Array.isArray(controllerProvider.middlewares.useAfter)).toBe(true); - expect(controllerProvider.middlewares.useAfter[0]).toBeInstanceOf(Function); + expect(controllerProvider.middlewares.useAfter![0]).toBeInstanceOf(Function); expect(Array.isArray(controllerProvider.middlewares.useBefore)).toBe(true); - expect(controllerProvider.middlewares.useBefore[0]).toBeInstanceOf(Function); + expect(controllerProvider.middlewares.useBefore![0]).toBeInstanceOf(Function); }); }); diff --git a/packages/di/src/common/domain/ControllerProvider.ts b/packages/di/src/common/domain/ControllerProvider.ts index f0811545765..e1d9d704437 100644 --- a/packages/di/src/common/domain/ControllerProvider.ts +++ b/packages/di/src/common/domain/ControllerProvider.ts @@ -1,8 +1,13 @@ -import {ControllerMiddlewares} from "../decorators/controller.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; import {Provider} from "./Provider.js"; import {ProviderType} from "./ProviderType.js"; +export interface ControllerMiddlewares { + useBefore: TokenProvider[]; + use: TokenProvider[]; + useAfter: TokenProvider[]; +} + export class ControllerProvider extends Provider { public tokenRouter: string; @@ -15,7 +20,7 @@ export class ControllerProvider extends Provider { * * @returns {any[]} */ - get middlewares(): ControllerMiddlewares { + get middlewares(): Partial { return Object.assign( { use: [], @@ -30,7 +35,7 @@ export class ControllerProvider extends Provider { * * @param middlewares */ - set middlewares(middlewares: ControllerMiddlewares) { + set middlewares(middlewares: Partial) { const mdlwrs = this.middlewares; const concat = (key: string, a: any, b: any) => (a[key] = a[key].concat(b[key])); diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index 8534c01e264..61f030c9d50 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -30,7 +30,9 @@ export class Provider implements ProviderOpts { this.provide = token; this.useClass = token as Type; - Object.assign(this, options); + Object.assign(this, { + ...options + }); } get token() { @@ -130,6 +132,10 @@ export class Provider implements ProviderOpts { return this.store.get("childrenControllers", []); } + set children(children: TokenProvider[]) { + this.store.set("childrenControllers", children); + } + get(key: string) { return this.store.get(key) || this._tokenStore.get(key); } diff --git a/packages/di/src/common/domain/ProviderType.ts b/packages/di/src/common/domain/ProviderType.ts index 492b486fb4f..43dbcdd2d90 100644 --- a/packages/di/src/common/domain/ProviderType.ts +++ b/packages/di/src/common/domain/ProviderType.ts @@ -1,7 +1,5 @@ export enum ProviderType { VALUE = "value", - FACTORY = "factory", - SERVICE = "service", PROVIDER = "provider", MODULE = "module", CONTROLLER = "controller", diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts index 6394c16d855..15dc6d83690 100644 --- a/packages/di/src/common/fn/inject.ts +++ b/packages/di/src/common/fn/inject.ts @@ -1,5 +1,5 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; -import {TokenProvider} from "../interfaces/TokenProvider.js"; +import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {injector} from "./injector.js"; import {invokeOptions, localsContainer} from "./localsContainer.js"; diff --git a/packages/di/src/common/fn/injectable.spec.ts b/packages/di/src/common/fn/injectable.spec.ts new file mode 100644 index 00000000000..b40bbf0f3b3 --- /dev/null +++ b/packages/di/src/common/fn/injectable.spec.ts @@ -0,0 +1,80 @@ +import {DITest, logger} from "../../node/index.js"; +import {ProviderScope} from "../domain/ProviderScope.js"; +import {ProviderType} from "../domain/ProviderType.js"; +import {inject} from "./inject.js"; +import {controller, injectable, interceptor} from "./injectable.js"; + +class Nested { + get() { + return "hello"; + } +} + +class MyClass { + nested = inject(Nested); + logger = logger(); +} + +class MyController {} + +injectable(Nested).scope(ProviderScope.SINGLETON).class(Nested); +injectable(MyClass); + +describe("injectable", () => { + describe("injectable()", () => { + it("should define a singleton scope", async () => { + const instance = await DITest.invoke(MyClass); + + expect(instance.nested).toBeInstanceOf(Nested); + expect(instance.nested.get()).toEqual("hello"); + }); + it("should create a factory", async () => { + const builder = injectable(Symbol.for("Test")).factory(() => "test"); + const provider = builder.inspect(); + + expect(provider.type).toEqual(ProviderType.PROVIDER); + expect(builder.token()).toEqual(Symbol.for("Test")); + }); + it("should create an async factory", async () => { + const builder = injectable(Symbol.for("Test")).asyncFactory(() => Promise.resolve("test")); + const provider = builder.inspect(); + + expect(provider.type).toEqual(ProviderType.PROVIDER); + expect(builder.token()).toEqual(Symbol.for("Test")); + }); + it("should create a value", async () => { + const builder = injectable(Symbol.for("Test")).value({ + test: "test" + }); + const provider = builder.inspect(); + + expect(provider.type).toEqual(ProviderType.VALUE); + expect(builder.token()).toEqual(Symbol.for("Test")); + }); + }); + + describe("controller()", () => { + it("should define a singleton scope", async () => { + const builder = controller(MyController) + .path("/my-controller") + .scope(ProviderScope.REQUEST) + .middlewares({ + use: [() => {}] + }); + + const provider = builder.inspect(); + + expect(provider.type).toEqual(ProviderType.CONTROLLER); + expect(provider.scope).toEqual(ProviderScope.REQUEST); + }); + }); + + describe("interceptor()", () => { + it("should define a singleton scope", async () => { + const builder = interceptor(MyController); + const provider = builder.inspect(); + + expect(provider.type).toEqual(ProviderType.INTERCEPTOR); + }); + }); +}); diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts index 51929f02ad3..45bf76e8dbb 100644 --- a/packages/di/src/common/fn/injectable.ts +++ b/packages/di/src/common/fn/injectable.ts @@ -1,6 +1,98 @@ -import {registerProvider} from "../registries/ProviderRegistry.js"; +import "../registries/ProviderRegistry.js"; -/** - * @alias {registerProvider} Alias of registerProvider - */ -export const injectable = registerProvider; +import {Store, type Type} from "@tsed/core"; + +import {ControllerProvider} from "../domain/ControllerProvider.js"; +import type {Provider} from "../domain/Provider.js"; +import {ProviderType} from "../domain/ProviderType.js"; +import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; +import type {TokenProvider} from "../interfaces/TokenProvider.js"; +import {GlobalProviders} from "../registries/GlobalProviders.js"; + +type ProviderBuilder = { + [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder; +} & { + inspect(): BaseProvider; + store(): Store; + token(): Token; + factory(f: (...args: unknown[]) => unknown): ProviderBuilder; + asyncFactory(f: (...args: unknown[]) => Promise): ProviderBuilder; + value(v: unknown): ProviderBuilder; + class(c: Type): ProviderBuilder; +}; + +export function providerBuilder(props: string[], baseOpts: Partial> = {}) { + return ( + token: Token, + options: Partial> = {} + ): ProviderBuilder> => { + const provider = GlobalProviders.merge(token, { + ...options, + ...baseOpts, + provide: token + }); + + return props.reduce( + (acc, prop) => { + return { + ...acc, + [prop]: function (value: any) { + (provider as any)[prop] = value; + return this; + } + }; + }, + { + factory(factory: any) { + provider.useFactory = factory; + return this; + }, + asyncFactory(asyncFactory: any) { + provider.useAsyncFactory = asyncFactory; + return this; + }, + value(value: any) { + provider.useValue = value; + provider.type = ProviderType.VALUE; + return this; + }, + class(k: any) { + provider.useClass = k; + return this; + }, + store() { + return provider.store; + }, + inspect() { + return provider; + }, + token() { + return provider.token as Token; + } + } as ProviderBuilder> + ); + }; +} + +type PickedProps = + | "scope" + | "path" + | "alias" + | "useFactory" + | "useAsyncFactory" + | "useValue" + | "useClass" + | "hooks" + | "deps" + | "resolvers" + | "imports" + | "configuration"; + +const Props = ["type", "scope", "path", "alias", "hooks", "deps", "resolvers", "imports", "configuration"]; +export const injectable = providerBuilder(Props); +export const interceptor = providerBuilder(Props, { + type: ProviderType.INTERCEPTOR +}); +export const controller = providerBuilder([...Props, "middlewares"], { + type: ProviderType.CONTROLLER +}); diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index 9b3ca327f76..db152d5abe7 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -50,12 +50,7 @@ export class GlobalProviderRegistry extends Map { const meta = this.createIfNotExists(target, options); Object.keys(options).forEach((key) => { - let value = (options as never)[key]; - // if (key === "type") { - // value = String(value); - // } - - meta[key] = value; + meta[key] = (options as never)[key]; }); this.set(target, meta); diff --git a/packages/di/src/common/registries/ProviderRegistry.spec.ts b/packages/di/src/common/registries/ProviderRegistry.spec.ts index a3efc8c6a13..5b1b26c360e 100644 --- a/packages/di/src/common/registries/ProviderRegistry.spec.ts +++ b/packages/di/src/common/registries/ProviderRegistry.spec.ts @@ -1,7 +1,5 @@ -import {ProviderScope} from "../domain/ProviderScope.js"; -import {ProviderType} from "../domain/ProviderType.js"; import {GlobalProviders} from "./GlobalProviders.js"; -import {registerProvider, registerValue} from "./ProviderRegistry.js"; +import {registerProvider} from "./ProviderRegistry.js"; describe("ProviderRegistry", () => { describe("registerProvider()", () => { @@ -13,18 +11,6 @@ describe("ProviderRegistry", () => { vi.resetAllMocks(); }); - it("should throw an error when provide field is not given", () => { - // GIVEN - let actualError; - try { - registerProvider({provide: undefined}); - } catch (er) { - actualError = er; - } - - expect(actualError.message).toEqual("Provider.provide is required"); - }); - it("should add provider", () => { class Test {} @@ -35,52 +21,4 @@ describe("ProviderRegistry", () => { }); }); }); - describe("registerValue()", () => { - beforeEach(() => { - vi.spyOn(GlobalProviders, "merge"); - vi.spyOn(GlobalProviders, "has").mockReturnValue(false); - }); - afterEach(() => { - vi.resetAllMocks(); - }); - - it("should add provider (1)", () => { - const token = Symbol.for("CustomTokenValue"); - - registerValue(token, "myValue"); - - expect(GlobalProviders.merge).toHaveBeenCalledWith(token, { - provide: token, - useValue: "myValue", - scope: ProviderScope.SINGLETON, - type: ProviderType.VALUE - }); - }); - - it("should add provider", () => { - const token = Symbol.for("CustomTokenValue"); - - registerValue({provide: token, useValue: "myValue", scope: ProviderScope.REQUEST}); - - expect(GlobalProviders.merge).toHaveBeenCalledWith(token, { - provide: token, - useValue: "myValue", - scope: ProviderScope.REQUEST, - type: ProviderType.VALUE - }); - }); - - it("should add provider (legacy)", () => { - const token = Symbol.for("CustomTokenValue2"); - - registerValue(token, "myValue"); - - expect(GlobalProviders.merge).toHaveBeenCalledWith(token, { - provide: token, - useValue: "myValue", - scope: ProviderScope.SINGLETON, - type: ProviderType.VALUE - }); - }); - }); }); diff --git a/packages/di/src/common/registries/ProviderRegistry.ts b/packages/di/src/common/registries/ProviderRegistry.ts index c7ca06c0295..f99177ac320 100644 --- a/packages/di/src/common/registries/ProviderRegistry.ts +++ b/packages/di/src/common/registries/ProviderRegistry.ts @@ -1,91 +1,14 @@ -import {Provider} from "../domain/Provider.js"; -import {ProviderScope} from "../domain/ProviderScope.js"; +import {ControllerProvider} from "../domain/ControllerProvider.js"; import {ProviderType} from "../domain/ProviderType.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import {GlobalProviders} from "./GlobalProviders.js"; -/** - * - */ -GlobalProviders.createRegistry(ProviderType.CONTROLLER, Provider); +GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider); /** * Register a provider configuration. * @param {ProviderOpts} provider */ -export function registerProvider(provider: Partial>) { - if (!provider.provide) { - throw new Error("Provider.provide is required"); - } - +export function registerProvider(provider: Partial> & Pick, "provide">) { return GlobalProviders.merge(provider.provide, provider); } - -/** - * Add a new value in the `ProviderRegistry`. - * - * #### Example with symbol definition - * - * - * ```typescript - * import {registerValue, InjectorService} from "@tsed/di"; - * - * const MyValue = Symbol.from("MyValue") - * - * registerValue({token: MyValue, useValue: "myValue"}); - * - * @Service() - * export class OtherService { - * constructor(@Inject(MyValue) myValue: string){ - * console.log(myValue); /// "myValue" - * } - * } - * ``` - */ -export const registerValue = (provider: any | ProviderOpts, value?: any): void => { - if (!provider.provide) { - provider = { - provide: provider - }; - } - - provider = Object.assign( - { - scope: ProviderScope.SINGLETON, - useValue: value - }, - provider, - {type: ProviderType.VALUE} - ); - GlobalProviders.merge(provider.provide, provider); -}; - -/** - * Add a new controller in the `ProviderRegistry`. This controller will be built when `InjectorService` will be loaded. - * - * #### Example - * - * ```typescript - * import {registerController, InjectorService} from "@tsed/di"; - * - * export default class MyController { - * constructor(){} - * transform() { - * return "test"; - * } - * } - * - * registerController({provide: MyController}); - * // or - * registerController(MyController); - * - * const injector = new InjectorService(); - * injector.load(); - * - * const myController = injector.get(MyController); - * myController.getFoo(); // test - * ``` - * - * @param provider Provider configuration. - */ -export const registerController = GlobalProviders.createRegisterFn(ProviderType.CONTROLLER); diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index 2ccdd43decc..e2cab619ba0 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -53,7 +53,6 @@ describe("InjectorService", () => { }); expect(!!injector.getMany(ProviderType.VALUE).length).toEqual(true); - expect(!!injector.getMany(ProviderType.FACTORY).length).toEqual(false); }); }); diff --git a/packages/di/src/node/services/DILogger.ts b/packages/di/src/node/services/DILogger.ts index 2a4304116fa..3b76e6d8c55 100644 --- a/packages/di/src/node/services/DILogger.ts +++ b/packages/di/src/node/services/DILogger.ts @@ -3,7 +3,4 @@ import {Logger} from "@tsed/logger"; import {injectable} from "../../common/fn/injectable.js"; import {logger} from "../fn/logger.js"; -injectable({ - provide: Logger, - useFactory: logger -}); +injectable(Logger).factory(logger); diff --git a/packages/platform/platform-params/vitest.config.mts b/packages/platform/platform-params/vitest.config.mts index f9d941dcb43..947a6a0a2a9 100644 --- a/packages/platform/platform-params/vitest.config.mts +++ b/packages/platform/platform-params/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 99.2, - branches: 90.69, + branches: 90.62, functions: 100, lines: 99.2 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/components-scan/vitest.config.mts b/packages/third-parties/components-scan/vitest.config.mts index d2598fb346b..eeab450fe2d 100644 --- a/packages/third-parties/components-scan/vitest.config.mts +++ b/packages/third-parties/components-scan/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 100, + branches: 95.83, functions: 100, lines: 100 } } } } -); \ No newline at end of file +);