From 666a12afecb1a6409466fb353ff76ab9230a3705 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 17 Nov 2024 10:39:18 +0100 Subject: [PATCH 01/32] chore: fix coverage configuration --- .github/workflows/build.yml | 10 +++++----- packages/core/vitest.config.mts | 10 +++++----- packages/di/vitest.config.mts | 10 +++++----- packages/engines/vitest.config.mts | 10 +++++----- packages/graphql/apollo/vitest.config.mts | 8 ++++---- packages/graphql/typegraphql/vitest.config.mts | 6 +++--- packages/orm/adapters-redis/vitest.config.mts | 10 +++++----- packages/orm/adapters/vitest.config.mts | 8 ++++---- packages/orm/mikro-orm/vitest.config.mts | 10 +++++----- packages/orm/mongoose/vitest.config.mts | 10 +++++----- packages/orm/objection/vitest.config.mts | 10 +++++----- packages/orm/prisma/vitest.config.mts | 10 +++++----- .../platform/platform-cache/vitest.config.mts | 2 +- .../platform-exceptions/vitest.config.mts | 2 +- .../platform/platform-express/vitest.config.mts | 6 +++--- .../platform/platform-http/vitest.config.mts | 8 ++++---- packages/platform/platform-koa/vitest.config.mts | 6 +++--- .../platform-middlewares/vitest.config.mts | 8 ++++---- .../platform/platform-params/vitest.config.mts | 6 +++--- .../platform/platform-router/vitest.config.mts | 4 ++-- .../platform-serverless/vitest.config.mts | 6 +++--- .../platform/platform-views/vitest.config.mts | 8 ++++---- packages/security/jwks/vitest.config.mts | 10 +++++----- .../vitest.config.mts | 10 +++++----- .../security/oidc-provider/vitest.config.mts | 10 +++++----- packages/security/passport/vitest.config.mts | 10 +++++----- packages/third-parties/bullmq/vitest.config.mts | 8 ++++---- .../components-scan/vitest.config.mts | 4 ++-- .../event-emitter/vitest.config.mts | 8 ++++---- packages/third-parties/formio/vitest.config.mts | 8 ++++---- packages/third-parties/pulse/vitest.config.mts | 2 +- .../schema-formio/vitest.config.mts | 6 +++--- .../third-parties/socketio/vitest.config.mts | 6 +++--- packages/third-parties/sse/vitest.config.mts | 4 ++-- .../third-parties/temporal/vitest.config.mts | 6 +++--- .../third-parties/terminus/vitest.config.mts | 8 ++++---- packages/third-parties/vike/vitest.config.mts | 8 ++++---- tools/vitest/presets/index.js | 16 ++++++++++------ 38 files changed, 148 insertions(+), 144 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7e8594abca..2635b9d9461 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -360,11 +360,11 @@ jobs: - name: Install dependencies run: yarn install --network-timeout 500000 - - name: Download Core vitest config files - uses: actions/download-artifact@v4 - with: - pattern: vitest-config-* - merge-multiple: true + # - name: Download Core vitest config files + # uses: actions/download-artifact@v4 + # with: + # pattern: vitest-config-* + # merge-multiple: true - name: "Git status" run: git status - name: Release packages diff --git a/packages/core/vitest.config.mts b/packages/core/vitest.config.mts index d759e817941..e26f44e6da9 100644 --- a/packages/core/vitest.config.mts +++ b/packages/core/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 97.31, + branches: 95.69, + functions: 95.74, + lines: 97.31 } } } } -); +); \ No newline at end of file diff --git a/packages/di/vitest.config.mts b/packages/di/vitest.config.mts index d759e817941..819c81fbc31 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: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 98.91, + branches: 97.04, + functions: 97.15, + lines: 98.91 } } } } -); +); \ No newline at end of file diff --git a/packages/engines/vitest.config.mts b/packages/engines/vitest.config.mts index d759e817941..681c873af46 100644 --- a/packages/engines/vitest.config.mts +++ b/packages/engines/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 80.69, + branches: 85.79, + functions: 78.4, + lines: 80.69 } } } } -); +); \ No newline at end of file diff --git a/packages/graphql/apollo/vitest.config.mts b/packages/graphql/apollo/vitest.config.mts index d6e3e594664..943bb1aecc9 100644 --- a/packages/graphql/apollo/vitest.config.mts +++ b/packages/graphql/apollo/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 78.42, - branches: 82.35, - functions: 66.66, - lines: 78.42 + statements: 85, + branches: 85.29, + functions: 91.66, + lines: 85 } } } diff --git a/packages/graphql/typegraphql/vitest.config.mts b/packages/graphql/typegraphql/vitest.config.mts index df0f6fcb17d..ad905ec8acc 100644 --- a/packages/graphql/typegraphql/vitest.config.mts +++ b/packages/graphql/typegraphql/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 96.9, - branches: 77.77, + statements: 94.8, + branches: 72.72, functions: 100, - lines: 96.9 + lines: 94.8 } } } diff --git a/packages/orm/adapters-redis/vitest.config.mts b/packages/orm/adapters-redis/vitest.config.mts index d759e817941..d38618a9187 100644 --- a/packages/orm/adapters-redis/vitest.config.mts +++ b/packages/orm/adapters-redis/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 99.62, + branches: 95.65, + functions: 100, + lines: 99.62 } } } } -); +); \ No newline at end of file diff --git a/packages/orm/adapters/vitest.config.mts b/packages/orm/adapters/vitest.config.mts index d759e817941..75820f3947d 100644 --- a/packages/orm/adapters/vitest.config.mts +++ b/packages/orm/adapters/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 99.17, + branches: 98.48, + functions: 96.66, + lines: 99.17 } } } diff --git a/packages/orm/mikro-orm/vitest.config.mts b/packages/orm/mikro-orm/vitest.config.mts index f8dc9b023ac..fc66778a331 100644 --- a/packages/orm/mikro-orm/vitest.config.mts +++ b/packages/orm/mikro-orm/vitest.config.mts @@ -12,12 +12,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 97.75, + branches: 97.46, + functions: 100, + lines: 97.75 } } } } -); +); \ No newline at end of file diff --git a/packages/orm/mongoose/vitest.config.mts b/packages/orm/mongoose/vitest.config.mts index f8dc9b023ac..8c1552f1fc9 100644 --- a/packages/orm/mongoose/vitest.config.mts +++ b/packages/orm/mongoose/vitest.config.mts @@ -12,12 +12,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 97.98, + branches: 96.18, + functions: 100, + lines: 97.98 } } } } -); +); \ No newline at end of file diff --git a/packages/orm/objection/vitest.config.mts b/packages/orm/objection/vitest.config.mts index d759e817941..12d1b543cae 100644 --- a/packages/orm/objection/vitest.config.mts +++ b/packages/orm/objection/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 94.33, + branches: 98.66, + functions: 92.3, + lines: 94.33 } } } } -); +); \ No newline at end of file diff --git a/packages/orm/prisma/vitest.config.mts b/packages/orm/prisma/vitest.config.mts index d759e817941..a79dbf60936 100644 --- a/packages/orm/prisma/vitest.config.mts +++ b/packages/orm/prisma/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 91.11, + branches: 92.15, + functions: 92.59, + lines: 91.11 } } } } -); +); \ No newline at end of file diff --git a/packages/platform/platform-cache/vitest.config.mts b/packages/platform/platform-cache/vitest.config.mts index 713a830f8d8..333a9657f4c 100644 --- a/packages/platform/platform-cache/vitest.config.mts +++ b/packages/platform/platform-cache/vitest.config.mts @@ -11,7 +11,7 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 98.92, + branches: 98.26, functions: 100, lines: 100 } diff --git a/packages/platform/platform-exceptions/vitest.config.mts b/packages/platform/platform-exceptions/vitest.config.mts index cdd22a2d467..016601524b4 100644 --- a/packages/platform/platform-exceptions/vitest.config.mts +++ b/packages/platform/platform-exceptions/vitest.config.mts @@ -11,7 +11,7 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 96.66, + branches: 97.29, functions: 100, lines: 100 } diff --git a/packages/platform/platform-express/vitest.config.mts b/packages/platform/platform-express/vitest.config.mts index c7f2bf97a85..62b9336d242 100644 --- a/packages/platform/platform-express/vitest.config.mts +++ b/packages/platform/platform-express/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.52, - branches: 96, + statements: 96.61, + branches: 95, functions: 100, - lines: 97.52 + lines: 96.61 } } } diff --git a/packages/platform/platform-http/vitest.config.mts b/packages/platform/platform-http/vitest.config.mts index 7763d2e726d..f5429eb293b 100644 --- a/packages/platform/platform-http/vitest.config.mts +++ b/packages/platform/platform-http/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.97, - branches: 97.45, - functions: 95.29, - lines: 97.97 + statements: 97.21, + branches: 95.14, + functions: 95.31, + lines: 97.21 } } } diff --git a/packages/platform/platform-koa/vitest.config.mts b/packages/platform/platform-koa/vitest.config.mts index 56d3bb2591d..d33c0c74734 100644 --- a/packages/platform/platform-koa/vitest.config.mts +++ b/packages/platform/platform-koa/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.28, - branches: 96.38, + statements: 99.15, + branches: 95.6, functions: 100, - lines: 99.28 + lines: 99.15 } } } diff --git a/packages/platform/platform-middlewares/vitest.config.mts b/packages/platform/platform-middlewares/vitest.config.mts index dd186fcbbe9..954bfd9f049 100644 --- a/packages/platform/platform-middlewares/vitest.config.mts +++ b/packages/platform/platform-middlewares/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 83.81, - branches: 90.47, - functions: 63.63, - lines: 83.81 + statements: 77.59, + branches: 95.83, + functions: 81.81, + lines: 77.59 } } } diff --git a/packages/platform/platform-params/vitest.config.mts b/packages/platform/platform-params/vitest.config.mts index ccb40e519a1..86d5bcbfdb1 100644 --- a/packages/platform/platform-params/vitest.config.mts +++ b/packages/platform/platform-params/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.55, - branches: 95.45, + statements: 99.2, + branches: 90.55, functions: 100, - lines: 99.55 + lines: 99.2 } } } diff --git a/packages/platform/platform-router/vitest.config.mts b/packages/platform/platform-router/vitest.config.mts index cc161ec7188..857adbd63b3 100644 --- a/packages/platform/platform-router/vitest.config.mts +++ b/packages/platform/platform-router/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 97.47, + branches: 94.92, functions: 100, lines: 100 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-serverless/vitest.config.mts b/packages/platform/platform-serverless/vitest.config.mts index dc30eeafb8c..5d52e5c80d0 100644 --- a/packages/platform/platform-serverless/vitest.config.mts +++ b/packages/platform/platform-serverless/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 98.9, - branches: 97.22, + statements: 98.53, + branches: 96.36, functions: 100, - lines: 98.9 + lines: 98.53 } } } diff --git a/packages/platform/platform-views/vitest.config.mts b/packages/platform/platform-views/vitest.config.mts index 3127fa60e6a..7841aa19b37 100644 --- a/packages/platform/platform-views/vitest.config.mts +++ b/packages/platform/platform-views/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 94.24, - branches: 88.88, - functions: 78.57, - lines: 94.24 + statements: 92.25, + branches: 94.73, + functions: 76.92, + lines: 92.25 } } } diff --git a/packages/security/jwks/vitest.config.mts b/packages/security/jwks/vitest.config.mts index d2598fb346b..d759e817941 100644 --- a/packages/security/jwks/vitest.config.mts +++ b/packages/security/jwks/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts index 17fefad7305..d759e817941 100644 --- a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts +++ b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 90, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/security/oidc-provider/vitest.config.mts b/packages/security/oidc-provider/vitest.config.mts index af27d616070..d759e817941 100644 --- a/packages/security/oidc-provider/vitest.config.mts +++ b/packages/security/oidc-provider/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.69, - branches: 92.75, - functions: 100, - lines: 97.69 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/security/passport/vitest.config.mts b/packages/security/passport/vitest.config.mts index 4e6d7701311..d759e817941 100644 --- a/packages/security/passport/vitest.config.mts +++ b/packages/security/passport/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.05, - branches: 95.31, - functions: 100, - lines: 99.05 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/bullmq/vitest.config.mts b/packages/third-parties/bullmq/vitest.config.mts index f514ed806e7..452b24fe8da 100644 --- a/packages/third-parties/bullmq/vitest.config.mts +++ b/packages/third-parties/bullmq/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.16, - branches: 94.54, - functions: 90, - lines: 97.16 + statements: 98.92, + branches: 98.55, + functions: 100, + lines: 98.92 } } } diff --git a/packages/third-parties/components-scan/vitest.config.mts b/packages/third-parties/components-scan/vitest.config.mts index 3612c5647fb..aa8bed784fe 100644 --- a/packages/third-parties/components-scan/vitest.config.mts +++ b/packages/third-parties/components-scan/vitest.config.mts @@ -15,7 +15,7 @@ export default defineConfig( ], thresholds: { statements: 100, - branches: 94.44, + branches: 95.65, functions: 100, lines: 100, @@ -23,4 +23,4 @@ export default defineConfig( } } } -); +); \ No newline at end of file diff --git a/packages/third-parties/event-emitter/vitest.config.mts b/packages/third-parties/event-emitter/vitest.config.mts index 790fdc76eee..00611ba4831 100644 --- a/packages/third-parties/event-emitter/vitest.config.mts +++ b/packages/third-parties/event-emitter/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 83.21, - branches: 90.47, - functions: 75, - lines: 83.21 + statements: 81.72, + branches: 100, + functions: 90, + lines: 81.72 } } } diff --git a/packages/third-parties/formio/vitest.config.mts b/packages/third-parties/formio/vitest.config.mts index 946caa1d8da..b01f2296164 100644 --- a/packages/third-parties/formio/vitest.config.mts +++ b/packages/third-parties/formio/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 94.3, - branches: 88.83, - functions: 82.81, - lines: 94.3 + statements: 95.77, + branches: 96.66, + functions: 96.85, + lines: 95.77 } } } diff --git a/packages/third-parties/pulse/vitest.config.mts b/packages/third-parties/pulse/vitest.config.mts index 6218da60e17..69d33025271 100644 --- a/packages/third-parties/pulse/vitest.config.mts +++ b/packages/third-parties/pulse/vitest.config.mts @@ -13,7 +13,7 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 96.87, + branches: 100, functions: 100, lines: 100 } diff --git a/packages/third-parties/schema-formio/vitest.config.mts b/packages/third-parties/schema-formio/vitest.config.mts index 161af18d2cc..22f0e9e31bb 100644 --- a/packages/third-parties/schema-formio/vitest.config.mts +++ b/packages/third-parties/schema-formio/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.71, - branches: 98.48, + statements: 99.62, + branches: 98.71, functions: 100, - lines: 99.71 + lines: 99.62 } } } diff --git a/packages/third-parties/socketio/vitest.config.mts b/packages/third-parties/socketio/vitest.config.mts index cc10f12c720..5293c3bb219 100644 --- a/packages/third-parties/socketio/vitest.config.mts +++ b/packages/third-parties/socketio/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.93, - branches: 98.11, + statements: 99.59, + branches: 98.87, functions: 100, - lines: 99.93 + lines: 99.59 } } } diff --git a/packages/third-parties/sse/vitest.config.mts b/packages/third-parties/sse/vitest.config.mts index a7157bc2f79..73cb4db6220 100644 --- a/packages/third-parties/sse/vitest.config.mts +++ b/packages/third-parties/sse/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 53.84, + statements: 52.28, branches: 75, functions: 64.28, - lines: 53.84 + lines: 52.28 } } } diff --git a/packages/third-parties/temporal/vitest.config.mts b/packages/third-parties/temporal/vitest.config.mts index 00b2de77d5a..b6ea67a4c90 100644 --- a/packages/third-parties/temporal/vitest.config.mts +++ b/packages/third-parties/temporal/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 95.54, - branches: 85, + statements: 91.86, + branches: 78.26, functions: 88.88, - lines: 95.54 + lines: 91.86 } } } diff --git a/packages/third-parties/terminus/vitest.config.mts b/packages/third-parties/terminus/vitest.config.mts index 74dbfeec960..d2598fb346b 100644 --- a/packages/third-parties/terminus/vitest.config.mts +++ b/packages/third-parties/terminus/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 98.14, - branches: 95.23, - functions: 92.3, - lines: 98.14 + statements: 100, + branches: 100, + functions: 100, + lines: 100 } } } diff --git a/packages/third-parties/vike/vitest.config.mts b/packages/third-parties/vike/vitest.config.mts index 5c5754a9a32..d2598fb346b 100644 --- a/packages/third-parties/vike/vitest.config.mts +++ b/packages/third-parties/vike/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 96.72, - branches: 90.9, - functions: 83.33, - lines: 96.72 + statements: 100, + branches: 100, + functions: 100, + lines: 100 } } } diff --git a/tools/vitest/presets/index.js b/tools/vitest/presets/index.js index a67828f9de4..bac8f698514 100644 --- a/tools/vitest/presets/index.js +++ b/tools/vitest/presets/index.js @@ -1,7 +1,6 @@ import swc from "unplugin-swc"; import {defineConfig} from "vitest/config"; -import {resolveWorkspaceFiles} from "../plugins/resolveWorkspaceFiles.js"; import {alias} from "./alias.js"; export const presets = defineConfig({ @@ -17,9 +16,14 @@ export const presets = defineConfig({ coverage: { enabled: true, reporter: ["text", "json", "html"], - all: true, include: ["src/**/*.{tsx,ts}"], exclude: [ + "**/node_modules/**", + "**/@tsed/**", + "**/exports.ts", + "**/interfaces/**", + "**/*fixtures.ts", + "**/fixtures/**", "**/*.spec.{ts,tsx}", "**/*.stories.{ts,tsx}", "**/*.d.ts", @@ -31,12 +35,11 @@ export const presets = defineConfig({ } }, plugins: [ - resolveWorkspaceFiles(), swc.vite({ - sourceMaps: "inline", - + sourceMaps: true, + inlineSourcesContent: true, jsc: { - target: "es2022", + target: "esnext", externalHelpers: true, keepClassNames: true, parser: { @@ -59,6 +62,7 @@ export const presets = defineConfig({ lazy: false, noInterop: false }, + minify: false, isModule: true }) ] From f4fa686728c1b0f266273732aae6b352158596fa Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 15 Nov 2024 17:33:40 +0100 Subject: [PATCH 02/32] refactor(di): move locals arg to DIInvokeOptions --- docs/docs/injection-scopes.md | 4 +- .../src/common/decorators/autoInjectable.ts | 2 +- packages/di/src/common/fn/inject.ts | 5 +- packages/di/src/common/fn/injectMany.ts | 2 +- packages/di/src/common/integration/di.spec.ts | 4 +- .../di/src/common/integration/request.spec.ts | 6 +- .../di/src/common/interfaces/InvokeOptions.ts | 8 +-- .../src/common/interfaces/RegistrySettings.ts | 3 +- .../interfaces/ResolvedInvokeOptions.ts | 4 +- .../common/registries/GlobalProviders.spec.ts | 6 +- .../src/common/registries/GlobalProviders.ts | 4 +- .../common/services/InjectorService.spec.ts | 29 ++++----- .../di/src/common/services/InjectorService.ts | 62 +++++++++---------- packages/di/src/node/domain/DIContext.ts | 2 - .../apollo/src/services/ApolloService.ts | 2 +- .../orm/adapters/src/services/Adapters.ts | 2 +- .../ioredis/src/domain/IORedisStore.spec.ts | 1 + .../utils/registerConnectionProvider.spec.ts | 2 +- packages/orm/mikro-orm/src/MikroOrmModule.ts | 13 ++-- .../src/builder/PlatformParams.ts | 2 +- .../src/domain/PlatformRouters.ts | 6 +- .../routers-injection.integration.spec.ts | 17 +++-- 22 files changed, 93 insertions(+), 93 deletions(-) diff --git a/docs/docs/injection-scopes.md b/docs/docs/injection-scopes.md index 0e2bb402c4a..5044b37dc6b 100644 --- a/docs/docs/injection-scopes.md +++ b/docs/docs/injection-scopes.md @@ -75,9 +75,7 @@ Instance scope used on a provider tells the injector to create a new instance ea With the functional API, you can also rebuild any service on the fly by calling @@inject@@ with the `rebuild` flag: ```typescript -import {inject, injector} from "@tsed/di"; +import {inject} from "@tsed/di"; const myService = inject(MyService, {rebuild: true}); -// similar to -const myService2 = injector().invoke(MyService, {rebuild: true}); ``` diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index ec792b0e519..a29a6e5f756 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -17,7 +17,7 @@ function resolveAutoInjectableArgs(token: Type, args: unknown[]) { list.push(args[i]); } else { const value = deps[i]; - const instance = isArray(value) ? inj!.getMany(value[0], locals, {parent: token}) : inj!.invoke(value, locals, {parent: token}); + const instance = isArray(value) ? inj!.getMany(value[0], {locals, parent: token}) : inj!.invoke(value, {locals, parent: token}); list.push(instance); } diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts index 15dc6d83690..9ce3d7c929c 100644 --- a/packages/di/src/common/fn/inject.ts +++ b/packages/di/src/common/fn/inject.ts @@ -21,8 +21,9 @@ import {invokeOptions, localsContainer} from "./localsContainer.js"; * @decorator */ export function inject(token: TokenProvider, opts?: Partial>): T { - return injector().invoke(token, opts?.locals || localsContainer(), { + return injector().invoke(token, { ...opts, - ...invokeOptions() + ...invokeOptions(), + locals: opts?.locals || localsContainer() }); } diff --git a/packages/di/src/common/fn/injectMany.ts b/packages/di/src/common/fn/injectMany.ts index c0358dad2af..c3e7a6ad516 100644 --- a/packages/di/src/common/fn/injectMany.ts +++ b/packages/di/src/common/fn/injectMany.ts @@ -3,5 +3,5 @@ import {injector} from "./injector.js"; import {localsContainer} from "./localsContainer.js"; export function injectMany(token: string | symbol, opts?: Partial>): T[] { - return injector().getMany(token, opts?.locals || localsContainer(), opts); + return injector().getMany(token, {...opts, locals: opts?.locals || localsContainer()}); } diff --git a/packages/di/src/common/integration/di.spec.ts b/packages/di/src/common/integration/di.spec.ts index 81ea8e251b4..b71fbdc6171 100644 --- a/packages/di/src/common/integration/di.spec.ts +++ b/packages/di/src/common/integration/di.spec.ts @@ -61,8 +61,8 @@ describe("DI", () => { expect(injector.invoke(ServiceInstance) === injector.invoke(ServiceInstance)).toEqual(false); const locals = new LocalsContainer(); - expect(injector.invoke(ServiceRequest, locals)).toEqual(injector.invoke(ServiceRequest, locals)); - expect(injector.invoke(ServiceInstance, locals) === injector.invoke(ServiceInstance, locals)).toEqual(false); + expect(injector.invoke(ServiceRequest, {locals})).toEqual(injector.invoke(ServiceRequest, {locals})); + expect(injector.invoke(ServiceInstance, {locals}) === injector.invoke(ServiceInstance, {locals})).toEqual(false); }); }); diff --git a/packages/di/src/common/integration/request.spec.ts b/packages/di/src/common/integration/request.spec.ts index dda0418b2d1..703321aa587 100644 --- a/packages/di/src/common/integration/request.spec.ts +++ b/packages/di/src/common/integration/request.spec.ts @@ -54,9 +54,9 @@ describe("DI Request", () => { const locals = new LocalsContainer(); // WHEN - const result1: any = injector.invoke(ServiceRequest, locals); - const result2: any = injector.invoke(ServiceRequest, locals); - const serviceSingleton1: any = injector.invoke(ServiceSingleton, locals); + const result1: any = injector.invoke(ServiceRequest, {locals}); + const result2: any = injector.invoke(ServiceRequest, {locals}); + const serviceSingleton1: any = injector.invoke(ServiceSingleton, {locals}); const serviceSingleton2: any = injector.get(ServiceSingleton); vi.spyOn(result1, "$onDestroy").mockResolvedValue(undefined); diff --git a/packages/di/src/common/interfaces/InvokeOptions.ts b/packages/di/src/common/interfaces/InvokeOptions.ts index f292e5644fd..c07e34ebed4 100644 --- a/packages/di/src/common/interfaces/InvokeOptions.ts +++ b/packages/di/src/common/interfaces/InvokeOptions.ts @@ -15,10 +15,10 @@ export interface InvokeOptions { * Parent provider. */ parent?: TokenProvider; - /** - * Scope used by the injector to build the provider. - */ - scope: ProviderScope; + // /** + // * Scope used by the injector to build the provider. + // */ + // scope: ProviderScope; /** * If true, the injector will rebuild the instance. */ diff --git a/packages/di/src/common/interfaces/RegistrySettings.ts b/packages/di/src/common/interfaces/RegistrySettings.ts index 91444bc3fe9..96ebee88da2 100644 --- a/packages/di/src/common/interfaces/RegistrySettings.ts +++ b/packages/di/src/common/interfaces/RegistrySettings.ts @@ -15,8 +15,7 @@ export interface RegistrySettings { /** * * @param provider - * @param {Map} locals * @param options */ - onInvoke?(provider: Provider, locals: LocalsContainer, options: ResolvedInvokeOptions & {injector: InjectorService}): void; + onInvoke?(provider: Provider, options: ResolvedInvokeOptions): void; } diff --git a/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts b/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts index 93181c0f961..dd829fe1ebf 100644 --- a/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts +++ b/packages/di/src/common/interfaces/ResolvedInvokeOptions.ts @@ -1,14 +1,14 @@ +import type {LocalsContainer} from "../domain/LocalsContainer.js"; import type {Provider} from "../domain/Provider.js"; -import type {ProviderScope} from "../domain/ProviderScope.js"; import type {TokenProvider} from "./TokenProvider.js"; export interface ResolvedInvokeOptions { token: TokenProvider; parent?: TokenProvider; - scope: ProviderScope; deps: TokenProvider[]; imports: (TokenProvider | [TokenProvider])[]; provider: Provider; + locals: LocalsContainer; construct(deps: TokenProvider[]): any; } diff --git a/packages/di/src/common/registries/GlobalProviders.spec.ts b/packages/di/src/common/registries/GlobalProviders.spec.ts index 25cd6d877fd..5a57d6bd131 100644 --- a/packages/di/src/common/registries/GlobalProviders.spec.ts +++ b/packages/di/src/common/registries/GlobalProviders.spec.ts @@ -100,13 +100,13 @@ describe("GlobalProviderRegistry", () => { const locals = new LocalsContainer(); const resolvedOptions = { token: provider.token, - injector: new InjectorService() + locals } as any; GlobalProviders.createRegistry("type:test", Provider, opts); - GlobalProviders.onInvoke(provider, locals, resolvedOptions); + GlobalProviders.onInvoke(provider, resolvedOptions); - expect(opts.onInvoke).toHaveBeenCalledWith(provider, locals, resolvedOptions); + expect(opts.onInvoke).toHaveBeenCalledWith(provider, resolvedOptions); }); }); }); diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index db152d5abe7..89662822a66 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -80,11 +80,11 @@ export class GlobalProviderRegistry extends Map { return this; } - onInvoke(provider: Provider, locals: LocalsContainer, options: ResolvedInvokeOptions & {injector: InjectorService}) { + onInvoke(provider: Provider, options: ResolvedInvokeOptions) { const settings = this.#settings.get(provider.type); if (settings?.onInvoke) { - settings.onInvoke(provider, locals, options); + settings.onInvoke(provider, options); } } diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index e2cab619ba0..e588e047054 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -87,16 +87,17 @@ describe("InjectorService", () => { // WHEN - const result1: any = injector.invoke(token, locals); - const result2: any = injector.invoke(token, locals, {rebuild: true}); + const result1: any = injector.invoke(token, {locals}); + const result2: any = injector.invoke(token, {locals, rebuild: true}); // THEN expect(result1 !== result2).toEqual(true); expect(injector.getProvider).toHaveBeenCalledWith(token); expect(injector.get("alias")).toBeInstanceOf(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, locals, {rebuild: true}); - expect((injector as any).invoke).toHaveBeenCalledWith(InjectorService, locals, { + expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals, rebuild: true}); + expect(injector.invoke).toHaveBeenCalledWith(InjectorService, { + locals, parent: token }); }); @@ -123,18 +124,18 @@ describe("InjectorService", () => { const locals2 = new LocalsContainer(); // LocalContainer for the second request // WHEN REQ1 - const result1: any = injector.invoke(token, locals); - const result2: any = injector.invoke(token, locals); + const result1: any = injector.invoke(token, {locals}); + const result2: any = injector.invoke(token, {locals}); // WHEN REQ2 - const result3: any = injector.invoke(token, locals2); + const result3: any = injector.invoke(token, {locals: locals2}); // THEN expect(result1).toEqual(result2); expect(result2 !== result3).toEqual(true); expect(injector.getProvider).toHaveBeenCalledWith(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, locals, {}); + expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals}); expect(locals.get(token)).toEqual(result1); expect(locals2.get(token)).toEqual(result3); @@ -162,14 +163,14 @@ describe("InjectorService", () => { const locals = new LocalsContainer(); // LocalContainer for the first request // WHEN REQ1 - const result1: any = injector.invoke(token, locals); - const result2: any = injector.invoke(token, locals); + const result1: any = injector.invoke(token, {locals}); + const result2: any = injector.invoke(token, {locals}); // THEN expect(result1 !== result2).toEqual(true); expect(injector.getProvider).toHaveBeenCalledWith(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, locals, {}); + expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals}); expect(locals.has(token)).toEqual(false); return expect(injector.get).not.toHaveBeenCalled(); @@ -196,7 +197,7 @@ describe("InjectorService", () => { // THEN expect(result).toBeInstanceOf(token); - expect(GlobalProviders.onInvoke).toHaveBeenCalledWith(provider, expect.any(LocalsContainer), expect.anything()); + expect(GlobalProviders.onInvoke).toHaveBeenCalledWith(provider, expect.anything()); }); it("should invoke the provider from container (2)", async () => { // GIVEN @@ -218,8 +219,8 @@ describe("InjectorService", () => { // WHEN - const result1: any = injector.invoke(token, locals); - const result2: any = injector.invoke(token, locals); + const result1: any = injector.invoke(token, {locals}); + const result2: any = injector.invoke(token, {locals}); // THEN expect(result1).toEqual(result2); diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 690f0328c4d..1dbb19bbba0 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -131,9 +131,9 @@ export class InjectorService extends Container { /** * Return all instance of the same provider type */ - getMany(type: any, locals?: LocalsContainer, options?: Partial): Type[] { + getMany(type: any, options?: Partial): Type[] { return this.getProviders(type).map((provider) => { - return this.invoke(provider.token, locals, options)!; + return this.invoke(provider.token, options)!; }); } @@ -170,12 +170,11 @@ export class InjectorService extends Container { * ``` * * @param token The injectable class to invoke. Class parameters are injected according constructor signature. - * @param locals Optional object. If preset then any argument Class are read from this object first, before the `InjectorService` is consulted. - * @param options + * @param options {InvokeOptions} Optional options to invoke the class. * @returns {Type} The class constructed. */ - public invoke(token: TokenProvider, locals?: LocalsContainer, options: Partial = {}): Type { - let instance: any = locals ? locals.get(token) : undefined; + public invoke(token: TokenProvider, options: Partial = {}): Type { + let instance: any = options.locals ? options.locals.get(token) : undefined; if (instance !== undefined) { return instance; @@ -203,7 +202,7 @@ export class InjectorService extends Container { }; if (!provider || options.rebuild) { - instance = this.resolve(token, locals, options); + instance = this.resolve(token, options); if (this.hasProvider(token)) { set(instance); @@ -212,7 +211,7 @@ export class InjectorService extends Container { return instance; } - instance = this.resolve(token, locals, options); + instance = this.resolve(token, options); switch (this.scopeOf(provider)) { case ProviderScope.SINGLETON: @@ -238,11 +237,11 @@ export class InjectorService extends Container { return instance; case ProviderScope.REQUEST: - if (locals) { - locals.set(token, instance); + if (options.locals) { + options.locals.set(token, instance); if (provider.hooks && provider.hooks.$onDestroy) { - locals.hooks.on("$onDestroy", (...args: any[]) => provider.hooks!.$onDestroy(instance, ...args)); + options.locals.hooks.on("$onDestroy", (...args: any[]) => provider.hooks!.$onDestroy(instance, ...args)); } } @@ -434,12 +433,8 @@ export class InjectorService extends Container { * @param options * @private */ - protected resolve( - target: TokenProvider, - locals: LocalsContainer = new LocalsContainer(), - options: Partial = {} - ): T | Promise { - const resolvedOpts = this.mapInvokeOptions(target, locals, options); + protected resolve(target: TokenProvider, options: Partial = {}): T | Promise { + const resolvedOpts = this.mapInvokeOptions(target, options); if (!resolvedOpts) { return undefined as T; @@ -448,7 +443,7 @@ export class InjectorService extends Container { const {token, deps, construct, imports, provider} = resolvedOpts; if (provider) { - GlobalProviders.onInvoke(provider, locals, {...resolvedOpts, injector: this}); + GlobalProviders.onInvoke(provider, resolvedOpts); } let instance: any; @@ -461,12 +456,18 @@ export class InjectorService extends Container { currentDependency = {token, index, deps}; if (isArray(token)) { - return this.getMany(token[0], locals, options); + return this.getMany(token[0], options); } const useOpts = provider?.store?.get(`${DI_USE_PARAM_OPTIONS}:${index}`) || options.useOpts; - return isInheritedFrom(token, Provider, 1) ? provider : this.invoke(token, locals, {parent, useOpts}); + return isInheritedFrom(token, Provider, 1) + ? provider + : this.invoke(token, { + parent, + locals: options.locals, + useOpts + }); }; // Invoke manually imported providers @@ -491,7 +492,7 @@ export class InjectorService extends Container { if (instance && isClass(classOf(instance))) { Reflect.defineProperty(instance, DI_INVOKE_OPTIONS, { - get: () => ({rebuild: options.rebuild, locals}) + get: () => ({rebuild: options.rebuild, locals: options.locals}) }); } @@ -542,17 +543,15 @@ export class InjectorService extends Container { /** * Create options to invoke a provider or class. * @param token - * @param locals * @param options */ - private mapInvokeOptions( - token: TokenProvider, - locals: Map, - options: Partial - ): ResolvedInvokeOptions | false { + private mapInvokeOptions(token: TokenProvider, options: Partial): ResolvedInvokeOptions | false { + const locals = options.locals || new LocalsContainer(); + + options.locals = locals; + let imports: (TokenProvider | [TokenProvider])[] | undefined = options.imports; let deps: TokenProvider[] | undefined = options.deps; - let scope = options.scope; let construct; if (!token || token === Object) { @@ -565,7 +564,7 @@ export class InjectorService extends Container { provider = new Provider(token); this.resolvers.forEach((resolver) => { - const result = resolver.get(token, locals.get(DI_USE_PARAM_OPTIONS)); + const result = resolver.get(token, options.locals!.get(DI_USE_PARAM_OPTIONS)); if (result !== undefined) { provider.useFactory = () => result; @@ -575,7 +574,6 @@ export class InjectorService extends Container { provider = this.getProvider(token)!; } - scope = scope || this.scopeOf(provider); deps = deps || provider.deps; imports = imports || provider.imports; @@ -598,11 +596,11 @@ export class InjectorService extends Container { return { token, - scope: scope || Store.from(token).get("scope") || ProviderScope.SINGLETON, deps: deps! || [], imports: imports || [], construct, - provider + provider, + locals }; } diff --git a/packages/di/src/node/domain/DIContext.ts b/packages/di/src/node/domain/DIContext.ts index abd9f65797f..af48eacaf87 100644 --- a/packages/di/src/node/domain/DIContext.ts +++ b/packages/di/src/node/domain/DIContext.ts @@ -1,6 +1,4 @@ import {injector, InjectorService, LocalsContainer} from "../../common/index.js"; -import {logger} from "../fn/logger.js"; -import {runInContext} from "../utils/asyncHookContext.js"; import {ContextLogger, ContextLoggerOptions} from "./ContextLogger.js"; export interface DIContextOptions extends Omit { diff --git a/packages/graphql/apollo/src/services/ApolloService.ts b/packages/graphql/apollo/src/services/ApolloService.ts index be86e9e33d6..d3411218d99 100644 --- a/packages/graphql/apollo/src/services/ApolloService.ts +++ b/packages/graphql/apollo/src/services/ApolloService.ts @@ -193,7 +193,7 @@ export class ApolloService { locals.set(ApolloServer, server); dataSourcesContainer.forEach((provider, key) => { - alteredContext.dataSources[key] = injector.invoke(provider.token, locals); + alteredContext.dataSources[key] = injector.invoke(provider.token, {locals}); }); return alteredContext; diff --git a/packages/orm/adapters/src/services/Adapters.ts b/packages/orm/adapters/src/services/Adapters.ts index 1daf605ac65..60e94248ee7 100644 --- a/packages/orm/adapters/src/services/Adapters.ts +++ b/packages/orm/adapters/src/services/Adapters.ts @@ -16,7 +16,7 @@ export class Adapters { invokeAdapter(options: AdapterInvokeOptions): Adapter { const {adapter = this.injector.settings.get("adapters.Adapter", MemoryAdapter), ...props} = options; - return this.injector.invoke>(adapter, options.locals, { + return this.injector.invoke>(adapter, { useOpts: props }); } diff --git a/packages/orm/ioredis/src/domain/IORedisStore.spec.ts b/packages/orm/ioredis/src/domain/IORedisStore.spec.ts index 63f5a109460..de72754ae49 100644 --- a/packages/orm/ioredis/src/domain/IORedisStore.spec.ts +++ b/packages/orm/ioredis/src/domain/IORedisStore.spec.ts @@ -117,6 +117,7 @@ vi.mock("ioredis", () => { } return { + default: {Redis}, Redis }; }); diff --git a/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts b/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts index b33a15db853..ded9d677c62 100644 --- a/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts +++ b/packages/orm/ioredis/src/utils/registerConnectionProvider.spec.ts @@ -38,7 +38,7 @@ vi.mock("ioredis", () => { } } - return {Redis: MockRedis}; + return {Redis: MockRedis, default: {Redis: MockRedis}}; }); const REDIS_CONNECTION = Symbol.for("REDIS_CONNECTION"); diff --git a/packages/orm/mikro-orm/src/MikroOrmModule.ts b/packages/orm/mikro-orm/src/MikroOrmModule.ts index 5bd15734ee2..cf047816c3f 100644 --- a/packages/orm/mikro-orm/src/MikroOrmModule.ts +++ b/packages/orm/mikro-orm/src/MikroOrmModule.ts @@ -54,7 +54,14 @@ export class MikroOrmModule implements OnDestroy, OnInit, AlterRunInContext { public async $onInit(): Promise { const container = new LocalsContainer(); - await Promise.all(this.settings.map((opts) => this.registry.register({...opts, subscribers: this.getSubscribers(opts, container)}))); + await Promise.all( + this.settings.map((opts) => + this.registry.register({ + ...opts, + subscribers: this.getSubscribers(opts, container) + }) + ) + ); } public $onDestroy(): Promise { @@ -70,13 +77,11 @@ export class MikroOrmModule implements OnDestroy, OnInit, AlterRunInContext { } private getUnmanagedSubscribers(opts: Pick, container: LocalsContainer) { - const diOpts = {scope: ProviderScope.INSTANCE}; - return (opts.subscribers ?? []).map((subscriber) => { // Starting from https://github.com/mikro-orm/mikro-orm/issues/4231 mikro-orm // accept also accepts class reference, not just instances. if (isFunction(subscriber)) { - return this.injector.invoke(subscriber, container, diOpts); + return this.injector.invoke(subscriber, {locals: container}); } return subscriber; diff --git a/packages/platform/platform-params/src/builder/PlatformParams.ts b/packages/platform/platform-params/src/builder/PlatformParams.ts index 5ee51105471..09a7b713ff1 100644 --- a/packages/platform/platform-params/src/builder/PlatformParams.ts +++ b/packages/platform/platform-params/src/builder/PlatformParams.ts @@ -53,7 +53,7 @@ export class PlatformParams { return async (scope: PlatformParamsScope) => { const container = provider.scope === ProviderScope.REQUEST ? scope.$ctx.container : undefined; - const [instance, args] = await Promise.all([this.injector.invoke(token, container), getArguments(scope)]); + const [instance, args] = await Promise.all([this.injector.invoke(token, {locals: container}), getArguments(scope)]); return instance[propertyKey].call(instance, ...args, scope.$ctx); }; diff --git a/packages/platform/platform-router/src/domain/PlatformRouters.ts b/packages/platform/platform-router/src/domain/PlatformRouters.ts index 2c019ea4fd4..4bff0c58807 100644 --- a/packages/platform/platform-router/src/domain/PlatformRouters.ts +++ b/packages/platform/platform-router/src/domain/PlatformRouters.ts @@ -1,5 +1,5 @@ import {getValue, Hooks, Type} from "@tsed/core"; -import {ControllerProvider, GlobalProviders, Injectable, InjectorService, Provider, ProviderType, TokenProvider} from "@tsed/di"; +import {ControllerProvider, GlobalProviders, Injectable, injector, InjectorService, Provider, ProviderType, TokenProvider} from "@tsed/di"; import {PlatformParamsCallback} from "@tsed/platform-params"; import {concatPath, getOperationsRoutes, JsonMethodStore, OPERATION_HTTP_VERBS} from "@tsed/schema"; @@ -36,8 +36,8 @@ function createInjectableRouter(injector: InjectorService, provider: ControllerP } GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider, { - onInvoke(provider: ControllerProvider, locals: any, {injector}) { - const router = createInjectableRouter(injector, provider); + onInvoke(provider: ControllerProvider, {locals}) { + const router = createInjectableRouter(injector(), provider); locals.set(PlatformRouter, router); } }); diff --git a/packages/platform/platform-router/test/routers-injection.integration.spec.ts b/packages/platform/platform-router/test/routers-injection.integration.spec.ts index 07282438774..758db130050 100644 --- a/packages/platform/platform-router/test/routers-injection.integration.spec.ts +++ b/packages/platform/platform-router/test/routers-injection.integration.spec.ts @@ -1,4 +1,4 @@ -import {Controller, ControllerProvider, InjectorService} from "@tsed/di"; +import {Controller, ControllerProvider, injector} from "@tsed/di"; import {PlatformParams} from "@tsed/platform-params"; import {PlatformRouter} from "../src/domain/PlatformRouter.js"; @@ -14,12 +14,11 @@ class CustomStaticsCtrl { } function createAppRouterFixture() { - const injector = new InjectorService(); - const platformRouters = injector.invoke(PlatformRouters); - const platformParams = injector.invoke(PlatformParams); - const appRouter = injector.invoke(PlatformRouter); + const platformRouters = injector({rebuild: true}).invoke(PlatformRouters); + const platformParams = injector().invoke(PlatformParams); + const appRouter = injector().invoke(PlatformRouter); - injector.addProvider(CustomStaticsCtrl, {}); + injector().addProvider(CustomStaticsCtrl, {}); return {injector, appRouter, platformRouters, platformParams}; } @@ -34,9 +33,9 @@ describe("Routers injection", () => { const router = platformRouters.from(CustomStaticsCtrl); const router1 = platformRouters.from(CustomStaticsCtrl); - const provider = injector.getProvider(CustomStaticsCtrl)!; - const router2 = injector.get(provider.tokenRouter); - const controller = injector.invoke(CustomStaticsCtrl)!; + const provider = injector().getProvider(CustomStaticsCtrl)!; + const router2 = injector().get(provider.tokenRouter); + const controller = injector().invoke(CustomStaticsCtrl)!; expect(router).toEqual(router1); expect(router).toEqual(router2); From 1e94a0146be394a42b487655c7d56d4666ec4d68 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 15 Nov 2024 17:46:36 +0100 Subject: [PATCH 03/32] feat(di): add Provider.getUseOpts --- packages/di/src/common/domain/Provider.ts | 5 ++++ .../di/src/common/interfaces/DIResolver.ts | 3 ++- .../di/src/common/services/InjectorService.ts | 25 +++---------------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index 6fd797d3c40..9fbcea438b9 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -1,5 +1,6 @@ import {type AbstractType, classOf, getClassOrSymbol, isClass, methodsOf, nameOf, Store, Type} from "@tsed/core"; +import {DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {ProviderScope} from "./ProviderScope.js"; @@ -136,6 +137,10 @@ export class Provider implements ProviderOpts { this.store.set("childrenControllers", children); } + getArgOpts(index: number) { + return this.store.get(`${DI_USE_PARAM_OPTIONS}:${index}`); + } + get(key: string) { return this.store.get(key) || this._tokenStore.get(key); } diff --git a/packages/di/src/common/interfaces/DIResolver.ts b/packages/di/src/common/interfaces/DIResolver.ts index b84740526fb..0741ed85efc 100644 --- a/packages/di/src/common/interfaces/DIResolver.ts +++ b/packages/di/src/common/interfaces/DIResolver.ts @@ -2,5 +2,6 @@ import type {TokenProvider} from "./TokenProvider.js"; export interface DIResolver { deps?: TokenProvider[]; - get(type: TokenProvider, options: any): T | undefined; + + get(type: TokenProvider): T | undefined; } diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 1dbb19bbba0..859c5bab630 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -1,17 +1,4 @@ -import { - classOf, - deepClone, - deepMerge, - Hooks, - isArray, - isClass, - isFunction, - isInheritedFrom, - isObject, - isPromise, - nameOf, - Store -} from "@tsed/core"; +import {classOf, deepClone, deepMerge, Hooks, isArray, isClass, isFunction, isInheritedFrom, isObject, isPromise, nameOf} from "@tsed/core"; import {DI_INVOKE_OPTIONS, DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; import {Configuration} from "../decorators/configuration.js"; @@ -119,7 +106,7 @@ export class InjectorService extends Container { if (!this.hasProvider(token)) { for (const resolver of this.resolvers) { - const result = resolver.get(token, options); + const result = resolver.get(token); if (result !== undefined) { return result; @@ -221,7 +208,6 @@ export class InjectorService extends Container { if (!provider.isAsync() || !isPromise(instance)) { set(instance); - // locals?.delete(DI_USE_PARAM_OPTIONS); return instance; } @@ -233,7 +219,6 @@ export class InjectorService extends Container { return instance; }); - // locals?.delete(DI_USE_PARAM_OPTIONS); return instance; case ProviderScope.REQUEST: @@ -245,8 +230,6 @@ export class InjectorService extends Container { } } - // locals?.delete(DI_USE_PARAM_OPTIONS); - return instance; } @@ -459,7 +442,7 @@ export class InjectorService extends Container { return this.getMany(token[0], options); } - const useOpts = provider?.store?.get(`${DI_USE_PARAM_OPTIONS}:${index}`) || options.useOpts; + const useOpts = provider?.getArgOpts(index) || options.useOpts; return isInheritedFrom(token, Provider, 1) ? provider @@ -564,7 +547,7 @@ export class InjectorService extends Container { provider = new Provider(token); this.resolvers.forEach((resolver) => { - const result = resolver.get(token, options.locals!.get(DI_USE_PARAM_OPTIONS)); + const result = resolver.get(token); if (result !== undefined) { provider.useFactory = () => result; From f7fa7d9a812b7ad0f6ecf438757d56df92206fc3 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 15 Nov 2024 19:18:50 +0100 Subject: [PATCH 04/32] fix(di): remove unused resolvers options BREAKING CHANGE: external DI resolvers is removed. There no needs --- docs/docs/configuration/index.md | 16 ------- .../snippets/module-resolvers.ts | 13 ------ .../snippets/server-resolvers.ts | 13 ------ packages/di/src/common/decorators/module.ts | 11 +---- packages/di/src/common/fn/injectable.ts | 4 +- packages/di/src/common/index.ts | 1 - .../src/common/integration/resolvers.spec.ts | 36 --------------- .../interfaces/DIConfigurationOptions.ts | 5 --- .../di/src/common/interfaces/DIResolver.ts | 7 --- .../di/src/common/interfaces/ProviderOpts.ts | 5 --- .../common/services/DIConfiguration.spec.ts | 11 ----- .../di/src/common/services/DIConfiguration.ts | 18 ++++---- .../common/services/InjectorService.spec.ts | 37 +++------------- .../di/src/common/services/InjectorService.ts | 44 +++---------------- packages/di/src/node/domain/DIContext.spec.ts | 1 - 15 files changed, 24 insertions(+), 198 deletions(-) delete mode 100644 docs/docs/configuration/snippets/module-resolvers.ts delete mode 100644 docs/docs/configuration/snippets/server-resolvers.ts delete mode 100644 packages/di/src/common/integration/resolvers.spec.ts delete mode 100644 packages/di/src/common/interfaces/DIResolver.ts diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 66da40518b9..968182511f5 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -288,22 +288,6 @@ export class Server {} Logger configuration. See [logger section for more detail](/docs/logger). -### resolvers - External DI - -- type: @@DIResolver@@ - -Ts.ED has its own DI container, but sometimes you have to work with other DI like Inversify or TypeDI. The version -5.39.0+ -now allows you to configure multiple external DI by using the `resolvers` options. - -The resolvers options can be configured as following: - -<<< @/docs/configuration/snippets/server-resolvers.ts - -It's also possible to register resolvers with the @@Module@@ decorator: - -<<< @/docs/configuration/snippets/module-resolvers.ts - ### views Object to configure Views engines with Ts.ED engines or Consolidate (deprecated). See more diff --git a/docs/docs/configuration/snippets/module-resolvers.ts b/docs/docs/configuration/snippets/module-resolvers.ts deleted file mode 100644 index 9ee6ef87be6..00000000000 --- a/docs/docs/configuration/snippets/module-resolvers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Module} from "@tsed/di"; -import {myContainer} from "./inversify.config"; - -@Module({ - resolvers: [ - { - get(token: any) { - return myContainer.get(token); - } - } - ] -}) -export class MyModule {} diff --git a/docs/docs/configuration/snippets/server-resolvers.ts b/docs/docs/configuration/snippets/server-resolvers.ts deleted file mode 100644 index aeded388ec8..00000000000 --- a/docs/docs/configuration/snippets/server-resolvers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Configuration} from "@tsed/di"; -import {myContainer} from "./inversify.config"; - -@Configuration({ - resolvers: [ - { - get(token: any) { - return myContainer.get(token); - } - } - ] -}) -export class Server {} diff --git a/packages/di/src/common/decorators/module.ts b/packages/di/src/common/decorators/module.ts index e10464551a4..5006c58b086 100644 --- a/packages/di/src/common/decorators/module.ts +++ b/packages/di/src/common/decorators/module.ts @@ -2,7 +2,6 @@ import {useDecorators} from "@tsed/core"; import {ProviderScope} from "../domain/ProviderScope.js"; import {ProviderType} from "../domain/ProviderType.js"; -import {DIResolver} from "../interfaces/DIResolver.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; import {Configuration} from "./configuration.js"; import {Injectable} from "./injectable.js"; @@ -20,10 +19,6 @@ export interface ModuleOptions extends Omit { * Explicit token must be injected in the constructor */ deps?: TokenProvider[]; - /** - * A list of resolvers to inject provider from external DI. - */ - resolvers?: DIResolver[]; /** * Additional properties are stored as provider configuration. @@ -36,14 +31,13 @@ export interface ModuleOptions extends Omit { * * ## Options * - imports: List of Provider which must be built by injector before invoking the module - * - resolvers: List of external DI must be used to resolve unknown provider * - deps: List of provider must be injected to the module constructor (explicit declaration) * * @param options * @decorator */ export function Module(options: Partial = {}) { - const {scopes, imports, resolvers, deps, scope, ...configuration} = options; + const {scopes, imports, deps, scope, ...configuration} = options; return useDecorators( Configuration(configuration), @@ -52,8 +46,7 @@ export function Module(options: Partial = {}) { scope: ProviderScope.SINGLETON, imports, deps, - injectable: false, - resolvers + injectable: false }) ); } diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts index 77889837bb3..7e6d60f0d03 100644 --- a/packages/di/src/common/fn/injectable.ts +++ b/packages/di/src/common/fn/injectable.ts @@ -74,9 +74,9 @@ export function providerBuilder(props: }; } -type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "resolvers" | "imports" | "configuration"; +type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration"; -const Props = ["type", "scope", "path", "alias", "hooks", "deps", "resolvers", "imports", "configuration"]; +const Props = ["type", "scope", "path", "alias", "hooks", "deps", "imports", "configuration"]; export const injectable = providerBuilder(Props); export const interceptor = providerBuilder(Props, { type: ProviderType.INTERCEPTOR diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index 96f8027cda9..567c2d1ec0d 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -40,7 +40,6 @@ export * from "./fn/refValue.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/InterceptorContext.js"; export * from "./interfaces/InterceptorMethods.js"; diff --git a/packages/di/src/common/integration/resolvers.spec.ts b/packages/di/src/common/integration/resolvers.spec.ts deleted file mode 100644 index 70dad0eb217..00000000000 --- a/packages/di/src/common/integration/resolvers.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Container} from "../domain/Container.js"; -import {InjectorService} from "../services/InjectorService.js"; - -describe("DI Resolvers", () => { - describe("create new injector", () => { - it("should load all providers with the SINGLETON scope only", async () => { - class ExternalService { - constructor() {} - } - - class MyService { - constructor(public externalService: ExternalService) {} - } - - const externalDi = new Map(); - externalDi.set(ExternalService, "MyClass"); - // GIVEN - const injector = new InjectorService(); - injector.settings.resolvers.push(externalDi); - - const container = new Container(); - container.add(MyService, { - deps: [ExternalService] - }); - - // WHEN - expect(injector.get(ExternalService)).toEqual("MyClass"); - - await injector.load(container); - - // THEN - expect(injector.get(MyService)).toBeInstanceOf(MyService); - expect(injector.get(MyService)!.externalService).toEqual("MyClass"); - }); - }); -}); diff --git a/packages/di/src/common/interfaces/DIConfigurationOptions.ts b/packages/di/src/common/interfaces/DIConfigurationOptions.ts index ddbb8913c74..94e6d28b5c6 100644 --- a/packages/di/src/common/interfaces/DIConfigurationOptions.ts +++ b/packages/di/src/common/interfaces/DIConfigurationOptions.ts @@ -1,5 +1,4 @@ import type {ProviderScope} from "../domain/ProviderScope.js"; -import type {DIResolver} from "./DIResolver.js"; import type {ImportTokenProviderOpts} from "./ImportTokenProviderOpts.js"; import type {TokenProvider} from "./TokenProvider.js"; @@ -15,10 +14,6 @@ declare global { interface Configuration extends Record { scopes: {[key: string]: ProviderScope}; - /** - * Define a list of resolvers (it can be an external DI). - */ - resolvers: DIResolver[]; /** * Define dependencies to build the provider */ diff --git a/packages/di/src/common/interfaces/DIResolver.ts b/packages/di/src/common/interfaces/DIResolver.ts deleted file mode 100644 index 0741ed85efc..00000000000 --- a/packages/di/src/common/interfaces/DIResolver.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {TokenProvider} from "./TokenProvider.js"; - -export interface DIResolver { - deps?: TokenProvider[]; - - get(type: TokenProvider): T | undefined; -} diff --git a/packages/di/src/common/interfaces/ProviderOpts.ts b/packages/di/src/common/interfaces/ProviderOpts.ts index 9f191a5657f..05324a3d184 100644 --- a/packages/di/src/common/interfaces/ProviderOpts.ts +++ b/packages/di/src/common/interfaces/ProviderOpts.ts @@ -2,7 +2,6 @@ import type {Type} from "@tsed/core"; import type {ProviderScope} from "../domain/ProviderScope.js"; import type {ProviderType} from "../domain/ProviderType.js"; -import type {DIResolver} from "./DIResolver.js"; import type {TokenProvider} from "./TokenProvider.js"; export interface ProviderOpts { @@ -46,10 +45,6 @@ export interface ProviderOpts { * Scope used by the injector to build the provider. */ scope?: ProviderScope; - /** - * A list of resolvers which will be used to resolve missing Symbol/Class when injector invoke a Class. This property allow external DI usage. - */ - resolvers?: DIResolver[]; /** * hooks to intercept custom events diff --git a/packages/di/src/common/services/DIConfiguration.spec.ts b/packages/di/src/common/services/DIConfiguration.spec.ts index 0d231b7b970..7706a72a5a8 100644 --- a/packages/di/src/common/services/DIConfiguration.spec.ts +++ b/packages/di/src/common/services/DIConfiguration.spec.ts @@ -44,7 +44,6 @@ describe("DIConfiguration", () => { expect(obj).toEqual({ imports: [], logger: {}, - resolvers: [], routes: [], scopes: {} }); @@ -69,14 +68,4 @@ describe("DIConfiguration", () => { expect(configuration.imports).toEqual([]); }); }); - - describe("resolvers()", () => { - it("should get resolvers", () => { - // GIVEN - const configuration = new DIConfiguration(); - - configuration.resolvers = []; - expect(configuration.resolvers).toEqual([]); - }); - }); }); diff --git a/packages/di/src/common/services/DIConfiguration.ts b/packages/di/src/common/services/DIConfiguration.ts index 952dae3c772..1b9c8468cc4 100644 --- a/packages/di/src/common/services/DIConfiguration.ts +++ b/packages/di/src/common/services/DIConfiguration.ts @@ -2,7 +2,6 @@ import {Env, getValue, setValue} from "@tsed/core"; import type {ProviderScope} from "../domain/ProviderScope.js"; import type {DILoggerOptions} from "../interfaces/DILoggerOptions.js"; -import type {DIResolver} from "../interfaces/DIResolver.js"; import type {ImportTokenProviderOpts} from "../interfaces/ImportTokenProviderOpts.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import type {TokenRoute} from "../interfaces/TokenRoute.js"; @@ -14,7 +13,6 @@ export class DIConfiguration { constructor(initialProps = {}) { Object.entries({ scopes: {}, - resolvers: [], imports: [], routes: [], logger: {}, @@ -55,14 +53,14 @@ export class DIConfiguration { set scopes(value: Record) { this.map.set("scopes", value); } - - get resolvers(): DIResolver[] { - return this.getRaw("resolvers"); - } - - set resolvers(resolvers: DIResolver[]) { - this.map.set("resolvers", resolvers); - } + // + // get resolvers(): DIResolver[] { + // return this.getRaw("resolvers"); + // } + // + // set resolvers(resolvers: DIResolver[]) { + // this.map.set("resolvers", resolvers); + // } get imports(): (TokenProvider | ImportTokenProviderOpts)[] { return this.get("imports")!; diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index e588e047054..0b904cb1f2e 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -663,8 +663,7 @@ describe("InjectorService", () => { scopes: { provider_custom: ProviderScope.SINGLETON } - }, - resolvers: [vi.fn() as any] + } }); injector.add(Symbol.for("TOKEN2"), { @@ -679,37 +678,11 @@ describe("InjectorService", () => { injector.resolveConfiguration(); // THEN - expect(injector.resolvers.length).toEqual(1); - }); - }); - - describe("resolvers", () => { - it("should load all providers with the SINGLETON scope only", async () => { - class ExternalService { - constructor() {} - } - - class MyService { - constructor(public externalService: ExternalService) {} - } - - const externalDi = new Map(); - externalDi.set(ExternalService, "MyClass"); - // GIVEN - const injector = new InjectorService(); - injector.settings.resolvers.push(externalDi); - - const container = new Container(); - container.add(MyService, { - deps: [ExternalService] + expect(injector.settings.scopes).toEqual({ + provider_custom: "singleton", + provider_custom_2: "singleton", + value: "singleton" }); - - // WHEN - await injector.load(container); - - // THEN - expect(injector.get(MyService)).toBeInstanceOf(MyService); - expect(injector.get(MyService)!.externalService).toEqual("MyClass"); }); }); diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 859c5bab630..2cb97c3f905 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -55,10 +55,6 @@ export class InjectorService extends Container { this.#cache.set(InjectorService, this); } - get resolvers() { - return this.settings.resolvers!; - } - get scopes() { return this.settings.scopes || {}; } @@ -98,21 +94,7 @@ export class InjectorService extends Container { * @param options */ get(token: TokenProvider, options: Record = {}): T | undefined { - const instance = this.getInstance(token); - - if (instance !== undefined) { - return instance; - } - - if (!this.hasProvider(token)) { - for (const resolver of this.resolvers) { - const result = resolver.get(token); - - if (result !== undefined) { - return result; - } - } - } + return this.#cache.get(token); } /** @@ -171,16 +153,16 @@ export class InjectorService extends Container { return this.settings as unknown as Type; } - instance = !options.rebuild ? this.getInstance(token) : undefined; + if (token === DI_USE_PARAM_OPTIONS) { + return options.useOpts as Type; + } + + instance = !options.rebuild ? this.#cache.get(token) : undefined; if (instance != undefined) { return instance; } - if (token === DI_USE_PARAM_OPTIONS) { - return options.useOpts as Type; - } - const provider = this.ensureProvider(token); const set = (instance: any) => { @@ -324,16 +306,12 @@ export class InjectorService extends Container { super.forEach((provider) => { if (provider.configuration && provider.type !== "server:module") { Object.entries(provider.configuration).forEach(([key, value]) => { - if (!["resolvers", "mount", "imports"].includes(key)) { + if (!["mount", "imports"].includes(key)) { value = mergedConfiguration.has(key) ? deepMerge(mergedConfiguration.get(key), value) : deepClone(value); mergedConfiguration.set(key, value); } }); } - - if (provider.resolvers) { - this.settings.resolvers = this.settings.resolvers.concat(provider.resolvers); - } }); mergedConfiguration.forEach((value, key) => { @@ -545,14 +523,6 @@ export class InjectorService extends Container { if (!this.hasProvider(token)) { provider = new Provider(token); - - this.resolvers.forEach((resolver) => { - const result = resolver.get(token); - - if (result !== undefined) { - provider.useFactory = () => result; - } - }); } else { provider = this.getProvider(token)!; } diff --git a/packages/di/src/node/domain/DIContext.spec.ts b/packages/di/src/node/domain/DIContext.spec.ts index a0ea4fe9f39..ca3302e4acf 100644 --- a/packages/di/src/node/domain/DIContext.spec.ts +++ b/packages/di/src/node/domain/DIContext.spec.ts @@ -1,6 +1,5 @@ import {logger} from "../fn/logger.js"; import {DITest} from "../services/DITest.js"; -import {bindContext, getAsyncStore} from "../utils/asyncHookContext.js"; import {DIContext} from "./DIContext.js"; describe("DIContext", () => { From 18ce1e2255796a86ce687773f7eb327e87879356 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 15 Nov 2024 19:49:25 +0100 Subject: [PATCH 05/32] fix(di): remove injector.loadModule() --- packages/di/src/common/index.ts | 1 - .../di/src/common/interfaces/InvokeOptions.ts | 4 -- .../common/services/InjectorService.spec.ts | 31 ---------- .../di/src/common/services/InjectorService.ts | 60 ++++++------------- .../src/common/builder/PlatformBuilder.ts | 32 +++++++--- .../platform-http/src/common/index.ts | 1 + .../common/utils/resolveControllers.spec.ts | 10 ++-- .../src/common/utils/resolveControllers.ts | 16 ++--- .../src/importProviders.spec.ts | 2 +- 9 files changed, 59 insertions(+), 98 deletions(-) rename packages/{di => platform/platform-http}/src/common/utils/resolveControllers.spec.ts (61%) rename packages/{di => platform/platform-http}/src/common/utils/resolveControllers.ts (77%) diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index 567c2d1ec0d..f1f1cb856ee 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -60,4 +60,3 @@ export * from "./utils/colors.js"; export * from "./utils/createContainer.js"; export * from "./utils/getConfiguration.js"; export * from "./utils/getConstructorDependencies.js"; -export * from "./utils/resolveControllers.js"; diff --git a/packages/di/src/common/interfaces/InvokeOptions.ts b/packages/di/src/common/interfaces/InvokeOptions.ts index c07e34ebed4..31f44a4e81d 100644 --- a/packages/di/src/common/interfaces/InvokeOptions.ts +++ b/packages/di/src/common/interfaces/InvokeOptions.ts @@ -15,10 +15,6 @@ export interface InvokeOptions { * Parent provider. */ parent?: TokenProvider; - // /** - // * Scope used by the injector to build the provider. - // */ - // scope: ProviderScope; /** * If true, the injector will rebuild the instance. */ diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index 0b904cb1f2e..138faa9002d 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -566,37 +566,6 @@ describe("InjectorService", () => { }); }); }); - describe("loadModule()", () => { - it("should load DI with a rootModule (SINGLETON + deps)", async () => { - // GIVEN - @Injectable() - class RootModule {} - - const token = class Test {}; - const provider = new Provider(token); - - provider.scope = ProviderScope.SINGLETON; - provider.deps = [InjectorService]; - - const injector = new InjectorService(); - - await injector.loadModule(RootModule); - - expect(injector.get(RootModule)).toBeInstanceOf(RootModule); - }); - - it("should load DI with a rootModule", async () => { - // GIVEN - @Injectable() - class RootModule {} - - const injector = new InjectorService(); - - await injector.loadModule(RootModule); - - expect(injector.get(RootModule)).toBeInstanceOf(RootModule); - }); - }); describe("resolveConfiguration()", () => { it("should load configuration from each providers", () => { diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 2cb97c3f905..6ecaf3dc06f 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -16,7 +16,6 @@ import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {createContainer} from "../utils/createContainer.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; -import {resolveControllers} from "../utils/resolveControllers.js"; import {DIConfiguration} from "./DIConfiguration.js"; /** @@ -240,42 +239,6 @@ export class InjectorService extends Container { } } - /** - * Boostrap injector from container and resolve configuration. - * - * @param container - */ - bootstrap(container: Container = createContainer()) { - // Clone all providers in the container - this.addProviders(container); - - // Resolve all configuration - this.resolveConfiguration(); - - // allow mocking or changing provider instance before loading injector - this.resolveImportsProviders(); - - return this; - } - - /** - * Load injector from a given module - * @param rootModule - */ - loadModule(rootModule: TokenProvider) { - this.settings.routes = this.settings.routes.concat(resolveControllers(this.settings)); - - const container = createContainer(); - container.delete(rootModule); - - container.addProvider(rootModule, { - type: "server:module", - scope: ProviderScope.SINGLETON - }); - - return this.load(container); - } - /** * Build all providers from given container (or GlobalProviders) and emit `$onInit` event. * @@ -358,6 +321,24 @@ export class InjectorService extends Container { await this.emit("$onDestroy"); } + /** + * Boostrap injector from container and resolve configuration. + * + * @param container + */ + protected bootstrap(container: Container = createContainer()) { + // Clone all providers in the container + this.addProviders(container); + + // Resolve all configuration + this.resolveConfiguration(); + + // allow mocking or changing provider instance before loading injector + this.resolveImportsProviders(); + + return this; + } + /** * Ensure that a provider is added to the container. * @protected @@ -373,10 +354,6 @@ export class InjectorService extends Container { return this.getProvider(token)!; } - protected getInstance(token: any) { - return this.#cache.get(token); - } - /** * Invoke a class method and inject service. * @@ -390,7 +367,6 @@ export class InjectorService extends Container { * #### Example * * @param target - * @param locals * @param options * @private */ diff --git a/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts b/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts index cdecd7f83c6..9fe75d212ed 100644 --- a/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts +++ b/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts @@ -1,5 +1,14 @@ import {isClass, isFunction, isString, nameOf, Type} from "@tsed/core"; -import {colors, InjectorService, ProviderOpts, setLoggerConfiguration, TokenProvider} from "@tsed/di"; +import { + colors, + createContainer, + injector, + InjectorService, + ProviderOpts, + ProviderScope, + setLoggerConfiguration, + TokenProvider +} from "@tsed/di"; import {getMiddlewaresForHook, PlatformMiddlewareLoadingOptions} from "@tsed/platform-middlewares"; import {PlatformLayer} from "@tsed/platform-router"; import type {IncomingMessage, ServerResponse} from "http"; @@ -19,6 +28,7 @@ import {CreateServerReturn} from "../utils/createServer.js"; import {getConfiguration} from "../utils/getConfiguration.js"; import {getStaticsOptions} from "../utils/getStaticsOptions.js"; import {printRoutes} from "../utils/printRoutes.js"; +import {resolveControllers} from "../utils/resolveControllers.js"; /** * @platform @@ -29,7 +39,6 @@ export class PlatformBuilder { readonly name: string = ""; protected startedAt = new Date(); protected current = new Date(); - readonly #injector: InjectorService; readonly #rootModule: Type; readonly #adapter: PlatformAdapter; #promise: Promise; @@ -45,14 +54,14 @@ export class PlatformBuilder { configuration.PLATFORM_NAME = adapterKlass.NAME; this.name = adapterKlass.NAME; - this.#injector = createInjector({ + createInjector({ adapter: adapterKlass, settings: configuration }); this.log(`Loading ${adapterKlass.NAME.toUpperCase()} platform adapter...`); - this.#adapter = this.#injector.get>(PlatformAdapter)!; + this.#adapter = injector().get>(PlatformAdapter)!; this.createHttpServers(); @@ -60,11 +69,11 @@ export class PlatformBuilder { } get injector(): InjectorService { - return this.#injector; + return injector(); } get rootModule(): any { - return this.#injector.get(this.#rootModule); + return injector().get(this.#rootModule); } get app(): PlatformApplication { @@ -221,7 +230,16 @@ export class PlatformBuilder { const {injector} = this; this.log("Build providers"); - await injector.loadModule(this.#rootModule); + this.settings.routes = this.settings.routes.concat(resolveControllers(this.settings)); + + const container = createContainer(); + container.delete(this.#rootModule); + container.addProvider(this.#rootModule, { + type: "server:module", + scope: ProviderScope.SINGLETON + }); + + await injector.load(container); this.log("Settings and injector loaded..."); diff --git a/packages/platform/platform-http/src/common/index.ts b/packages/platform/platform-http/src/common/index.ts index 6cc70807aa7..a0b5ff96af4 100644 --- a/packages/platform/platform-http/src/common/index.ts +++ b/packages/platform/platform-http/src/common/index.ts @@ -57,4 +57,5 @@ export * from "./utils/listenServer.js"; export * from "./utils/mapReturnedResponse.js"; export * from "./utils/printRoutes.js"; export * from "./utils/registerPlatformAdapter.js"; +export * from "./utils/resolveControllers.js"; export * from "./utils/setResponseHeaders.js"; diff --git a/packages/di/src/common/utils/resolveControllers.spec.ts b/packages/platform/platform-http/src/common/utils/resolveControllers.spec.ts similarity index 61% rename from packages/di/src/common/utils/resolveControllers.spec.ts rename to packages/platform/platform-http/src/common/utils/resolveControllers.spec.ts index b17ad472a6b..35b546b3bf9 100644 --- a/packages/di/src/common/utils/resolveControllers.spec.ts +++ b/packages/platform/platform-http/src/common/utils/resolveControllers.spec.ts @@ -1,10 +1,10 @@ import {nameOf} from "@tsed/core"; -import {Controller} from "../decorators/controller.js"; -import {M1Ctrl1} from "./__mock__/module1/controllers/M1Ctrl1.js"; -import {Module1} from "./__mock__/module1/Module1.js"; -import {M2Ctrl} from "./__mock__/module2/controllers/M2Ctrl.js"; -import {Module2} from "./__mock__/module2/Module2.js"; +import {Controller} from "../../../../../di/src/common/decorators/controller.js"; +import {M1Ctrl1} from "../../../../../di/src/common/utils/__mock__/module1/controllers/M1Ctrl1.js"; +import {Module1} from "../../../../../di/src/common/utils/__mock__/module1/Module1.js"; +import {M2Ctrl} from "../../../../../di/src/common/utils/__mock__/module2/controllers/M2Ctrl.js"; +import {Module2} from "../../../../../di/src/common/utils/__mock__/module2/Module2.js"; import {resolveControllers} from "./resolveControllers.js"; @Controller("/root") diff --git a/packages/di/src/common/utils/resolveControllers.ts b/packages/platform/platform-http/src/common/utils/resolveControllers.ts similarity index 77% rename from packages/di/src/common/utils/resolveControllers.ts rename to packages/platform/platform-http/src/common/utils/resolveControllers.ts index 31824b9b43f..cc69a4440db 100644 --- a/packages/di/src/common/utils/resolveControllers.ts +++ b/packages/platform/platform-http/src/common/utils/resolveControllers.ts @@ -1,14 +1,12 @@ import {isArray, isClass} from "@tsed/core"; - -import {Provider} from "../domain/Provider.js"; -import {ProviderType} from "../domain/ProviderType.js"; -import {TokenProvider} from "../interfaces/TokenProvider.js"; -import {TokenRoute} from "../interfaces/TokenRoute.js"; -import {GlobalProviders} from "../registries/GlobalProviders.js"; +import {GlobalProviders, Provider, ProviderType, type TokenProvider, type TokenRoute} from "@tsed/di"; const lookupProperties = ["mount", "imports"]; -export function getTokens(config: any): {route?: string; token: TokenProvider}[] { +/** + * @ignore + */ +function getTokens(config: any): {route?: string; token: TokenProvider}[] { if (!config) { return []; } @@ -33,6 +31,9 @@ export function getTokens(config: any): {route?: string; token: TokenProvider}[] }, []); } +/** + * @ignore + */ function resolveRecursively(providers: {token: TokenProvider; route?: string}[]) { return providers .map(({token}) => GlobalProviders.get(token)) @@ -44,6 +45,7 @@ function resolveRecursively(providers: {token: TokenProvider; route?: string}[]) * Return controllers and is base route according to his configuration in module configuration. * * @param settings + * @ignore */ export function resolveControllers(settings: Partial): TokenRoute[] { const providers = lookupProperties.flatMap((property) => getTokens(settings[property])); diff --git a/packages/third-parties/components-scan/src/importProviders.spec.ts b/packages/third-parties/components-scan/src/importProviders.spec.ts index b3b3b0be8cb..c43f722997c 100644 --- a/packages/third-parties/components-scan/src/importProviders.spec.ts +++ b/packages/third-parties/components-scan/src/importProviders.spec.ts @@ -1,5 +1,5 @@ import {nameOf} from "@tsed/core"; -import {resolveControllers} from "@tsed/di"; +import {resolveControllers} from "@tsed/platform-http"; import {importProviders} from "./importProviders.js"; From 0a200093d0434f72f183ab76c53c6893fff5a4fc Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 16 Nov 2024 09:37:40 +0100 Subject: [PATCH 06/32] fix(di): remove default scope configuration on DIConfiguration level BREAKING CHANGE: configuration.scope is removed. This options doesn't make sense since a $ctx exists. ProviderScope.REQUEST must to be declared explicitly on each controller. --- .gitignore | 2 ++ docs/docs/configuration/index.md | 17 ------------ docs/docs/injection-scopes.md | 2 +- .../src/common/decorators/autoInjectable.ts | 2 +- packages/di/src/common/domain/Provider.ts | 22 ++++++++++++---- packages/di/src/common/fn/injectMany.ts | 11 +++++++- .../common/services/DIConfiguration.spec.ts | 13 +--------- .../di/src/common/services/DIConfiguration.ts | 17 ------------ .../common/services/InjectorService.spec.ts | 25 ++++-------------- .../di/src/common/services/InjectorService.ts | 26 +++++-------------- .../orm/adapters/src/services/Adapters.ts | 12 ++++----- 11 files changed, 49 insertions(+), 100 deletions(-) diff --git a/.gitignore b/.gitignore index 252eb30a561..db69eb4aac0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ lib-cov # Coverage directory used by tools like istanbul coverage coverage-* + + # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 968182511f5..36dfdb35620 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -265,23 +265,6 @@ Add providers or modules here. These modules or provider will be built before th -### scopes - -- type: `{[key: string]: ProviderScope}` - -Change the default scope for a given provider. See [injection scopes](/docs/injection-scopes) for more details. - -```typescript -import {Configuration, ProviderScope, ProviderType} from "@tsed/di"; - -@Configuration({ - scopes: { - [ProviderType.CONTROLLER]: ProviderScope.REQUEST - } -}) -export class Server {} -``` - ### logger - type: @@PlatformLoggerSettings@@ diff --git a/docs/docs/injection-scopes.md b/docs/docs/injection-scopes.md index 5044b37dc6b..f0c4614b31b 100644 --- a/docs/docs/injection-scopes.md +++ b/docs/docs/injection-scopes.md @@ -2,7 +2,7 @@ The scope of a [Provider](/docs/providers.md) defines the lifecycle and visibility of that bean in the context in which it is used. -Ts.ED DI defines 3 types of @@Scope@@ which can be used on injectable classes: +Ts.ED DI defines 3 types of @@ProviderScope@@ which can be used on injectable classes: - `singleton`: The default scope. The provider is created during server initialization and is shared across all requests. - `request`: A new instance of the provider is created for each incoming request. diff --git a/packages/di/src/common/decorators/autoInjectable.ts b/packages/di/src/common/decorators/autoInjectable.ts index a29a6e5f756..b141f8dea1f 100644 --- a/packages/di/src/common/decorators/autoInjectable.ts +++ b/packages/di/src/common/decorators/autoInjectable.ts @@ -17,7 +17,7 @@ function resolveAutoInjectableArgs(token: Type, args: unknown[]) { list.push(args[i]); } else { const value = deps[i]; - const instance = isArray(value) ? inj!.getMany(value[0], {locals, parent: token}) : inj!.invoke(value, {locals, parent: token}); + const instance = isArray(value) ? inj.getMany(value[0], {locals, parent: token}) : inj.invoke(value, {locals, parent: token}); list.push(instance); } diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index 9fbcea438b9..374785634eb 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -110,7 +110,7 @@ export class Provider implements ProviderOpts { return ProviderScope.SINGLETON; } - return this.get("scope"); + return this.get("scope", ProviderScope.SINGLETON); } /** @@ -122,7 +122,7 @@ export class Provider implements ProviderOpts { } get configuration(): Partial { - return this.get("configuration"); + return this.get("configuration")!; } set configuration(configuration: Partial) { @@ -140,9 +140,21 @@ export class Provider implements ProviderOpts { getArgOpts(index: number) { return this.store.get(`${DI_USE_PARAM_OPTIONS}:${index}`); } - - get(key: string) { - return this.store.get(key) || this._tokenStore.get(key); + /** + * Retrieves a value from the provider's store. + * @param key The key to look up + * @returns The value if found, undefined otherwise + */ + get(key: string): Type | undefined; + /** + * Retrieves a value from the provider's store with a default fallback. + * @param key The key to look up + * @param defaultValue The value to return if key is not found + * @returns The found value or defaultValue + */ + get(key: string, defaultValue: Type): Type; + get(key: string, defaultValue?: Type): Type | undefined { + return this.store.get(key) || this._tokenStore.get(key) || defaultValue; } isAsync(): boolean { diff --git a/packages/di/src/common/fn/injectMany.ts b/packages/di/src/common/fn/injectMany.ts index c3e7a6ad516..412d4cbe7df 100644 --- a/packages/di/src/common/fn/injectMany.ts +++ b/packages/di/src/common/fn/injectMany.ts @@ -2,6 +2,15 @@ import type {InvokeOptions} from "../interfaces/InvokeOptions.js"; import {injector} from "./injector.js"; import {localsContainer} from "./localsContainer.js"; +/** + * Injects multiple instances of a given token using the injector service. + * @param token - The injection token to resolve + * @param opts - Optional configuration for the injection + * @param opts.useOpts - Options for instance creation + * @param opts.rebuild - Whether to rebuild the instance + * @param opts.locals - Local container overrides + * @returns Array of resolved instances + */ export function injectMany(token: string | symbol, opts?: Partial>): T[] { - return injector().getMany(token, {...opts, locals: opts?.locals || localsContainer()}); + return injector().getMany(token, {...opts, locals: opts?.locals || localsContainer()} as InvokeOptions); } diff --git a/packages/di/src/common/services/DIConfiguration.spec.ts b/packages/di/src/common/services/DIConfiguration.spec.ts index 7706a72a5a8..26afc9d209b 100644 --- a/packages/di/src/common/services/DIConfiguration.spec.ts +++ b/packages/di/src/common/services/DIConfiguration.spec.ts @@ -44,21 +44,10 @@ describe("DIConfiguration", () => { expect(obj).toEqual({ imports: [], logger: {}, - routes: [], - scopes: {} + routes: [] }); }); }); - describe("scopes()", () => { - it("should get scopes", () => { - // GIVEN - const configuration = new DIConfiguration(); - - configuration.scopes = {}; - expect(configuration.scopes).toEqual({}); - }); - }); - describe("imports()", () => { it("should get imports", () => { // GIVEN diff --git a/packages/di/src/common/services/DIConfiguration.ts b/packages/di/src/common/services/DIConfiguration.ts index 1b9c8468cc4..e6dc695ef70 100644 --- a/packages/di/src/common/services/DIConfiguration.ts +++ b/packages/di/src/common/services/DIConfiguration.ts @@ -12,7 +12,6 @@ export class DIConfiguration { constructor(initialProps = {}) { Object.entries({ - scopes: {}, imports: [], routes: [], logger: {}, @@ -46,22 +45,6 @@ export class DIConfiguration { this.map.set("env", value); } - get scopes(): Record { - return this.map.get("scopes"); - } - - set scopes(value: Record) { - this.map.set("scopes", value); - } - // - // get resolvers(): DIResolver[] { - // return this.getRaw("resolvers"); - // } - // - // set resolvers(resolvers: DIResolver[]) { - // this.map.set("resolvers", resolvers); - // } - get imports(): (TokenProvider | ImportTokenProviderOpts)[] { return this.get("imports")!; } diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index 138faa9002d..3813a8677b4 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -616,29 +616,18 @@ describe("InjectorService", () => { // GIVEN const injector = new InjectorService(); - injector.settings.set({ - scopes: { - [ProviderType.VALUE]: ProviderScope.SINGLETON - } - }); - - expect(injector.settings.get("scopes")).toEqual({ - [ProviderType.VALUE]: ProviderScope.SINGLETON - }); - injector.add(Symbol.for("TOKEN1"), { configuration: { - custom: "config", - scopes: { - provider_custom: ProviderScope.SINGLETON + custom: { + config: "1" } } }); injector.add(Symbol.for("TOKEN2"), { configuration: { - scopes: { - provider_custom_2: ProviderScope.SINGLETON + custom: { + config2: "1" } } }); @@ -647,11 +636,7 @@ describe("InjectorService", () => { injector.resolveConfiguration(); // THEN - expect(injector.settings.scopes).toEqual({ - provider_custom: "singleton", - provider_custom_2: "singleton", - value: "singleton" - }); + expect(injector.settings.get("custom")).toEqual({config: "1", config2: "1"}); }); }); diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 6ecaf3dc06f..96448669836 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -18,6 +18,8 @@ import {createContainer} from "../utils/createContainer.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; import {DIConfiguration} from "./DIConfiguration.js"; +const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; + /** * This service contain all services collected by `@Service` or services declared manually with `InjectorService.factory()` or `InjectorService.service()`. * @@ -54,18 +56,6 @@ export class InjectorService extends Container { this.#cache.set(InjectorService, this); } - get scopes() { - return this.settings.scopes || {}; - } - - /** - * Retrieve default scope for a given provider. - * @param provider - */ - public scopeOf(provider: Provider) { - return provider.scope || this.scopes[String(provider.type)] || ProviderScope.SINGLETON; - } - /** * Return a list of instance build by the injector. */ @@ -101,7 +91,7 @@ export class InjectorService extends Container { */ getMany(type: any, options?: Partial): Type[] { return this.getProviders(type).map((provider) => { - return this.invoke(provider.token, options)!; + return this.invoke(provider.token, options); }); } @@ -181,7 +171,7 @@ export class InjectorService extends Container { instance = this.resolve(token, options); - switch (this.scopeOf(provider)) { + switch (provider.scope) { case ProviderScope.SINGLETON: if (provider.hooks && !options.rebuild) { this.registerHooks(provider, instance); @@ -206,9 +196,7 @@ export class InjectorService extends Container { if (options.locals) { options.locals.set(token, instance); - if (provider.hooks && provider.hooks.$onDestroy) { - options.locals.hooks.on("$onDestroy", (...args: any[]) => provider.hooks!.$onDestroy(instance, ...args)); - } + options.locals?.hooks.on("$onDestroy", (...args: unknown[]) => provider.hooks?.$onDestroy(instance, ...args)); } return instance; @@ -233,7 +221,7 @@ export class InjectorService extends Container { */ loadSync() { for (const [, provider] of this) { - if (!this.has(provider.token) && this.scopeOf(provider) === ProviderScope.SINGLETON) { + if (!this.has(provider.token) && provider.scope === ProviderScope.SINGLETON) { this.invoke(provider.token); } } @@ -269,7 +257,7 @@ export class InjectorService extends Container { super.forEach((provider) => { if (provider.configuration && provider.type !== "server:module") { Object.entries(provider.configuration).forEach(([key, value]) => { - if (!["mount", "imports"].includes(key)) { + if (!EXCLUDED_CONFIGURATION_KEYS.includes(key)) { value = mergedConfiguration.has(key) ? deepMerge(mergedConfiguration.get(key), value) : deepClone(value); mergedConfiguration.set(key, value); } diff --git a/packages/orm/adapters/src/services/Adapters.ts b/packages/orm/adapters/src/services/Adapters.ts index 60e94248ee7..c79d09cf186 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 {Inject, Injectable, InjectorService} from "@tsed/di"; +import {constant, inject, injectable} from "@tsed/di"; import {MemoryAdapter} from "../adapters/MemoryAdapter.js"; import {Adapter, AdapterConstructorOptions} from "../domain/Adapter.js"; @@ -8,16 +8,14 @@ export interface AdapterInvokeOptions extends AdapterConstructorOpt adapter?: Type>; } -@Injectable() export class Adapters { - @Inject() - injector: InjectorService; - invokeAdapter(options: AdapterInvokeOptions): Adapter { - const {adapter = this.injector.settings.get("adapters.Adapter", MemoryAdapter), ...props} = options; + const {adapter = constant("adapters.Adapter", MemoryAdapter), ...props} = options; - return this.injector.invoke>(adapter, { + return inject>(adapter, { useOpts: props }); } } + +injectable(Adapters); From c157178f47f0b3ec9d60f896e0c066fa7dc8f28a Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Thu, 21 Nov 2024 18:06:48 +0100 Subject: [PATCH 07/32] feat(hooks): create @tsed/hooks package --- package.json | 2 +- packages/core/src/domain/Hooks.spec.ts | 59 ----- packages/core/src/domain/Hooks.ts | 108 -------- packages/core/src/index.ts | 1 - packages/core/vitest.config.mts | 8 +- packages/di/package.json | 5 + .../di/src/common/domain/LocalsContainer.ts | 2 +- packages/di/src/common/fn/events.spec.ts | 102 -------- packages/di/src/common/fn/events.ts | 33 --- packages/di/src/common/index.ts | 2 +- .../di/src/common/services/InjectorService.ts | 45 ++-- packages/di/src/common/utils/discoverHooks.ts | 15 ++ .../src/common/utils/registerProviderHooks.ts | 13 + packages/di/src/node/domain/ContextLogger.ts | 2 +- packages/di/tsconfig.json | 3 + packages/hooks/.npmignore | 8 + packages/hooks/package.json | 45 ++++ packages/hooks/readme.md | 64 +++++ packages/hooks/src/Hooks.spec.ts | 219 ++++++++++++++++ packages/hooks/src/Hooks.ts | 241 ++++++++++++++++++ packages/hooks/src/index.ts | 1 + packages/hooks/tsconfig.esm.json | 26 ++ packages/hooks/tsconfig.json | 16 ++ packages/hooks/tsconfig.spec.json | 14 + packages/hooks/vitest.config.mts | 21 ++ packages/hooks/webpack.config.cjs | 4 + packages/orm/adapters-redis/package.json | 2 + .../src/adapters/RedisAdapter.ts | 3 +- .../ioredis/src/domain/IORedisStore.spec.ts | 3 +- packages/platform/platform-http/package.json | 1 + .../platform-serverless-http/package.json | 2 +- .../platform/platform-serverless/package.json | 2 + .../src/hooks/alterAfterDeserialize.ts | 2 +- .../src/hooks/alterBeforeDeserialize.ts | 2 +- packages/specs/schema/package.json | 2 + .../specs/schema/src/domain/JsonSchema.ts | 16 +- .../specs/schema/src/hooks/alterIgnore.ts | 3 +- packages/specs/schema/tsconfig.json | 3 + tsconfig.json | 3 + yarn.lock | 30 ++- 40 files changed, 777 insertions(+), 356 deletions(-) delete mode 100644 packages/core/src/domain/Hooks.spec.ts delete mode 100644 packages/core/src/domain/Hooks.ts delete mode 100644 packages/di/src/common/fn/events.spec.ts delete mode 100644 packages/di/src/common/fn/events.ts create mode 100644 packages/di/src/common/utils/discoverHooks.ts create mode 100644 packages/di/src/common/utils/registerProviderHooks.ts create mode 100644 packages/hooks/.npmignore create mode 100644 packages/hooks/package.json create mode 100644 packages/hooks/readme.md create mode 100644 packages/hooks/src/Hooks.spec.ts create mode 100644 packages/hooks/src/Hooks.ts create mode 100644 packages/hooks/src/index.ts create mode 100644 packages/hooks/tsconfig.esm.json create mode 100644 packages/hooks/tsconfig.json create mode 100644 packages/hooks/tsconfig.spec.json create mode 100644 packages/hooks/vitest.config.mts create mode 100644 packages/hooks/webpack.config.cjs diff --git a/package.json b/package.json index 672bf932f12..73a555c24e5 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test:ci": "yarn test:lint && yarn test:core && yarn test:specs && yarn test:platform && yarn test:integration && yarn test:graphql && yarn test:orm && yarn test:security && yarn test:third-parties", "test:lint": "eslint '**/*.{ts,js}'", "test:lint:fix": "eslint '**/*.{ts,js}' --fix", - "test:core": "lerna run test:ci --scope '@tsed/{core,di,platform-http,engines,normalize-path}' --stream --concurrency 2", + "test:core": "lerna run test:ci --scope '@tsed/{core,di,hooks,platform-http,engines,normalize-path}' --stream --concurrency 2", "test:platform": "lerna run test:ci --ignore '@tsed/platform-{express,koa}' --scope '@tsed/platform-*' --stream --concurrency 2", "test:integration": "lerna run test:ci --scope '@tsed/platform-{express,koa}' --stream --concurrency 2", "test:orm": "lerna run test:ci --scope '@tsed/{adapters,adapters-redis,mikro-orm,mongoose,objection,prisma}' --stream --concurrency 4", diff --git a/packages/core/src/domain/Hooks.spec.ts b/packages/core/src/domain/Hooks.spec.ts deleted file mode 100644 index e8313e5ca65..00000000000 --- a/packages/core/src/domain/Hooks.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {Hooks} from "./Hooks.js"; - -describe("Hooks", () => { - describe("emit", () => { - it("should listen a hook and calls listener", () => { - const hooks = new Hooks(); - const fn = vi.fn(); - - hooks.on("event", fn); - - hooks.emit("event", ["arg1"]); - - expect(fn).toHaveBeenCalledWith("arg1"); - - hooks.off("event", fn); - }); - it("should async listen a hook and calls listener", async () => { - const hooks = new Hooks(); - const fn = vi.fn(); - - hooks.on("event", fn); - - await hooks.asyncEmit("event", ["arg1"]); - - expect(fn).toHaveBeenCalledWith("arg1"); - - hooks.off("event", fn); - }); - }); - describe("alter", () => { - it("should listen a hook and calls listener", () => { - const hooks = new Hooks(); - const fn = vi.fn().mockReturnValue("valueAltered"); - - hooks.on("event", fn); - - const value = hooks.alter("event", "value"); - - expect(fn).toHaveBeenCalledWith("value"); - expect(value).toBe("valueAltered"); - - hooks.off("event", fn); - }); - it("should async listen a hook and calls listener", async () => { - const hooks = new Hooks(); - const fn = vi.fn().mockReturnValue("valueAltered"); - - hooks.on("event", fn); - - await hooks.asyncAlter("event", "value", ["arg1"]); - - expect(fn).toHaveBeenCalledWith("value", "arg1"); - - hooks.off("event", fn); - - hooks.destroy(); - }); - }); -}); diff --git a/packages/core/src/domain/Hooks.ts b/packages/core/src/domain/Hooks.ts deleted file mode 100644 index 79dcaa80526..00000000000 --- a/packages/core/src/domain/Hooks.ts +++ /dev/null @@ -1,108 +0,0 @@ -export class Hooks { - #listeners: Record = {}; - - has(event: string) { - return !!this.#listeners[event]; - } - /** - * Listen a hook event - * @param event - * @param cb - */ - on(event: string, cb: Function) { - if (!this.#listeners[event]) { - this.#listeners[event] = []; - } - - this.#listeners[event].push(cb); - - return this; - } - - /** - * Remove a listener attached to an event - * @param event - * @param cb - */ - off(event: string, cb: Function) { - if (this.#listeners[event]) { - this.#listeners[event] = this.#listeners[event].filter((item) => item === cb); - } - - return this; - } - - /** - * Trigger an event and call listener. - * @param event - * @param args - * @param callThis - */ - emit(event: string, args: any[] = [], callThis: any = null): void { - const listeners = this.#listeners[event]; - - if (listeners?.length) { - for (const cb of listeners) { - cb.call(callThis, ...args); - } - } - } - - /** - * Trigger an event, listener alter given value and return it. - * @param event - * @param value - * @param args - * @param callThis - */ - alter(event: string, value: Arg, args: any[] = [], callThis: any = null): AlteredArg { - const listeners = this.#listeners[event]; - - if (listeners?.length) { - for (const cb of listeners) { - value = cb.call(callThis, value, ...args); - } - } - - return value as unknown as AlteredArg; - } - - /** - * Trigger an event and call async listener. - * @param event - * @param args - * @param callThis - */ - async asyncEmit(event: string, args: any[] = [], callThis: any = null): Promise { - const listeners = this.#listeners[event]; - - if (listeners?.length) { - const promises = listeners.map((cb) => cb.call(callThis, ...args)); - - await Promise.all(promises); - } - } - - /** - * Trigger an event, async listener alter given value and return it. - * @param event - * @param value - * @param args - * @param callThis - */ - async asyncAlter(event: string, value: Arg, args: any[] = [], callThis: any = null): Promise { - const listeners = this.#listeners[event]; - - if (listeners?.length) { - for (const cb of listeners) { - value = await cb.call(callThis, value, ...args); - } - } - - return value as unknown as AlteredArg; - } - - destroy() { - this.#listeners = {}; - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8d45739dfa6..5b79a9a1ec0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,6 @@ export * from "./decorators/storeSet.js"; export * from "./domain/AnyToPromise.js"; export * from "./domain/DecoratorTypes.js"; export * from "./domain/Env.js"; -export * from "./domain/Hooks.js"; export * from "./domain/Metadata.js"; export * from "./domain/Store.js"; export * from "./domain/Type.js"; diff --git a/packages/core/vitest.config.mts b/packages/core/vitest.config.mts index e26f44e6da9..2dcbe52a9d0 100644 --- a/packages/core/vitest.config.mts +++ b/packages/core/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.31, - branches: 95.69, - functions: 95.74, - lines: 97.31 + statements: 97.33, + branches: 95.59, + functions: 96.21, + lines: 97.33 } } } diff --git a/packages/di/package.json b/packages/di/package.json index d73d27e5bdd..2e88b6dc0e5 100644 --- a/packages/di/package.json +++ b/packages/di/package.json @@ -31,6 +31,7 @@ "devDependencies": { "@tsed/barrels": "workspace:*", "@tsed/core": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/logger": "^6.7.8", "@tsed/schema": "workspace:*", "@tsed/typescript": "workspace:*", @@ -42,6 +43,7 @@ }, "peerDependencies": { "@tsed/core": "8.0.0-rc.5", + "@tsed/hooks": "8.0.0-rc.5", "@tsed/logger": ">=6.7.5", "@tsed/schema": "8.0.0-rc.5" }, @@ -49,6 +51,9 @@ "@tsed/core": { "optional": false }, + "@tsed/hooks": { + "optional": false + }, "@tsed/logger": { "optional": false }, diff --git a/packages/di/src/common/domain/LocalsContainer.ts b/packages/di/src/common/domain/LocalsContainer.ts index 06a1d38233a..42ec9d7008f 100644 --- a/packages/di/src/common/domain/LocalsContainer.ts +++ b/packages/di/src/common/domain/LocalsContainer.ts @@ -1,4 +1,4 @@ -import {Hooks} from "@tsed/core"; +import {Hooks} from "@tsed/hooks"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; diff --git a/packages/di/src/common/fn/events.spec.ts b/packages/di/src/common/fn/events.spec.ts deleted file mode 100644 index efc44aea7df..00000000000 --- a/packages/di/src/common/fn/events.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {beforeEach} from "vitest"; - -import {DITest} from "../../node/index.js"; -import {Injectable} from "../decorators/injectable.js"; -import {registerProvider} from "../registries/ProviderRegistry.js"; -import {$alter, $alterAsync, $emit} from "./events.js"; -import {injector} from "./injector.js"; - -@Injectable() -class Test { - $event(value: any) {} - - $alterValue(value: any) { - return "alteredValue"; - } - - $alterAsyncValue(value: any) { - return Promise.resolve("alteredValue"); - } -} - -describe("events", () => { - beforeEach(() => DITest.create()); - afterEach(() => DITest.reset()); - - describe("$emit()", () => { - it("should alter value", async () => { - // GIVEN - const service = DITest.get(Test); - - vi.spyOn(service, "$event"); - - await $emit("$event", "value"); - - expect(service.$event).toHaveBeenCalledWith("value"); - }); - it("should alter value (factory)", () => { - registerProvider({ - provide: "TOKEN", - useFactory: () => { - return {}; - }, - hooks: { - $alterValue(instance: any, value: any) { - return "alteredValue"; - } - } - }); - - // GIVEN - injector().invoke("TOKEN"); - - const value = $alter("$alterValue", "value"); - - expect(value).toEqual("alteredValue"); - }); - }); - describe("$alter()", () => { - it("should alter value", async () => { - // GIVEN - const service = await DITest.invoke(Test); - vi.spyOn(service, "$alterValue"); - - const value = $alter("$alterValue", "value"); - - expect(service.$alterValue).toHaveBeenCalledWith("value"); - expect(value).toEqual("alteredValue"); - }); - it("should alter value (factory)", () => { - registerProvider({ - provide: "TOKEN", - useFactory: () => { - return {}; - }, - hooks: { - $alterValue(instance: any, value: any) { - return "alteredValue"; - } - } - }); - - // GIVEN - injector().invoke("TOKEN"); - - const value = $alter("$alterValue", "value"); - - expect(value).toEqual("alteredValue"); - }); - }); - describe("$alterAsync()", () => { - it("should alter value", async () => { - const service = await DITest.invoke(Test)!; - - vi.spyOn(service, "$alterAsyncValue"); - - const value = await $alterAsync("$alterAsyncValue", "value"); - - expect(service.$alterAsyncValue).toHaveBeenCalledWith("value"); - expect(value).toEqual("alteredValue"); - }); - }); -}); diff --git a/packages/di/src/common/fn/events.ts b/packages/di/src/common/fn/events.ts deleted file mode 100644 index c35154f81d7..00000000000 --- a/packages/di/src/common/fn/events.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {injector} from "./injector.js"; - -/** - * Alter value attached to an event asynchronously. - * @param eventName - * @param value - * @param args - * @param callThis - */ -export function $alterAsync(eventName: string, value: any, ...args: unknown[]) { - return injector().hooks.asyncAlter(eventName, value, args); -} - -/** - * Emit an event to all service. See service [lifecycle hooks](/docs/services.md#lifecycle-hooks). - * @param eventName The event name to emit at all services. - * @param args List of the parameters to give to each service. - * @returns A list of promises. - */ -export function $emit(eventName: string, ...args: unknown[]): Promise { - return injector().hooks.asyncEmit(eventName, args); -} - -/** - * Alter value attached to an event. - * @param eventName - * @param value - * @param args - * @param callThis - */ -export function $alter(eventName: string, value: unknown, ...args: unknown[]): T { - return injector().hooks.alter(eventName, value, args); -} diff --git a/packages/di/src/common/index.ts b/packages/di/src/common/index.ts index f1f1cb856ee..7ec7b8c6059 100644 --- a/packages/di/src/common/index.ts +++ b/packages/di/src/common/index.ts @@ -29,7 +29,6 @@ export * from "./errors/InjectionError.js"; export * from "./errors/InvalidPropertyTokenError.js"; export * from "./fn/configuration.js"; export * from "./fn/constant.js"; -export * from "./fn/events.js"; export * from "./fn/inject.js"; export * from "./fn/injectable.js"; export * from "./fn/injectMany.js"; @@ -58,5 +57,6 @@ export * from "./services/DILogger.js"; export * from "./services/InjectorService.js"; export * from "./utils/colors.js"; export * from "./utils/createContainer.js"; +export * from "./utils/discoverHooks.js"; export * from "./utils/getConfiguration.js"; export * from "./utils/getConstructorDependencies.js"; diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 96448669836..933e845c573 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -1,4 +1,5 @@ -import {classOf, deepClone, deepMerge, Hooks, isArray, isClass, isFunction, isInheritedFrom, isObject, isPromise, nameOf} from "@tsed/core"; +import {classOf, deepClone, deepMerge, isArray, isClass, isFunction, isInheritedFrom, isObject, isPromise, nameOf} from "@tsed/core"; +import {$alter, $asyncAlter, $asyncEmit, $off} from "@tsed/hooks"; import {DI_INVOKE_OPTIONS, DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; import {Configuration} from "../decorators/configuration.js"; @@ -16,6 +17,7 @@ import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {createContainer} from "../utils/createContainer.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; +import {registerHooks} from "../utils/registerProviderHooks.js"; import {DIConfiguration} from "./DIConfiguration.js"; const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; @@ -34,11 +36,10 @@ const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; * import MyService3 from "./services/service3.js"; * * // When all services are imported, you can load InjectorService. - * const injector = new InjectorService() * - * await injector.load(); + * await injector().load(); * - * const myService1 = injector.get(MyServcice1); + * const myService1 = injector.get(MyService1); * ``` */ @Injectable({ @@ -47,7 +48,6 @@ const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; export class InjectorService extends Container { public settings: DIConfiguration = new DIConfiguration(); public logger: DILogger = console; - readonly hooks = new Hooks(); private resolvedConfiguration: boolean = false; #cache = new LocalsContainer(); @@ -173,8 +173,8 @@ export class InjectorService extends Container { switch (provider.scope) { case ProviderScope.SINGLETON: - if (provider.hooks && !options.rebuild) { - this.registerHooks(provider, instance); + if (!options.rebuild) { + registerHooks(provider, instance); } if (!provider.isAsync() || !isPromise(instance)) { @@ -233,6 +233,8 @@ export class InjectorService extends Container { * @param container */ async load(container: Container = createContainer()) { + await $asyncEmit("$beforeInit"); + this.bootstrap(container); // build async and sync provider @@ -241,8 +243,7 @@ export class InjectorService extends Container { // load sync provider this.loadSync(); - await this.emit("$beforeInit"); - await this.emit("$onInit"); + await $asyncEmit("$onInit"); } /** @@ -277,9 +278,10 @@ export class InjectorService extends Container { * @param eventName The event name to emit at all services. * @param args List of the parameters to give to each service. * @returns A list of promises. + * @deprecated use $asyncEmit instead */ - public emit(eventName: string, ...args: any[]): Promise { - return this.hooks.asyncEmit(eventName, args); + public emit(eventName: string, ...args: unknown[]) { + return $asyncEmit(eventName, args); } /** @@ -287,9 +289,10 @@ export class InjectorService extends Container { * @param eventName * @param value * @param args + * @deprecated use $alter instead */ public alter(eventName: string, value: any, ...args: any[]): T { - return this.hooks.alter(eventName, value, args); + return $alter(eventName, value, args); } /** @@ -297,16 +300,20 @@ export class InjectorService extends Container { * @param eventName * @param value * @param args + * @deprecated use $asyncAlter instead */ public alterAsync(eventName: string, value: any, ...args: any[]): Promise { - return this.hooks.asyncAlter(eventName, value, args); + return $asyncAlter(eventName, value, args); } /** * Destroy the injector and all services. */ async destroy() { - await this.emit("$onDestroy"); + await $asyncEmit("$onDestroy"); + this.#cache.forEach((_, token) => { + $off(token); + }); } /** @@ -520,14 +527,4 @@ export class InjectorService extends Container { locals }; } - - private registerHooks(provider: Provider, instance: any) { - if (provider.hooks) { - Object.entries(provider.hooks).forEach(([event, cb]) => { - const callback = (...args: any[]) => cb(this.get(provider.token) || instance, ...args); - - this.hooks.on(event, callback); - }); - } - } } diff --git a/packages/di/src/common/utils/discoverHooks.ts b/packages/di/src/common/utils/discoverHooks.ts new file mode 100644 index 00000000000..394dc4bc0c6 --- /dev/null +++ b/packages/di/src/common/utils/discoverHooks.ts @@ -0,0 +1,15 @@ +import {type AbstractType, methodsOf, type Type} from "@tsed/core"; + +export function discoverHooks(token: Type | AbstractType) { + return methodsOf(token).reduce((hooks, {propertyKey}) => { + if (String(propertyKey).startsWith("$")) { + const listener = (instance: any, ...args: any[]) => instance?.[propertyKey](...args); + + return { + ...hooks, + [propertyKey]: listener + }; + } + return hooks; + }, {} as any); +} diff --git a/packages/di/src/common/utils/registerProviderHooks.ts b/packages/di/src/common/utils/registerProviderHooks.ts new file mode 100644 index 00000000000..1a901adee80 --- /dev/null +++ b/packages/di/src/common/utils/registerProviderHooks.ts @@ -0,0 +1,13 @@ +import {$on} from "@tsed/hooks"; + +import {Provider} from "../domain/Provider.js"; + +export function registerHooks(provider: Provider, instance: any) { + if (provider.hooks) { + Object.entries(provider.hooks).forEach(([event, cb]) => { + const callback = (...args: any[]) => cb(instance, ...args); + + $on(event, provider.token, callback); + }); + } +} diff --git a/packages/di/src/node/domain/ContextLogger.ts b/packages/di/src/node/domain/ContextLogger.ts index ace9f355558..8740b0e74a9 100644 --- a/packages/di/src/node/domain/ContextLogger.ts +++ b/packages/di/src/node/domain/ContextLogger.ts @@ -1,4 +1,4 @@ -import {Hooks} from "@tsed/core"; +import {Hooks} from "@tsed/hooks"; import {levels, LogLevel} from "@tsed/logger"; import {DILogger} from "../../common/index.js"; diff --git a/packages/di/tsconfig.json b/packages/di/tsconfig.json index 5c1c1046579..96f3b896002 100644 --- a/packages/di/tsconfig.json +++ b/packages/di/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core/tsconfig.json" }, + { + "path": "../hooks/tsconfig.json" + }, { "path": "../specs/schema/tsconfig.json" }, diff --git a/packages/hooks/.npmignore b/packages/hooks/.npmignore new file mode 100644 index 00000000000..672ed765244 --- /dev/null +++ b/packages/hooks/.npmignore @@ -0,0 +1,8 @@ +src +test +coverage +tsconfig.json +tsconfig.*.json +__mock__ +*.spec.js +*.tsbuildinfo diff --git a/packages/hooks/package.json b/packages/hooks/package.json new file mode 100644 index 00000000000..1438a60d75d --- /dev/null +++ b/packages/hooks/package.json @@ -0,0 +1,45 @@ +{ + "name": "@tsed/hooks", + "description": "Hooks module for Ts.ED Framework", + "type": "module", + "version": "8.0.0-rc.5", + "source": "./src/index.ts", + "main": "./lib/esm/index.js", + "module": "./lib/esm/index.js", + "typings": "./lib/types/index.d.ts", + "browser": "./lib/browser/core.umd.min.js", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "browser": "./lib/browser/hooks.umd.min.js", + "import": "./lib/esm/index.js", + "default": "./lib/esm/index.js" + }, + "./**/*.js": { + "types": "./lib/types/**/*.d.ts", + "import": "./lib/esm/**/*.js", + "default": "./lib/esm/**/*.js" + } + }, + "scripts": { + "build": "yarn build:ts && yarn run build:browser", + "build:browser": "webpack", + "build:ts": "tsc --build tsconfig.json", + "test": "vitest run", + "test:ci": "vitest run --coverage.thresholds.autoUpdate=true" + }, + "dependencies": { + "reflect-metadata": "^0.2.2", + "tslib": "2.7.0" + }, + "devDependencies": { + "@tsed/monorepo-utils": "2.3.9", + "@tsed/typescript": "workspace:*", + "@tsed/vitest": "workspace:*", + "eslint": "9.12.0", + "typescript": "5.4.5", + "vite": "^5.4.8", + "vitest": "2.1.2", + "webpack": "^5.75.0" + } +} diff --git a/packages/hooks/readme.md b/packages/hooks/readme.md new file mode 100644 index 00000000000..73e4aef3e02 --- /dev/null +++ b/packages/hooks/readme.md @@ -0,0 +1,64 @@ +

+ Ts.ED logo +

+ +
+

@tsed/core

+ +[![Build & Release](https://github.com/tsedio/tsed/workflows/Build%20&%20Release/badge.svg)](https://github.com/tsedio/tsed/actions?query=workflow%3A%22Build+%26+Release%22) +[![PR Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/tsedio/tsed/blob/master/CONTRIBUTING.md) +[![npm version](https://badge.fury.io/js/%40tsed%2Fcommon.svg)](https://badge.fury.io/js/%40tsed%2Fcommon) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![github](https://img.shields.io/static/v1?label=Github%20sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/romakita) +[![opencollective](https://img.shields.io/static/v1?label=OpenCollective%20sponsor&message=%E2%9D%A4&logo=OpenCollective&color=%23fe8e86)](https://opencollective.com/tsed) + +
+ +
+ Website +   •   + Getting started +   •   + Slack +   •   + Twitter +
+ +
+ +A package of Ts.ED framework. See website: https://tsed.io/ + +# Installation + +```bash +npm install --save @tsed/core +``` + +## Contributors + +Please read [contributing guidelines here](https://tsed.io/contributing.html). + + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/tsed#backer)] + + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/tsed#sponsor)] + +## License + +The MIT License (MIT) + +Copyright (c) 2016 - 2022 Romain Lenzotti + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/hooks/src/Hooks.spec.ts b/packages/hooks/src/Hooks.spec.ts new file mode 100644 index 00000000000..a359ce7d344 --- /dev/null +++ b/packages/hooks/src/Hooks.spec.ts @@ -0,0 +1,219 @@ +import {Hooks} from "./Hooks.js"; + +describe("Hooks", () => { + describe("off()", () => { + it("should remove a listener (using event, cb)", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + const fn2 = vi.fn(); + const fn3 = vi.fn(); + + hooks.on("event", fn); + hooks.on("event2", fn2); + hooks.on("event2", fn3); + + expect(hooks.has("event")).toBe(true); + expect(hooks.has("event2")).toBe(true); + + hooks.off("event", fn); + hooks.off("event2", fn2); + + expect(hooks.has("event")).toBe(false); + expect(hooks.has("event2")).toBe(true); + + hooks.off("event3", fn2); + + expect(hooks.has("event2")).toBe(true); + + hooks.emit("event", ["arg1"]); + + expect(fn).not.toHaveBeenCalled(); + }); + it("should remove a listener (using ref)", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + const fn2 = vi.fn(); + const ref = Symbol("ref"); + + hooks.on("event", ref, fn); + hooks.on("event2", ref, fn2); + + expect(hooks.has("event")).toBe(true); + expect(hooks.has("event2")).toBe(true); + + hooks.off(ref); + + expect(hooks.has("event")).toBe(false); + expect(hooks.has("event2")).toBe(false); + + hooks.emit("event", ["arg1"]); + hooks.emit("event2", ["arg1"]); + + expect(fn).not.toHaveBeenCalled(); + expect(fn2).not.toHaveBeenCalled(); + }); + }); + describe("once()", () => { + it("should call once a listener (using event, cb)", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + + hooks.once("event", fn); + + expect(hooks.has("event")).toBe(true); + + hooks.emit("event", ["arg1"]); + + expect(hooks.has("event")).toBe(false); + expect(fn).toHaveBeenCalled(); + }); + it("should call once a listener (using ref)", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + const fn2 = vi.fn(); + const ref = Symbol("ref"); + + hooks.once("event", ref, fn); + hooks.once("event2", ref, fn2); + + expect(hooks.has("event")).toBe(true); + expect(hooks.has("event2")).toBe(true); + + hooks.emit("event", ["arg1"]); + hooks.emit("event2", ["arg1"]); + + expect(hooks.has("event")).toBe(false); + expect(hooks.has("event2")).toBe(false); + expect(fn).toHaveBeenCalled(); + expect(fn2).toHaveBeenCalled(); + }); + }); + describe("emit()", () => { + it("should listen a hook and calls listener", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + + hooks.on("event", fn); + + hooks.emit("event", ["arg1"]); + + expect(fn).toHaveBeenCalledWith("arg1"); + + hooks.off("event", fn); + }); + it("should calls listeners that match listeners with matching ref and without ref", () => { + const hooks = new Hooks(); + const fn: any = vi.fn(); + fn.id = "f1"; + + const fn2: any = vi.fn(); + fn.id = "f2"; + + const fn3: any = vi.fn(); + fn3.id = "f3"; + const ref = Symbol("ref"); + + hooks.on("event", ref, fn); + hooks.on("event2", ref, fn2); + hooks.on("event2", fn3); + + hooks.emit("event", ref, ["arg1"]); + + expect(fn).toHaveBeenCalledWith("arg1"); + expect(fn2).not.toHaveBeenCalledWith("arg1"); + expect(fn3).not.toHaveBeenCalledWith("arg1"); + + hooks.emit("event2", ref, ["arg1"]); + + expect(fn2).toHaveBeenCalledWith("arg1"); + expect(fn3).toHaveBeenCalledWith("arg1"); + + hooks.off(ref); + }); + it("should not call listeners if the ref mismatch", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + const fn2 = vi.fn(); + const fn3 = vi.fn(); + const ref = Symbol("ref"); + const ref2 = Symbol("ref"); + + hooks.on("event", ref, fn); + hooks.on("event2", ref, fn2); + hooks.on("event2", ref, fn3); + + hooks.emit("event", ref2, ["arg1"]); + + expect(fn).not.toHaveBeenCalledWith("arg1"); + expect(fn2).not.toHaveBeenCalledWith("arg1"); + expect(fn3).not.toHaveBeenCalledWith("arg1"); + + hooks.off(ref); + }); + it("should call", () => { + const hooks = new Hooks(); + const fn = vi.fn(); + const fn2 = vi.fn(); + const fn3 = vi.fn(); + const ref = Symbol("ref"); + const ref2 = Symbol("ref"); + + hooks.on("event", ref, fn); + hooks.on("event2", ref, fn2); + hooks.on("event2", ref, fn3); + + hooks.emit("event", ref2, ["arg1"]); + + expect(fn).not.toHaveBeenCalledWith("arg1"); + expect(fn2).not.toHaveBeenCalledWith("arg1"); + expect(fn3).not.toHaveBeenCalledWith("arg1"); + + hooks.off(ref); + }); + }); + describe("asyncEmit()", () => { + it("should async listen a hook and calls listener", async () => { + const hooks = new Hooks(); + const fn = vi.fn(); + + hooks.on("event", fn); + + await hooks.asyncEmit("event", ["arg1"]); + + expect(fn).toHaveBeenCalledWith("arg1"); + + hooks.off("event", fn); + }); + }); + describe("alter()", () => { + it("should listen a hook and calls listener", () => { + const hooks = new Hooks(); + const fn = vi.fn().mockReturnValue("valueAltered"); + + hooks.on("event", fn); + + const value = hooks.alter("event", "value"); + + expect(fn).toHaveBeenCalledWith("value"); + expect(value).toBe("valueAltered"); + + hooks.off("event", fn); + }); + }); + describe("alterAsync()", () => { + it("should async listen a hook and calls listener", async () => { + const hooks = new Hooks(); + const fn = vi.fn().mockReturnValue("valueAltered"); + + hooks.on("event", fn); + + await hooks.asyncAlter("event", "value", ["arg1"]); + + expect(fn).toHaveBeenCalledWith("value", "arg1"); + + hooks.off("event", fn); + + hooks.destroy(); + }); + }); +}); diff --git a/packages/hooks/src/Hooks.ts b/packages/hooks/src/Hooks.ts new file mode 100644 index 00000000000..6aabb12c602 --- /dev/null +++ b/packages/hooks/src/Hooks.ts @@ -0,0 +1,241 @@ +export type HookRef = string | symbol | any | Function; +export type HookListener = Function; + +type HookItem = {cb: HookListener; ref?: HookRef}; + +function match(ref: HookRef | unknown[] | undefined, item: HookItem) { + return !ref || !item.ref || (ref && item.ref === ref); +} + +export class Hooks { + #listeners: Map = new Map(); + + /** + * Check if an event has listeners + * @param event + */ + has(event: string) { + return !!this.#listeners.get(event)?.length; + } + + /** + * Listen a hook event + * @param event The event name + * @param ref The reference of the listener + * @param cb The callback + */ + on(event: string, ref: HookRef, cb: HookListener): this; + /** + * Listen a hook event + * @param event The event name + * @param cb The callback + */ + on(event: string, cb: HookListener): this; + on(event: string, cbORef: HookRef | HookListener, cb?: HookListener) { + let ref: HookRef | HookListener | undefined = cbORef; + + if (!cb) { + cb = ref as HookListener; + ref = undefined; + } + + const items = this.#listeners.get(event) || []; + + items.push({ + cb, + ref + }); + + this.#listeners.set(event, items); + + return this; + } + + /** + * Listen a hook event once + * + * @param event The event name + * @param ref The reference of the listener + * @param cb The callback + */ + once(event: string, ref: HookRef, cb: HookListener): this; + /** + * Listen a hook event once + * @param event + * @param cb + */ + once(event: string, cb: HookListener): this; + once(event: string, ref: HookRef | HookListener, cb?: HookListener) { + if (!cb) { + cb = ref as HookListener; + } + + const onceCb = (...args: unknown[]) => { + cb(...args); + this.off(event, onceCb); + }; + + this.on(event, ref, onceCb); + + return this; + } + + /** + * Remove a listener attached to an event + * @param ref + */ + off(ref: HookRef): this; + /** + * Remove a listener attached to an event + * @param event + * @param cb + */ + off(event: string, cb: HookListener): this; + off(event: string | HookRef, cb?: HookListener) { + const set = (event: string, items: HookItem[]) => { + if (items.length) { + this.#listeners.set(event, items); + } else { + this.#listeners.delete(event); + } + }; + + if (typeof event === "string" && cb) { + let items = this.#listeners.get(event); + + if (items) { + set( + event, + items.filter((item) => item.cb !== cb) + ); + } + } else { + const ref = event as HookRef; + + this.#listeners.forEach((items, event) => { + set( + event, + items.filter((item) => item.ref !== ref) + ); + }); + } + + return this; + } + + /** + * Trigger an event without arguments. + * @param event The event name + */ + emit(event: string): void; + /** + * Trigger an event and call listener. + * @param event + * @param args + * @param callThis + */ + emit(event: string, args: unknown[]): void; + /** + * Trigger an event with arguments and call only listener attached to the given ref. + * @param event The event name + * @param ref The reference of the listener + * @param args The arguments + */ + emit(event: string, ref: HookRef, args?: unknown[]): void; + emit(event: string, ref?: HookRef | unknown[], args?: unknown[]): void { + if (Array.isArray(ref)) { + args = ref; + ref = undefined; + } + + args ||= []; + + const items = this.#listeners.get(event); + + if (items?.length) { + for (const item of items) { + if (match(ref, item)) { + item.cb.apply(null, args); + } + } + } + } + + /** + * Trigger an event and call async listener. + * @param event The event name + */ + async asyncEmit(event: string): Promise; + async asyncEmit(event: string, args: unknown[]): Promise; + async asyncEmit(event: string, ref: HookRef, args?: unknown[]): Promise; + async asyncEmit(event: string, ref?: HookRef | unknown[], args?: unknown[]): Promise { + if (Array.isArray(ref)) { + args = ref; + ref = undefined; + } + + const items = this.#listeners.get(event); + + if (items?.length) { + const promises = items.filter((item) => match(ref, item)).map((item) => item.cb.apply(null, args)); + + await Promise.all(promises); + } + } + + /** + * Trigger an event, listener alter given value and return it. + * @param event + * @param value + * @param args + * @param callThis + */ + alter(event: string, value: Arg, args: unknown[] = [], callThis: unknown = null): AlteredArg { + const items = this.#listeners.get(event); + + if (items?.length) { + for (const {cb} of items) { + value = cb.call(callThis, value, ...args); + } + } + + return value as unknown as AlteredArg; + } + + /** + * Trigger an event, async listener alter given value and return it. + * @param event + * @param value + * @param args + * @param callThis + */ + async asyncAlter( + event: string, + value: Arg, + args: unknown[] = [], + callThis: unknown = null + ): Promise { + const items = this.#listeners.get(event); + + if (items?.length) { + for (const item of items) { + value = await item.cb.call(callThis, value, ...args); + } + } + + return value as unknown as AlteredArg; + } + + destroy() { + this.#listeners.clear(); + } +} + +export const hooks = new Hooks(); +export const $on: typeof hooks.on = hooks.on.bind(hooks); +export const $once: typeof hooks.once = hooks.once.bind(hooks); +export const $off: typeof hooks.off = hooks.off.bind(hooks); +export const $emit: typeof hooks.emit = hooks.emit.bind(hooks); +export const $asyncEmit: typeof hooks.asyncEmit = hooks.asyncEmit.bind(hooks); +export const $alter: typeof hooks.alter = hooks.alter.bind(hooks); +export const $asyncAlter: typeof hooks.asyncAlter = hooks.asyncAlter.bind(hooks); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts new file mode 100644 index 00000000000..b476023b361 --- /dev/null +++ b/packages/hooks/src/index.ts @@ -0,0 +1 @@ +export * from "./Hooks.js"; diff --git a/packages/hooks/tsconfig.esm.json b/packages/hooks/tsconfig.esm.json new file mode 100644 index 00000000000..8954049da4a --- /dev/null +++ b/packages/hooks/tsconfig.esm.json @@ -0,0 +1,26 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "./lib/esm", + "declarationDir": "./lib/types", + "declaration": true, + "composite": true, + "noEmit": false + }, + "include": ["src/**/*.ts", "src/**/*.json"], + "exclude": [ + "node_modules", + "test", + "lib", + "benchmark", + "coverage", + "spec", + "**/*.benchmark.ts", + "**/*.spec.ts", + "keys", + "**/__mock__/**", + "webpack.config.js" + ] +} diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json new file mode 100644 index 00000000000..ffa3a8cd7e1 --- /dev/null +++ b/packages/hooks/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": true + }, + "include": [], + "references": [ + { + "path": "./tsconfig.esm.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/hooks/tsconfig.spec.json b/packages/hooks/tsconfig.spec.json new file mode 100644 index 00000000000..13d7433fcae --- /dev/null +++ b/packages/hooks/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "@tsed/typescript/tsconfig.node.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "..", + "declaration": false, + "composite": false, + "noEmit": true, + "paths": {}, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src/**/*.spec.ts", "test/**/*.spec.ts", "vitest.config.mts"], + "exclude": ["node_modules", "lib", "benchmark", "coverage"] +} diff --git a/packages/hooks/vitest.config.mts b/packages/hooks/vitest.config.mts new file mode 100644 index 00000000000..d2598fb346b --- /dev/null +++ b/packages/hooks/vitest.config.mts @@ -0,0 +1,21 @@ +// @ts-ignore +import {presets} from "@tsed/vitest/presets"; +import {defineConfig} from "vitest/config"; + +export default defineConfig( + { + ...presets, + test: { + ...presets.test, + coverage: { + ...presets.test.coverage, + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100 + } + } + } + } +); \ No newline at end of file diff --git a/packages/hooks/webpack.config.cjs b/packages/hooks/webpack.config.cjs new file mode 100644 index 00000000000..10b6d97d05e --- /dev/null +++ b/packages/hooks/webpack.config.cjs @@ -0,0 +1,4 @@ +module.exports = require("@tsed/webpack-config").create({ + root: __dirname, + name: "hooks" +}); diff --git a/packages/orm/adapters-redis/package.json b/packages/orm/adapters-redis/package.json index 72366475cb1..544f0f397a9 100644 --- a/packages/orm/adapters-redis/package.json +++ b/packages/orm/adapters-redis/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@tsed/barrels": "workspace:*", "@tsed/core": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/typescript": "workspace:*", "eslint": "9.12.0", "typescript": "5.4.5", @@ -38,6 +39,7 @@ "@tsed/adapters": "8.0.0-rc.5", "@tsed/core": "8.0.0-rc.5", "@tsed/di": "8.0.0-rc.5", + "@tsed/hooks": "8.0.0-rc.5", "@tsed/platform-http": "8.0.0-rc.5", "ioredis": ">=5.2.3", "ioredis-mock": ">=8.2.2", diff --git a/packages/orm/adapters-redis/src/adapters/RedisAdapter.ts b/packages/orm/adapters-redis/src/adapters/RedisAdapter.ts index f26dc0671be..4f4dbcfc26a 100644 --- a/packages/orm/adapters-redis/src/adapters/RedisAdapter.ts +++ b/packages/orm/adapters-redis/src/adapters/RedisAdapter.ts @@ -1,6 +1,7 @@ import {Adapter, AdapterConstructorOptions, AdapterModel} from "@tsed/adapters"; -import {cleanObject, Hooks, isObject, isString} from "@tsed/core"; +import {cleanObject, isObject, isString} from "@tsed/core"; import {Configuration, Inject, Opts} from "@tsed/di"; +import {Hooks} from "@tsed/hooks"; import {IORedis, IOREDIS_CONNECTIONS} from "@tsed/ioredis"; import type {ChainableCommander, Redis} from "ioredis"; import {v4 as uuid} from "uuid"; diff --git a/packages/orm/ioredis/src/domain/IORedisStore.spec.ts b/packages/orm/ioredis/src/domain/IORedisStore.spec.ts index de72754ae49..f2dca9034da 100644 --- a/packages/orm/ioredis/src/domain/IORedisStore.spec.ts +++ b/packages/orm/ioredis/src/domain/IORedisStore.spec.ts @@ -1,4 +1,5 @@ -import {catchAsyncError, Hooks} from "@tsed/core"; +import {catchAsyncError} from "@tsed/core"; +import {Hooks} from "@tsed/hooks"; import {type Cache, caching} from "cache-manager"; import {Redis} from "ioredis"; diff --git a/packages/platform/platform-http/package.json b/packages/platform/platform-http/package.json index e4151c2b67c..b3092177f43 100644 --- a/packages/platform/platform-http/package.json +++ b/packages/platform/platform-http/package.json @@ -76,6 +76,7 @@ "@tsed/core": "workspace:*", "@tsed/di": "workspace:*", "@tsed/exceptions": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/json-mapper": "workspace:*", "@tsed/logger": "^6.7.8", "@tsed/logger-file": "^6.7.8", diff --git a/packages/platform/platform-serverless-http/package.json b/packages/platform/platform-serverless-http/package.json index 515fa6b624c..fc77c070a29 100644 --- a/packages/platform/platform-serverless-http/package.json +++ b/packages/platform/platform-serverless-http/package.json @@ -63,8 +63,8 @@ }, "devDependencies": { "@tsed/barrels": "workspace:*", - "@tsed/core": "workspace:*", "@tsed/di": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/platform-http": "workspace:*", "@tsed/platform-serverless-testing": "workspace:*", "@tsed/typescript": "workspace:*", diff --git a/packages/platform/platform-serverless/package.json b/packages/platform/platform-serverless/package.json index 15db188d7e9..e83b9679a86 100644 --- a/packages/platform/platform-serverless/package.json +++ b/packages/platform/platform-serverless/package.json @@ -23,7 +23,9 @@ }, "dependencies": { "@tsed/core": "workspace:*", + "@tsed/di": "workspace:*", "@tsed/exceptions": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/json-mapper": "workspace:*", "@tsed/platform-exceptions": "workspace:*", "@tsed/platform-params": "workspace:*", diff --git a/packages/specs/json-mapper/src/hooks/alterAfterDeserialize.ts b/packages/specs/json-mapper/src/hooks/alterAfterDeserialize.ts index 7237af2b0f6..31bd85f6e43 100644 --- a/packages/specs/json-mapper/src/hooks/alterAfterDeserialize.ts +++ b/packages/specs/json-mapper/src/hooks/alterAfterDeserialize.ts @@ -1,4 +1,4 @@ -import {Hooks} from "@tsed/core"; +import type {Hooks} from "@tsed/hooks"; export function alterAfterDeserialize(data: any, schema: {$hooks: Hooks}, options: any) { return schema?.$hooks?.alter("afterDeserialize", data, [options]); diff --git a/packages/specs/json-mapper/src/hooks/alterBeforeDeserialize.ts b/packages/specs/json-mapper/src/hooks/alterBeforeDeserialize.ts index 44fdcba488d..63475fb09e9 100644 --- a/packages/specs/json-mapper/src/hooks/alterBeforeDeserialize.ts +++ b/packages/specs/json-mapper/src/hooks/alterBeforeDeserialize.ts @@ -1,4 +1,4 @@ -import {Hooks} from "@tsed/core"; +import type {Hooks} from "@tsed/hooks"; export function alterBeforeDeserialize(data: any, schema: {$hooks: Hooks}, options: any) { return schema?.$hooks?.alter("beforeDeserialize", data, [options]); diff --git a/packages/specs/schema/package.json b/packages/specs/schema/package.json index 34fc9ecb8d5..f2b18e8008f 100644 --- a/packages/specs/schema/package.json +++ b/packages/specs/schema/package.json @@ -48,6 +48,7 @@ "@apidevtools/swagger-parser": "10.1.0", "@tsed/barrels": "workspace:*", "@tsed/core": "workspace:*", + "@tsed/hooks": "workspace:*", "@tsed/openspec": "workspace:*", "@tsed/typescript": "workspace:*", "@types/fs-extra": "11.0.4", @@ -63,6 +64,7 @@ }, "peerDependencies": { "@tsed/core": "8.0.0-rc.5", + "@tsed/hooks": "8.0.0-rc.5", "@tsed/openspec": "8.0.0-rc.5" }, "peerDependenciesMeta": { diff --git a/packages/specs/schema/src/domain/JsonSchema.ts b/packages/specs/schema/src/domain/JsonSchema.ts index 5bad5d02691..ef87f1dfc09 100644 --- a/packages/specs/schema/src/domain/JsonSchema.ts +++ b/packages/specs/schema/src/domain/JsonSchema.ts @@ -1,17 +1,5 @@ -import { - ancestorsOf, - classOf, - Hooks, - isArray, - isClass, - isFunction, - isObject, - isPrimitiveClass, - nameOf, - Type, - uniq, - ValueOf -} from "@tsed/core"; +import {ancestorsOf, classOf, isArray, isClass, isFunction, isObject, isPrimitiveClass, nameOf, Type, uniq, ValueOf} from "@tsed/core"; +import {Hooks} from "@tsed/hooks"; import type {JSONSchema6, JSONSchema6Definition, JSONSchema6Type, JSONSchema6TypeName, JSONSchema6Version} from "json-schema"; import {IgnoreCallback} from "../interfaces/IgnoreCallback.js"; diff --git a/packages/specs/schema/src/hooks/alterIgnore.ts b/packages/specs/schema/src/hooks/alterIgnore.ts index 928bdb55e2f..d715f881fb3 100644 --- a/packages/specs/schema/src/hooks/alterIgnore.ts +++ b/packages/specs/schema/src/hooks/alterIgnore.ts @@ -1,4 +1,5 @@ -import {Hooks, isBoolean} from "@tsed/core"; +import {isBoolean} from "@tsed/core"; +import type {Hooks} from "@tsed/hooks"; /** * @ignore diff --git a/packages/specs/schema/tsconfig.json b/packages/specs/schema/tsconfig.json index 232a0d3ea1e..5c4a012ad33 100644 --- a/packages/specs/schema/tsconfig.json +++ b/packages/specs/schema/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../../core/tsconfig.json" }, + { + "path": "../../hooks/tsconfig.json" + }, { "path": "../openspec/tsconfig.json" }, diff --git a/tsconfig.json b/tsconfig.json index 38f2669a353..73cbf6892b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,9 @@ { "path": "./packages/core/tsconfig.json" }, + { + "path": "./packages/hooks/tsconfig.json" + }, { "path": "./packages/engines/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 71677261e0b..ed2faaceed9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6529,6 +6529,7 @@ __metadata: "@tsed/adapters": "workspace:*" "@tsed/barrels": "workspace:*" "@tsed/core": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/ioredis": "workspace:*" "@tsed/typescript": "workspace:*" eslint: "npm:9.12.0" @@ -6539,6 +6540,7 @@ __metadata: "@tsed/adapters": 8.0.0-rc.5 "@tsed/core": 8.0.0-rc.5 "@tsed/di": 8.0.0-rc.5 + "@tsed/hooks": 8.0.0-rc.5 "@tsed/platform-http": 8.0.0-rc.5 ioredis: ">=5.2.3" ioredis-mock: ">=8.2.2" @@ -6778,6 +6780,7 @@ __metadata: dependencies: "@tsed/barrels": "workspace:*" "@tsed/core": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/logger": "npm:^6.7.8" "@tsed/schema": "workspace:*" "@tsed/typescript": "workspace:*" @@ -6790,11 +6793,14 @@ __metadata: webpack: "npm:^5.75.0" peerDependencies: "@tsed/core": 8.0.0-rc.5 + "@tsed/hooks": 8.0.0-rc.5 "@tsed/logger": ">=6.7.5" "@tsed/schema": 8.0.0-rc.5 peerDependenciesMeta: "@tsed/core": optional: false + "@tsed/hooks": + optional: false "@tsed/logger": optional: false "@tsed/schema": @@ -6985,6 +6991,23 @@ __metadata: languageName: unknown linkType: soft +"@tsed/hooks@workspace:*, @tsed/hooks@workspace:packages/hooks": + version: 0.0.0-use.local + resolution: "@tsed/hooks@workspace:packages/hooks" + dependencies: + "@tsed/monorepo-utils": "npm:2.3.9" + "@tsed/typescript": "workspace:*" + "@tsed/vitest": "workspace:*" + eslint: "npm:9.12.0" + reflect-metadata: "npm:^0.2.2" + tslib: "npm:2.7.0" + typescript: "npm:5.4.5" + vite: "npm:^5.4.8" + vitest: "npm:2.1.2" + webpack: "npm:^5.75.0" + languageName: unknown + linkType: soft + "@tsed/integration@workspace:tools/integration": version: 0.0.0-use.local resolution: "@tsed/integration@workspace:tools/integration" @@ -7519,6 +7542,7 @@ __metadata: "@tsed/di": "workspace:*" "@tsed/engines": "workspace:*" "@tsed/exceptions": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/json-mapper": "workspace:*" "@tsed/logger": "npm:^6.7.8" "@tsed/logger-file": "npm:^6.7.8" @@ -7813,8 +7837,8 @@ __metadata: resolution: "@tsed/platform-serverless-http@workspace:packages/platform/platform-serverless-http" dependencies: "@tsed/barrels": "workspace:*" - "@tsed/core": "workspace:*" "@tsed/di": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/platform-http": "workspace:*" "@tsed/platform-serverless-testing": "workspace:*" "@tsed/typescript": "workspace:*" @@ -7882,7 +7906,9 @@ __metadata: dependencies: "@tsed/barrels": "workspace:*" "@tsed/core": "workspace:*" + "@tsed/di": "workspace:*" "@tsed/exceptions": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/json-mapper": "workspace:*" "@tsed/platform-exceptions": "workspace:*" "@tsed/platform-params": "workspace:*" @@ -8139,6 +8165,7 @@ __metadata: "@apidevtools/swagger-parser": "npm:10.1.0" "@tsed/barrels": "workspace:*" "@tsed/core": "workspace:*" + "@tsed/hooks": "workspace:*" "@tsed/openspec": "workspace:*" "@tsed/typescript": "workspace:*" "@types/fs-extra": "npm:11.0.4" @@ -8159,6 +8186,7 @@ __metadata: webpack: "npm:^5.75.0" peerDependencies: "@tsed/core": 8.0.0-rc.5 + "@tsed/hooks": 8.0.0-rc.5 "@tsed/openspec": 8.0.0-rc.5 peerDependenciesMeta: "@tsed/core": From 9e07d6e61f24981b3490be7548140b23993d5020 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Thu, 21 Nov 2024 18:08:55 +0100 Subject: [PATCH 08/32] refactor(di): remove new InjectorService in test --- .../common/decorators/configuration.spec.ts | 12 +- packages/di/src/common/domain/Provider.ts | 15 +- .../common/integration/async-factory.spec.ts | 8 +- packages/di/src/common/integration/di.spec.ts | 55 ++-- .../di/src/common/integration/request.spec.ts | 16 +- .../src/common/integration/singleton.spec.ts | 23 +- .../di/src/common/services/DILogger.spec.ts | 12 +- .../common/services/InjectorService.spec.ts | 280 ++++++++---------- .../di/src/node/domain/ContextLogger.spec.ts | 14 +- packages/di/src/node/services/DITest.ts | 2 +- .../di/src/node/utils/attachLogger.spec.ts | 7 +- packages/di/src/node/utils/attachLogger.ts | 8 +- .../node/utils/setLoggerConfiguration.spec.ts | 25 +- .../src/node/utils/setLoggerConfiguration.ts | 7 +- packages/di/src/node/utils/setLoggerFormat.ts | 16 +- packages/di/src/node/utils/setLoggerLevel.ts | 9 +- 16 files changed, 231 insertions(+), 278 deletions(-) diff --git a/packages/di/src/common/decorators/configuration.spec.ts b/packages/di/src/common/decorators/configuration.spec.ts index e44cc14ad79..3679962d333 100644 --- a/packages/di/src/common/decorators/configuration.spec.ts +++ b/packages/di/src/common/decorators/configuration.spec.ts @@ -1,13 +1,16 @@ import {Store} from "@tsed/core"; +import {afterEach} from "vitest"; import {Container} from "../domain/Container.js"; import {Provider} from "../domain/Provider.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {InjectorService} from "../services/InjectorService.js"; import {Configuration} from "./configuration.js"; import {Injectable} from "./injectable.js"; describe("@Configuration", () => { + afterEach(() => destroyInjector()); it("should declare a new provider with custom configuration", () => { @Configuration({}) class Test {} @@ -28,16 +31,15 @@ describe("@Configuration", () => { constructor(@Configuration() public config: Configuration) {} } - const injector = new InjectorService(); const container = new Container(); - injector.setProvider(Test, GlobalProviders.get(Test)!.clone()); + injector().setProvider(Test, GlobalProviders.get(Test)!.clone()); - await injector.load(container); + await injector().load(container); - const instance = injector.invoke(Test); + const instance = injector().invoke(Test); - expect(instance.config).toEqual(injector.settings); + expect(instance.config).toEqual(injector().settings); expect(instance.config.get("feature")).toEqual("feature"); }); }); diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index 374785634eb..7601946b7a4 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -1,8 +1,9 @@ -import {type AbstractType, classOf, getClassOrSymbol, isClass, methodsOf, nameOf, Store, Type} from "@tsed/core"; +import {type AbstractType, classOf, getClassOrSymbol, isClass, nameOf, Store, Type} from "@tsed/core"; import {DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; +import {discoverHooks} from "../utils/discoverHooks.js"; import {ProviderScope} from "./ProviderScope.js"; import {ProviderType} from "./ProviderType.js"; @@ -63,16 +64,7 @@ export class Provider implements ProviderOpts { if (isClass(value)) { this._useClass = classOf(value); this._store = Store.from(value); - - this.hooks = methodsOf(this._useClass).reduce((hooks, {propertyKey}) => { - if (String(propertyKey).startsWith("$")) { - return { - ...hooks, - [propertyKey]: (instance: any, ...args: any[]) => instance?.[propertyKey](...args) - }; - } - return hooks; - }, {} as any); + this.hooks = discoverHooks(this._useClass); } } @@ -140,6 +132,7 @@ export class Provider implements ProviderOpts { getArgOpts(index: number) { return this.store.get(`${DI_USE_PARAM_OPTIONS}:${index}`); } + /** * Retrieves a value from the provider's store. * @param key The key to look up diff --git a/packages/di/src/common/integration/async-factory.spec.ts b/packages/di/src/common/integration/async-factory.spec.ts index 05af3a1fc74..db10c151d2f 100644 --- a/packages/di/src/common/integration/async-factory.spec.ts +++ b/packages/di/src/common/integration/async-factory.spec.ts @@ -1,13 +1,16 @@ import {isPromise} from "@tsed/core"; +import {afterEach, beforeEach} from "vitest"; import {Inject} from "../decorators/inject.js"; import {Injectable} from "../decorators/injectable.js"; import {Container} from "../domain/Container.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "../services/InjectorService.js"; describe("DI", () => { + afterEach(() => destroyInjector()); describe("create new injector", () => { const ASYNC_FACTORY = Symbol.for("ASYNC_FACTORY"); @@ -40,16 +43,15 @@ describe("DI", () => { it("should load all providers with the SINGLETON scope only", async () => { // GIVEN - const injector = new InjectorService(); const container = new Container(); container.add(ASYNC_FACTORY); container.add(Server); - const server = injector.invoke(Server); + const server = injector().invoke(Server); expect(isPromise(server.asyncFactory)).toEqual(true); // WHEN - await injector.load(container); + await injector().load(container); expect(isPromise(server.asyncFactory)).toEqual(false); expect(server.asyncFactory.connection).toEqual(true); diff --git a/packages/di/src/common/integration/di.spec.ts b/packages/di/src/common/integration/di.spec.ts index b71fbdc6171..c243cdadbdc 100644 --- a/packages/di/src/common/integration/di.spec.ts +++ b/packages/di/src/common/integration/di.spec.ts @@ -1,3 +1,5 @@ +import {afterEach} from "vitest"; + import {Inject} from "../decorators/inject.js"; import {Injectable} from "../decorators/injectable.js"; import {Scope} from "../decorators/scope.js"; @@ -5,12 +7,15 @@ import {Service} from "../decorators/service.js"; import {Container} from "../domain/Container.js"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import {ProviderScope} from "../domain/ProviderScope.js"; +import {inject} from "../fn/inject.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {OnDestroy} from "../interfaces/OnDestroy.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; -import {InjectorService} from "../services/InjectorService.js"; describe("DI", () => { - describe("create new injector", () => { + afterEach(() => destroyInjector()); + + describe("from injector global container", () => { @Service() @Scope(ProviderScope.INSTANCE) class ServiceInstance { @@ -43,30 +48,28 @@ describe("DI", () => { it("should load all providers with the SINGLETON scope only", async () => { // GIVEN - const injector = new InjectorService(); const providers = new Container(); providers.add(ServiceInstance); providers.add(ServiceSingleton); providers.add(ServiceRequest); // WHEN - await injector.load(providers); + await injector().load(providers); // THEN - expect(injector.get(ServiceSingleton)).toEqual(injector.invoke(ServiceSingleton)); - expect(injector.get(ServiceRequest)).toBeUndefined(); - expect(injector.get(ServiceInstance)).toBeUndefined(); + expect(injector().get(ServiceSingleton)).toEqual(inject(ServiceSingleton)); + expect(injector().get(ServiceRequest)).toBeUndefined(); + expect(injector().get(ServiceInstance)).toBeUndefined(); - expect(injector.invoke(ServiceRequest) === injector.invoke(ServiceRequest)).toEqual(false); - expect(injector.invoke(ServiceInstance) === injector.invoke(ServiceInstance)).toEqual(false); + expect(injector().invoke(ServiceRequest) === injector().invoke(ServiceRequest)).toEqual(false); + expect(inject(ServiceInstance) === inject(ServiceInstance)).toEqual(false); const locals = new LocalsContainer(); - expect(injector.invoke(ServiceRequest, {locals})).toEqual(injector.invoke(ServiceRequest, {locals})); - expect(injector.invoke(ServiceInstance, {locals}) === injector.invoke(ServiceInstance, {locals})).toEqual(false); + expect(inject(ServiceRequest, {locals})).toEqual(inject(ServiceRequest, {locals})); + expect(inject(ServiceInstance, {locals}) === inject(ServiceInstance, {locals})).toEqual(false); }); }); - - describe("it should invoke service with abstract class", () => { + describe("invoke class with abstract class", () => { abstract class BaseService {} @Injectable() @@ -86,18 +89,16 @@ describe("DI", () => { }); it("should inject the expected class", async () => { - const injector = new InjectorService(); const providers = new Container(); providers.add(MyService); providers.add(NestedService); - await injector.load(providers); + await injector().load(providers); - expect(injector.get(MyService)!.nested).toBeInstanceOf(NestedService); + expect(injector().get(MyService)!.nested).toBeInstanceOf(NestedService); }); }); - - describe("it should invoke service with a symbol", () => { + describe("invoke class with a symbol", () => { interface BaseService {} const BaseService: unique symbol = Symbol("BaseService"); @@ -117,17 +118,15 @@ describe("DI", () => { }); it("should inject the expected class", async () => { - const injector = new InjectorService(); const providers = new Container(); providers.add(MyService); providers.add(NestedService); - await injector.load(providers); + await injector().load(providers); - expect(injector.get(MyService)!.nested).toBeInstanceOf(NestedService); + expect(injector().get(MyService)!.nested).toBeInstanceOf(NestedService); }); }); - describe("invoke class with a provider", () => { it("should invoke class with a another useClass", async () => { @Injectable() @@ -135,20 +134,18 @@ describe("DI", () => { class FakeMyClass {} - const injector = new InjectorService(); - - injector.addProvider(MyClass, { + injector().addProvider(MyClass, { useClass: FakeMyClass }); - const instance = injector.invoke(MyClass); + const instance = inject(MyClass); expect(instance).toBeInstanceOf(FakeMyClass); - expect(injector.get(MyClass)).toBeInstanceOf(FakeMyClass); + expect(injector().get(MyClass)).toBeInstanceOf(FakeMyClass); - await injector.load(); + await injector().load(); - expect(injector.get(MyClass)).toBeInstanceOf(FakeMyClass); + expect(injector().get(MyClass)).toBeInstanceOf(FakeMyClass); }); }); }); diff --git a/packages/di/src/common/integration/request.spec.ts b/packages/di/src/common/integration/request.spec.ts index 703321aa587..b3c1c87f7ee 100644 --- a/packages/di/src/common/integration/request.spec.ts +++ b/packages/di/src/common/integration/request.spec.ts @@ -1,13 +1,18 @@ +import {beforeEach} from "vitest"; + import {Scope} from "../decorators/scope.js"; import {Service} from "../decorators/service.js"; import {Container} from "../domain/Container.js"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import {ProviderScope} from "../domain/ProviderScope.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {OnDestroy} from "../interfaces/OnDestroy.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {InjectorService} from "../services/InjectorService.js"; describe("DI Request", () => { + beforeEach(() => destroyInjector()); + @Service() @Scope(ProviderScope.INSTANCE) class ServiceInstance { @@ -41,23 +46,22 @@ describe("DI Request", () => { describe("when invoke a service declared as REQUEST", () => { it("should get a new instance of ServiceRequest", async () => { // GIVEN - const injector = new InjectorService(); const container = new Container(); container.addProvider(ServiceSingleton); container.addProvider(ServiceRequest); container.addProvider(ServiceInstance); - await injector.load(container); + await injector().load(container); // we use a local container to create a new context const locals = new LocalsContainer(); // WHEN - const result1: any = injector.invoke(ServiceRequest, {locals}); - const result2: any = injector.invoke(ServiceRequest, {locals}); - const serviceSingleton1: any = injector.invoke(ServiceSingleton, {locals}); - const serviceSingleton2: any = injector.get(ServiceSingleton); + const result1: any = injector().invoke(ServiceRequest, {locals}); + const result2: any = injector().invoke(ServiceRequest, {locals}); + const serviceSingleton1: any = injector().invoke(ServiceSingleton, {locals}); + const serviceSingleton2: any = injector().get(ServiceSingleton); vi.spyOn(result1, "$onDestroy").mockResolvedValue(undefined); diff --git a/packages/di/src/common/integration/singleton.spec.ts b/packages/di/src/common/integration/singleton.spec.ts index 01f48e337d9..ab7d86b734a 100644 --- a/packages/di/src/common/integration/singleton.spec.ts +++ b/packages/di/src/common/integration/singleton.spec.ts @@ -2,6 +2,7 @@ import {Scope} from "../decorators/scope.js"; import {Service} from "../decorators/service.js"; import {Container} from "../domain/Container.js"; import {ProviderScope} from "../domain/ProviderScope.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {InjectorService} from "../services/InjectorService.js"; @@ -39,6 +40,7 @@ describe("DI Singleton", () => { ) {} } + afterEach(() => destroyInjector()); afterAll(() => { GlobalProviders.delete(ServiceSingleton); GlobalProviders.delete(ServiceRequest); @@ -50,8 +52,8 @@ describe("DI Singleton", () => { describe("when it has a SINGLETON dependency", () => { it("should get the service instance", async () => { // GIVEN - const injector = new InjectorService(); const container = new Container(); + container.addProvider(ServiceSingleton); container.addProvider(ServiceRequest); container.addProvider(ServiceInstance); @@ -59,17 +61,15 @@ describe("DI Singleton", () => { container.addProvider(ServiceSingletonWithInstanceDep); // WHEN - await injector.load(container); + await injector().load(container); // THEN - expect(injector.get(ServiceSingleton)!).toBeInstanceOf(ServiceSingleton); + expect(injector().get(ServiceSingleton)!).toBeInstanceOf(ServiceSingleton); }); }); describe("when it has a REQUEST dependency", () => { it("should get the instance and REQUEST dependency should be considered as local SINGLETON", async () => { // GIVEN - const injector = new InjectorService(); - const container = new Container(); container.addProvider(ServiceSingleton); container.addProvider(ServiceRequest); @@ -78,9 +78,9 @@ describe("DI Singleton", () => { container.addProvider(ServiceSingletonWithInstanceDep); // WHEN - await injector.load(container); - const serviceSingletonWithReqDep = injector.get(ServiceSingletonWithRequestDep)!; - const serviceRequest = injector.get(ServiceRequest)!; + await injector().load(container); + const serviceSingletonWithReqDep = injector().get(ServiceSingletonWithRequestDep)!; + const serviceRequest = injector().get(ServiceRequest)!; // THEN expect(serviceSingletonWithReqDep).toBeInstanceOf(ServiceSingletonWithRequestDep); @@ -97,7 +97,6 @@ describe("DI Singleton", () => { describe("when it has a INSTANCE dependency", () => { it("should get the service instance", async () => { // GIVEN - const injector = new InjectorService(); const container = new Container(); container.addProvider(ServiceSingleton); container.addProvider(ServiceRequest); @@ -105,9 +104,9 @@ describe("DI Singleton", () => { container.addProvider(ServiceSingletonWithRequestDep); container.addProvider(ServiceSingletonWithInstanceDep); // WHEN - await injector.load(container); - const serviceWithInstDep = injector.get(ServiceSingletonWithInstanceDep)!; - const serviceInstance = injector.get(ServiceInstance)!; + await injector().load(container); + const serviceWithInstDep = injector().get(ServiceSingletonWithInstanceDep)!; + const serviceInstance = injector().get(ServiceInstance)!; // THEN expect(serviceWithInstDep).toBeInstanceOf(ServiceSingletonWithInstanceDep); diff --git a/packages/di/src/common/services/DILogger.spec.ts b/packages/di/src/common/services/DILogger.spec.ts index 2eddde47253..26720de9d43 100644 --- a/packages/di/src/common/services/DILogger.spec.ts +++ b/packages/di/src/common/services/DILogger.spec.ts @@ -1,6 +1,7 @@ -import {Container, Inject, Injectable, InjectorService, LOGGER} from "../../common/index.js"; +import {Container, destroyInjector, Inject, Injectable, injector, LOGGER} from "../../common/index.js"; describe("DILogger", () => { + afterEach(() => destroyInjector()); it("should inject logger in another service", async () => { @Injectable() class MyService { @@ -8,15 +9,14 @@ describe("DILogger", () => { logger: LOGGER; } - const injector = new InjectorService(); - injector.logger = console; + injector().logger = console; const container = new Container(); container.add(MyService); - await injector.load(container); - const logger = injector.get(MyService)!.logger; + await injector().load(container); + const logger = injector().get(MyService)!.logger; - expect(logger).toEqual(injector.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 3813a8677b4..613b4a0bcd1 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -6,6 +6,8 @@ import {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; import {ProviderScope} from "../domain/ProviderScope.js"; import {ProviderType} from "../domain/ProviderType.js"; +import {inject} from "../fn/inject.js"; +import {destroyInjector, injector} from "../fn/injector.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "./InjectorService.js"; @@ -25,43 +27,43 @@ class Test { } describe("InjectorService", () => { + afterEach(() => destroyInjector()); + describe("has()", () => { it("should return true", () => { - expect(new InjectorService().has(InjectorService)).toBe(true); + expect(injector().has(InjectorService)).toBe(true); }); it("should return false", () => { - expect(new InjectorService().has(Test)).toBe(false); + expect(injector().has(Test)).toBe(false); }); }); - describe("get()", () => { it("should return element", () => { - expect(new InjectorService().get(InjectorService)).toBeInstanceOf(InjectorService); + expect(injector().get(InjectorService)).toBeInstanceOf(InjectorService); }); it("should return undefined", () => { - expect(new InjectorService().get(Test)).toBeUndefined(); + expect(injector().get(Test)).toBeUndefined(); }); }); describe("getMany()", () => { it("should return all instance", () => { - const injector = new InjectorService(); - injector.addProvider("token", { + injector().addProvider("token", { type: ProviderType.VALUE, useValue: 1 }); - expect(!!injector.getMany(ProviderType.VALUE).length).toEqual(true); + expect(!!injector().getMany(ProviderType.VALUE).length).toEqual(true); + + injector().delete("token"); }); }); - describe("toArray()", () => { it("should return instances", () => { - expect(new InjectorService().toArray()).toBeInstanceOf(Array); + expect(injector().toArray()).toBeInstanceOf(Array); }); }); - describe("invoke()", () => { describe("when we call invoke with rebuild options (SINGLETON)", () => { it("should invoke the provider from container", async () => { @@ -73,30 +75,29 @@ describe("InjectorService", () => { provider.deps = [InjectorService]; provider.alias = "alias"; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); - vi.spyOn(injector as any, "resolve"); - vi.spyOn(injector as any, "invoke"); - vi.spyOn(injector, "getProvider"); + vi.spyOn(injector() as any, "resolve"); + vi.spyOn(injector() as any, "invoke"); + vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); // WHEN - const result1: any = injector.invoke(token, {locals}); - const result2: any = injector.invoke(token, {locals, rebuild: true}); + const result1: any = inject(token, {locals}); + const result2: any = inject(token, {locals, rebuild: true}); // THEN expect(result1 !== result2).toEqual(true); - expect(injector.getProvider).toHaveBeenCalledWith(token); - expect(injector.get("alias")).toBeInstanceOf(token); + expect(injector().getProvider).toHaveBeenCalledWith(token); + expect(injector().get("alias")).toBeInstanceOf(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals, rebuild: true}); - expect(injector.invoke).toHaveBeenCalledWith(InjectorService, { + expect((injector() as any).resolve).toHaveBeenCalledWith(token, {locals, rebuild: true}); + expect(injector().invoke).toHaveBeenCalledWith(InjectorService, { locals, parent: token }); @@ -110,36 +111,35 @@ describe("InjectorService", () => { const provider = new Provider(token); provider.scope = ProviderScope.REQUEST; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); - vi.spyOn(injector as any, "resolve"); - vi.spyOn(injector, "get"); - vi.spyOn(injector, "getProvider"); + vi.spyOn(injector() as any, "resolve"); + vi.spyOn(injector(), "get"); + vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); // LocalContainer for the first request const locals2 = new LocalsContainer(); // LocalContainer for the second request // WHEN REQ1 - const result1: any = injector.invoke(token, {locals}); - const result2: any = injector.invoke(token, {locals}); + const result1: any = inject(token, {locals}); + const result2: any = inject(token, {locals}); // WHEN REQ2 - const result3: any = injector.invoke(token, {locals: locals2}); + const result3: any = inject(token, {locals: locals2}); // THEN expect(result1).toEqual(result2); expect(result2 !== result3).toEqual(true); - expect(injector.getProvider).toHaveBeenCalledWith(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals}); + expect(injector().getProvider).toHaveBeenCalledWith(token); + expect((injector() as any).resolve).toHaveBeenCalledWith(token, {locals}); expect(locals.get(token)).toEqual(result1); expect(locals2.get(token)).toEqual(result3); - return expect(injector.get).not.toHaveBeenCalled(); + expect(injector().get).not.toHaveBeenCalled(); }); }); describe("when provider is a INSTANCE", () => { @@ -150,30 +150,30 @@ describe("InjectorService", () => { const provider = new Provider(token); provider.scope = ProviderScope.INSTANCE; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); - vi.spyOn(injector as any, "resolve"); - vi.spyOn(injector, "get"); - vi.spyOn(injector, "getProvider"); + vi.spyOn(injector() as any, "resolve"); + vi.spyOn(injector(), "get"); + vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); // LocalContainer for the first request // WHEN REQ1 - const result1: any = injector.invoke(token, {locals}); - const result2: any = injector.invoke(token, {locals}); + const result1: any = inject(token, {locals}); + const result2: any = inject(token, {locals}); // THEN expect(result1 !== result2).toEqual(true); - expect(injector.getProvider).toHaveBeenCalledWith(token); - expect((injector as any).resolve).toHaveBeenCalledWith(token, {locals}); + expect(injector().getProvider).toHaveBeenCalledWith(token); + expect((injector() as any).resolve).toHaveBeenCalledWith(token, {locals}); expect(locals.has(token)).toEqual(false); + expect(injector().get).not.toHaveBeenCalled(); - return expect(injector.get).not.toHaveBeenCalled(); + await injector().destroy(); }); }); describe("when provider is a SINGLETON", () => { @@ -189,11 +189,10 @@ describe("InjectorService", () => { const provider = new Provider(token); provider.scope = ProviderScope.SINGLETON; - const injector = new InjectorService(); - injector.set(token, provider); + injector().set(token, provider); // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toBeInstanceOf(token); @@ -206,87 +205,81 @@ describe("InjectorService", () => { const provider = new Provider(token); provider.scope = ProviderScope.SINGLETON; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); - vi.spyOn(injector as any, "resolve"); - vi.spyOn(injector, "getProvider"); + vi.spyOn(injector() as any, "resolve"); + vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); // WHEN - const result1: any = injector.invoke(token, {locals}); - const result2: any = injector.invoke(token, {locals}); + const result1: any = inject(token, {locals}); + const result2: any = inject(token, {locals}); // THEN expect(result1).toEqual(result2); - return expect((injector as any).resolve).not.toHaveBeenCalled(); + return expect((injector() as any).resolve).not.toHaveBeenCalled(); }); }); describe("when provider is a Value (useValue)", () => { it("should invoke the provider from container (1)", async () => { // GIVEN - const token = Symbol.for("TokenValue"); + const token = Symbol.for("TokenValue1"); const provider = new Provider(token); provider.scope = ProviderScope.SINGLETON; provider.useValue = "TEST"; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toEqual("TEST"); }); - it("should invoke the provider from container (2)", async () => { // GIVEN - const token = Symbol.for("TokenValue"); + const token = Symbol.for("TokenValue2"); const provider = new Provider(token); provider.scope = ProviderScope.SINGLETON; provider.useValue = () => "TEST"; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toEqual("TEST"); }); - it("should invoke the provider from container with falsy value", async () => { // GIVEN - const token = Symbol.for("TokenValue"); + const token = Symbol.for("TokenValue3"); const provider = new Provider(token); provider.scope = ProviderScope.SINGLETON; provider.useValue = null; - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toEqual(null); @@ -301,14 +294,13 @@ describe("InjectorService", () => { provider.scope = ProviderScope.SINGLETON; provider.useFactory = () => ({factory: "factory"}); - const injector = new InjectorService(); const container = new Container(); container.set(token, provider); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toEqual({factory: "factory"}); @@ -335,23 +327,22 @@ describe("InjectorService", () => { providerSync.hooks = {$onDestroy: vi.fn(), $onInit: vi.fn()}; providerSync.useFactory = (asyncInstance: any) => asyncInstance.factory; - const injector = new InjectorService(); const container = new Container(); container.set(tokenChild, providerChild); container.set(token, provider); container.set(tokenSync, providerSync); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token); - const result2: any = injector.invoke(tokenSync); + const result: any = inject(token); + const result2: any = inject(tokenSync); // THEN expect(result).toEqual({factory: "test async factory"}); expect(result2).toEqual("test async factory"); - await injector.emit("$onInit"); + await injector().emit("$onInit"); expect(providerSync.hooks.$onInit).toHaveBeenCalledWith("test async factory"); }); @@ -373,16 +364,15 @@ describe("InjectorService", () => { return Promise.resolve({factory: dep.factory + " factory2"}); }; - const injector = new InjectorService(); const container = new Container(); container.set(tokenChild, providerChild); container.set(token, provider); container.set(token2, provider2); - await injector.load(container); + await injector().load(container); // WHEN - const result: any = injector.invoke(token2); + const result: any = inject(token2); // THEN expect(result).toEqual({factory: "test async factory factory2"}); @@ -393,10 +383,8 @@ describe("InjectorService", () => { // GIVEN const token = class {}; - const injector = new InjectorService(); - // WHEN - const result: any = injector.invoke(token); + const result: any = inject(token); // THEN expect(result).toBeInstanceOf(token); @@ -421,14 +409,13 @@ describe("InjectorService", () => { provider3.scope = ProviderScope.SINGLETON; provider3.deps = [undefined] as never; - const injector = new InjectorService(); - injector.set(token2, provider2); - injector.set(token3, provider3); + injector().set(token2, provider2); + injector().set(token3, provider3); // WHEN let actualError; try { - injector.invoke(token3); + inject(token3); } catch (er) { actualError = er; } @@ -456,14 +443,13 @@ describe("InjectorService", () => { provider3.scope = ProviderScope.SINGLETON; provider3.deps = [Object]; - const injector = new InjectorService(); - injector.set(token2, provider2); - injector.set(token3, provider3); + injector().set(token2, provider2); + injector().set(token3, provider3); // WHEN let actualError; try { - injector.invoke(token3); + inject(token3); } catch (er) { actualError = er; } @@ -474,10 +460,8 @@ describe("InjectorService", () => { it("should try to inject string token (optional)", () => { // GIVEN - const injector = new InjectorService(); - // WHEN - const result = injector.invoke("token.not.found"); + const result = inject("token.not.found"); // THEN expect(result).toEqual(undefined); @@ -505,15 +489,14 @@ describe("InjectorService", () => { provider3.scope = ProviderScope.SINGLETON; provider3.deps = [token2]; - const injector = new InjectorService(); - injector.set(token1, provider1); - injector.set(token2, provider2); - injector.set(token3, provider3); + injector().set(token1, provider1); + injector().set(token2, provider2); + injector().set(token3, provider3); // WHEN let actualError; try { - injector.invoke(token3); + inject(token3); } catch (er) { actualError = er; } @@ -527,9 +510,9 @@ describe("InjectorService", () => { describe("when provider has Provider as dependencies", () => { it("should inject Provider", () => { // GIVEN - const injector = new InjectorService(); + const token = Symbol.for("TokenProvider1"); - injector.add(token, { + injector().add(token, { deps: [Provider], configuration: { test: "test" @@ -540,18 +523,18 @@ describe("InjectorService", () => { }); // WHEN - const instance: any = injector.invoke(token)!; + const instance: any = inject(token)!; // THEN - expect(instance).toEqual({to: injector.getProvider(token)}); + expect(instance).toEqual({to: injector().getProvider(token)}); }); }); describe("when provider has Configuration as dependencies", () => { it("should inject Provider", () => { // GIVEN - const injector = new InjectorService(); + const token = Symbol.for("TokenProvider1"); - injector.add(token, { + injector().add(token, { deps: [Configuration], useFactory(settings: any) { return {to: settings}; @@ -559,30 +542,28 @@ describe("InjectorService", () => { }); // WHEN - const instance: any = injector.invoke(token)!; + const instance: any = inject(token)!; // THEN - expect(instance).toEqual({to: injector.settings}); + expect(instance).toEqual({to: injector().settings}); }); }); }); - describe("resolveConfiguration()", () => { it("should load configuration from each providers", () => { // GIVEN - const injector = new InjectorService(); - injector.settings.set({ + injector().settings.set({ scopes: { [ProviderType.VALUE]: ProviderScope.SINGLETON } }); - expect(injector.settings.get("scopes")).toEqual({ + expect(injector().settings.get("scopes")).toEqual({ [ProviderType.VALUE]: ProviderScope.SINGLETON }); - injector.add(Symbol.for("TOKEN1"), { + injector().add(Symbol.for("TOKEN1"), { configuration: { custom: "config", scopes: { @@ -591,7 +572,7 @@ describe("InjectorService", () => { } }); - injector.add(Symbol.for("TOKEN2"), { + injector().add(Symbol.for("TOKEN2"), { configuration: { scopes: { provider_custom_2: ProviderScope.SINGLETON @@ -600,13 +581,13 @@ describe("InjectorService", () => { }); // WHEN - injector.resolveConfiguration(); + injector().resolveConfiguration(); // should load only once the configuration - injector.resolveConfiguration(); + injector().resolveConfiguration(); // THEN - expect(injector.settings.get("custom")).toEqual("config"); - expect(injector.settings.get("scopes")).toEqual({ + expect(injector().settings.get("custom")).toEqual("config"); + expect(injector().settings.get("scopes")).toEqual({ provider_custom_2: "singleton", provider_custom: "singleton", value: "singleton" @@ -614,9 +595,8 @@ describe("InjectorService", () => { }); it("should load configuration from each providers (with resolvers)", () => { // GIVEN - const injector = new InjectorService(); - injector.add(Symbol.for("TOKEN1"), { + injector().add(Symbol.for("TOKEN1"), { configuration: { custom: { config: "1" @@ -624,7 +604,7 @@ describe("InjectorService", () => { } }); - injector.add(Symbol.for("TOKEN2"), { + injector().add(Symbol.for("TOKEN2"), { configuration: { custom: { config2: "1" @@ -633,13 +613,12 @@ describe("InjectorService", () => { }); // WHEN - injector.resolveConfiguration(); + injector().resolveConfiguration(); // THEN - expect(injector.settings.get("custom")).toEqual({config: "1", config2: "1"}); + expect(injector().settings.get("custom")).toEqual({config: "1", config2: "1"}); }); }); - describe("alter()", () => { it("should alter value", () => { @Injectable() @@ -652,12 +631,12 @@ describe("InjectorService", () => { vi.spyOn(Test.prototype, "$alterValue"); // GIVEN - const injector = new InjectorService(); - injector.invoke(Test); - const service = injector.get(Test)!; + inject(Test); + + const service = injector().get(Test)!; - const value = injector.alter("$alterValue", "value"); + const value = injector().alter("$alterValue", "value"); expect(service.$alterValue).toHaveBeenCalledWith("value"); expect(value).toEqual("alteredValue"); @@ -676,43 +655,36 @@ describe("InjectorService", () => { }); // GIVEN - const injector = new InjectorService(); - injector.invoke("TOKEN"); - const value = injector.alter("$alterValue", "value"); + inject("TOKEN"); + + const value = injector().alter("$alterValue", "value"); expect(value).toEqual("alteredValue"); }); }); - describe("alterAsync()", () => { it("should alter value", async () => { @Injectable() class Test { $alterValue(value: any) { - return Promise.resolve("alteredValue"); + return Promise.resolve(value + ":alteredValue"); } } vi.spyOn(Test.prototype, "$alterValue"); // GIVEN - const injector = new InjectorService(); - injector.invoke(Test); - const service = injector.get(Test)!; + inject(Test); - const value = await injector.alterAsync("$alterValue", "value"); + const value = await injector().alterAsync("$alterValue", "value"); - expect(service.$alterValue).toHaveBeenCalledWith("value"); - expect(value).toEqual("alteredValue"); + expect(value).toEqual("value:alteredValue"); }); }); - describe("imports", () => { it("should load all provider and override by configuration a provider (use)", async () => { - const injector = new InjectorService(); - @Injectable() class TestService { get() { @@ -720,7 +692,7 @@ describe("InjectorService", () => { } } - injector.settings.set("imports", [ + injector().settings.set("imports", [ { token: TestService, use: { @@ -729,14 +701,12 @@ describe("InjectorService", () => { } ]); - await injector.load(); + await injector().load(); - const result = injector.get(TestService)!.get(); + const result = injector().get(TestService)!.get(); expect(result).toEqual("world"); }); it("should load all provider and override by configuration a provider (useClass)", async () => { - const injector = new InjectorService(); - @Injectable() class TestService { get() { @@ -751,21 +721,19 @@ describe("InjectorService", () => { } } - injector.settings.set("imports", [ + injector().settings.set("imports", [ { token: TestService, useClass: FsTestService } ]); - await injector.load(); + await injector().load(); - const result = injector.get(TestService)!.get(); + const result = injector().get(TestService)!.get(); expect(result).toEqual("fs"); }); it("should load all provider and override by configuration a provider (useFactory)", async () => { - const injector = new InjectorService(); - @Injectable() class TestService { get() { @@ -773,7 +741,7 @@ describe("InjectorService", () => { } } - injector.settings.set("imports", [ + injector().settings.set("imports", [ { token: TestService, useFactory: () => { @@ -786,14 +754,12 @@ describe("InjectorService", () => { } ]); - await injector.load(); + await injector().load(); - const result = injector.get(TestService)!.get(); + const result = injector().get(TestService)!.get(); expect(result).toEqual("world"); }); it("should load all provider and override by configuration a provider (useAsyncFactory)", async () => { - const injector = new InjectorService(); - @Injectable() class TestService { get() { @@ -801,7 +767,7 @@ describe("InjectorService", () => { } } - injector.settings.set("imports", [ + injector().settings.set("imports", [ { token: TestService, useAsyncFactory: () => { @@ -814,9 +780,9 @@ describe("InjectorService", () => { } ]); - await injector.load(); + await injector().load(); - const result = injector.get(TestService)!.get(); + const result = injector().get(TestService)!.get(); expect(result).toEqual("world"); }); }); diff --git a/packages/di/src/node/domain/ContextLogger.spec.ts b/packages/di/src/node/domain/ContextLogger.spec.ts index 7e33d8f2baf..af68d4b7326 100644 --- a/packages/di/src/node/domain/ContextLogger.spec.ts +++ b/packages/di/src/node/domain/ContextLogger.spec.ts @@ -1,4 +1,3 @@ -import {InjectorService} from "../../common/index.js"; import {ContextLogger} from "./ContextLogger.js"; function getIgnoreLogFixture(ignore: string[], url: string) { @@ -26,8 +25,7 @@ describe("ContextLogger", () => { }, logger, id: "id", - dateStart: new Date("2019-01-01"), - injector: new InjectorService() + dateStart: new Date("2019-01-01") }); contextLogger.alterIgnoreLog(getIgnoreLogFixture(["/admin"], "/")); @@ -120,8 +118,7 @@ describe("ContextLogger", () => { }, logger, id: "id", - startDate: new Date("2019-01-01"), - injector: new InjectorService() + startDate: new Date("2019-01-01") }); contextLogger.alterIgnoreLog(getIgnoreLogFixture(["/admin"], "/url")); @@ -195,8 +192,7 @@ describe("ContextLogger", () => { startDate: new Date("2019-01-01"), additionalProps: { url: "/" - }, - injector: new InjectorService() + } }); contextLogger.alterIgnoreLog(getIgnoreLogFixture(["/admin"], "/admin")); @@ -227,8 +223,7 @@ describe("ContextLogger", () => { }, id: "id", startDate: new Date("2019-01-01"), - maxStackSize: 2, - injector: new InjectorService() + maxStackSize: 2 }); contextLogger.maxStackSize = 2; @@ -261,7 +256,6 @@ describe("ContextLogger", () => { logger, id: "id", dateStart: new Date("2019-01-01"), - injector: new InjectorService(), level: "off" }); diff --git a/packages/di/src/node/services/DITest.ts b/packages/di/src/node/services/DITest.ts index 0258ff5b431..b6c2f1e3366 100644 --- a/packages/di/src/node/services/DITest.ts +++ b/packages/di/src/node/services/DITest.ts @@ -45,7 +45,7 @@ export class DITest { settings: DITest.configure(settings) }); - setLoggerConfiguration(inj); + setLoggerConfiguration(); return inj; } diff --git a/packages/di/src/node/utils/attachLogger.spec.ts b/packages/di/src/node/utils/attachLogger.spec.ts index 14782a42c55..b271e6a47c6 100644 --- a/packages/di/src/node/utils/attachLogger.spec.ts +++ b/packages/di/src/node/utils/attachLogger.spec.ts @@ -1,15 +1,14 @@ import {Logger} from "@tsed/logger"; -import {InjectorService} from "../../common/index.js"; +import {injector} from "../../common/index.js"; import {attachLogger} from "./attachLogger.js"; describe("attachLogger", () => { it("should attach logger", () => { - const injector = new InjectorService(); const $log = new Logger("test"); - attachLogger(injector, $log); + attachLogger($log); - expect(injector.logger).toEqual($log); + expect(injector().logger).toEqual($log); }); }); diff --git a/packages/di/src/node/utils/attachLogger.ts b/packages/di/src/node/utils/attachLogger.ts index 4ed6a035fba..d8a1c1ca0a7 100644 --- a/packages/di/src/node/utils/attachLogger.ts +++ b/packages/di/src/node/utils/attachLogger.ts @@ -1,7 +1,7 @@ -import {DILogger, InjectorService} from "../../common/index.js"; +import {DILogger, injector, InjectorService} from "../../common/index.js"; import {setLoggerConfiguration} from "./setLoggerConfiguration.js"; -export function attachLogger(injector: InjectorService, $log: DILogger) { - injector.logger = $log; - setLoggerConfiguration(injector); +export function attachLogger($log: DILogger) { + injector().logger = $log; + setLoggerConfiguration(); } diff --git a/packages/di/src/node/utils/setLoggerConfiguration.spec.ts b/packages/di/src/node/utils/setLoggerConfiguration.spec.ts index 685ba2bd584..4d48e4333dd 100644 --- a/packages/di/src/node/utils/setLoggerConfiguration.spec.ts +++ b/packages/di/src/node/utils/setLoggerConfiguration.spec.ts @@ -1,29 +1,28 @@ import {Logger} from "@tsed/logger"; +import {afterEach} from "vitest"; -import {InjectorService} from "../../common/index.js"; +import {destroyInjector, injector} from "../../common/index.js"; import {setLoggerConfiguration} from "./setLoggerConfiguration.js"; describe("setLoggerConfiguration", () => { + afterEach(() => destroyInjector()); it("should change the logger level depending on the configuration", () => { - const injector = new InjectorService(); + injector().settings.set("logger.level", "info"); - injector.settings.set("logger.level", "info"); + setLoggerConfiguration(); - setLoggerConfiguration(injector); - - expect(injector.logger.level).toEqual("info"); + expect(injector().logger.level).toEqual("info"); }); it("should call $log.appenders.set()", () => { - const injector = new InjectorService(); - injector.logger = new Logger(); + injector().logger = new Logger(); - vi.spyOn(injector.logger.appenders, "set").mockResolvedValue(undefined); + vi.spyOn(injector().logger.appenders, "set").mockResolvedValue(undefined); - injector.settings.set("logger.format", "format"); + injector().settings.set("logger.format", "format"); - setLoggerConfiguration(injector); + setLoggerConfiguration(); - expect(injector.logger.appenders.set).toHaveBeenCalledWith("stdout", { + expect(injector().logger.appenders.set).toHaveBeenCalledWith("stdout", { type: "stdout", levels: ["info", "debug"], layout: { @@ -32,7 +31,7 @@ describe("setLoggerConfiguration", () => { } }); - expect(injector.logger.appenders.set).toHaveBeenCalledWith("stderr", { + expect(injector().logger.appenders.set).toHaveBeenCalledWith("stderr", { levels: ["trace", "fatal", "error", "warn"], type: "stderr", layout: { diff --git a/packages/di/src/node/utils/setLoggerConfiguration.ts b/packages/di/src/node/utils/setLoggerConfiguration.ts index da96dd7f24b..c141f58701e 100644 --- a/packages/di/src/node/utils/setLoggerConfiguration.ts +++ b/packages/di/src/node/utils/setLoggerConfiguration.ts @@ -1,8 +1,7 @@ -import type {InjectorService} from "../../common/index.js"; import {setLoggerFormat} from "./setLoggerFormat.js"; import {setLoggerLevel} from "./setLoggerLevel.js"; -export function setLoggerConfiguration(injector: InjectorService) { - setLoggerLevel(injector); - setLoggerFormat(injector); +export function setLoggerConfiguration() { + setLoggerLevel(); + setLoggerFormat(); } diff --git a/packages/di/src/node/utils/setLoggerFormat.ts b/packages/di/src/node/utils/setLoggerFormat.ts index 46e77c0ddd2..79dc2199144 100644 --- a/packages/di/src/node/utils/setLoggerFormat.ts +++ b/packages/di/src/node/utils/setLoggerFormat.ts @@ -1,18 +1,18 @@ -import type {InjectorService} from "../../common/index.js"; +import {injector} from "../../common/index.js"; +import {logger} from "../fn/logger.js"; /** * @ignore - * @param injector */ -export function setLoggerFormat(injector: InjectorService) { - const {level, format} = injector.settings.logger; +export function setLoggerFormat() { + const {level, format} = injector().settings.logger; if (level) { - injector.logger.level = level; + injector().logger.level = level; } - if (format && injector.logger.appenders) { - injector.logger.appenders.set("stdout", { + if (format && injector().logger.appenders) { + logger().appenders.set("stdout", { type: "stdout", levels: ["info", "debug"], layout: { @@ -21,7 +21,7 @@ export function setLoggerFormat(injector: InjectorService) { } }); - injector.logger.appenders.set("stderr", { + logger().appenders.set("stderr", { levels: ["trace", "fatal", "error", "warn"], type: "stderr", layout: { diff --git a/packages/di/src/node/utils/setLoggerLevel.ts b/packages/di/src/node/utils/setLoggerLevel.ts index 5caafaa9708..d7a6f88d720 100644 --- a/packages/di/src/node/utils/setLoggerLevel.ts +++ b/packages/di/src/node/utils/setLoggerLevel.ts @@ -1,13 +1,12 @@ -import type {InjectorService} from "../../common/index.js"; +import {injector} from "../../common/index.js"; /** * @ignore - * @param injector */ -export function setLoggerLevel(injector: InjectorService) { - const {level} = injector.settings.logger; +export function setLoggerLevel() { + const {level} = injector().settings.logger; if (level) { - injector.logger.level = level; + injector().logger.level = level; } } From ec6971d1a9cafef1a3aa2d559f52b1266a6198e4 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 14:01:56 +0100 Subject: [PATCH 09/32] feat(di): add priority/alias props on Provider --- packages/di/src/common/domain/Provider.ts | 5 +++-- packages/di/src/common/fn/injectable.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/di/src/common/domain/Provider.ts b/packages/di/src/common/domain/Provider.ts index 7601946b7a4..bda580834c0 100644 --- a/packages/di/src/common/domain/Provider.ts +++ b/packages/di/src/common/domain/Provider.ts @@ -16,11 +16,12 @@ export class Provider implements ProviderOpts { public type: ProviderType | TokenProvider = ProviderType.PROVIDER; public deps: TokenProvider[]; public imports: (TokenProvider | [TokenProvider])[]; - public alias?: string; + public alias: string; + public priority: number; public useFactory?: Function; public useAsyncFactory?: Function; public useValue?: unknown; - public hooks?: Record>; + public hooks: Record> = {}; private _useClass: Type; private _provide: TokenProvider; private _store: Store; diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts index 7e6d60f0d03..d9b261a65b3 100644 --- a/packages/di/src/common/fn/injectable.ts +++ b/packages/di/src/common/fn/injectable.ts @@ -74,9 +74,9 @@ export function providerBuilder(props: }; } -type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration"; +type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration" | "priority"; -const Props = ["type", "scope", "path", "alias", "hooks", "deps", "imports", "configuration"]; +const Props = ["type", "scope", "path", "alias", "hooks", "deps", "imports", "configuration", "priority"]; export const injectable = providerBuilder(Props); export const interceptor = providerBuilder(Props, { type: ProviderType.INTERCEPTOR From f4af7949289016c8fe342fae1e3d605519674b13 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 18:20:54 +0100 Subject: [PATCH 10/32] feat(di): add $beforeInvoke, $beforeInvoke:type, $afterInvoke --- packages/di/src/common/fn/inject.ts | 2 +- packages/di/src/common/fn/injectable.spec.ts | 7 ++ packages/di/src/common/fn/refValue.spec.ts | 4 + .../src/common/interfaces/RegistrySettings.ts | 10 -- .../common/registries/GlobalProviders.spec.ts | 18 ---- .../src/common/registries/GlobalProviders.ts | 15 --- .../common/services/InjectorService.spec.ts | 32 +++--- .../di/src/common/services/InjectorService.ts | 97 ++++++++++++++----- .../src/common/utils/registerProviderHooks.ts | 13 --- 9 files changed, 104 insertions(+), 94 deletions(-) delete mode 100644 packages/di/src/common/utils/registerProviderHooks.ts diff --git a/packages/di/src/common/fn/inject.ts b/packages/di/src/common/fn/inject.ts index 9ce3d7c929c..bb0b699c3e5 100644 --- a/packages/di/src/common/fn/inject.ts +++ b/packages/di/src/common/fn/inject.ts @@ -21,7 +21,7 @@ import {invokeOptions, localsContainer} from "./localsContainer.js"; * @decorator */ export function inject(token: TokenProvider, opts?: Partial>): T { - return injector().invoke(token, { + return injector().resolve(token, { ...opts, ...invokeOptions(), locals: opts?.locals || localsContainer() diff --git a/packages/di/src/common/fn/injectable.spec.ts b/packages/di/src/common/fn/injectable.spec.ts index b40bbf0f3b3..9fc4aed2840 100644 --- a/packages/di/src/common/fn/injectable.spec.ts +++ b/packages/di/src/common/fn/injectable.spec.ts @@ -1,3 +1,5 @@ +import {Store} from "@tsed/core"; + import {DITest, logger} from "../../node/index.js"; import {ProviderScope} from "../domain/ProviderScope.js"; import {ProviderType} from "../domain/ProviderType.js"; @@ -28,6 +30,11 @@ describe("injectable", () => { expect(instance.nested).toBeInstanceOf(Nested); expect(instance.nested.get()).toEqual("hello"); }); + it("should define class with scope", async () => { + injectable(MyClass).scope(ProviderScope.SINGLETON).class(MyClass).store().set("test", "test"); + + expect(Store.from(MyClass).get("test")).toEqual("test"); + }); it("should create a factory", async () => { const builder = injectable(Symbol.for("Test")).factory(() => "test"); const provider = builder.inspect(); diff --git a/packages/di/src/common/fn/refValue.spec.ts b/packages/di/src/common/fn/refValue.spec.ts index cccaba62fb4..2586f279d83 100644 --- a/packages/di/src/common/fn/refValue.spec.ts +++ b/packages/di/src/common/fn/refValue.spec.ts @@ -23,6 +23,10 @@ describe("refValue()", () => { const test = await DITest.invoke(Test); expect(test.test.value).toEqual("off"); + + test.test.value = "test"; + + expect(test.test.value).toEqual("test"); }); it("should create a getter with default value", async () => { expect(configuration().get("logger.test")).toEqual(undefined); diff --git a/packages/di/src/common/interfaces/RegistrySettings.ts b/packages/di/src/common/interfaces/RegistrySettings.ts index 96ebee88da2..41d8f706aaa 100644 --- a/packages/di/src/common/interfaces/RegistrySettings.ts +++ b/packages/di/src/common/interfaces/RegistrySettings.ts @@ -1,9 +1,6 @@ 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"; /** * @ignore @@ -11,11 +8,4 @@ import type {ResolvedInvokeOptions} from "./ResolvedInvokeOptions.js"; export interface RegistrySettings { injectable?: boolean; model?: Type; - - /** - * - * @param provider - * @param options - */ - onInvoke?(provider: Provider, options: ResolvedInvokeOptions): void; } diff --git a/packages/di/src/common/registries/GlobalProviders.spec.ts b/packages/di/src/common/registries/GlobalProviders.spec.ts index 5a57d6bd131..6f858fbea65 100644 --- a/packages/di/src/common/registries/GlobalProviders.spec.ts +++ b/packages/di/src/common/registries/GlobalProviders.spec.ts @@ -91,22 +91,4 @@ describe("GlobalProviderRegistry", () => { expect(Object.keys(provider.hooks || {})).toEqual(["$onInit", "$onReady"]); }); }); - describe("onInvoke()", () => { - it("should call the onInvoke hook", () => { - const opts = { - onInvoke: vi.fn() - }; - const provider = new Provider(class {}, {type: "type:test"}); - const locals = new LocalsContainer(); - const resolvedOptions = { - token: provider.token, - locals - } as any; - - GlobalProviders.createRegistry("type:test", Provider, opts); - GlobalProviders.onInvoke(provider, resolvedOptions); - - expect(opts.onInvoke).toHaveBeenCalledWith(provider, resolvedOptions); - }); - }); }); diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index 89662822a66..7c142df537c 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -80,14 +80,6 @@ export class GlobalProviderRegistry extends Map { return this; } - onInvoke(provider: Provider, options: ResolvedInvokeOptions) { - const settings = this.#settings.get(provider.type); - - if (settings?.onInvoke) { - settings.onInvoke(provider, options); - } - } - getRegistrySettings(target: TokenProvider): RegistrySettings { let type: TokenProvider | ProviderType = ProviderType.PROVIDER; @@ -107,13 +99,6 @@ export class GlobalProviderRegistry extends Map { ); } - createRegisterFn(type: string) { - return (provider: any | ProviderOpts, instance?: any): void => { - provider = Object.assign({instance}, provider, {type}); - this.merge(provider.provide, provider); - }; - } - /** * * @param key diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index 613b4a0bcd1..dbe39edcd25 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -1,3 +1,5 @@ +import {$emit} from "@tsed/hooks"; + import {Configuration} from "../decorators/configuration.js"; import {Inject} from "../decorators/inject.js"; import {Injectable} from "../decorators/injectable.js"; @@ -8,10 +10,18 @@ import {ProviderScope} from "../domain/ProviderScope.js"; import {ProviderType} from "../domain/ProviderType.js"; import {inject} from "../fn/inject.js"; import {destroyInjector, injector} from "../fn/injector.js"; -import {GlobalProviders} from "../registries/GlobalProviders.js"; import {registerProvider} from "../registries/ProviderRegistry.js"; import {InjectorService} from "./InjectorService.js"; +vi.mock("@tsed/hooks", async (importOriginal) => { + const mod = await importOriginal(); + + return { + ...mod, + $emit: vi.fn() + }; +}); + class Test { @Inject() prop: InjectorService; @@ -80,8 +90,8 @@ describe("InjectorService", () => { await injector().load(container); + vi.spyOn(injector() as any, "invokeToken"); vi.spyOn(injector() as any, "resolve"); - vi.spyOn(injector() as any, "invoke"); vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); @@ -96,8 +106,8 @@ describe("InjectorService", () => { expect(injector().getProvider).toHaveBeenCalledWith(token); expect(injector().get("alias")).toBeInstanceOf(token); - expect((injector() as any).resolve).toHaveBeenCalledWith(token, {locals, rebuild: true}); - expect(injector().invoke).toHaveBeenCalledWith(InjectorService, { + expect((injector() as any).invokeToken).toHaveBeenCalledWith(token, {locals, rebuild: true}); + expect(injector().resolve).toHaveBeenCalledWith(InjectorService, { locals, parent: token }); @@ -177,12 +187,6 @@ describe("InjectorService", () => { }); }); describe("when provider is a SINGLETON", () => { - beforeAll(() => { - vi.spyOn(GlobalProviders, "onInvoke").mockReturnValue(undefined); - }); - afterAll(() => { - vi.resetAllMocks(); - }); it("should invoke the provider from container", () => { // GIVEN const token = class Test {}; @@ -196,7 +200,9 @@ describe("InjectorService", () => { // THEN expect(result).toBeInstanceOf(token); - expect(GlobalProviders.onInvoke).toHaveBeenCalledWith(provider, expect.anything()); + expect($emit).toHaveBeenCalledWith("$beforeInvoke", token, [expect.any(Object)]); + expect($emit).toHaveBeenCalledWith("$beforeInvoke:provider", [expect.any(Object)]); + expect($emit).toHaveBeenCalledWith("$afterInvoke", token, [result, expect.any(Object)]); }); it("should invoke the provider from container (2)", async () => { // GIVEN @@ -210,7 +216,7 @@ describe("InjectorService", () => { await injector().load(container); - vi.spyOn(injector() as any, "resolve"); + vi.spyOn(injector() as any, "invokeToken"); vi.spyOn(injector(), "getProvider"); const locals = new LocalsContainer(); @@ -223,7 +229,7 @@ describe("InjectorService", () => { // THEN expect(result1).toEqual(result2); - return expect((injector() as any).resolve).not.toHaveBeenCalled(); + return expect((injector() as any).invokeToken).not.toHaveBeenCalled(); }); }); describe("when provider is a Value (useValue)", () => { diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 933e845c573..8f3355087e9 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -1,5 +1,5 @@ import {classOf, deepClone, deepMerge, isArray, isClass, isFunction, isInheritedFrom, isObject, isPromise, nameOf} from "@tsed/core"; -import {$alter, $asyncAlter, $asyncEmit, $off} from "@tsed/hooks"; +import {$alter, $asyncAlter, $asyncEmit, $emit, $off, $on} from "@tsed/hooks"; import {DI_INVOKE_OPTIONS, DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; import {Configuration} from "../decorators/configuration.js"; @@ -17,7 +17,6 @@ import type {TokenProvider} from "../interfaces/TokenProvider.js"; import {GlobalProviders} from "../registries/GlobalProviders.js"; import {createContainer} from "../utils/createContainer.js"; import {getConstructorDependencies} from "../utils/getConstructorDependencies.js"; -import {registerHooks} from "../utils/registerProviderHooks.js"; import {DIConfiguration} from "./DIConfiguration.js"; const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; @@ -54,6 +53,7 @@ export class InjectorService extends Container { constructor() { super(); this.#cache.set(InjectorService, this); + this.#cache.set(Configuration, this.settings); } /** @@ -80,9 +80,8 @@ export class InjectorService extends Container { * ``` * * @param token The class or symbol registered in InjectorService. - * @param options */ - get(token: TokenProvider, options: Record = {}): T | undefined { + get(token: TokenProvider): T | undefined { return this.#cache.get(token); } @@ -91,7 +90,7 @@ export class InjectorService extends Container { */ getMany(type: any, options?: Partial): Type[] { return this.getProviders(type).map((provider) => { - return this.invoke(provider.token, options); + return this.resolve(provider.token, options); }); } @@ -112,7 +111,9 @@ export class InjectorService extends Container { } /** - * Invoke the class and inject all services that required by the class constructor. + * Resolve the token depending on his provider configuration. + * + * If the token isn't cached, the injector will invoke the provider and cache the result. * * #### Example * @@ -131,17 +132,13 @@ export class InjectorService extends Container { * @param options {InvokeOptions} Optional options to invoke the class. * @returns {Type} The class constructed. */ - public invoke(token: TokenProvider, options: Partial = {}): Type { + public resolve(token: TokenProvider, options: Partial = {}): Type { let instance: any = options.locals ? options.locals.get(token) : undefined; if (instance !== undefined) { return instance; } - if (token === Configuration) { - return this.settings as unknown as Type; - } - if (token === DI_USE_PARAM_OPTIONS) { return options.useOpts as Type; } @@ -160,7 +157,7 @@ export class InjectorService extends Container { }; if (!provider || options.rebuild) { - instance = this.resolve(token, options); + instance = this.invokeToken(token, options); if (this.hasProvider(token)) { set(instance); @@ -169,12 +166,12 @@ export class InjectorService extends Container { return instance; } - instance = this.resolve(token, options); + instance = this.invokeToken(token, options); switch (provider.scope) { case ProviderScope.SINGLETON: if (!options.rebuild) { - registerHooks(provider, instance); + this.registerHooks(provider, options); } if (!provider.isAsync() || !isPromise(instance)) { @@ -194,9 +191,9 @@ export class InjectorService extends Container { case ProviderScope.REQUEST: if (options.locals) { - options.locals.set(token, instance); + options.locals.set(provider.token, instance); - options.locals?.hooks.on("$onDestroy", (...args: unknown[]) => provider.hooks?.$onDestroy(instance, ...args)); + this.registerHooks(provider, options); } return instance; @@ -205,13 +202,40 @@ export class InjectorService extends Container { return instance; } + /** + * Resolve the token depending on his provider configuration. + * + * If the token isn't cached, the injector will invoke the provider and cache the result. + * + * #### Example + * + * ```typescript + * import {InjectorService} from "@tsed/di"; + * import MyService from "./services.js"; + * + * class OtherService { + * constructor(injectorService: InjectorService) { + * const myService = injectorService.invoke(MyService); + * } + * } + * ``` + * + * @param token The injectable class to invoke. Class parameters are injected according constructor signature. + * @param options {InvokeOptions} Optional options to invoke the class. + * @returns {Type} The class constructed. + * @alias InjectorService.resolve + */ + public invoke(token: TokenProvider, options: Partial = {}): Type { + return this.resolve(token, options); + } + /** * Build only providers which are asynchronous. */ async loadAsync() { for (const [, provider] of this) { if (!this.has(provider.token) && provider.isAsync()) { - await this.invoke(provider.token); + await this.resolve(provider.token); } } } @@ -222,7 +246,7 @@ export class InjectorService extends Container { loadSync() { for (const [, provider] of this) { if (!this.has(provider.token) && provider.scope === ProviderScope.SINGLETON) { - this.invoke(provider.token); + this.resolve(provider.token); } } } @@ -317,7 +341,7 @@ export class InjectorService extends Container { } /** - * Boostrap injector from container and resolve configuration. + * Boostrap injector from container and invokeToken configuration. * * @param container */ @@ -365,7 +389,7 @@ export class InjectorService extends Container { * @param options * @private */ - protected resolve(target: TokenProvider, options: Partial = {}): T | Promise { + protected invokeToken(target: TokenProvider, options: Partial = {}): T | Promise { const resolvedOpts = this.mapInvokeOptions(target, options); if (!resolvedOpts) { @@ -374,9 +398,8 @@ export class InjectorService extends Container { const {token, deps, construct, imports, provider} = resolvedOpts; - if (provider) { - GlobalProviders.onInvoke(provider, resolvedOpts); - } + $emit("$beforeInvoke", token, [resolvedOpts]); + $emit(`$beforeInvoke:${String(provider.type)}`, [resolvedOpts]); let instance: any; let currentDependency: any = false; @@ -395,7 +418,7 @@ export class InjectorService extends Container { return isInheritedFrom(token, Provider, 1) ? provider - : this.invoke(token, { + : this.resolve(token, { parent, locals: options.locals, useOpts @@ -428,6 +451,8 @@ export class InjectorService extends Container { }); } + $emit("$afterInvoke", token, [instance, resolvedOpts]); + return instance; } @@ -527,4 +552,28 @@ export class InjectorService extends Container { locals }; } + + private registerHooks(provider: Provider, options: Partial) { + if (provider.hooks) { + if (provider.scope === ProviderScope.REQUEST) { + if (options.locals && provider.hooks?.$onDestroy) { + const {locals} = options; + + options.locals.hooks.on("$onDestroy", (...args: unknown[]) => { + return provider.hooks?.$onDestroy?.(locals.get(provider.token), ...args); + }); + } + + return; + } + + Object.entries(provider.hooks).forEach(([event, cb]) => { + const callback = (...args: any[]) => { + return cb(this.#cache.get(provider.token), ...args); + }; + + $on(event, provider.token, callback); + }); + } + } } diff --git a/packages/di/src/common/utils/registerProviderHooks.ts b/packages/di/src/common/utils/registerProviderHooks.ts deleted file mode 100644 index 1a901adee80..00000000000 --- a/packages/di/src/common/utils/registerProviderHooks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {$on} from "@tsed/hooks"; - -import {Provider} from "../domain/Provider.js"; - -export function registerHooks(provider: Provider, instance: any) { - if (provider.hooks) { - Object.entries(provider.hooks).forEach(([event, cb]) => { - const callback = (...args: any[]) => cb(instance, ...args); - - $on(event, provider.token, callback); - }); - } -} From 5eeabba79e18e2f1d810db511215b1876187cc36 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 11:57:59 +0100 Subject: [PATCH 11/32] feat(di): injector.get resolve token when token isn't already cached BREAKING CHANGE: Provider with Request scope can be invoked using injector.get(). Before injector.get() returns undefined --- packages/di/src/common/integration/di.spec.ts | 6 +- .../src/common/integration/singleton.spec.ts | 6 +- .../common/services/InjectorService.spec.ts | 8 +- .../di/src/common/services/InjectorService.ts | 80 +++++++++++-------- packages/di/src/node/services/DITest.spec.ts | 36 ++++----- packages/di/src/node/services/DITest.ts | 5 +- packages/di/vitest.config.mts | 8 +- 7 files changed, 78 insertions(+), 71 deletions(-) diff --git a/packages/di/src/common/integration/di.spec.ts b/packages/di/src/common/integration/di.spec.ts index c243cdadbdc..8bb1e31c200 100644 --- a/packages/di/src/common/integration/di.spec.ts +++ b/packages/di/src/common/integration/di.spec.ts @@ -58,8 +58,10 @@ describe("DI", () => { // THEN expect(injector().get(ServiceSingleton)).toEqual(inject(ServiceSingleton)); - expect(injector().get(ServiceRequest)).toBeUndefined(); - expect(injector().get(ServiceInstance)).toBeUndefined(); + expect(injector().get(ServiceRequest)).toBeInstanceOf(ServiceRequest); + expect(injector().has(ServiceRequest)).toBeFalsy(); + expect(injector().get(ServiceInstance)).toBeInstanceOf(ServiceInstance); + expect(injector().has(ServiceRequest)).toBeFalsy(); expect(injector().invoke(ServiceRequest) === injector().invoke(ServiceRequest)).toEqual(false); expect(inject(ServiceInstance) === inject(ServiceInstance)).toEqual(false); diff --git a/packages/di/src/common/integration/singleton.spec.ts b/packages/di/src/common/integration/singleton.spec.ts index ab7d86b734a..ec2fd132f56 100644 --- a/packages/di/src/common/integration/singleton.spec.ts +++ b/packages/di/src/common/integration/singleton.spec.ts @@ -91,7 +91,8 @@ describe("DI Singleton", () => { expect(serviceSingletonWithReqDep.serviceRequest).toEqual(serviceSingletonWithReqDep.serviceRequest2); // The service isn't registered in the injectorService - expect(serviceRequest).toBeUndefined(); + expect(serviceRequest).toBeDefined(); + expect(injector().has(ServiceRequest)).toEqual(false); }); }); describe("when it has a INSTANCE dependency", () => { @@ -117,7 +118,8 @@ describe("DI Singleton", () => { expect(serviceWithInstDep.serviceInstance === serviceWithInstDep.serviceInstance2).toEqual(false); // The service isn't registered in the injectorService - expect(serviceInstance).toBeUndefined(); + expect(serviceInstance).toBeInstanceOf(ServiceInstance); + expect(injector().has(ServiceInstance)).toEqual(false); }); }); }); diff --git a/packages/di/src/common/services/InjectorService.spec.ts b/packages/di/src/common/services/InjectorService.spec.ts index dbe39edcd25..f7264569144 100644 --- a/packages/di/src/common/services/InjectorService.spec.ts +++ b/packages/di/src/common/services/InjectorService.spec.ts @@ -53,8 +53,8 @@ describe("InjectorService", () => { expect(injector().get(InjectorService)).toBeInstanceOf(InjectorService); }); - it("should return undefined", () => { - expect(injector().get(Test)).toBeUndefined(); + it("should return Test", () => { + expect(injector().get(Test)).toBeInstanceOf(Test); }); }); describe("getMany()", () => { @@ -222,14 +222,12 @@ describe("InjectorService", () => { const locals = new LocalsContainer(); // WHEN - const result1: any = inject(token, {locals}); const result2: any = inject(token, {locals}); // THEN expect(result1).toEqual(result2); - - return expect((injector() as any).invokeToken).not.toHaveBeenCalled(); + expect((injector() as any).invokeToken).not.toHaveBeenCalled(); }); }); describe("when provider is a Value (useValue)", () => { diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 8f3355087e9..2f494d2defc 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -3,7 +3,6 @@ import {$alter, $asyncAlter, $asyncEmit, $emit, $off, $on} from "@tsed/hooks"; import {DI_INVOKE_OPTIONS, DI_USE_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 {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; @@ -41,11 +40,7 @@ const EXCLUDED_CONFIGURATION_KEYS = ["mount", "imports"]; * const myService1 = injector.get(MyService1); * ``` */ -@Injectable({ - scope: ProviderScope.SINGLETON -}) export class InjectorService extends Container { - public settings: DIConfiguration = new DIConfiguration(); public logger: DILogger = console; private resolvedConfiguration: boolean = false; #cache = new LocalsContainer(); @@ -53,7 +48,15 @@ export class InjectorService extends Container { constructor() { super(); this.#cache.set(InjectorService, this); - this.#cache.set(Configuration, this.settings); + this.#cache.set(Configuration, new DIConfiguration()); + } + + get settings(): DIConfiguration { + return this.#cache.get(Configuration); + } + + set settings(settings: DIConfiguration) { + this.#cache.set(Configuration, settings); } /** @@ -80,8 +83,13 @@ export class InjectorService extends Container { * ``` * * @param token The class or symbol registered in InjectorService. + * @param options */ - get(token: TokenProvider): T | undefined { + get(token: TokenProvider, options?: Partial): T { + if (!this.has(token)) { + return this.resolve(token, options); + } + return this.#cache.get(token); } @@ -151,16 +159,12 @@ export class InjectorService extends Container { const provider = this.ensureProvider(token); - const set = (instance: any) => { - this.#cache.set(token, instance); - provider?.alias && this.alias(token, provider.alias); - }; - + // maybe not necessary if (!provider || options.rebuild) { instance = this.invokeToken(token, options); - if (this.hasProvider(token)) { - set(instance); + if (provider) { + return this.setToCache(provider!, instance); } return instance; @@ -174,21 +178,7 @@ export class InjectorService extends Container { this.registerHooks(provider, options); } - if (!provider.isAsync() || !isPromise(instance)) { - set(instance); - return instance; - } - - // store promise to lock token in cache - set(instance); - - instance = instance.then((instance: any) => { - set(instance); - - return instance; - }); - return instance; - + return this.setToCache(provider, instance); case ProviderScope.REQUEST: if (options.locals) { options.locals.set(provider.token, instance); @@ -245,6 +235,7 @@ export class InjectorService extends Container { */ loadSync() { for (const [, provider] of this) { + // TODO try to lazy provider instead initiate all providers (&& provider.hasRegisteredHooks()) if (!this.has(provider.token) && provider.scope === ProviderScope.SINGLETON) { this.resolve(provider.token); } @@ -271,7 +262,8 @@ export class InjectorService extends Container { } /** - * Load all configurations registered on providers + * Load all configurations registered on providers via @Configuration decorator. + * It inspects for each provider some fields like imports, mount, etc. to resolve the configuration. */ resolveConfiguration() { if (this.resolvedConfiguration) { @@ -414,14 +406,12 @@ export class InjectorService extends Container { return this.getMany(token[0], options); } - const useOpts = provider?.getArgOpts(index) || options.useOpts; - return isInheritedFrom(token, Provider, 1) ? provider : this.resolve(token, { parent, locals: options.locals, - useOpts + useOpts: provider?.getArgOpts(index) || options.useOpts }); }; @@ -576,4 +566,28 @@ export class InjectorService extends Container { }); } } + + private setToCache(provider: Provider, instance: any) { + const set = (instance: any) => { + this.#cache.set(provider.token, instance); + provider?.alias && this.alias(provider.token, provider.alias); + }; + + if ("isAsync" in provider && !provider.isAsync() && !isPromise(instance)) { + set(instance); + + return instance; + } + + // store promise to lock token in cache + set(instance); + + instance = instance.then((instance: any) => { + set(instance); + + return instance; + }); + + return instance; + } } diff --git a/packages/di/src/node/services/DITest.spec.ts b/packages/di/src/node/services/DITest.spec.ts index b3d75ac9ea3..956c2009cf7 100644 --- a/packages/di/src/node/services/DITest.spec.ts +++ b/packages/di/src/node/services/DITest.spec.ts @@ -1,22 +1,16 @@ -import {Logger} from "@tsed/logger"; - -import {Inject, Injectable, InjectorService, registerProvider, Service} from "../../index.js"; +import {Inject, Injectable, injectable, InjectorService} from "../../index.js"; import {DITest} from "../services/DITest.js"; class Model {} -const SQLITE_DATA_SOURCE = Symbol.for("SQLITE_DATA_SOURCE"); - -registerProvider({ - provide: SQLITE_DATA_SOURCE, - type: "typeorm:datasource", - deps: [Logger], - useAsyncFactory(logger: Logger) { +const SQLITE_DATA_SOURCE = injectable(Symbol.for("SQLITE_DATA_SOURCE")) + .type("typeorm:datasource") + .asyncFactory(() => { return Promise.resolve({ id: "sqlite" }); - } -}); + }) + .token(); export abstract class AbstractDao { private readonly dao: any; @@ -91,24 +85,22 @@ describe("DITest", () => { }); it("should return a service with pre mocked dependencies (invoke + mock)", async () => { + const dao = { + initialize: vi.fn(), + getRepository: vi.fn().mockReturnValue({ + repository: false + }) + }; const service = await DITest.invoke(FileDao, [ { token: SQLITE_DATA_SOURCE, - use: { - initialize: vi.fn(), - getRepository: vi.fn().mockReturnValue({ - repository: false - }) - } + use: dao } ]); - const repository = DITest.get(SQLITE_DATA_SOURCE); - - expect(repository.getRepository).toHaveBeenCalledWith(Model); - const result = service.getRepository(); + expect(dao.getRepository).toHaveBeenCalledWith(Model); expect(result).toEqual({ repository: false }); diff --git a/packages/di/src/node/services/DITest.ts b/packages/di/src/node/services/DITest.ts index b6c2f1e3366..95a39c6d31f 100644 --- a/packages/di/src/node/services/DITest.ts +++ b/packages/di/src/node/services/DITest.ts @@ -98,10 +98,9 @@ export class DITest { /** * Return the instance from injector registry * @param target - * @param options */ - static get(target: TokenProvider, options: any = {}): T { - return injector().get(target, options)!; + static get(target: TokenProvider): T { + return injector().get(target)!; } static createDIContext() { diff --git a/packages/di/vitest.config.mts b/packages/di/vitest.config.mts index 819c81fbc31..e5350ff695c 100644 --- a/packages/di/vitest.config.mts +++ b/packages/di/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 98.91, - branches: 97.04, - functions: 97.15, - lines: 98.91 + statements: 98.69, + branches: 97.2, + functions: 97.02, + lines: 98.69 } } } From c13437aa782052dda0f81348f585b7c4f4c07a56 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 12:49:26 +0100 Subject: [PATCH 12/32] fix(di): add global flag to register correctly provider on GlobalRegistry vs injector.container --- .../di/src/common/decorators/inject.spec.ts | 34 +++----- packages/di/src/common/fn/injectable.ts | 73 +---------------- .../src/common/registries/GlobalProviders.ts | 20 +++-- .../registries/ProviderRegistry.spec.ts | 3 +- .../src/common/registries/ProviderRegistry.ts | 7 +- .../di/src/common/services/InjectorService.ts | 9 +++ .../di/src/common/utils/providerBuilder.ts | 81 +++++++++++++++++++ 7 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 packages/di/src/common/utils/providerBuilder.ts diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index a1c3f9f9c9d..bedec448513 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -34,13 +34,14 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); }); it("should inject service and async factory", async () => { + // const inj = injector({rebuild: true}); + // GIVEN class Test { constructor(public type: string) {} @@ -69,12 +70,10 @@ describe("@Inject()", () => { test: Test; } - const inj = injector({rebuild: true}); - - await inj.load(); + await injector().load(); - const parent1 = await inj.invoke(Parent1); - const parent2 = await inj.invoke(Parent2); + const parent1 = inject(Parent1); + const parent2 = inject(Parent2); expect(parent1.test).toBeInstanceOf(Test); expect(parent2.test).toBeInstanceOf(Test); @@ -87,8 +86,7 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -101,8 +99,7 @@ describe("@Inject()", () => { test: InjectorService; } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(Test); + const instance = inject(Test); expect(instance).toBeInstanceOf(Test); expect(instance.test).toBeInstanceOf(InjectorService); @@ -151,11 +148,9 @@ describe("@Inject()", () => { instances: InterfaceGroup[]; } - const inj = injector({rebuild: true}); + await injector().load(); - await inj.load(); - - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); @@ -212,8 +207,7 @@ describe("@Inject()", () => { constructor(@Inject(InjectorService) readonly injector: InjectorService) {} } - const inj = injector({rebuild: true}); - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.injector).toBeInstanceOf(InjectorService); }); @@ -263,11 +257,9 @@ describe("@Inject()", () => { constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {} } - const inj = injector({rebuild: true}); - - await inj.load(); + await injector().load(); - const instance = await inj.invoke(MyInjectable); + const instance = inject(MyInjectable); expect(instance.instances).toBeInstanceOf(Array); expect(instance.instances).toHaveLength(3); diff --git a/packages/di/src/common/fn/injectable.ts b/packages/di/src/common/fn/injectable.ts index d9b261a65b3..70226c77bbb 100644 --- a/packages/di/src/common/fn/injectable.ts +++ b/packages/di/src/common/fn/injectable.ts @@ -1,78 +1,7 @@ -import "../registries/ProviderRegistry.js"; - -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> - ); - }; -} +import {providerBuilder} from "../utils/providerBuilder.js"; type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration" | "priority"; diff --git a/packages/di/src/common/registries/GlobalProviders.ts b/packages/di/src/common/registries/GlobalProviders.ts index 7c142df537c..e0ba1e5c62c 100644 --- a/packages/di/src/common/registries/GlobalProviders.ts +++ b/packages/di/src/common/registries/GlobalProviders.ts @@ -1,13 +1,10 @@ import {getClassOrSymbol, Type} from "@tsed/core"; -import type {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; import {ProviderType} from "../domain/ProviderType.js"; import {ProviderOpts} from "../interfaces/ProviderOpts.js"; import {RegistrySettings} from "../interfaces/RegistrySettings.js"; -import {ResolvedInvokeOptions} from "../interfaces/ResolvedInvokeOptions.js"; import {TokenProvider} from "../interfaces/TokenProvider.js"; -import type {InjectorService} from "../services/InjectorService.js"; export class GlobalProviderRegistry extends Map { #settings: Map = new Map(); @@ -47,6 +44,10 @@ export class GlobalProviderRegistry extends Map { * @param options */ merge(target: TokenProvider, options: Partial) { + if (options.global === false) { + return GlobalProviders.createProvider(target, options); + } + const meta = this.createIfNotExists(target, options); Object.keys(options).forEach((key) => { @@ -99,18 +100,21 @@ export class GlobalProviderRegistry extends Map { ); } + protected createProvider(key: TokenProvider, options: Partial>) { + const type = options.type || ProviderType.PROVIDER; + const {model = Provider} = this.#settings.get(type) || {}; + + return new model(key, options); + } + /** * * @param key * @param options */ protected createIfNotExists(key: TokenProvider, options: Partial): Provider { - const type = options.type || ProviderType.PROVIDER; - if (!this.has(key)) { - const {model = Provider} = this.#settings.get(type) || {}; - - const item = new model(key, options); + const item = this.createProvider(key, options); this.set(key, item); } diff --git a/packages/di/src/common/registries/ProviderRegistry.spec.ts b/packages/di/src/common/registries/ProviderRegistry.spec.ts index 5b1b26c360e..1b465864235 100644 --- a/packages/di/src/common/registries/ProviderRegistry.spec.ts +++ b/packages/di/src/common/registries/ProviderRegistry.spec.ts @@ -17,7 +17,8 @@ describe("ProviderRegistry", () => { registerProvider({provide: Test}); expect(GlobalProviders.merge).toHaveBeenCalledWith(Test, { - provide: Test + provide: Test, + global: true }); }); }); diff --git a/packages/di/src/common/registries/ProviderRegistry.ts b/packages/di/src/common/registries/ProviderRegistry.ts index f99177ac320..f7d7b9ec67d 100644 --- a/packages/di/src/common/registries/ProviderRegistry.ts +++ b/packages/di/src/common/registries/ProviderRegistry.ts @@ -1,5 +1,6 @@ import {ControllerProvider} from "../domain/ControllerProvider.js"; import {ProviderType} from "../domain/ProviderType.js"; +import {injectable} from "../fn/injectable.js"; import type {ProviderOpts} from "../interfaces/ProviderOpts.js"; import {GlobalProviders} from "./GlobalProviders.js"; @@ -7,8 +8,8 @@ GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider); /** * Register a provider configuration. - * @param {ProviderOpts} provider + * @param {ProviderOpts} opts */ -export function registerProvider(provider: Partial> & Pick, "provide">) { - return GlobalProviders.merge(provider.provide, provider); +export function registerProvider(opts: Partial> & Pick, "provide">) { + return injectable(opts.provide, opts as unknown as Partial).inspect(); } diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 2f494d2defc..b1eb732abaa 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -44,6 +44,7 @@ export class InjectorService extends Container { public logger: DILogger = console; private resolvedConfiguration: boolean = false; #cache = new LocalsContainer(); + #loaded: boolean = false; constructor() { super(); @@ -59,6 +60,10 @@ export class InjectorService extends Container { this.#cache.set(Configuration, settings); } + isLoaded() { + return this.#loaded; + } + /** * Return a list of instance build by the injector. */ @@ -248,6 +253,10 @@ export class InjectorService extends Container { * @param container */ async load(container: Container = createContainer()) { + // avoid provider registration in the GlobalContainer during the loading phase + // using injectable() or providerBuilder() + this.#loaded = true; + await $asyncEmit("$beforeInit"); this.bootstrap(container); diff --git a/packages/di/src/common/utils/providerBuilder.ts b/packages/di/src/common/utils/providerBuilder.ts new file mode 100644 index 00000000000..d942c4a5239 --- /dev/null +++ b/packages/di/src/common/utils/providerBuilder.ts @@ -0,0 +1,81 @@ +import "../registries/ProviderRegistry.js"; + +import {Store, type Type} from "@tsed/core"; + +import {ProviderType} from "../domain/ProviderType.js"; +import {hasInjector, injector} from "../fn/injector.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 merged = { + global: !hasInjector() || injector().isLoaded(), + ...options, + ...baseOpts, + provide: token + }; + + const provider = GlobalProviders.merge(token, merged); + + if (!merged.global) { + injector().setProvider(token, provider); + } + + 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> + ); + }; +} From 3ae291974e2559ae0bbae4f37166b5122b60479f Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 18:59:08 +0100 Subject: [PATCH 13/32] fix(di): use DIConfiguration to cache configuration instance instead of Configuration --- packages/di/src/common/decorators/configuration.ts | 10 ++++++++-- packages/di/src/common/services/DIConfiguration.ts | 3 ++- packages/di/src/common/services/InjectorService.ts | 7 +++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/di/src/common/decorators/configuration.ts b/packages/di/src/common/decorators/configuration.ts index 0b58f2ce549..f079cb99d6a 100644 --- a/packages/di/src/common/decorators/configuration.ts +++ b/packages/di/src/common/decorators/configuration.ts @@ -1,7 +1,9 @@ import {DecoratorParameters, decoratorTypeOf, DecoratorTypes} from "@tsed/core"; import {configuration} from "../fn/configuration.js"; -import {DIConfiguration} from "../services/DIConfiguration.js"; +import {injectable} from "../fn/injectable.js"; +import {injector} from "../fn/injector.js"; +import {CONFIGURATION, DIConfiguration} from "../services/DIConfiguration.js"; import {Inject} from "./inject.js"; /** @@ -20,9 +22,13 @@ export function Configuration(settings: Partial = {}): Funct break; default: case DecoratorTypes.PARAM_CTOR: - return Inject(Configuration)(args[0], args[1], args[2] as number); + return Inject(DIConfiguration)(args[0], args[1], args[2] as number); } }; } export type Configuration = TsED.DIConfiguration & DIConfiguration; + +// To maintain compatibility with the previous implementation, we need to declare Configuration as +// injectable token. +injectable(Configuration).factory(() => injector().settings); diff --git a/packages/di/src/common/services/DIConfiguration.ts b/packages/di/src/common/services/DIConfiguration.ts index e6dc695ef70..9d9ab2c9935 100644 --- a/packages/di/src/common/services/DIConfiguration.ts +++ b/packages/di/src/common/services/DIConfiguration.ts @@ -1,11 +1,12 @@ import {Env, getValue, setValue} from "@tsed/core"; -import type {ProviderScope} from "../domain/ProviderScope.js"; import type {DILoggerOptions} from "../interfaces/DILoggerOptions.js"; import type {ImportTokenProviderOpts} from "../interfaces/ImportTokenProviderOpts.js"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; import type {TokenRoute} from "../interfaces/TokenRoute.js"; +export const CONFIGURATION = Symbol.for("CONFIGURATION"); + export class DIConfiguration { readonly default: Map = new Map(); protected map: Map = new Map(); diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index b1eb732abaa..39c9d24d25f 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -2,7 +2,6 @@ import {classOf, deepClone, deepMerge, isArray, isClass, isFunction, isInherited import {$alter, $asyncAlter, $asyncEmit, $emit, $off, $on} from "@tsed/hooks"; import {DI_INVOKE_OPTIONS, DI_USE_PARAM_OPTIONS} from "../constants/constants.js"; -import {Configuration} from "../decorators/configuration.js"; import {Container} from "../domain/Container.js"; import {LocalsContainer} from "../domain/LocalsContainer.js"; import {Provider} from "../domain/Provider.js"; @@ -49,15 +48,15 @@ export class InjectorService extends Container { constructor() { super(); this.#cache.set(InjectorService, this); - this.#cache.set(Configuration, new DIConfiguration()); + this.#cache.set(DIConfiguration, new DIConfiguration()); } get settings(): DIConfiguration { - return this.#cache.get(Configuration); + return this.#cache.get(DIConfiguration); } set settings(settings: DIConfiguration) { - this.#cache.set(Configuration, settings); + this.#cache.set(DIConfiguration, settings); } isLoaded() { From fb3288fdc4f83aec7b2e894d2c3fca3243189ae4 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 19:06:53 +0100 Subject: [PATCH 14/32] fix(di): make injector really a singleton --- .../di/src/common/decorators/inject.spec.ts | 2 -- .../src/common/decorators/lazyInject.spec.ts | 14 +++++----- packages/di/src/common/fn/injector.ts | 27 ++++--------------- .../di/src/common/utils/providerBuilder.ts | 4 +-- packages/di/src/node/services/DITest.ts | 15 ++++------- packages/di/vitest.config.mts | 4 +-- 6 files changed, 20 insertions(+), 46 deletions(-) diff --git a/packages/di/src/common/decorators/inject.spec.ts b/packages/di/src/common/decorators/inject.spec.ts index bedec448513..1e979969950 100644 --- a/packages/di/src/common/decorators/inject.spec.ts +++ b/packages/di/src/common/decorators/inject.spec.ts @@ -40,8 +40,6 @@ describe("@Inject()", () => { expect(instance.test).toBeInstanceOf(InjectorService); }); it("should inject service and async factory", async () => { - // const inj = injector({rebuild: true}); - // GIVEN class Test { constructor(public type: string) {} diff --git a/packages/di/src/common/decorators/lazyInject.spec.ts b/packages/di/src/common/decorators/lazyInject.spec.ts index 32a1968bf1e..74a17e1582f 100644 --- a/packages/di/src/common/decorators/lazyInject.spec.ts +++ b/packages/di/src/common/decorators/lazyInject.spec.ts @@ -1,5 +1,6 @@ import {catchAsyncError, classOf, nameOf} from "@tsed/core"; +import {inject} from "../fn/inject.js"; import {injector} from "../fn/injector.js"; import type {MyLazyModule} from "./__mock__/lazy.module.js"; import {Injectable} from "./injectable.js"; @@ -13,14 +14,13 @@ describe("LazyInject", () => { lazy: Promise; } - const inj = injector({rebuild: true}); - const service = await inj.invoke(MyInjectable); - const nbProviders = inj.getProviders().length; + const service = inject(MyInjectable); + const nbProviders = injector().getProviders().length; const lazyService = await service.lazy; expect(nameOf(classOf(lazyService))).toEqual("MyLazyModule"); - expect(nbProviders).not.toEqual(inj.getProviders().length); + expect(nbProviders).not.toEqual(injector().getProviders().length); }); it("should throw an error when the module doesn't exists", async () => { @@ -31,8 +31,7 @@ describe("LazyInject", () => { lazy?: Promise; } - const inj = injector({rebuild: true}); - const service = await inj.invoke(MyInjectable); + const service = inject(MyInjectable); const error = await catchAsyncError(() => service.lazy); expect(error?.message).toContain("Failed to load url lazy-module"); @@ -46,8 +45,7 @@ describe("LazyInject", () => { lazy?: Promise; } - const inj = injector({rebuild: true}); - const service = await inj.invoke(MyInjectable); + const service = inject(MyInjectable); const lazyService = await service.lazy; expect(lazyService).toEqual(undefined); diff --git a/packages/di/src/common/fn/injector.ts b/packages/di/src/common/fn/injector.ts index 7cb80e16de7..e1259e460ae 100644 --- a/packages/di/src/common/fn/injector.ts +++ b/packages/di/src/common/fn/injector.ts @@ -1,10 +1,9 @@ import {InjectorService} from "../services/InjectorService.js"; -let globalInjector: InjectorService | undefined; +let globalInjector: InjectorService = new InjectorService(); -type InjectorFnOpts = {rebuild?: boolean; logger?: any; settings?: Partial}; /** - * Create or return the existing injector service. + * Return the existing injector service. * * Example: * @@ -17,27 +16,11 @@ type InjectorFnOpts = {rebuild?: boolean; logger?: any; settings?: Partial(props: options: Partial> = {} ): ProviderBuilder> => { const merged = { - global: !hasInjector() || injector().isLoaded(), + global: !injector().isLoaded(), ...options, ...baseOpts, provide: token diff --git a/packages/di/src/node/services/DITest.ts b/packages/di/src/node/services/DITest.ts index 95a39c6d31f..9c8918ff485 100644 --- a/packages/di/src/node/services/DITest.ts +++ b/packages/di/src/node/services/DITest.ts @@ -6,7 +6,6 @@ import { createContainer, destroyInjector, DI_INJECTABLE_PROPS, - hasInjector, inject, injector, InjectorService, @@ -39,11 +38,9 @@ export class DITest { * Create a new injector with the right default services */ static createInjector(settings: any = {}): InjectorService { - const inj = injector({ - rebuild: true, - logger: $log, - settings: DITest.configure(settings) - }); + const inj = injector(); + injector().logger = $log; + inj.settings.set(DITest.configure(settings)); setLoggerConfiguration(); @@ -54,10 +51,8 @@ export class DITest { * Resets the test injector of the test context, so it won't pollute your next test. Call this in your `tearDown` logic. */ static async reset() { - if (hasInjector()) { - await destroyInjector(); - cleanAllLocalsContainer(); - } + await destroyInjector(); + cleanAllLocalsContainer(); } /** diff --git a/packages/di/vitest.config.mts b/packages/di/vitest.config.mts index e5350ff695c..f5235ca53ab 100644 --- a/packages/di/vitest.config.mts +++ b/packages/di/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 98.69, - branches: 97.2, + branches: 97.18, functions: 97.02, lines: 98.69 } } } } -); \ No newline at end of file +); From b9484d0aa3c401eae35d9cef83cb04bde4c6835a Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Thu, 21 Nov 2024 16:33:15 +0100 Subject: [PATCH 15/32] refactor(vike): improve dynamic typing import --- packages/di/src/common/fn/lazyInject.spec.ts | 1 + packages/di/src/common/fn/lazyInject.ts | 9 ++++----- packages/third-parties/vike/src/services/ViteService.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/di/src/common/fn/lazyInject.spec.ts b/packages/di/src/common/fn/lazyInject.spec.ts index 6fb88f5f111..90acb886545 100644 --- a/packages/di/src/common/fn/lazyInject.spec.ts +++ b/packages/di/src/common/fn/lazyInject.spec.ts @@ -11,6 +11,7 @@ describe("lazyInject", () => { const service = await lazyInject(() => import("./__mock__/lazy.import.module.js")); expect(service).toBeDefined(); + expect(service.called).toBeTruthy(); }); it("should optionally lazy load module", async () => { diff --git a/packages/di/src/common/fn/lazyInject.ts b/packages/di/src/common/fn/lazyInject.ts index 5ab6ca7aa96..9b0d4706453 100644 --- a/packages/di/src/common/fn/lazyInject.ts +++ b/packages/di/src/common/fn/lazyInject.ts @@ -1,6 +1,7 @@ import {isFunction} from "@tsed/core"; import type {TokenProvider} from "../interfaces/TokenProvider.js"; +import {inject} from "./inject.js"; import {injector} from "./injector.js"; /** @@ -9,10 +10,8 @@ import {injector} from "./injector.js"; export async function lazyInject(factory: () => Promise<{default: TokenProvider}>): Promise { const {default: token} = await factory(); - let instance = injector().get(token) as unknown; - - if (!instance) { - instance = await injector().invoke(token); + if (!injector().has(token)) { + const instance = await inject(token); const instanceWithHook = instance as unknown as {$onInit?: () => Promise}; @@ -21,7 +20,7 @@ export async function lazyInject(factory: () => Promise<{default: TokenPr } } - return instance as unknown as Promise; + return injector().get(token) as unknown as Promise; } export async function optionalLazyInject( diff --git a/packages/third-parties/vike/src/services/ViteService.ts b/packages/third-parties/vike/src/services/ViteService.ts index 2154537f12f..ccc37fa5799 100644 --- a/packages/third-parties/vike/src/services/ViteService.ts +++ b/packages/third-parties/vike/src/services/ViteService.ts @@ -42,7 +42,7 @@ export class ViteService { stateSnapshot: this.config.stateSnapshot && this.config.stateSnapshot() }; - const {renderPage} = await import(ViteService.moduleName); + const {renderPage} = await import("vike/server"); const pageContext = await renderPage({ view, From ab1e2395e51881a50bc0e5f59dbffce3c301bd40 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Thu, 21 Nov 2024 18:42:29 +0100 Subject: [PATCH 16/32] refactor(platform-router): use functional API to inject service in routers --- .../domain/PlatformHandlerMetadata.spec.ts | 15 +++--- .../src/domain/PlatformHandlerMetadata.ts | 8 +-- .../src/domain/PlatformRouter.ts | 12 ++--- .../src/domain/PlatformRouters.ts | 54 +++++++++++-------- ...lter-endpoint-handlers.integration.spec.ts | 15 +++--- .../routers-injection.integration.spec.ts | 16 +++--- .../routers-middlewares.integration.spec.ts | 17 +++--- .../test/routers-nested.integration.spec.ts | 25 ++++----- .../test/routers.integration.spec.ts | 29 +++++----- .../platform-router/vitest.config.mts | 4 +- 10 files changed, 105 insertions(+), 90 deletions(-) diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts index bdeba6a9c23..8af3f0c591b 100644 --- a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.spec.ts @@ -1,4 +1,4 @@ -import {Controller, InjectorService} from "@tsed/di"; +import {Controller, destroyInjector, injector} from "@tsed/di"; import {Err, Next, Req} from "@tsed/platform-http"; import {Middleware} from "@tsed/platform-middlewares"; import {Get, JsonMethodStore} from "@tsed/schema"; @@ -8,13 +8,14 @@ import {PlatformHandlerMetadata} from "./PlatformHandlerMetadata.js"; import {PlatformHandlerType} from "./PlatformHandlerType.js"; describe("PlatformHandlerMetadata", () => { + afterEach(() => destroyInjector()); describe("from()", () => { it("should return PlatformMetadata", () => { const meta = new PlatformHandlerMetadata({ type: PlatformHandlerType.CUSTOM, handler: () => {} }); - const result = PlatformHandlerMetadata.from({} as any, meta); + const result = PlatformHandlerMetadata.from(meta); expect(result).toEqual(meta); }); }); @@ -95,11 +96,10 @@ describe("PlatformHandlerMetadata", () => { test(@Req() req: Req, @Next() next: Next) {} } - const injector = new InjectorService(); - injector.addProvider(Test); + injector().addProvider(Test); const options = { - provider: injector.getProvider(Test), + provider: injector().getProvider(Test), propertyKey: "test", type: PlatformHandlerType.ENDPOINT }; @@ -128,11 +128,10 @@ describe("PlatformHandlerMetadata", () => { use(@Err() error: any, @Next() next: Next) {} } - const injector = new InjectorService(); - injector.addProvider(Test); + injector().addProvider(Test); const options = { - provider: injector.getProvider(Test), + provider: injector().getProvider(Test), propertyKey: "use", type: PlatformHandlerType.MIDDLEWARE }; diff --git a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts index c6803a167c3..7fb0ea50f19 100644 --- a/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts +++ b/packages/platform/platform-router/src/domain/PlatformHandlerMetadata.ts @@ -1,5 +1,5 @@ import {nameOf} from "@tsed/core"; -import {DIContext, InjectorService, Provider, ProviderScope, TokenProvider} from "@tsed/di"; +import {DIContext, injector, Provider, ProviderScope, TokenProvider} from "@tsed/di"; import {ParamTypes} from "@tsed/platform-params"; import {EndpointMetadata, JsonEntityStore, JsonParameterStore} from "@tsed/schema"; @@ -77,13 +77,13 @@ export class PlatformHandlerMetadata { return JsonEntityStore.fromMethod(this.provider!.useClass, this.propertyKey!); } - static from(injector: InjectorService, input: any, opts: PlatformHandlerMetadataOpts = {}): PlatformHandlerMetadata { + static from(input: any, opts: PlatformHandlerMetadataOpts = {}): PlatformHandlerMetadata { if (input instanceof PlatformHandlerMetadata) { return input; } if (input instanceof EndpointMetadata) { - const provider = injector.getProvider(opts.token)!; + const provider = injector().getProvider(opts.token)!; return new PlatformHandlerMetadata({ provider, @@ -93,7 +93,7 @@ export class PlatformHandlerMetadata { }); } - const provider = injector.getProvider(input); + const provider = injector().getProvider(input); if (provider) { return new PlatformHandlerMetadata({ diff --git a/packages/platform/platform-router/src/domain/PlatformRouter.ts b/packages/platform/platform-router/src/domain/PlatformRouter.ts index c8011fc0247..83645151daa 100644 --- a/packages/platform/platform-router/src/domain/PlatformRouter.ts +++ b/packages/platform/platform-router/src/domain/PlatformRouter.ts @@ -1,5 +1,5 @@ import {isString} from "@tsed/core"; -import {Injectable, InjectorService, Provider, ProviderScope, Scope} from "@tsed/di"; +import {injectable, Provider, ProviderScope} from "@tsed/di"; import {concatPath} from "@tsed/schema"; import {formatMethod} from "../utils/formatMethod.js"; @@ -11,15 +11,11 @@ function printHandler(handler: any) { return handler.toString().split("{")[0].trim(); } -@Injectable() -@Scope(ProviderScope.INSTANCE) export class PlatformRouter { readonly layers: PlatformLayer[] = []; provider: Provider; #isBuilt = false; - constructor(protected readonly injector: InjectorService) {} - use(...handlers: any[]) { const layer = handlers.reduce( (layer: PlatformLayer, item) => { @@ -37,7 +33,7 @@ export class PlatformRouter { layer.path = layer.path || item.provider.path; } } else { - item = PlatformHandlerMetadata.from(this.injector, item); + item = PlatformHandlerMetadata.from(item); } layer.handlers.push(item); @@ -63,7 +59,7 @@ export class PlatformRouter { method: formatMethod(method), path, handlers: handlers.map((input) => { - return PlatformHandlerMetadata.from(this.injector, input, opts); + return PlatformHandlerMetadata.from(input, opts); }), opts }); @@ -131,3 +127,5 @@ export class PlatformRouter { return false; } } + +injectable(PlatformRouter).scope(ProviderScope.INSTANCE); diff --git a/packages/platform/platform-router/src/domain/PlatformRouters.ts b/packages/platform/platform-router/src/domain/PlatformRouters.ts index 4bff0c58807..7d3310de709 100644 --- a/packages/platform/platform-router/src/domain/PlatformRouters.ts +++ b/packages/platform/platform-router/src/domain/PlatformRouters.ts @@ -1,5 +1,17 @@ -import {getValue, Hooks, Type} from "@tsed/core"; -import {ControllerProvider, GlobalProviders, Injectable, injector, InjectorService, Provider, ProviderType, TokenProvider} from "@tsed/di"; +import {getValue, Type} from "@tsed/core"; +import { + constant, + ControllerProvider, + GlobalProviders, + inject, + Injectable, + injectable, + injector, + Provider, + ProviderType, + TokenProvider +} from "@tsed/di"; +import {Hooks} from "@tsed/hooks"; import {PlatformParamsCallback} from "@tsed/platform-params"; import {concatPath, getOperationsRoutes, JsonMethodStore, OPERATION_HTTP_VERBS} from "@tsed/schema"; @@ -10,25 +22,25 @@ import {PlatformRouter} from "./PlatformRouter.js"; let AUTO_INC = 0; -function getInjectableRouter(injector: InjectorService, provider: Provider): PlatformRouter { - return injector.get(provider.tokenRouter)!; +function getInjectableRouter(provider: Provider): PlatformRouter { + return injector().get(provider.tokenRouter)!; } function createTokenRouter(provider: ControllerProvider) { return (provider.tokenRouter = provider.tokenRouter || `${provider.name}_ROUTER_${AUTO_INC++}`); } -function createInjectableRouter(injector: InjectorService, provider: ControllerProvider): PlatformRouter { +function createInjectableRouter(provider: ControllerProvider): PlatformRouter { const tokenRouter = createTokenRouter(provider); - if (injector.has(tokenRouter)) { - return getInjectableRouter(injector, provider); + if (injector().has(tokenRouter)) { + return getInjectableRouter(provider); } - const router = injector.invoke(PlatformRouter); + const router = inject(PlatformRouter); router.provider = provider; - return injector + return injector() .add(tokenRouter, { useValue: router }) @@ -37,7 +49,7 @@ function createInjectableRouter(injector: InjectorService, provider: ControllerP GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider, { onInvoke(provider: ControllerProvider, {locals}) { - const router = createInjectableRouter(injector(), provider); + const router = createInjectableRouter(provider); locals.set(PlatformRouter, router); } }); @@ -48,28 +60,26 @@ export interface AlterEndpointHandlersArg { after: (Type | Function)[]; } -@Injectable() export class PlatformRouters { readonly hooks = new Hooks(); readonly allowedVerbs = OPERATION_HTTP_VERBS; - constructor(protected readonly injector: InjectorService) {} - prebuild() { - this.injector.getProviders(ProviderType.CONTROLLER).forEach((provider: ControllerProvider) => { - createInjectableRouter(this.injector, provider); - }); + injector() + .getProviders(ProviderType.CONTROLLER) + .forEach((provider: ControllerProvider) => { + createInjectableRouter(provider); + }); } from(token: TokenProvider, parentMiddlewares: any[] = []) { - const {injector} = this; - const provider = injector.getProvider(token)!; + const provider = injector().getProvider(token)!; if (!provider) { throw new Error("Token not found in the provider registry"); } - const router = createInjectableRouter(injector, provider); + const router = createInjectableRouter(provider); if (router.isBuilt()) { return router; @@ -80,7 +90,7 @@ export class PlatformRouters { const {children} = provider; // Set default to true in next major version - const appendChildrenRoutesFirst = this.injector.settings.get("router.appendChildrenRoutesFirst", false); + const appendChildrenRoutesFirst = constant("router.appendChildrenRoutesFirst", false); if (appendChildrenRoutesFirst) { children.forEach((token: Type) => { @@ -139,7 +149,7 @@ export class PlatformRouters { private sortHandlers(handlers: AlterEndpointHandlersArg) { const get = (token: TokenProvider) => { - return this.injector.getProvider(token)?.priority || 0; + return injector().getProvider(token)?.priority || 0; }; const sort = (p1: TokenProvider, p2: TokenProvider) => (get(p1) < get(p2) ? -1 : get(p1) > get(p2) ? 1 : 0); @@ -182,3 +192,5 @@ export class PlatformRouters { }); } } + +injectable(PlatformRouters); diff --git a/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts b/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts index d1bf300f199..6745d09095b 100644 --- a/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts +++ b/packages/platform/platform-router/test/routers-alter-endpoint-handlers.integration.spec.ts @@ -1,4 +1,4 @@ -import {Controller, DIContext, InjectorService} from "@tsed/di"; +import {Controller, DIContext, inject, injector} from "@tsed/di"; import {PlatformTest} from "@tsed/platform-http/testing"; import {UseBefore} from "@tsed/platform-middlewares"; import {Context, PlatformParams, PlatformParamsScope} from "@tsed/platform-params"; @@ -18,12 +18,13 @@ class MyController { } function createAppRouterFixture() { - const injector = new InjectorService(); - const platformRouters = injector.invoke(PlatformRouters); - const platformParams = injector.invoke(PlatformParams); - const appRouter = injector.invoke(PlatformRouter); + const platformRouters = inject(PlatformRouters); + const platformParams = inject(PlatformParams); + const appRouter = inject(PlatformRouter); - injector.addProvider(MyController, {}); + platformRouters.hooks.destroy(); + + injector().addProvider(MyController, {}); platformRouters.hooks.on("alterHandler", (handlerMetadata: PlatformHandlerMetadata) => { if (handlerMetadata.isRawFn() || handlerMetadata.isResponseFn()) { @@ -35,7 +36,7 @@ function createAppRouterFixture() { : platformParams.compileHandler(handlerMetadata); }); - return {injector, appRouter, platformRouters, platformParams}; + return {appRouter, platformRouters, platformParams}; } describe("routers with alter handlers", () => { diff --git a/packages/platform/platform-router/test/routers-injection.integration.spec.ts b/packages/platform/platform-router/test/routers-injection.integration.spec.ts index 758db130050..6e56785f73d 100644 --- a/packages/platform/platform-router/test/routers-injection.integration.spec.ts +++ b/packages/platform/platform-router/test/routers-injection.integration.spec.ts @@ -1,4 +1,4 @@ -import {Controller, ControllerProvider, injector} from "@tsed/di"; +import {Controller, ControllerProvider, inject, injector} from "@tsed/di"; import {PlatformParams} from "@tsed/platform-params"; import {PlatformRouter} from "../src/domain/PlatformRouter.js"; @@ -14,18 +14,20 @@ class CustomStaticsCtrl { } function createAppRouterFixture() { - const platformRouters = injector({rebuild: true}).invoke(PlatformRouters); - const platformParams = injector().invoke(PlatformParams); - const appRouter = injector().invoke(PlatformRouter); + const platformRouters = inject(PlatformRouters); + const platformParams = inject(PlatformParams); + const appRouter = inject(PlatformRouter); + + platformRouters.hooks.destroy(); injector().addProvider(CustomStaticsCtrl, {}); - return {injector, appRouter, platformRouters, platformParams}; + return {appRouter, platformRouters, platformParams}; } describe("Routers injection", () => { it("should load router and inject router to the given controller", () => { - const {injector, platformRouters} = createAppRouterFixture(); + const {platformRouters} = createAppRouterFixture(); // prebuild controllers to inject router in controller platformRouters.prebuild(); @@ -35,7 +37,7 @@ describe("Routers injection", () => { const provider = injector().getProvider(CustomStaticsCtrl)!; const router2 = injector().get(provider.tokenRouter); - const controller = injector().invoke(CustomStaticsCtrl)!; + const controller = inject(CustomStaticsCtrl)!; expect(router).toEqual(router1); expect(router).toEqual(router2); diff --git a/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts b/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts index 0dce08f3f18..76d05985f68 100644 --- a/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts +++ b/packages/platform/platform-router/test/routers-middlewares.integration.spec.ts @@ -1,4 +1,4 @@ -import {Controller, InjectorService} from "@tsed/di"; +import {Controller, inject, injector} from "@tsed/di"; import {PlatformTest} from "@tsed/platform-http/testing"; import {Middleware, UseBeforeEach} from "@tsed/platform-middlewares"; import {Context, PlatformParams} from "@tsed/platform-params"; @@ -22,15 +22,16 @@ class MyController { } function createAppRouterFixture() { - const injector = new InjectorService(); - const platformRouters = injector.invoke(PlatformRouters); - const platformParams = injector.invoke(PlatformParams); - const appRouter = injector.invoke(PlatformRouter); + const platformRouters = inject(PlatformRouters); + const platformParams = inject(PlatformParams); + const appRouter = inject(PlatformRouter); - injector.addProvider(MyMiddleware); - injector.addProvider(MyController, {}); + platformRouters.hooks.destroy(); - return {injector, appRouter, platformRouters, platformParams}; + injector().addProvider(MyMiddleware); + injector().addProvider(MyController, {}); + + return {appRouter, platformRouters, platformParams}; } describe("routers with middlewares", () => { diff --git a/packages/platform/platform-router/test/routers-nested.integration.spec.ts b/packages/platform/platform-router/test/routers-nested.integration.spec.ts index c125b4f8a77..94d3bc82f06 100644 --- a/packages/platform/platform-router/test/routers-nested.integration.spec.ts +++ b/packages/platform/platform-router/test/routers-nested.integration.spec.ts @@ -1,4 +1,4 @@ -import {Controller, InjectorService} from "@tsed/di"; +import {configuration, Controller, inject, injector, InjectorService} from "@tsed/di"; import {PlatformTest} from "@tsed/platform-http/testing"; import {PlatformParams} from "@tsed/platform-params"; import {Get, Post} from "@tsed/schema"; @@ -43,17 +43,18 @@ export class PlatformController { } function createAppRouterFixture() { - const injector = new InjectorService(); - const platformRouters = injector.invoke(PlatformRouters); - const platformParams = injector.invoke(PlatformParams); - const appRouter = injector.invoke(PlatformRouter); + const platformRouters = inject(PlatformRouters); + const platformParams = inject(PlatformParams); + const appRouter = inject(PlatformRouter); - injector.addProvider(FlaggedCommentController, {}); - injector.addProvider(CommentController, {}); - injector.addProvider(DomainController, {}); - injector.addProvider(PlatformController, {}); + platformRouters.hooks.destroy(); - return {injector, appRouter, platformRouters, platformParams}; + injector().addProvider(FlaggedCommentController, {}); + injector().addProvider(CommentController, {}); + injector().addProvider(DomainController, {}); + injector().addProvider(PlatformController, {}); + + return {appRouter, platformRouters, platformParams}; } describe("routers integration", () => { @@ -103,8 +104,8 @@ describe("routers integration", () => { }); it("should declare correctly with appendChildrenRoutesFirst", () => { - const {injector, platformRouters, appRouter} = createAppRouterFixture(); - injector.settings.set("router.appendChildrenRoutesFirst", true); + const {platformRouters, appRouter} = createAppRouterFixture(); + configuration().set("router.appendChildrenRoutesFirst", true); platformRouters.prebuild(); diff --git a/packages/platform/platform-router/test/routers.integration.spec.ts b/packages/platform/platform-router/test/routers.integration.spec.ts index b3820f34111..bdabd58f1f3 100644 --- a/packages/platform/platform-router/test/routers.integration.spec.ts +++ b/packages/platform/platform-router/test/routers.integration.spec.ts @@ -1,5 +1,5 @@ import {catchError} from "@tsed/core"; -import {Controller, InjectorService} from "@tsed/di"; +import {Controller, inject, injector} from "@tsed/di"; import {PlatformContext} from "@tsed/platform-http"; import {PlatformTest} from "@tsed/platform-http/testing"; import {UseBefore} from "@tsed/platform-middlewares"; @@ -64,17 +64,19 @@ class MyController { } function createAppRouterFixture() { - const injector = new InjectorService(); - const platformRouters = injector.invoke(PlatformRouters); - const platformParams = injector.invoke(PlatformParams); - const appRouter = injector.invoke(PlatformRouter); + const platformRouters = inject(PlatformRouters); + const platformParams = inject(PlatformParams); + const appRouter = inject(PlatformRouter); - injector.addProvider(NestedController, {}); + platformRouters.hooks.destroy(); + + injector().addProvider(NestedController, {}); platformRouters.hooks.on("alterEndpointHandlers", (handlers: AlterEndpointHandlersArg) => { handlers.after.push(useResponseHandler(() => "hello")); return handlers; }); + platformRouters.hooks.on("alterHandler", (handlerMetadata: PlatformHandlerMetadata) => { if (handlerMetadata.isInjectable()) { return platformParams.compileHandler(handlerMetadata); @@ -83,7 +85,7 @@ function createAppRouterFixture() { return handlerMetadata.handler; }); - return {injector, appRouter, platformRouters, platformParams}; + return {appRouter, platformRouters, platformParams}; } describe("routers integration", () => { @@ -91,8 +93,8 @@ describe("routers integration", () => { afterEach(() => PlatformTest.reset()); describe("getLayers()", () => { it("should declare router", () => { - const {injector, platformRouters} = createAppRouterFixture(); - injector.addProvider(MyController, {}); + const {platformRouters} = createAppRouterFixture(); + injector().addProvider(MyController, {}); const hookStub = vi.fn().mockImplementation((o) => o); @@ -104,8 +106,8 @@ describe("routers integration", () => { expect(router.inspect()).toMatchSnapshot(); }); it("should declare router - appRouter", async () => { - const {injector, appRouter, platformRouters} = createAppRouterFixture(); - injector.addProvider(MyController, {}); + const {appRouter, platformRouters} = createAppRouterFixture(); + injector().addProvider(MyController, {}); const router = platformRouters.from(MyController); @@ -142,10 +144,9 @@ describe("routers integration", () => { describe("use()", () => { it("should call method", () => { - const injector = new InjectorService(); - injector.addProvider(NestedController, {}); + injector().addProvider(NestedController, {}); - const router = new PlatformRouter(injector); + const router = new PlatformRouter(); router.use("/hello", function h() {}); diff --git a/packages/platform/platform-router/vitest.config.mts b/packages/platform/platform-router/vitest.config.mts index 857adbd63b3..3205093cb01 100644 --- a/packages/platform/platform-router/vitest.config.mts +++ b/packages/platform/platform-router/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 94.92, + branches: 94.24, functions: 100, lines: 100 } } } } -); +); \ No newline at end of file From aa6fd67ce1002864f9480ba50c5aa2077fda118c Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 09:38:41 +0100 Subject: [PATCH 17/32] refactor(platform-params): use functional API to inject/declare services --- .../src/builder/PlatformParams.ts | 22 ++++--- .../src/pipes/DeserializerPipe.ts | 5 +- .../src/pipes/ParseExpressionPipe.ts | 7 +-- .../src/pipes/ValidationPipe.spec.ts | 57 ++++++++++--------- .../src/pipes/ValidationPipe.ts | 49 +++++++++++----- .../platform-params/vitest.config.mts | 6 +- packages/specs/ajv/src/services/AjvService.ts | 2 + 7 files changed, 87 insertions(+), 61 deletions(-) diff --git a/packages/platform/platform-params/src/builder/PlatformParams.ts b/packages/platform/platform-params/src/builder/PlatformParams.ts index 09a7b713ff1..61b83449497 100644 --- a/packages/platform/platform-params/src/builder/PlatformParams.ts +++ b/packages/platform/platform-params/src/builder/PlatformParams.ts @@ -1,4 +1,4 @@ -import {DIContext, Inject, Injectable, InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; +import {DIContext, Inject, Injectable, injectable, injector, InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; import {JsonMethodStore, JsonParameterStore, PipeMethods} from "@tsed/schema"; import {ParamValidationError} from "../errors/ParamValidationError.js"; @@ -11,21 +11,15 @@ export type PlatformParamsCallback = (sco * Platform Params abstraction layer. * @platform */ -@Injectable({ - scope: ProviderScope.SINGLETON, - imports: [ParseExpressionPipe] -}) -export class PlatformParams { - @Inject() - protected injector: InjectorService; +export class PlatformParams { getPipes(param: JsonParameterStore) { const get = (pipe: TokenProvider) => { - return this.injector.getProvider(pipe)!.priority || 0; + return injector().getProvider(pipe)!.priority || 0; }; const sort = (p1: TokenProvider, p2: TokenProvider) => (get(p1) < get(p2) ? -1 : get(p1) > get(p2) ? 1 : 0); - const map = (token: TokenProvider) => this.injector.get(token)!; + const map = (token: TokenProvider) => injector().get(token)!; return [ParseExpressionPipe, ...param.pipes.sort(sort)].map(map).filter(Boolean); } @@ -47,13 +41,15 @@ export class PlatformParams { return (scope: PlatformParamsScope) => handler(scope.$ctx); } + const inj = injector(); const store = JsonMethodStore.fromMethod(token, propertyKey); const getArguments = this.compile(store); - const provider = this.injector.getProvider(token)!; + const provider = inj.getProvider(token)!; return async (scope: PlatformParamsScope) => { const container = provider.scope === ProviderScope.REQUEST ? scope.$ctx.container : undefined; - const [instance, args] = await Promise.all([this.injector.invoke(token, {locals: container}), getArguments(scope)]); + + const [instance, args] = await Promise.all([inj.invoke(token, {locals: container}), getArguments(scope)]); return instance[propertyKey].call(instance, ...args, scope.$ctx); }; @@ -86,3 +82,5 @@ export class PlatformParams { }, scope); } } + +injectable(PlatformParams).imports([ParseExpressionPipe]); diff --git a/packages/platform/platform-params/src/pipes/DeserializerPipe.ts b/packages/platform/platform-params/src/pipes/DeserializerPipe.ts index b4676259ebb..a8d8a5b13f3 100644 --- a/packages/platform/platform-params/src/pipes/DeserializerPipe.ts +++ b/packages/platform/platform-params/src/pipes/DeserializerPipe.ts @@ -1,8 +1,7 @@ -import {Injectable} from "@tsed/di"; +import {Injectable, injectable} from "@tsed/di"; import {deserialize} from "@tsed/json-mapper"; import {JsonParameterStore, PipeMethods} from "@tsed/schema"; -@Injectable() export class DeserializerPipe implements PipeMethods { transform(value: any, param: JsonParameterStore) { return deserialize(value, { @@ -12,3 +11,5 @@ export class DeserializerPipe implements PipeMethods { }); } } + +injectable(DeserializerPipe); diff --git a/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts b/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts index 2a780993138..3c5a05c589a 100644 --- a/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts +++ b/packages/platform/platform-params/src/pipes/ParseExpressionPipe.ts @@ -1,13 +1,10 @@ import {getValue} from "@tsed/core"; -import {Injectable} from "@tsed/di"; +import {Injectable, injectable} from "@tsed/di"; import {JsonParameterStore, PipeMethods} from "@tsed/schema"; import {PlatformParamsScope} from "../builder/PlatformParams.js"; import {ParamTypes} from "../domain/ParamTypes.js"; -@Injectable({ - priority: -1000 -}) export class ParseExpressionPipe implements PipeMethods { transform(scope: PlatformParamsScope, param: JsonParameterStore) { const {paramType, type} = param; @@ -32,3 +29,5 @@ export class ParseExpressionPipe implements PipeMethods { return [dataPath, expression].filter(Boolean).join("."); } } + +injectable(ParseExpressionPipe).priority(-1000); diff --git a/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts b/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts index f9cb5f5926d..e32b7099875 100644 --- a/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts +++ b/packages/platform/platform-params/src/pipes/ValidationPipe.spec.ts @@ -524,12 +524,14 @@ describe("ValidationPipe", () => { expect(result.message).toEqual("It should have required parameter 'test'"); }); it("should cast data if it's possible", async () => { - const validator = await PlatformTest.invoke(ValidationPipe); - // @ts-ignore - validator.validator = { + const defaultValidator = { validate: vi.fn().mockResolvedValue("1") }; + const validator = await PlatformTest.invoke(ValidationPipe); + + (validator as any).validators.set("default", defaultValidator); + class Test { @Post("/") test(@QueryParams("test") @Required() type: string) {} @@ -542,20 +544,21 @@ describe("ValidationPipe", () => { // THEN expect(result).toEqual("1"); - // @ts-ignore - expect(validator.validator.validate).toHaveBeenCalledWith("1", { + expect(defaultValidator.validate).toHaveBeenCalledWith("1", { collectionType: undefined, schema: {type: "string", minLength: 1}, type: undefined }); }); it("should cast string to array", async () => { - const validator = await PlatformTest.invoke(ValidationPipe); - // @ts-ignore - validator.validator = { + const defaultValidator = { validate: vi.fn().mockImplementation((o) => o) }; + const validator = await PlatformTest.invoke(ValidationPipe); + + (validator as any).validators.set("default", defaultValidator); + class Test { @Post("/") test(@QueryParams("test") @Required() type: string[]) {} @@ -568,16 +571,17 @@ describe("ValidationPipe", () => { // THEN expect(result).toEqual([1]); - // @ts-ignore - expect(validator.validator.validate).toHaveBeenCalledWith([1], expect.any(Object)); + expect(defaultValidator.validate).toHaveBeenCalledWith([1], expect.any(Object)); }); it("shouldn't cast object", async () => { - const validator = await PlatformTest.invoke(ValidationPipe); - // @ts-ignore - validator.validator = { + const defaultValidator = { validate: vi.fn().mockImplementation((o) => o) }; + const validator = await PlatformTest.invoke(ValidationPipe); + + (validator as any).validators.set("default", defaultValidator); + class Test { @Post("/") test(@QueryParams("test") @Required() type: any) {} @@ -590,16 +594,17 @@ describe("ValidationPipe", () => { // THEN expect(result).toEqual({}); - // @ts-ignore - expect(validator.validator.validate).toHaveBeenCalledWith({}, expect.any(Object)); + expect(defaultValidator.validate).toHaveBeenCalledWith({}, expect.any(Object)); }); it("should cast null string to null", async () => { - const validator = await PlatformTest.invoke(ValidationPipe); - // @ts-ignore - validator.validator = { + const defaultValidator = { validate: vi.fn().mockImplementation((o) => o) }; + const validator = await PlatformTest.invoke(ValidationPipe); + + (validator as any).validators.set("default", defaultValidator); + class Test { @Post("/") test(@QueryParams("test") type: string[]) {} @@ -612,16 +617,17 @@ describe("ValidationPipe", () => { // THEN expect(result).toEqual(null); - // @ts-ignore - expect(validator.validator.validate).toHaveBeenCalledWith(null, expect.any(Object)); + expect(defaultValidator.validate).toHaveBeenCalledWith(null, expect.any(Object)); }); it("should not process undefined value", async () => { - const validator = await PlatformTest.invoke(ValidationPipe); - // @ts-ignore - validator.validator = { - validate: vi.fn().mockResolvedValue("1") + const defaultValidator = { + validate: vi.fn().mockImplementation((o) => o) }; + const validator = await PlatformTest.invoke(ValidationPipe); + + (validator as any).validators.set("default", defaultValidator); + class Test { @Post("/") test(@QueryParams("test") type: string) {} @@ -634,7 +640,6 @@ describe("ValidationPipe", () => { // THEN expect(result).toEqual(undefined); - // @ts-ignore - expect(validator.validator.validate).not.toHaveBeenCalled(); + expect(defaultValidator.validate).not.toHaveBeenCalled(); }); }); diff --git a/packages/platform/platform-params/src/pipes/ValidationPipe.ts b/packages/platform/platform-params/src/pipes/ValidationPipe.ts index 5d6da2aa777..a08a0a3eb97 100644 --- a/packages/platform/platform-params/src/pipes/ValidationPipe.ts +++ b/packages/platform/platform-params/src/pipes/ValidationPipe.ts @@ -1,4 +1,4 @@ -import {Inject, Injectable} from "@tsed/di"; +import {constant, injectable, injectMany} from "@tsed/di"; import {deserialize} from "@tsed/json-mapper"; import {getJsonSchema, JsonParameterStore, PipeMethods} from "@tsed/schema"; @@ -15,16 +15,28 @@ function cast(value: any, metadata: JsonParameterStore) { } } -export type ValidatorServiceMethods = {validate(value: any, options: any): Promise}; +export interface ValidatorServiceMethods { + readonly name: string; + + validate(value: any, options: any): Promise; +} -@Injectable({ - type: "validator" -}) export class ValidationPipe implements PipeMethods { - private validator: ValidatorServiceMethods; + private validators: Map = new Map(); + + constructor() { + const validators = injectMany("validator:service"); + const defaultValidator = constant("validators.default"); - constructor(@Inject("validator:service") validators: ValidatorServiceMethods[]) { - this.validator = validators[0]; + validators.length && this.validators.set("default", validators[0]); + + validators.map((service) => { + this.validators.set(service.name, service); + + if (service.name === defaultValidator) { + this.validators.set("default", service); + } + }); } coerceTypes(value: any, metadata: JsonParameterStore) { @@ -52,7 +64,7 @@ export class ValidationPipe implements PipeMethods { } transform(value: any, metadata: JsonParameterStore): Promise { - if (!this.validator) { + if (!this.validators.size) { this.checkIsRequired(value, metadata); return value; } @@ -74,11 +86,18 @@ export class ValidationPipe implements PipeMethods { customKeys: true }); - return this.validator.validate(value, { - schema, - type: metadata.isClass ? metadata.type : undefined, - collectionType: metadata.collectionType - }); + // TODO retrieve the right validator from metadata + const validator = this.validators.get("default"); + + if (validator) { + return validator.validate(value, { + schema, + type: metadata.isClass ? metadata.type : undefined, + collectionType: metadata.collectionType + }); + } + + return value; } protected checkIsRequired(value: any, metadata: JsonParameterStore) { @@ -89,3 +108,5 @@ export class ValidationPipe implements PipeMethods { return true; } } + +injectable(ValidationPipe).type("validator"); diff --git a/packages/platform/platform-params/vitest.config.mts b/packages/platform/platform-params/vitest.config.mts index 86d5bcbfdb1..5c9969fa5a4 100644 --- a/packages/platform/platform-params/vitest.config.mts +++ b/packages/platform/platform-params/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.2, - branches: 90.55, + statements: 97.5, + branches: 87.5, functions: 100, - lines: 99.2 + lines: 97.5 } } } diff --git a/packages/specs/ajv/src/services/AjvService.ts b/packages/specs/ajv/src/services/AjvService.ts index 55da096e0f6..22126275de1 100644 --- a/packages/specs/ajv/src/services/AjvService.ts +++ b/packages/specs/ajv/src/services/AjvService.ts @@ -20,6 +20,8 @@ export interface AjvValidateOptions extends Record { type: "validator:service" }) export class AjvService { + readonly name = "ajv"; + @Constant("ajv.errorFormatter", defaultErrorFormatter) protected errorFormatter: ErrorFormatter; From e25e54fc9f9642a1eb3c0633778c6405be03a597 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 09:48:25 +0100 Subject: [PATCH 18/32] refactor(platform-log-request): use functional API to inject/declare services --- .../src/services/PlatformLogRequestFactory.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/platform/platform-log-request/src/services/PlatformLogRequestFactory.ts b/packages/platform/platform-log-request/src/services/PlatformLogRequestFactory.ts index 14cb1a062f5..bae92ef1f43 100644 --- a/packages/platform/platform-log-request/src/services/PlatformLogRequestFactory.ts +++ b/packages/platform/platform-log-request/src/services/PlatformLogRequestFactory.ts @@ -1,14 +1,14 @@ -import {type BaseContext, Configuration, registerProvider} from "@tsed/di"; +import {type BaseContext, constant, injectable} from "@tsed/di"; import {defaultAlterLog} from "../utils/defaultAlterLog.js"; import {defaultLogResponse} from "../utils/defaultLogResponse.js"; -function factory(configuration: Configuration) { +function factory() { const { logRequest = true, alterLog = defaultAlterLog, onLogResponse = defaultLogResponse - } = configuration.get("logger"); + } = constant("logger", {}); return logRequest ? { @@ -18,14 +18,11 @@ function factory(configuration: Configuration) { : null; } -export const PlatformLogRequestFactory = Symbol.for("PLATFORM:LOGGER:REQUEST"); export type PlatformLogRequestFactory = ReturnType; -registerProvider({ - provide: PlatformLogRequestFactory, - deps: [Configuration], - useFactory: factory, - hooks: { +export const PlatformLogRequestFactory = injectable(Symbol.for("PLATFORM:LOGGER:REQUEST")) + .factory(factory) + .hooks({ $onRequest(instance: ReturnType, $ctx: BaseContext) { if (instance) { $ctx.logger.alterLog((obj: any, level) => instance.alterLog(level, obj, $ctx)); @@ -37,5 +34,5 @@ registerProvider({ instance.onLogResponse($ctx); } } - } -}); + }) + .token(); From 243bdcecccf6a98e3d365e9a65bcc519cc8afbb1 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 18:21:42 +0100 Subject: [PATCH 19/32] refactor(platform-router): use hook to create router on the fly --- .../src/domain/PlatformRouters.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/platform/platform-router/src/domain/PlatformRouters.ts b/packages/platform/platform-router/src/domain/PlatformRouters.ts index 7d3310de709..2faf63b9454 100644 --- a/packages/platform/platform-router/src/domain/PlatformRouters.ts +++ b/packages/platform/platform-router/src/domain/PlatformRouters.ts @@ -2,16 +2,15 @@ import {getValue, Type} from "@tsed/core"; import { constant, ControllerProvider, - GlobalProviders, inject, - Injectable, injectable, injector, Provider, ProviderType, + ResolvedInvokeOptions, TokenProvider } from "@tsed/di"; -import {Hooks} from "@tsed/hooks"; +import {$on, Hooks} from "@tsed/hooks"; import {PlatformParamsCallback} from "@tsed/platform-params"; import {concatPath, getOperationsRoutes, JsonMethodStore, OPERATION_HTTP_VERBS} from "@tsed/schema"; @@ -47,13 +46,6 @@ function createInjectableRouter(provider: ControllerProvider): PlatformRouter { .invoke(tokenRouter); } -GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider, { - onInvoke(provider: ControllerProvider, {locals}) { - const router = createInjectableRouter(provider); - locals.set(PlatformRouter, router); - } -}); - export interface AlterEndpointHandlersArg { before: (Type | Function)[]; endpoint: JsonMethodStore; @@ -194,3 +186,11 @@ export class PlatformRouters { } injectable(PlatformRouters); +/** + * Create injectable router for the current invoked provider. + * @ignore + */ +$on(`$beforeInvoke:${ProviderType.CONTROLLER}`, ({provider, locals}: ResolvedInvokeOptions) => { + const router = createInjectableRouter(provider as ControllerProvider); + locals.set(PlatformRouter, router); +}); From f581b444b7db6227b66f46c55c764ef429b6a2f2 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:01:31 +0100 Subject: [PATCH 20/32] refactor(platform-exceptions): use functional API to inject/declare services --- .../src/decorators/catch.ts | 7 ++-- .../src/services/PlatformExceptions.ts | 35 +++++++++---------- .../platform-exceptions/vitest.config.mts | 2 +- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/platform/platform-exceptions/src/decorators/catch.ts b/packages/platform/platform-exceptions/src/decorators/catch.ts index b31ef8443ea..8f1f4bc6647 100644 --- a/packages/platform/platform-exceptions/src/decorators/catch.ts +++ b/packages/platform/platform-exceptions/src/decorators/catch.ts @@ -1,5 +1,5 @@ import {Type} from "@tsed/core"; -import {registerProvider} from "@tsed/di"; +import {injectable, registerProvider} from "@tsed/di"; import {registerExceptionType} from "../domain/ExceptionFiltersContainer.js"; @@ -13,9 +13,6 @@ export function Catch(...types: (Type | string)[]) { types.forEach((type) => { registerExceptionType(type, target as any); }); - registerProvider({ - provide: target, - useClass: target - }); + injectable(target).class(target); }; } diff --git a/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts b/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts index 3e1872eb64e..c30873420a5 100644 --- a/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts +++ b/packages/platform/platform-exceptions/src/services/PlatformExceptions.ts @@ -1,5 +1,5 @@ import {ancestorsOf, classOf, nameOf} from "@tsed/core"; -import {DIContext, Inject, Injectable, InjectorService} from "@tsed/di"; +import {DIContext, inject, injectable, type TokenProvider} from "@tsed/di"; import {ErrorFilter} from "../components/ErrorFilter.js"; import {ExceptionFilter} from "../components/ExceptionFilter.js"; @@ -7,33 +7,34 @@ import {MongooseErrorFilter} from "../components/MongooseErrorFilter.js"; import {StringErrorFilter} from "../components/StringErrorFilter.js"; import {ExceptionFilterKey, ExceptionFiltersContainer} from "../domain/ExceptionFiltersContainer.js"; import {ResourceNotFound} from "../errors/ResourceNotFound.js"; -import {ExceptionFilterMethods} from "../interfaces/ExceptionFilterMethods.js"; /** * Catch all errors and return the json error with the right status code when it's possible. * * @platform */ -@Injectable({ - imports: [ErrorFilter, ExceptionFilter, MongooseErrorFilter, StringErrorFilter] -}) export class PlatformExceptions { - types: Map = new Map(); + types: Map = new Map(); - @Inject() - injector: InjectorService; - - $onInit() { + constructor() { ExceptionFiltersContainer.forEach((token, type) => { - this.types.set(type, this.injector.get(token)!); + this.types.set(type, token); }); } catch(error: unknown, ctx: DIContext) { + return this.resolve(error, ctx).catch(error, ctx); + } + + resourceNotFound(ctx: DIContext) { + return this.catch(new ResourceNotFound(ctx.request.url), ctx); + } + + protected resolve(error: any, ctx: DIContext) { const name = nameOf(classOf(error)); if (name && this.types.has(name)) { - return this.types.get(name)!.catch(error, ctx); + return inject(this.types.get(name)!); } const target = ancestorsOf(error) @@ -41,14 +42,12 @@ export class PlatformExceptions { .find((target) => this.types.has(target)); if (target) { - return this.types.get(target)!.catch(error, ctx); + return inject(this.types.get(target)!); } // default - return this.types.get(Error)!.catch(error, ctx); - } - - resourceNotFound(ctx: DIContext) { - return this.catch(new ResourceNotFound(ctx.request.url), ctx); + return inject(this.types.get(Error)!); } } + +injectable(PlatformExceptions).imports([ErrorFilter, ExceptionFilter, MongooseErrorFilter, StringErrorFilter]); diff --git a/packages/platform/platform-exceptions/vitest.config.mts b/packages/platform/platform-exceptions/vitest.config.mts index 016601524b4..acb87d53708 100644 --- a/packages/platform/platform-exceptions/vitest.config.mts +++ b/packages/platform/platform-exceptions/vitest.config.mts @@ -11,7 +11,7 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 97.29, + branches: 97.36, functions: 100, lines: 100 } From 5d9bfda79848257f2b8d3389e7929849b770b510 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:02:04 +0100 Subject: [PATCH 21/32] refactor(platform-response-filter): use functional API to inject/declare services --- .../src/decorators/responseFilter.ts | 8 +- .../services/PlatformResponseFilter.spec.ts | 305 ++++++++++-------- .../src/services/PlatformResponseFilter.ts | 46 +-- 3 files changed, 202 insertions(+), 157 deletions(-) diff --git a/packages/platform/platform-response-filter/src/decorators/responseFilter.ts b/packages/platform/platform-response-filter/src/decorators/responseFilter.ts index d47f4d7d71f..898dbbdf90e 100644 --- a/packages/platform/platform-response-filter/src/decorators/responseFilter.ts +++ b/packages/platform/platform-response-filter/src/decorators/responseFilter.ts @@ -1,5 +1,4 @@ -import {Type} from "@tsed/core"; -import {registerProvider} from "@tsed/di"; +import {injectable} from "@tsed/di"; import {registerResponseFilter, ResponseFilterKey} from "../domain/ResponseFiltersContainer.js"; @@ -13,9 +12,6 @@ export function ResponseFilter(...contentTypes: ResponseFilterKey[]): ClassDecor contentTypes.forEach((contentType) => { registerResponseFilter(contentType, target as any); }); - registerProvider({ - provide: target, - useClass: target as unknown as Type - }); + injectable(target).class(target); }; } diff --git a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts index f6040d63dbd..35ce0b813ba 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.spec.ts @@ -1,5 +1,4 @@ import {catchAsyncError} from "@tsed/core"; -import {PlatformContext} from "@tsed/platform-http"; import {PlatformTest} from "@tsed/platform-http/testing"; import {Context} from "@tsed/platform-params"; import {EndpointMetadata, Get, Returns, View} from "@tsed/schema"; @@ -15,177 +14,227 @@ class CustomJsonFilter implements ResponseFilterMethods { } } -describe("PlatformResponseFilter", () => { - beforeEach(() => - PlatformTest.create({ - responseFilters: [CustomJsonFilter] - }) - ); - afterEach(() => PlatformTest.reset()); +@ResponseFilter("application/json") +class ApplicationJsonFilter implements ResponseFilterMethods { + transform(data: unknown, ctx: Context) { + return {data, "content-type": "application/json"}; + } +} + +@ResponseFilter("*/*") +class AllFilter implements ResponseFilterMethods { + transform(data: unknown, ctx: Context) { + return {data, "content-type": "*/*"}; + } +} +describe("PlatformResponseFilter", () => { describe("transform()", () => { - it("should transform data for custom/json", async () => { - class Test { - @Get("/") - test() {} - } + describe("when filter list is given", () => { + beforeEach(() => + PlatformTest.create({ + responseFilters: [CustomJsonFilter, AllFilter, ApplicationJsonFilter] + }) + ); + afterEach(() => PlatformTest.reset()); + + it("should transform data for custom/json", async () => { + class Test { + @Get("/") + test() {} + } - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - const ctx = PlatformTest.createRequestContext(); - ctx.endpoint = EndpointMetadata.get(Test, "test"); - const data = {text: "test"}; + const ctx = PlatformTest.createRequestContext(); + ctx.endpoint = EndpointMetadata.get(Test, "test"); + const data = {text: "test"}; - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "get").mockReturnValue("custom/json"); - vi.spyOn(ctx.request, "accepts").mockReturnValue(["custom/json"]); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue("custom/json"); + vi.spyOn(ctx.request, "accepts").mockReturnValue(["custom/json"]); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - data: { - text: "test" - } + expect(result).toEqual({ + data: { + text: "test" + } + }); }); - }); - it("should transform data for application/json", async () => { - class Test { - @Get("/") - test() {} - } + it("should transform data for application/json", async () => { + class Test { + @Get("/") + test() {} + } - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - const ctx = PlatformTest.createRequestContext(); - ctx.endpoint = EndpointMetadata.get(Test, "test"); - const data = {text: "test"}; + const ctx = PlatformTest.createRequestContext(); + ctx.endpoint = EndpointMetadata.get(Test, "test"); + const data = {text: "test"}; - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); - vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); + vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - text: "test" + expect(result).toEqual({ + "content-type": "application/json", + data: { + text: "test" + } + }); }); - }); - it("should get content-type set from response", async () => { - class Test { - @Get("/") - test() {} - } + it("should return data without transformation", async () => { + class Test { + @Get("/") + test() {} + } - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - const ctx = PlatformTest.createRequestContext(); - ctx.endpoint = EndpointMetadata.get(Test, "test"); - const data = {text: "test"}; + const ctx = PlatformTest.createRequestContext(); + const data = {text: "test"}; - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue("text/json; charset: utf-8"); - vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); - vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); + vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - text: "test" + expect(result).toEqual({ + text: "test" + }); }); - }); - it("should transform data for any content type", async () => { - class Test { - @Get("/") - test() {} - } - - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + it("should get content-type set from response", async () => { + class Test { + @Get("/") + test() {} + } - const ctx = PlatformTest.createRequestContext(); - const data = {text: "test"}; - ctx.endpoint = EndpointMetadata.get(Test, "test"); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); - vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); + const ctx = PlatformTest.createRequestContext(); + ctx.endpoint = EndpointMetadata.get(Test, "test"); + const data = {text: "test"}; - // @ts-ignore - platformResponseFilter.types.set("*/*", { - transform(data: unknown, ctx: PlatformContext) { - return {data}; - } - }); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue("text/json; charset: utf-8"); + vi.spyOn(ctx.request, "get").mockReturnValue("application/json"); + vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - data: { - text: "test" - } + expect(result).toEqual({ + "content-type": "application/json", + data: { + text: "test" + } + }); }); - }); - it("should transform data for default content-type from metadata", async () => { - class Test { - @Get("/") - @(Returns(200).ContentType("application/json")) - test() {} - } + it("should transform data for any content type", async () => { + class Test { + @Get("/") + test() {} + } - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - const ctx = PlatformTest.createRequestContext(); - const data = {text: "test"}; - ctx.endpoint = EndpointMetadata.get(Test, "test"); + const ctx = PlatformTest.createRequestContext(); + const data = {text: "test"}; + ctx.endpoint = EndpointMetadata.get(Test, "test"); - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "accepts").mockReturnValue(false); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue("*/*"); + vi.spyOn(ctx.request, "accepts").mockReturnValue(["application/json"]); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - text: "test" + expect(result).toEqual({ + "content-type": "application/json", + data: { + text: "test" + } + }); }); }); - it("should transform data for default content-type from metadata with any response filter", async () => { - class Test { - @Get("/") - @(Returns(200).ContentType("application/json")) - test() {} - } - const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + describe("when filter list is not given", () => { + beforeEach(() => + PlatformTest.create({ + responseFilters: [AllFilter] + }) + ); + afterEach(() => PlatformTest.reset()); + it("should transform data for default content-type from metadata", async () => { + class Test { + @Get("/") + @(Returns(200).ContentType("application/json")) + test() {} + } - const ctx = PlatformTest.createRequestContext(); - const data = {text: "test"}; - ctx.endpoint = EndpointMetadata.get(Test, "test"); + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); - // @ts-ignore - platformResponseFilter.types.set("*/*", { - transform(data: unknown, ctx: PlatformContext) { - return {data}; - } - }); + const ctx = PlatformTest.createRequestContext(); + const data = {text: "test"}; + ctx.endpoint = EndpointMetadata.get(Test, "test"); - vi.spyOn(ctx.response, "contentType").mockReturnThis(); - vi.spyOn(ctx.response, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "get").mockReturnValue(undefined); - vi.spyOn(ctx.request, "accepts").mockReturnValue(false); + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "accepts").mockReturnValue(false); - const result = await platformResponseFilter.transform(data, ctx); + const result = await platformResponseFilter.transform(data, ctx); - expect(result).toEqual({ - data: { - text: "test" + expect(result).toEqual({ + "content-type": "*/*", + data: { + text: "test" + } + }); + }); + it("should transform data for default content-type from metadata with any response filter", async () => { + class Test { + @Get("/") + @(Returns(200).ContentType("application/json")) + test() {} } + + const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); + + const ctx = PlatformTest.createRequestContext(); + const data = {text: "test"}; + ctx.endpoint = EndpointMetadata.get(Test, "test"); + + vi.spyOn(ctx.response, "contentType").mockReturnThis(); + vi.spyOn(ctx.response, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "get").mockReturnValue(undefined); + vi.spyOn(ctx.request, "accepts").mockReturnValue(false); + + const result = await platformResponseFilter.transform(data, ctx); + + expect(result).toEqual({ + "content-type": "*/*", + data: { + text: "test" + } + }); }); }); }); describe("serialize()", () => { + beforeEach(() => + PlatformTest.create({ + responseFilters: [CustomJsonFilter, AllFilter, ApplicationJsonFilter] + }) + ); + afterEach(() => PlatformTest.reset()); it("should transform value", async () => { const platformResponseFilter = PlatformTest.get(PlatformResponseFilter); const ctx = PlatformTest.createRequestContext(); diff --git a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts index 26cb64c1756..979a6f7ccab 100644 --- a/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts +++ b/packages/platform/platform-response-filter/src/services/PlatformResponseFilter.ts @@ -1,5 +1,5 @@ import {isSerializable, Type} from "@tsed/core"; -import {BaseContext, Constant, Inject, Injectable, InjectorService} from "@tsed/di"; +import {BaseContext, constant, inject, injectable, TokenProvider} from "@tsed/di"; import {serialize} from "@tsed/json-mapper"; import {ResponseFilterKey, ResponseFiltersContainer} from "../domain/ResponseFiltersContainer.js"; @@ -10,31 +10,23 @@ import {renderView} from "../utils/renderView.js"; /** * @platform */ -@Injectable() export class PlatformResponseFilter { - protected types: Map = new Map(); + protected types: Map = new Map(); + protected responseFilters = constant[]>("responseFilters", []); + protected additionalProperties = constant("additionalProperties"); - @Inject() - protected injector: InjectorService; - - @Constant("responseFilters", []) - protected responseFilters: Type[]; - - @Constant("additionalProperties") - protected additionalProperties: boolean; - - get contentTypes(): ResponseFilterKey[] { - return [...this.types.keys()]; - } - - $onInit() { + constructor() { ResponseFiltersContainer.forEach((token, type) => { if (this.responseFilters.includes(token)) { - this.types.set(type, this.injector.get(token)!); + this.types.set(type, token); } }); } + get contentTypes(): ResponseFilterKey[] { + return [...this.types.keys()]; + } + getBestContentType(data: any, ctx: BaseContext) { const contentType = getContentType(data, ctx); @@ -62,12 +54,10 @@ export class PlatformResponseFilter { bestContentType && response.contentType(bestContentType); - if (this.types.has(bestContentType)) { - return this.types.get(bestContentType)!.transform(data, ctx); - } + const resolved = this.resolve(bestContentType); - if (this.types.has(ANY_CONTENT_TYPE)) { - return this.types.get(ANY_CONTENT_TYPE)!.transform(data, ctx); + if (resolved) { + return resolved.transform(data, ctx); } } @@ -102,6 +92,14 @@ export class PlatformResponseFilter { return data; } + private resolve(bestContentType: string) { + const token = this.types.get(bestContentType) || this.types.get(ANY_CONTENT_TYPE); + + if (token) { + return inject(token); + } + } + private getIncludes(ctx: BaseContext) { if (ctx.request.query.includes) { return [].concat(ctx.request.query.includes).flatMap((include: string) => include.split(",")); @@ -110,3 +108,5 @@ export class PlatformResponseFilter { return undefined; } } + +injectable(PlatformResponseFilter); From 61f1640c75488fe81a37b82489170fe2eb722b0b Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 21:25:43 +0100 Subject: [PATCH 22/32] refactor(platform-views): use functional API to inject/declare services --- .../platform-views/src/decorators/view.ts | 7 ++- .../src/services/PlatformViews.ts | 51 ++++++++----------- .../platform/platform-views/vitest.config.mts | 10 ++-- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/platform/platform-views/src/decorators/view.ts b/packages/platform/platform-views/src/decorators/view.ts index 3adc46b5791..d497727c0cd 100644 --- a/packages/platform/platform-views/src/decorators/view.ts +++ b/packages/platform/platform-views/src/decorators/view.ts @@ -1 +1,6 @@ -export {View} from "@tsed/schema"; +import {View as W} from "@tsed/schema"; + +/** + * @deprecated Use View from @tsed/schema package. + */ +export const View: typeof W = W; diff --git a/packages/platform/platform-views/src/services/PlatformViews.ts b/packages/platform/platform-views/src/services/PlatformViews.ts index a08a1655a76..9cd6f6a43b9 100644 --- a/packages/platform/platform-views/src/services/PlatformViews.ts +++ b/packages/platform/platform-views/src/services/PlatformViews.ts @@ -1,6 +1,9 @@ +import "../domain/PlatformViewsSettings.js"; + import {Env, getValue} from "@tsed/core"; -import {Constant, Inject, InjectorService, Module} from "@tsed/di"; +import {constant, injectable, ProviderType} from "@tsed/di"; import {engines, getEngine, requires} from "@tsed/engines"; +import {$asyncAlter} from "@tsed/hooks"; import Fs from "fs"; import {extname, join, resolve} from "path"; @@ -28,35 +31,14 @@ async function patchEJS(ejs: any) { /** * @platform */ -@Module({ - views: { - exists: true - } -}) export class PlatformViews { - @Constant("env") - env: Env; - - @Constant("views.root", `${process.cwd()}/views`) - readonly root: string; - - @Constant("views.cache") - readonly cache: boolean; - - @Constant("views.disabled", false) - readonly disabled: string; - - @Constant("views.viewEngine", "ejs") - readonly viewEngine: string; - - @Constant("views.extensions", {}) - protected extensionsOptions: PlatformViewsExtensionsTypes; - - @Constant("views.options", {}) - protected engineOptions: Record; - - @Inject() - protected injector: InjectorService; + readonly root = constant("views.root", `${process.cwd()}/views`); + readonly cache = constant("views.cache"); + readonly disabled = constant("views.disabled", false); + readonly viewEngine = constant("views.viewEngine", "ejs"); + protected env = constant("env"); + protected extensionsOptions = constant("views.extensions", {}); + protected engineOptions = constant>("views.options", {}); #extensions: Map; #engines = new Map(); @@ -121,7 +103,8 @@ export class PlatformViews { async render(viewPath: string, options: any = {}): Promise { const {$ctx} = options; - options = await this.injector.alterAsync("$alterRenderOptions", options, $ctx); + + options = await $asyncAlter("$alterRenderOptions", options, $ctx); const {path, extension} = this.#cachePaths.get(viewPath) || this.#cachePaths.set(viewPath, this.resolve(viewPath)).get(viewPath)!; const engine = this.getEngine(extension); @@ -158,3 +141,11 @@ export class PlatformViews { }; } } + +injectable(PlatformViews) + .type(ProviderType.MODULE) + .configuration({ + views: { + exists: true + } + } as never); diff --git a/packages/platform/platform-views/vitest.config.mts b/packages/platform/platform-views/vitest.config.mts index 7841aa19b37..13f570be767 100644 --- a/packages/platform/platform-views/vitest.config.mts +++ b/packages/platform/platform-views/vitest.config.mts @@ -9,13 +9,17 @@ export default defineConfig( ...presets.test, coverage: { ...presets.test.coverage, + exclude:[ + ...presets.test.coverage.exclude, + "src/decorators/view.ts", + ], thresholds: { - statements: 92.25, + statements: 91.3, branches: 94.73, functions: 76.92, - lines: 92.25 + lines: 91.3 } } } } -); \ No newline at end of file +); From fc71af06c042b85b76f11ff3fa61f7195052ee70 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:04:55 +0100 Subject: [PATCH 23/32] refactor(ajv): use functional API to inject/declare services --- packages/specs/ajv/src/services/Ajv.ts | 40 +++++++++---------- packages/specs/ajv/src/services/AjvService.ts | 19 +++------ 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/packages/specs/ajv/src/services/Ajv.ts b/packages/specs/ajv/src/services/Ajv.ts index a1b4b3855c8..06444ac100c 100644 --- a/packages/specs/ajv/src/services/Ajv.ts +++ b/packages/specs/ajv/src/services/Ajv.ts @@ -1,5 +1,5 @@ import {cleanObject} from "@tsed/core"; -import {Configuration, InjectorService, ProviderScope, registerProvider} from "@tsed/di"; +import {constant, inject, injectable, injector, InjectorService, ProviderScope} from "@tsed/di"; import {Ajv, Format, KeywordDefinition, Options, Vocabulary} from "ajv"; import AjvErrors from "ajv-errors"; import AjvFormats from "ajv-formats"; @@ -13,14 +13,14 @@ function getHandler(key: string, service: any) { } } -function getKeywordProviders(injector: InjectorService) { - return injector.getProviders("ajv:keyword"); +function getKeywordProviders() { + return injector().getProviders("ajv:keyword"); } -function bindKeywords(injector: InjectorService): Vocabulary { - return getKeywordProviders(injector).map((provider) => { +function bindKeywords(): Vocabulary { + return getKeywordProviders().map((provider) => { const options = provider.store.get>("ajv:keyword", {})!; - const service = injector.invoke(provider.token); + const service = inject(provider.token); return cleanObject({ coerceTypes: "array", @@ -33,14 +33,14 @@ function bindKeywords(injector: InjectorService): Vocabulary { }); } -function getFormatsProviders(injector: InjectorService) { - return injector.getProviders("ajv:formats"); +function getFormatsProviders() { + return injector().getProviders("ajv:formats"); } -function getFormats(injector: InjectorService): {name: string; options: Format}[] { - return getFormatsProviders(injector).map((provider) => { +function getFormats(): {name: string; options: Format}[] { + return getFormatsProviders().map((provider) => { const {name, options} = provider.store.get("ajv:formats", {})!; - const service = injector.invoke>(provider.token); + const service = inject>(provider.token); return { name, @@ -53,18 +53,15 @@ function getFormats(injector: InjectorService): {name: string; options: Format}[ }); } -registerProvider({ - // @ts-ignore - provide: Ajv, - deps: [Configuration, InjectorService], - scope: ProviderScope.SINGLETON, - useFactory(configuration: Configuration, injector: InjectorService) { - const {errorFormatter, keywords = [], ...props} = configuration.get("ajv") || {}; +injectable(Ajv) + .scope(ProviderScope.SINGLETON) + .factory(() => { + const {errorFormatter, keywords = [], ...props} = constant("ajv") || {}; const options: Options = { verbose: false, coerceTypes: true, strict: false, - keywords: [...keywords, ...bindKeywords(injector)], + keywords: [...keywords, ...bindKeywords()], discriminator: true, allErrors: true, ...props @@ -79,10 +76,9 @@ registerProvider({ // @ts-ignore AjvFormats(ajv as any); - getFormats(injector).forEach(({name, options}) => { + getFormats().forEach(({name, options}) => { ajv.addFormat(name, options); }); return ajv; - } -}); + }); diff --git a/packages/specs/ajv/src/services/AjvService.ts b/packages/specs/ajv/src/services/AjvService.ts index 22126275de1..85423077fa7 100644 --- a/packages/specs/ajv/src/services/AjvService.ts +++ b/packages/specs/ajv/src/services/AjvService.ts @@ -1,7 +1,7 @@ import "./Ajv.js"; import {deepClone, getValue, nameOf, prototypeOf, setValue, Type} from "@tsed/core"; -import {Constant, Inject, Injectable} from "@tsed/di"; +import {constant, inject, injectable} from "@tsed/di"; import {getJsonSchema, JsonEntityStore, JsonSchema, JsonSchemaObject} from "@tsed/schema"; import {Ajv, ErrorObject} from "ajv"; @@ -16,20 +16,11 @@ export interface AjvValidateOptions extends Record { collectionType?: Type | any; } -@Injectable({ - type: "validator:service" -}) export class AjvService { readonly name = "ajv"; - - @Constant("ajv.errorFormatter", defaultErrorFormatter) - protected errorFormatter: ErrorFormatter; - - @Constant("ajv.returnsCoercedValues") - protected returnsCoercedValues: boolean; - - @Inject(Ajv) - protected ajv: Ajv; + protected errorFormatter = constant("ajv.errorFormatter", defaultErrorFormatter); + protected returnsCoercedValues = constant("ajv.returnsCoercedValues"); + protected ajv = inject(Ajv); async validate(value: any, options: AjvValidateOptions | JsonSchema): Promise { let {schema: defaultSchema, type, collectionType, ...additionalOptions} = this.mapOptions(options); @@ -121,3 +112,5 @@ export class AjvService { return error.message; } } + +injectable(AjvService).type("validator:service"); From 857ea09b2807f120ea2d5cc6d5c408f749c01e3d Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:14:59 +0100 Subject: [PATCH 24/32] refactor(swagger): use functional API to inject/declare services --- .../specs/swagger/src/SwaggerModule.spec.ts | 16 +++--- packages/specs/swagger/src/SwaggerModule.ts | 52 +++++++------------ .../specs/swagger/src/decorators/hidden.ts | 36 ------------- packages/specs/swagger/src/index.ts | 1 - .../swagger/src/services/SwaggerService.ts | 9 ++-- .../swagger/test/swagger.integration.spec.ts | 4 +- .../swagger/test/swagger.operationId.spec.ts | 4 +- 7 files changed, 37 insertions(+), 85 deletions(-) delete mode 100644 packages/specs/swagger/src/decorators/hidden.ts diff --git a/packages/specs/swagger/src/SwaggerModule.spec.ts b/packages/specs/swagger/src/SwaggerModule.spec.ts index f70b8671eb6..6e39897c214 100644 --- a/packages/specs/swagger/src/SwaggerModule.spec.ts +++ b/packages/specs/swagger/src/SwaggerModule.spec.ts @@ -1,3 +1,5 @@ +import {logger} from "@tsed/di"; +import {application} from "@tsed/platform-http"; import {PlatformTest} from "@tsed/platform-http/testing"; import {PlatformRouter} from "@tsed/platform-router"; import Fs from "fs"; @@ -27,16 +29,16 @@ describe("SwaggerModule", () => { it("should add middlewares", async () => { const mod = await PlatformTest.invoke(SwaggerModule); - vi.spyOn(mod.app as any, "get").mockReturnValue(undefined); - vi.spyOn(mod.app as any, "use").mockReturnValue(undefined); + vi.spyOn(application(), "get").mockReturnValue(undefined as never); + vi.spyOn(application(), "use").mockReturnValue(undefined as never); vi.spyOn(PlatformRouter.prototype as any, "get").mockReturnValue(undefined); vi.spyOn(PlatformRouter.prototype as any, "statics").mockReturnValue(undefined); mod.$onRoutesInit(); mod.$onRoutesInit(); - expect(mod.app.use).toHaveBeenCalledWith("/doc", expect.any(Function)); - expect(mod.app.use).toHaveBeenCalledWith("/doc", expect.any(PlatformRouter)); + expect(application().use).toHaveBeenCalledWith("/doc", expect.any(Function)); + expect(application().use).toHaveBeenCalledWith("/doc", expect.any(PlatformRouter)); expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/swagger.json", expect.any(Function)); expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/main.css", expect.any(Function)); expect(PlatformRouter.prototype.get).toHaveBeenCalledWith("/", expect.any(Function)); @@ -51,12 +53,12 @@ describe("SwaggerModule", () => { const mod = await PlatformTest.invoke(SwaggerModule); vi.spyOn(Fs, "writeFile"); - vi.spyOn(mod.injector.logger, "info"); + vi.spyOn(logger(), "info"); mod.$onReady(); - expect(mod.injector.logger.info).toHaveBeenCalledWith("[default] Swagger JSON is available on https://0.0.0.0:8081/doc/swagger.json"); - expect(mod.injector.logger.info).toHaveBeenCalledWith("[default] Swagger UI is available on https://0.0.0.0:8081/doc/"); + expect(logger().info).toHaveBeenCalledWith("[default] Swagger JSON is available on https://0.0.0.0:8081/doc/swagger.json"); + expect(logger().info).toHaveBeenCalledWith("[default] Swagger UI is available on https://0.0.0.0:8081/doc/"); }); }); }); diff --git a/packages/specs/swagger/src/SwaggerModule.ts b/packages/specs/swagger/src/SwaggerModule.ts index 24dc7c5fb92..abd76f6c7c2 100644 --- a/packages/specs/swagger/src/SwaggerModule.ts +++ b/packages/specs/swagger/src/SwaggerModule.ts @@ -1,10 +1,11 @@ +import Fs from "node:fs"; +import {join} from "node:path"; + import {Env} from "@tsed/core"; -import {Configuration, Constant, Inject, InjectorService, Module} from "@tsed/di"; +import {configuration, constant, inject, injectable, logger, ProviderType} from "@tsed/di"; import {normalizePath} from "@tsed/normalize-path"; -import {OnReady, OnRoutesInit, PlatformApplication, PlatformContext} from "@tsed/platform-http"; +import {application, OnReady, OnRoutesInit, PlatformContext} from "@tsed/platform-http"; import {PlatformRouter, useContextHandler} from "@tsed/platform-router"; -import Fs from "fs"; -import {join} from "path"; import {ROOT_DIR, SWAGGER_UI_DIST} from "./constants.js"; import {SwaggerSettings} from "./interfaces/SwaggerSettings.js"; @@ -14,33 +15,15 @@ import {jsMiddleware} from "./middlewares/jsMiddleware.js"; import {redirectMiddleware} from "./middlewares/redirectMiddleware.js"; import {SwaggerService} from "./services/SwaggerService.js"; -/** - * @ignore - */ -@Module() export class SwaggerModule implements OnRoutesInit, OnReady { - @Inject() - injector: InjectorService; - - @Inject() - app: PlatformApplication; - - @Configuration() - configuration: Configuration; - - @Inject() - swaggerService: SwaggerService; - - @Constant("env") - env: Env; - - @Constant("logger.disableRoutesSummary") - disableRoutesSummary: boolean; + protected swaggerService = inject(SwaggerService); + protected env = constant("env"); + protected disableRoutesSummary = constant("logger.disableRoutesSummary"); private loaded = false; get settings() { - return ([] as SwaggerSettings[]).concat(this.configuration.get("swagger")).filter((o) => !!o); + return constant("swagger", []).filter((o) => !!o); } /** @@ -56,8 +39,8 @@ export class SwaggerModule implements OnRoutesInit, OnReady { this.settings.forEach((conf: SwaggerSettings) => { const {path = "/"} = conf; - this.app.use(path, useContextHandler(redirectMiddleware(path))); - this.app.use(path, this.createRouter(conf, urls)); + application().use(path, useContextHandler(redirectMiddleware(path))); + application().use(path, this.createRouter(conf, urls)); }); this.loaded = true; @@ -65,15 +48,15 @@ export class SwaggerModule implements OnRoutesInit, OnReady { $onReady() { // istanbul ignore next - if (this.configuration.getBestHost && !this.disableRoutesSummary) { - const host = this.configuration.getBestHost(); + if (configuration().getBestHost && !this.disableRoutesSummary) { + const host = configuration().getBestHost(); const url = host.toString(); const displayLog = (conf: SwaggerSettings) => { const {path = "/", fileName = "swagger.json", doc} = conf; - this.injector.logger.info(`[${doc || "default"}] Swagger JSON is available on ${url}${normalizePath(path, fileName)}`); - this.injector.logger.info(`[${doc || "default"}] Swagger UI is available on ${url}${path}/`); + logger().info(`[${doc || "default"}] Swagger JSON is available on ${url}${normalizePath(path, fileName)}`); + logger().info(`[${doc || "default"}] Swagger UI is available on ${url}${path}/`); }; this.settings.forEach((conf) => { @@ -119,12 +102,11 @@ export class SwaggerModule implements OnRoutesInit, OnReady { */ private createRouter(conf: SwaggerSettings, urls: string[]) { const {disableSpec = false, fileName = "swagger.json", cssPath, jsPath, viewPath = join(ROOT_DIR, "../views/index.ejs")} = conf; - const router = new PlatformRouter(this.injector); + const router = new PlatformRouter(); if (!disableSpec) { router.get(normalizePath("/", fileName), useContextHandler(this.middlewareSwaggerJson(conf))); } - if (viewPath) { if (cssPath) { router.get("/main.css", useContextHandler(cssMiddleware(cssPath))); @@ -147,3 +129,5 @@ export class SwaggerModule implements OnRoutesInit, OnReady { }; } } + +injectable(SwaggerModule).type(ProviderType.MODULE); diff --git a/packages/specs/swagger/src/decorators/hidden.ts b/packages/specs/swagger/src/decorators/hidden.ts deleted file mode 100644 index c931ec7faba..00000000000 --- a/packages/specs/swagger/src/decorators/hidden.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Hidden as H} from "@tsed/schema"; - -/** - * Disable documentation for the class and his endpoint. - * - * ````typescript - * @Controller('/') - * export class Ctrl { - * - * @Get('/') - * @Hidden() - * hiddenRoute(){ - * - * } - * } - * - * @Controller('/') - * @Hidden() - * export class Ctrl { - * @Get('/') - * hiddenRoute() { - * - * } - * @Get('/2') - * hiddenRoute2() { - * - * } - * } - * ``` - * - * @decorator - * @swagger - */ -export function Hidden() { - return H(); -} diff --git a/packages/specs/swagger/src/index.ts b/packages/specs/swagger/src/index.ts index 902b1d863da..485c8b88f4b 100644 --- a/packages/specs/swagger/src/index.ts +++ b/packages/specs/swagger/src/index.ts @@ -3,7 +3,6 @@ */ export * from "./constants.js"; export * from "./decorators/docs.js"; -export * from "./decorators/hidden.js"; export * from "./interfaces/interfaces.js"; export * from "./interfaces/SwaggerSettings.js"; export * from "./middlewares/cssMiddleware.js"; diff --git a/packages/specs/swagger/src/services/SwaggerService.ts b/packages/specs/swagger/src/services/SwaggerService.ts index baca0f36609..4c746fd4c71 100644 --- a/packages/specs/swagger/src/services/SwaggerService.ts +++ b/packages/specs/swagger/src/services/SwaggerService.ts @@ -1,5 +1,5 @@ import type {Type} from "@tsed/core"; -import {constant, Injectable} from "@tsed/di"; +import {constant, inject, injectable} from "@tsed/di"; import {OpenSpec2, OpenSpec3} from "@tsed/openspec"; import {Platform} from "@tsed/platform-http"; import {generateSpec} from "@tsed/schema"; @@ -8,11 +8,12 @@ import {SwaggerOS2Settings, SwaggerOS3Settings, SwaggerSettings} from "../interf import {includeRoute} from "../utils/includeRoute.js"; import {readSpec} from "../utils/readSpec.js"; -@Injectable() export class SwaggerService { + protected platform = inject(Platform); + #specs: Map = new Map(); - constructor(private platform: Platform) {} + constructor() {} /** * Generate Spec for the given configuration @@ -46,3 +47,5 @@ export class SwaggerService { return this.#specs.get(conf.path); } } + +injectable(SwaggerService); diff --git a/packages/specs/swagger/test/swagger.integration.spec.ts b/packages/specs/swagger/test/swagger.integration.spec.ts index 7c474cebd2d..a7718153a7f 100644 --- a/packages/specs/swagger/test/swagger.integration.spec.ts +++ b/packages/specs/swagger/test/swagger.integration.spec.ts @@ -3,10 +3,10 @@ import {ObjectID} from "@tsed/mongoose"; import {PlatformExpress} from "@tsed/platform-express"; import {PlatformTest} from "@tsed/platform-http/testing"; import {BodyParams, PathParams} from "@tsed/platform-params"; -import {Consumes, Description, Get, Post, Returns} from "@tsed/schema"; +import {Consumes, Description, Get, Hidden, Post, Returns} from "@tsed/schema"; import SuperTest from "supertest"; -import {Docs, Hidden} from "../src/index.js"; +import {Docs} from "../src/index.js"; import {Calendar} from "./app/models/Calendar.js"; import {Server} from "./app/Server.js"; diff --git a/packages/specs/swagger/test/swagger.operationId.spec.ts b/packages/specs/swagger/test/swagger.operationId.spec.ts index 03bfb032386..81c5a329da4 100644 --- a/packages/specs/swagger/test/swagger.operationId.spec.ts +++ b/packages/specs/swagger/test/swagger.operationId.spec.ts @@ -3,10 +3,10 @@ import {ObjectID} from "@tsed/mongoose"; import {PlatformExpress} from "@tsed/platform-express"; import {PlatformTest} from "@tsed/platform-http/testing"; import {BodyParams, PathParams} from "@tsed/platform-params"; -import {Consumes, Description, Get, Post, Returns} from "@tsed/schema"; +import {Consumes, Description, Get, Hidden, Post, Returns} from "@tsed/schema"; import SuperTest from "supertest"; -import {Docs, Hidden} from "../src/index.js"; +import {Docs} from "../src/index.js"; import {Calendar} from "./app/models/Calendar.js"; import {Server} from "./app/Server.js"; From 51aba8006296819e26699bbcc1fc5c52d6111e2e Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Fri, 22 Nov 2024 08:57:15 +0100 Subject: [PATCH 25/32] refactor(platform-http): use functional API to inject/declare services --- .../common/builder/PlatformBuilder.spec.ts | 28 +++++--- .../src/common/builder/PlatformBuilder.ts | 72 ++++++++++--------- .../src/common/decorators/PlatformProvider.ts | 8 ++- .../src/common/domain/PlatformContext.ts | 9 +-- .../platform-http/src/common/fn/adapter.ts | 29 ++++++++ .../src/common/fn/application.ts | 11 +++ .../platform-http/src/common/index.ts | 3 +- .../PlatformAcceptMimesMiddleware.ts | 12 ++-- .../middlewares/PlatformMulterMiddleware.ts | 17 ++--- .../src/common/services/Platform.spec.ts | 7 +- .../src/common/services/Platform.ts | 29 ++++---- .../src/common/services/PlatformAdapter.ts | 18 ++--- .../services/PlatformApplication.spec.ts | 4 +- .../common/services/PlatformApplication.ts | 20 +++--- .../src/common/services/PlatformHandler.ts | 33 ++++----- .../services/PlatformMiddlewaresChain.ts | 15 ++-- .../src/common/services/PlatformRequest.ts | 7 +- .../src/common/services/PlatformResponse.ts | 6 +- .../src/common/utils/createContext.spec.ts | 28 +++++--- .../src/common/utils/createContext.ts | 11 ++- .../src/common/utils/createHttpServer.spec.ts | 39 +++++----- .../src/common/utils/createHttpServer.ts | 11 ++- .../common/utils/createHttpsServer.spec.ts | 39 +++++----- .../src/common/utils/createHttpsServer.ts | 11 ++- .../src/common/utils/createInjector.ts | 4 +- .../src/common/utils/createServer.ts | 20 +++--- .../common/utils/registerPlatformAdapter.ts | 10 --- .../platform-http/src/testing/PlatformTest.ts | 23 +++--- .../platform/platform-http/vitest.config.mts | 8 +-- 29 files changed, 278 insertions(+), 254 deletions(-) create mode 100644 packages/platform/platform-http/src/common/fn/adapter.ts create mode 100644 packages/platform/platform-http/src/common/fn/application.ts delete mode 100644 packages/platform/platform-http/src/common/utils/registerPlatformAdapter.ts diff --git a/packages/platform/platform-http/src/common/builder/PlatformBuilder.spec.ts b/packages/platform/platform-http/src/common/builder/PlatformBuilder.spec.ts index 194c2c52d28..527f1a780c5 100644 --- a/packages/platform/platform-http/src/common/builder/PlatformBuilder.spec.ts +++ b/packages/platform/platform-http/src/common/builder/PlatformBuilder.spec.ts @@ -1,5 +1,6 @@ import {catchAsyncError, Type} from "@tsed/core"; import {Configuration, configuration, Controller, destroyInjector, Injectable, injector, Module} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; import {FakeAdapter} from "../../testing/FakeAdapter.js"; import {AfterInit} from "../interfaces/AfterInit.js"; @@ -12,6 +13,15 @@ import {OnReady} from "../interfaces/OnReady.js"; import {Platform} from "../services/Platform.js"; import {PlatformBuilder} from "./PlatformBuilder.js"; +vi.mock("@tsed/hooks", async (importOriginal) => { + const mod = await importOriginal(); + + return { + ...mod, + $asyncEmit: vi.fn() + }; +}); + @Controller("/") class RestCtrl {} @@ -255,7 +265,8 @@ describe("PlatformBuilder", () => { describe("bootstrap()", () => { it("should bootstrap platform", async () => { // WHEN - const spyOn = vi.spyOn(injector().hooks, "asyncEmit").mockResolvedValue(undefined); + vi.mocked($asyncEmit).mockResolvedValue(undefined); + const stub = ServerModule.prototype.$beforeRoutesInit; const server = await PlatformCustom.bootstrap(ServerModule, { httpPort: false, @@ -269,13 +280,12 @@ describe("PlatformBuilder", () => { expect(server.listenServers).toHaveBeenCalledWith(); expect(server.loadStatics).toHaveBeenCalledWith("$beforeRoutesInit"); expect(server.loadStatics).toHaveBeenCalledWith("$afterRoutesInit"); - expect(spyOn).toHaveBeenCalledWith("$afterInit", []); - expect(spyOn).toHaveBeenCalledWith("$beforeRoutesInit", []); - expect(spyOn).toHaveBeenCalledWith("$afterRoutesInit", []); - expect(spyOn).toHaveBeenCalledWith("$afterListen", []); - expect(spyOn).toHaveBeenCalledWith("$beforeListen", []); - expect(spyOn).toHaveBeenCalledWith("$onServerReady", []); - expect(spyOn).toHaveBeenCalledWith("$onReady", []); + expect($asyncEmit).toHaveBeenCalledWith("$afterInit", []); + expect($asyncEmit).toHaveBeenCalledWith("$beforeRoutesInit", []); + expect($asyncEmit).toHaveBeenCalledWith("$afterRoutesInit", []); + expect($asyncEmit).toHaveBeenCalledWith("$afterListen", []); + expect($asyncEmit).toHaveBeenCalledWith("$beforeListen", []); + expect($asyncEmit).toHaveBeenCalledWith("$onReady", []); // THEN expect(server.rootModule).toBeInstanceOf(ServerModule); @@ -283,7 +293,7 @@ describe("PlatformBuilder", () => { expect(server.name).toEqual("custom"); await server.stop(); - expect(spyOn).toHaveBeenCalledWith("$onDestroy", []); + expect($asyncEmit).toHaveBeenCalledWith("$onDestroy", []); }); }); describe("adapter()", () => { diff --git a/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts b/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts index 9fe75d212ed..5c3befd9df0 100644 --- a/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts +++ b/packages/platform/platform-http/src/common/builder/PlatformBuilder.ts @@ -1,14 +1,19 @@ -import {isClass, isFunction, isString, nameOf, Type} from "@tsed/core"; +import {type Env, isClass, isFunction, isString, nameOf, Type} from "@tsed/core"; import { colors, + configuration, + constant, createContainer, + destroyInjector, injector, InjectorService, + logger, ProviderOpts, ProviderScope, setLoggerConfiguration, TokenProvider } from "@tsed/di"; +import {$asyncAlter, $asyncEmit} from "@tsed/hooks"; import {getMiddlewaresForHook, PlatformMiddlewareLoadingOptions} from "@tsed/platform-middlewares"; import {PlatformLayer} from "@tsed/platform-router"; import type {IncomingMessage, ServerResponse} from "http"; @@ -18,6 +23,8 @@ import type Https from "https"; import {PlatformStaticsSettings} from "../config/interfaces/PlatformStaticsSettings.js"; import {PlatformRouteDetails} from "../domain/PlatformRouteDetails.js"; +import {adapter as $adapter} from "../fn/adapter.js"; +import {application} from "../fn/application.js"; import {Route} from "../interfaces/Route.js"; import {Platform} from "../services/Platform.js"; import {PlatformAdapter, PlatformBuilderSettings} from "../services/PlatformAdapter.js"; @@ -68,20 +75,16 @@ export class PlatformBuilder { this.log("Injector created..."); } - get injector(): InjectorService { - return injector(); - } - get rootModule(): any { return injector().get(this.#rootModule); } get app(): PlatformApplication { - return this.injector.get>(PlatformApplication)!; + return injector().get>(PlatformApplication)!; } get platform() { - return this.injector.get(Platform)!; + return injector().get(Platform)!; } get adapter() { @@ -109,15 +112,22 @@ export class PlatformBuilder { * @returns {PlatformConfiguration} */ get settings() { - return this.injector.settings; + return configuration(); } get logger() { - return this.injector.logger; + return logger(); } get disableBootstrapLog() { - return this.settings.get("logger.disableBootstrapLog"); + return constant("logger.disableBootstrapLog"); + } + + /** + * @deprecated use injector() instead of this method. + */ + get injector(): InjectorService { + return injector(); } static create(module: Type, settings: PlatformBuilderSettings) { @@ -148,7 +158,7 @@ export class PlatformBuilder { } log(...data: any[]) { - return !this.disableBootstrapLog && this.logger.info(...data, this.diff()); + return !this.disableBootstrapLog && logger().info(...data, this.diff()); } /** @@ -173,7 +183,7 @@ export class PlatformBuilder { */ public addControllers(endpoint: string, controllers: TokenProvider | TokenProvider[]) { [].concat(controllers as never[]).forEach((token: TokenProvider) => { - this.settings.routes.push({token, route: endpoint}); + configuration().routes.push({token, route: endpoint}); }); } @@ -181,7 +191,7 @@ export class PlatformBuilder { // init adapter (Express, Koa, etc...) await this.#adapter.onInit(); - setLoggerConfiguration(this.injector); + setLoggerConfiguration(); // create the middleware mapping to be executed to the expected hook await this.mapTokenMiddlewares(); @@ -206,7 +216,7 @@ export class PlatformBuilder { await this.loadStatics("$beforeRoutesInit"); await this.callHook("$beforeRoutesInit"); - const routes = this.injector.settings.get("routes"); + const routes = configuration().get("routes"); this.platform.addRoutes(routes); @@ -227,10 +237,10 @@ export class PlatformBuilder { } async loadInjector() { - const {injector} = this; this.log("Build providers"); + const settings = configuration(); - this.settings.routes = this.settings.routes.concat(resolveControllers(this.settings)); + settings.set("routes", settings.get("routes").concat(resolveControllers(settings))); const container = createContainer(); container.delete(this.#rootModule); @@ -239,7 +249,7 @@ export class PlatformBuilder { scope: ProviderScope.SINGLETON }); - await injector.load(container); + await injector().load(container); this.log("Settings and injector loaded..."); @@ -264,7 +274,7 @@ export class PlatformBuilder { async stop() { await this.callHook("$onDestroy"); - await this.injector.destroy(); + await destroyInjector(); this.#listeners.map(closeServer); } @@ -273,37 +283,35 @@ export class PlatformBuilder { const {startedAt} = this; await this.callHook("$onReady"); - await this.injector.emit("$onServerReady"); this.log(`Started in ${new Date().getTime() - startedAt.getTime()} ms`); } async callHook(hook: string, ...args: any[]) { - const {injector} = this; - if (!this.disableBootstrapLog) { - injector.logger.debug(`\x1B[1mCall hook ${hook}\x1B[22m`); + logger().debug(`\x1B[1mCall hook ${hook}\x1B[22m`); } // Load middlewares for the given hook this.loadMiddlewaresFor(hook); // call hooks added by providers - await injector.emit(hook, ...args); + await $asyncEmit(hook, args); } loadStatics(hook: string) { - const statics = this.settings.get("statics"); + const statics = constant("statics"); + const app = application(); getStaticsOptions(statics).forEach(({path, options}) => { if (options.hook === hook) { - this.platform.app.statics(path, options); + app.statics(path, options); } }); } useProvider(token: Type, settings?: Partial) { - this.injector.addProvider(token, settings); + injector().addProvider(token, settings); return this; } @@ -320,7 +328,7 @@ export class PlatformBuilder { this.#adapter.mapLayers(layers); const rawBody = - this.settings.get("rawBody") || + constant("rawBody") || layers.some(({handlers}) => { return handlers.some((handler) => handler.opts?.paramsTypes?.RAW_BODY); }); @@ -356,8 +364,6 @@ export class PlatformBuilder { } protected async logRoutes(layers: PlatformLayer[]) { - const {logger} = this; - this.log("Routes mounted..."); if (!this.settings.get("logger.disableRoutesSummary") && !this.disableBootstrapLog) { @@ -371,13 +377,13 @@ export class PlatformBuilder { } as PlatformRouteDetails; }); - logger.info(printRoutes(await this.injector.alterAsync("$logRoutes", routes))); + logger().info(printRoutes(await $asyncAlter("$logRoutes", routes))); } } protected async mapTokenMiddlewares() { - let middlewares = this.injector.settings.get("middlewares", []); - const {env} = this.injector.settings; + let middlewares = constant("middlewares", []); + const env = constant("env"); const defaultHook = "$beforeRoutesInit"; const promises = middlewares.map(async (middleware: PlatformMiddlewareLoadingOptions): Promise => { @@ -422,7 +428,7 @@ export class PlatformBuilder { middlewares = await Promise.all(promises); - this.injector.settings.set( + configuration().set( "middlewares", middlewares.filter((middleware) => middleware.use) ); diff --git a/packages/platform/platform-http/src/common/decorators/PlatformProvider.ts b/packages/platform/platform-http/src/common/decorators/PlatformProvider.ts index 5e1e53cf5c5..da7f53313ca 100644 --- a/packages/platform/platform-http/src/common/decorators/PlatformProvider.ts +++ b/packages/platform/platform-http/src/common/decorators/PlatformProvider.ts @@ -1,10 +1,14 @@ import {Type} from "@tsed/core"; +import {adapter} from "../fn/adapter.js"; import {PlatformAdapter} from "../services/PlatformAdapter.js"; -import {registerPlatformAdapter} from "../utils/registerPlatformAdapter.js"; +/** + * Register a new platform adapter. + * @decorator + */ export function PlatformProvider() { return (klass: Type) => { - registerPlatformAdapter(klass); + adapter(klass); }; } diff --git a/packages/platform/platform-http/src/common/domain/PlatformContext.ts b/packages/platform/platform-http/src/common/domain/PlatformContext.ts index 1b5e964c69e..23a34fd62a4 100644 --- a/packages/platform/platform-http/src/common/domain/PlatformContext.ts +++ b/packages/platform/platform-http/src/common/domain/PlatformContext.ts @@ -1,4 +1,5 @@ -import {$emit, DIContext, DIContextOptions} from "@tsed/di"; +import {DIContext, DIContextOptions, injector} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; import {PlatformHandlerMetadata} from "@tsed/platform-router"; import {EndpointMetadata} from "@tsed/schema"; import {IncomingMessage, ServerResponse} from "http"; @@ -77,15 +78,15 @@ export class PlatformContext< } get app() { - return this.injector.get(PlatformApplication)!; + return injector().get(PlatformApplication)!; } start() { - return $emit("$onRequest", this); + return $asyncEmit("$onRequest", [this]); } async finish() { - await Promise.all([$emit("$onResponse", this), this.destroy()]); + await Promise.all([$asyncEmit("$onResponse", [this]), this.destroy()]); this.#isFinished = true; } diff --git a/packages/platform/platform-http/src/common/fn/adapter.ts b/packages/platform/platform-http/src/common/fn/adapter.ts new file mode 100644 index 00000000000..8e0265ad7f7 --- /dev/null +++ b/packages/platform/platform-http/src/common/fn/adapter.ts @@ -0,0 +1,29 @@ +import {Type} from "@tsed/core"; +import {configuration, constant, refValue} from "@tsed/di"; + +import {PlatformAdapter} from "../services/PlatformAdapter.js"; + +const ADAPTER = "platform.adapter"; + +let globalAdapter: Type>; + +/** + * Set or Get the registered platform adapter. + * Ensure that the adapter is registered before using the platform. + */ +export function adapter(): Type> & {NAME: string}; +export function adapter(adapter: Type>): Type> & {NAME: string}; +/** + * Set the platform adapter + */ +export function adapter(adapter?: Type>) { + const ref = refValue(ADAPTER); + + if (adapter) { + globalAdapter ||= adapter; + } + + ref.value ||= globalAdapter; + + return ref.value; +} diff --git a/packages/platform/platform-http/src/common/fn/application.ts b/packages/platform/platform-http/src/common/fn/application.ts new file mode 100644 index 00000000000..1f2abc3804e --- /dev/null +++ b/packages/platform/platform-http/src/common/fn/application.ts @@ -0,0 +1,11 @@ +import {injector} from "@tsed/di"; + +import type {PlatformApplication} from "../services/PlatformApplication.js"; + +/** + * Return the injectable Application instance. + * @note Application is only available after PlatformExpress/PlatformKoa instance creation. + */ +export function application() { + return injector().get>("PlatformApplication")!; +} diff --git a/packages/platform/platform-http/src/common/index.ts b/packages/platform/platform-http/src/common/index.ts index a0b5ff96af4..537545ce86b 100644 --- a/packages/platform/platform-http/src/common/index.ts +++ b/packages/platform/platform-http/src/common/index.ts @@ -21,6 +21,8 @@ export * from "./domain/EndpointMetadata.js"; export * from "./domain/PlatformContext.js"; export * from "./domain/PlatformRouteDetails.js"; export * from "./exports.js"; +export * from "./fn/adapter.js"; +export * from "./fn/application.js"; export * from "./interfaces/AfterInit.js"; export * from "./interfaces/AfterListen.js"; export * from "./interfaces/AfterRoutesInit.js"; @@ -56,6 +58,5 @@ export * from "./utils/getStaticsOptions.js"; export * from "./utils/listenServer.js"; export * from "./utils/mapReturnedResponse.js"; export * from "./utils/printRoutes.js"; -export * from "./utils/registerPlatformAdapter.js"; export * from "./utils/resolveControllers.js"; export * from "./utils/setResponseHeaders.js"; diff --git a/packages/platform/platform-http/src/common/middlewares/PlatformAcceptMimesMiddleware.ts b/packages/platform/platform-http/src/common/middlewares/PlatformAcceptMimesMiddleware.ts index f016da0ba3d..a2b6553713d 100644 --- a/packages/platform/platform-http/src/common/middlewares/PlatformAcceptMimesMiddleware.ts +++ b/packages/platform/platform-http/src/common/middlewares/PlatformAcceptMimesMiddleware.ts @@ -1,19 +1,15 @@ import {uniq} from "@tsed/core"; -import {Constant} from "@tsed/di"; +import {constant, injectable, ProviderType} from "@tsed/di"; import {NotAcceptable} from "@tsed/exceptions"; -import {Middleware, MiddlewareMethods} from "@tsed/platform-middlewares"; +import {MiddlewareMethods} from "@tsed/platform-middlewares"; import {Context} from "@tsed/platform-params"; /** * @middleware * @platform */ -@Middleware({ - priority: -10 -}) export class PlatformAcceptMimesMiddleware implements MiddlewareMethods { - @Constant("acceptMimes", []) - acceptMimes: string[]; + acceptMimes = constant("acceptMimes", []); public use(@Context() ctx: Context): void { const {endpoint, request} = ctx; @@ -24,3 +20,5 @@ export class PlatformAcceptMimesMiddleware implements MiddlewareMethods { } } } + +injectable(PlatformAcceptMimesMiddleware).type(ProviderType.MIDDLEWARE).priority(-10); diff --git a/packages/platform/platform-http/src/common/middlewares/PlatformMulterMiddleware.ts b/packages/platform/platform-http/src/common/middlewares/PlatformMulterMiddleware.ts index cdd4f0055b0..34a35ef25e2 100644 --- a/packages/platform/platform-http/src/common/middlewares/PlatformMulterMiddleware.ts +++ b/packages/platform/platform-http/src/common/middlewares/PlatformMulterMiddleware.ts @@ -1,6 +1,6 @@ -import {Inject, Value} from "@tsed/di"; +import {constant, inject, injectable, ProviderType} from "@tsed/di"; import {BadRequest} from "@tsed/exceptions"; -import {Middleware, MiddlewareMethods} from "@tsed/platform-middlewares"; +import {MiddlewareMethods} from "@tsed/platform-middlewares"; import {Context} from "@tsed/platform-params"; import type {MulterError} from "multer"; @@ -23,21 +23,14 @@ export class MulterException extends BadRequest { /** * @middleware */ -@Middleware({ - priority: 10 -}) export class PlatformMulterMiddleware implements MiddlewareMethods { - @Value("multer", {}) // NOTE: don't use constant to getting multer configuration. See issue #1840 - protected settings: PlatformMulterSettings; - - @Inject() - protected app: PlatformApplication; + protected app = inject(PlatformApplication); async use(@Context() ctx: PlatformContext) { try { const {fields, options = {}} = ctx.endpoint.get(PlatformMulterMiddleware); const settings: PlatformMulterSettings = { - ...this.settings, + ...constant("multer", {}), ...options }; @@ -62,3 +55,5 @@ export class PlatformMulterMiddleware implements MiddlewareMethods { return conf.fields.map(({name, maxCount}) => ({name, maxCount})); } } + +injectable(PlatformMulterMiddleware).type(ProviderType.MIDDLEWARE).priority(-10); diff --git a/packages/platform/platform-http/src/common/services/Platform.spec.ts b/packages/platform/platform-http/src/common/services/Platform.spec.ts index 204fac9bd21..c94421d70fe 100644 --- a/packages/platform/platform-http/src/common/services/Platform.spec.ts +++ b/packages/platform/platform-http/src/common/services/Platform.spec.ts @@ -3,6 +3,7 @@ import {Controller} from "@tsed/di"; import {Get, Post} from "@tsed/schema"; import {PlatformTest} from "../../testing/PlatformTest.js"; +import {application} from "../fn/application.js"; import {Platform} from "./Platform.js"; @Controller("/my-route") @@ -74,7 +75,7 @@ describe("Platform", () => { // GIVEN const platform = await PlatformTest.get(Platform); - vi.spyOn(platform.app, "use"); + vi.spyOn(application(), "use"); // WHEN platform.addRoutes([{route: "/test", token: MyCtrl}]); @@ -83,7 +84,7 @@ describe("Platform", () => { // GIVEN const platform = await PlatformTest.get(Platform); - vi.spyOn(platform.app, "use"); + vi.spyOn(application(), "use"); // WHEN platform.addRoutes([{route: "/rest", token: MyNestedCtrl}]); @@ -101,7 +102,7 @@ describe("Platform", () => { // GIVEN const platform = await PlatformTest.get(Platform); - vi.spyOn(platform.app, "use"); + vi.spyOn(application(), "use"); // WHEN platform.addRoutes([{route: "/rest", token: DomainController}]); diff --git a/packages/platform/platform-http/src/common/services/Platform.ts b/packages/platform/platform-http/src/common/services/Platform.ts index 589ecd8175e..402b1af9d08 100644 --- a/packages/platform/platform-http/src/common/services/Platform.ts +++ b/packages/platform/platform-http/src/common/services/Platform.ts @@ -1,32 +1,24 @@ -import {ControllerProvider, Injectable, InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; +import {ControllerProvider, inject, injectable, injector, ProviderScope, TokenProvider} from "@tsed/di"; import {PlatformLayer, PlatformRouters} from "@tsed/platform-router"; +import {application} from "../fn/application.js"; import {Route, RouteController} from "../interfaces/Route.js"; -import {PlatformApplication} from "./PlatformApplication.js"; -import {PlatformHandler} from "./PlatformHandler.js"; /** * `Platform` is used to provide all routes collected by annotation `@Controller`. * * @platform */ -@Injectable({ - scope: ProviderScope.SINGLETON, - imports: [PlatformHandler] -}) export class Platform { + readonly platformRouters = inject(PlatformRouters); #layers: PlatformLayer[]; - constructor( - readonly injector: InjectorService, - readonly platformApplication: PlatformApplication, - readonly platformRouters: PlatformRouters - ) { - platformRouters.prebuild(); + constructor() { + this.platformRouters.prebuild(); } get app() { - return this.platformApplication; + return application(); } public addRoutes(routes: Route[]) { @@ -36,7 +28,8 @@ export class Platform { } public addRoute(route: string, token: TokenProvider) { - const provider = this.injector.getProvider(token) as ControllerProvider; + const app = application(); + const provider = injector().getProvider(token) as ControllerProvider; if (!provider || provider.hasParent()) { return this; @@ -44,13 +37,13 @@ export class Platform { const router = this.platformRouters.from(provider.token); - this.app.use(route, router); + app.use(route, router); return this; } public getLayers() { - this.#layers = this.#layers || this.platformRouters.getLayers(this.app); + this.#layers = this.#layers || this.platformRouters.getLayers(application()); return this.#layers; } @@ -82,3 +75,5 @@ export class Platform { return [...controllers.values()]; } } + +injectable(Platform).scope(ProviderScope.SINGLETON); diff --git a/packages/platform/platform-http/src/common/services/PlatformAdapter.ts b/packages/platform/platform-http/src/common/services/PlatformAdapter.ts index 4eee9174019..04ecba460bd 100644 --- a/packages/platform/platform-http/src/common/services/PlatformAdapter.ts +++ b/packages/platform/platform-http/src/common/services/PlatformAdapter.ts @@ -1,11 +1,12 @@ import {Type} from "@tsed/core"; -import {InjectorService, ProviderOpts, registerProvider} from "@tsed/di"; +import {injectable, ProviderOpts} from "@tsed/di"; import {PlatformContextHandler, PlatformHandlerMetadata, PlatformLayer} from "@tsed/platform-router"; import {IncomingMessage, ServerResponse} from "http"; import {PlatformMulter, PlatformMulterSettings} from "../config/interfaces/PlatformMulterSettings.js"; import {PlatformStaticsOptions} from "../config/interfaces/PlatformStaticsSettings.js"; import {PlatformContext} from "../domain/PlatformContext.js"; +import {application} from "../fn/application.js"; import {createHttpServer} from "../utils/createHttpServer.js"; import {createHttpsServer} from "../utils/createHttpsServer.js"; import {CreateServerReturn} from "../utils/createServer.js"; @@ -18,16 +19,13 @@ export abstract class PlatformAdapter { */ providers: ProviderOpts[]; - constructor(protected injector: InjectorService) {} - get app(): PlatformApplication { - return this.injector.get>("PlatformApplication")!; + return application(); } getServers(): CreateServerReturn[] { - return [createHttpServer(this.injector, this.app.callback()), createHttpsServer(this.injector, this.app.callback())].filter( - Boolean - ) as any[]; + const app = application(); + return [createHttpServer(app.callback()), createHttpsServer(app.callback())].filter(Boolean) as any[]; } onInit(): Promise | void { @@ -144,8 +142,4 @@ export class FakeAdapter extends PlatformAdapter { useContext() {} } -registerProvider({ - provide: PlatformAdapter, - deps: [InjectorService], - useClass: FakeAdapter -}); +injectable(PlatformAdapter).class(FakeAdapter); diff --git a/packages/platform/platform-http/src/common/services/PlatformApplication.spec.ts b/packages/platform/platform-http/src/common/services/PlatformApplication.spec.ts index f8f04bdaae0..392ac15a51e 100644 --- a/packages/platform/platform-http/src/common/services/PlatformApplication.spec.ts +++ b/packages/platform/platform-http/src/common/services/PlatformApplication.spec.ts @@ -1,3 +1,5 @@ +import {configuration} from "@tsed/di"; + import {PlatformTest} from "../../testing/PlatformTest.js"; import {createContext} from "../utils/createContext.js"; import {PlatformApplication} from "./PlatformApplication.js"; @@ -29,7 +31,7 @@ async function getPlatformApp() { use: platformHandler } ]); - platformApp.injector.settings.logger = {}; + configuration().logger = {}; platformApp.rawApp = createDriver() as any; return {platformApp, platformHandler}; diff --git a/packages/platform/platform-http/src/common/services/PlatformApplication.ts b/packages/platform/platform-http/src/common/services/PlatformApplication.ts index b7eefcdd279..106d2967ee6 100644 --- a/packages/platform/platform-http/src/common/services/PlatformApplication.ts +++ b/packages/platform/platform-http/src/common/services/PlatformApplication.ts @@ -1,4 +1,4 @@ -import {Injectable, InjectorService, ProviderScope} from "@tsed/di"; +import {inject, injectable, ProviderScope} from "@tsed/di"; import {PlatformRouter} from "@tsed/platform-router"; import {IncomingMessage, ServerResponse} from "http"; @@ -17,20 +17,16 @@ declare global { * * @platform */ -@Injectable({ - scope: ProviderScope.SINGLETON, - alias: "PlatformApplication" -}) export class PlatformApplication extends PlatformRouter { + adapter: PlatformAdapter = inject(PlatformAdapter); + rawApp: App; rawCallback: () => any; - constructor( - public adapter: PlatformAdapter, - public injector: InjectorService - ) { - super(injector); - const {app, callback} = adapter.createApp(); + constructor() { + super(); + + const {app, callback} = this.adapter.createApp(); this.rawApp = app; this.rawCallback = callback; @@ -54,3 +50,5 @@ export class PlatformApplication extends PlatformRouter return this.rawCallback(); } } + +injectable(PlatformApplication).scope(ProviderScope.SINGLETON).alias("PlatformApplication"); diff --git a/packages/platform/platform-http/src/common/services/PlatformHandler.ts b/packages/platform/platform-http/src/common/services/PlatformHandler.ts index 7c74392f992..34f33918a33 100644 --- a/packages/platform/platform-http/src/common/services/PlatformHandler.ts +++ b/packages/platform/platform-http/src/common/services/PlatformHandler.ts @@ -1,5 +1,5 @@ import {AnyPromiseResult, AnyToPromiseStatus, catchAsyncError} from "@tsed/core"; -import {Inject, Injectable, Provider, ProviderScope} from "@tsed/di"; +import {inject, injectable, Provider, ProviderScope} from "@tsed/di"; import {PlatformExceptions} from "@tsed/platform-exceptions"; import {PlatformParams, PlatformParamsCallback} from "@tsed/platform-params"; import {PlatformResponseFilter} from "@tsed/platform-response-filter"; @@ -22,28 +22,17 @@ import {PlatformMiddlewaresChain} from "./PlatformMiddlewaresChain.js"; * Platform Handler abstraction layer. Wrap original class method to a pure platform handler (Express, Koa, etc...). * @platform */ -@Injectable({ - scope: ProviderScope.SINGLETON -}) export class PlatformHandler { - @Inject() - protected responseFilter: PlatformResponseFilter; - - @Inject() - protected platformParams: PlatformParams; - - @Inject() - protected platformExceptions: PlatformExceptions; - - @Inject() - protected platformApplication: PlatformApplication; - - @Inject() - protected platformMiddlewaresChain: PlatformMiddlewaresChain; - - constructor(protected platformRouters: PlatformRouters) { + protected responseFilter = inject(PlatformResponseFilter); + protected platformParams = inject(PlatformParams); + protected platformExceptions = inject(PlatformExceptions); + protected platformApplication = inject(PlatformApplication); + protected platformMiddlewaresChain = inject(PlatformMiddlewaresChain); + protected platformRouters = inject(PlatformRouters); + + constructor() { // configure the router module - platformRouters.hooks + this.platformRouters.hooks .on("alterEndpointHandlers", (handlers: AlterEndpointHandlersArg, operationRoute: JsonOperationRoute) => { handlers = this.platformMiddlewaresChain.get(handlers, operationRoute); @@ -147,3 +136,5 @@ export class PlatformHandler { } } } + +injectable(PlatformHandler).scope(ProviderScope.SINGLETON); diff --git a/packages/platform/platform-http/src/common/services/PlatformMiddlewaresChain.ts b/packages/platform/platform-http/src/common/services/PlatformMiddlewaresChain.ts index 6520772448e..86a21ef3c7d 100644 --- a/packages/platform/platform-http/src/common/services/PlatformMiddlewaresChain.ts +++ b/packages/platform/platform-http/src/common/services/PlatformMiddlewaresChain.ts @@ -1,5 +1,5 @@ import {isClass} from "@tsed/core"; -import {Constant, Inject, Injectable, InjectorService, TokenProvider} from "@tsed/di"; +import {constant, inject, injectable} from "@tsed/di"; import {ParamTypes} from "@tsed/platform-params"; import {AlterEndpointHandlersArg} from "@tsed/platform-router"; import {JsonEntityStore, JsonOperationRoute} from "@tsed/schema"; @@ -8,16 +8,9 @@ import {PlatformAcceptMimesMiddleware} from "../middlewares/PlatformAcceptMimesM import {PlatformMulterMiddleware} from "../middlewares/PlatformMulterMiddleware.js"; import {PlatformAdapter} from "./PlatformAdapter.js"; -@Injectable() export class PlatformMiddlewaresChain { - @Constant("acceptMimes", []) - protected acceptMimes: string[]; - - @Inject(PlatformAdapter) - protected adapter: PlatformAdapter; - - @Inject(InjectorService) - protected injector: InjectorService; + protected acceptMimes = constant("acceptMimes", []); + protected adapter = inject(PlatformAdapter); get(handlers: AlterEndpointHandlersArg, operationRoute: JsonOperationRoute): AlterEndpointHandlersArg { const {ACCEPT_MIMES, FILE} = this.getParamTypes(handlers, operationRoute); @@ -58,3 +51,5 @@ export class PlatformMiddlewaresChain { ); } } + +injectable(PlatformMiddlewaresChain); diff --git a/packages/platform/platform-http/src/common/services/PlatformRequest.ts b/packages/platform/platform-http/src/common/services/PlatformRequest.ts index f14fe2be878..bd50ebb5434 100644 --- a/packages/platform/platform-http/src/common/services/PlatformRequest.ts +++ b/packages/platform/platform-http/src/common/services/PlatformRequest.ts @@ -1,4 +1,4 @@ -import {Injectable, ProviderScope, Scope} from "@tsed/di"; +import {Injectable, injectable, ProviderScope, Scope} from "@tsed/di"; import {IncomingHttpHeaders, IncomingMessage} from "http"; import type {PlatformContext} from "../domain/PlatformContext.js"; @@ -17,11 +17,8 @@ declare global { * Platform Request abstraction layer. * @platform */ -@Injectable() -@Scope(ProviderScope.INSTANCE) export class PlatformRequest { constructor(readonly $ctx: PlatformContext) {} - /** * The current @@PlatformResponse@@. */ @@ -180,3 +177,5 @@ export class PlatformRequest { return this.$ctx.event.request; } } + +injectable(PlatformRequest).scope(ProviderScope.INSTANCE); diff --git a/packages/platform/platform-http/src/common/services/PlatformResponse.ts b/packages/platform/platform-http/src/common/services/PlatformResponse.ts index b248742a805..316063596d7 100644 --- a/packages/platform/platform-http/src/common/services/PlatformResponse.ts +++ b/packages/platform/platform-http/src/common/services/PlatformResponse.ts @@ -1,5 +1,5 @@ import {isArray, isBoolean, isNumber, isStream, isString} from "@tsed/core"; -import {Injectable, lazyInject, ProviderScope, Scope} from "@tsed/di"; +import {injectable, lazyInject, ProviderScope} from "@tsed/di"; import {getStatusMessage} from "@tsed/schema"; import encodeUrl from "encodeurl"; import {OutgoingHttpHeaders, ServerResponse} from "http"; @@ -30,8 +30,6 @@ declare global { * Platform Response abstraction layer. * @platform */ -@Injectable() -@Scope(ProviderScope.INSTANCE) export class PlatformResponse = any> { data: any; @@ -386,3 +384,5 @@ export class PlatformResponse = any> { this.raw.send(data); } } + +injectable(PlatformResponse).scope(ProviderScope.INSTANCE); diff --git a/packages/platform/platform-http/src/common/utils/createContext.spec.ts b/packages/platform/platform-http/src/common/utils/createContext.spec.ts index b0111e99037..e7b9dc21c24 100644 --- a/packages/platform/platform-http/src/common/utils/createContext.spec.ts +++ b/packages/platform/platform-http/src/common/utils/createContext.spec.ts @@ -1,16 +1,26 @@ +import {configuration, injector} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; + import {PlatformTest} from "../../testing/PlatformTest.js"; import {PlatformResponse} from "../services/PlatformResponse.js"; import {createContext} from "./createContext.js"; +vi.mock("@tsed/hooks", async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + $asyncEmit: vi.fn() + }; +}); + async function createContextFixture(reqOpts?: any) { - const injector = PlatformTest.injector; const request = PlatformTest.createRequest(reqOpts); const response = PlatformTest.createResponse(); - injector.settings.logger.level = "info"; - injector.settings.logger.ignoreUrlPatterns = ["/admin", /\/admin2/]; + configuration().logger.level = "info"; + configuration().logger.ignoreUrlPatterns = ["/admin", /\/admin2/]; - const invoke = createContext(injector); + const invoke = createContext(); const ctx = await invoke({request, response}); ctx.response.getRes().on = vi.fn(); @@ -23,7 +33,7 @@ async function createContextFixture(reqOpts?: any) { ctx.logger.flush(); }; - return {call, injector, request, response, ctx}; + return {call, injector: injector(), request, response, ctx}; } describe("createContext", () => { @@ -36,18 +46,18 @@ describe("createContext", () => { // GIVEN const {injector, ctx, call} = await createContextFixture(); - vi.spyOn(injector.hooks, "asyncEmit").mockResolvedValue(undefined); + vi.mocked($asyncEmit).mockResolvedValue(undefined); vi.spyOn(injector.logger, "info").mockReturnValue(undefined); // WHEN await call(); // THEN - expect(injector.hooks.asyncEmit).toHaveBeenCalledWith("$onRequest", [ctx]); + expect($asyncEmit).toHaveBeenCalledWith("$onRequest", [ctx]); await vi.mocked(ctx.response.getRes().on).mock.calls[0][1](ctx); - expect(injector.hooks.asyncEmit).toHaveBeenCalledWith("$onResponse", [ctx]); + expect($asyncEmit).toHaveBeenCalledWith("$onResponse", [ctx]); }); it("should ignore logs", async () => { @@ -57,7 +67,7 @@ describe("createContext", () => { originalUrl: "/admin" }); - vi.spyOn(injector.hooks, "asyncEmit").mockResolvedValue(undefined); + vi.mocked($asyncEmit).mockResolvedValue(undefined); vi.spyOn(injector.logger, "info").mockReturnValue(undefined); // WHEN diff --git a/packages/platform/platform-http/src/common/utils/createContext.ts b/packages/platform/platform-http/src/common/utils/createContext.ts index d6df1762938..d3453c98601 100644 --- a/packages/platform/platform-http/src/common/utils/createContext.ts +++ b/packages/platform/platform-http/src/common/utils/createContext.ts @@ -1,4 +1,4 @@ -import {InjectorService} from "@tsed/di"; +import {configuration, injector} from "@tsed/di"; import {v4} from "uuid"; import {PlatformContext} from "../domain/PlatformContext.js"; @@ -23,13 +23,12 @@ export function buildIgnoreLog(ignoreUrlPatterns: any[] | undefined) { /** * Create the TsED context to wrap request, response, injector, etc... - * @param injector * @ignore */ -export function createContext(injector: InjectorService): (event: IncomingEvent) => PlatformContext { - const ResponseKlass = injector.getProvider(PlatformResponse)?.useClass; - const RequestKlass = injector.getProvider(PlatformRequest)?.useClass; - const {reqIdBuilder = defaultReqIdBuilder, ...loggerOptions} = injector.settings.logger; +export function createContext(): (event: IncomingEvent) => PlatformContext { + const ResponseKlass = injector().getProvider(PlatformResponse)?.useClass; + const RequestKlass = injector().getProvider(PlatformRequest)?.useClass; + const {reqIdBuilder = defaultReqIdBuilder, ...loggerOptions} = configuration().logger; const opts = { ...loggerOptions, diff --git a/packages/platform/platform-http/src/common/utils/createHttpServer.spec.ts b/packages/platform/platform-http/src/common/utils/createHttpServer.spec.ts index 87421349204..139524ecff9 100644 --- a/packages/platform/platform-http/src/common/utils/createHttpServer.spec.ts +++ b/packages/platform/platform-http/src/common/utils/createHttpServer.spec.ts @@ -1,4 +1,4 @@ -import {InjectorService} from "@tsed/di"; +import {configuration, destroyInjector, injector, InjectorService, logger} from "@tsed/di"; import Http from "http"; import {createHttpServer} from "./createHttpServer.js"; @@ -7,22 +7,22 @@ describe("createHttpServer", () => { afterEach(() => { vi.resetAllMocks(); }); + afterEach(() => destroyInjector()); it("should create an instance of Http (http port true)", async () => { - const injector = new InjectorService(); - injector.settings.set("httpPort", true); + configuration().set("httpPort", true); const fn: any = vi.fn(); - const listener: any = createHttpServer(injector, fn); + const listener: any = createHttpServer(fn); - expect(!!injector.get(Http.Server)).toEqual(true); + expect(!!injector().get(Http.Server)).toEqual(true); 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(logger(), "info").mockReturnValue(undefined); + vi.spyOn(logger(), "debug").mockReturnValue(undefined); 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) => { @@ -34,43 +34,40 @@ describe("createHttpServer", () => { await listener(); expect(server.listen).toHaveBeenCalledWith(true, "0.0.0.0"); - expect(injector.logger.info).toHaveBeenCalledWith("Listen server on http://0.0.0.0:8089"); + expect(logger().info).toHaveBeenCalledWith("Listen server on http://0.0.0.0:8089"); }); it("should create a raw object (http port false)", () => { - const injector = new InjectorService(); - injector.settings.set("httpPort", false); + configuration().set("httpPort", false); const fn: any = vi.fn(); - const listener = createHttpServer(injector, fn); + const listener = createHttpServer(fn); - expect(injector.get(Http.Server)).toEqual(null); + expect(injector().get(Http.Server)).toEqual(null); expect(listener).toBeUndefined(); }); it("should create an instance of Http (http port 0)", () => { - const injector = new InjectorService(); - injector.settings.set("httpPort", 0); + configuration().set("httpPort", 0); const fn: any = vi.fn(); - const listener = createHttpServer(injector, fn); + const listener = createHttpServer(fn); - expect(!!injector.get(Http.Server)).toEqual(true); + expect(!!injector().get(Http.Server)).toEqual(true); expect(listener).toBeInstanceOf(Function); }); it("should create an instance of Http (http port + address)", () => { - const injector = new InjectorService(); - injector.settings.set("httpPort", "0.0.0.0:8080"); + configuration().set("httpPort", "0.0.0.0:8080"); const fn: any = vi.fn(); - const listener = createHttpServer(injector, fn); + const listener = createHttpServer(fn); - expect(!!injector.get(Http.Server)).toEqual(true); + expect(!!injector().get(Http.Server)).toEqual(true); expect(listener).toBeInstanceOf(Function); }); diff --git a/packages/platform/platform-http/src/common/utils/createHttpServer.ts b/packages/platform/platform-http/src/common/utils/createHttpServer.ts index d1b67c5d8b6..b044ec03cf9 100644 --- a/packages/platform/platform-http/src/common/utils/createHttpServer.ts +++ b/packages/platform/platform-http/src/common/utils/createHttpServer.ts @@ -1,14 +1,13 @@ -import {InjectorService} from "@tsed/di"; +import {configuration, constant} from "@tsed/di"; import Http from "http"; import {createServer} from "./createServer.js"; -export function createHttpServer(injector: InjectorService, requestListener: Http.RequestListener) { - const {settings} = injector; - const httpOptions = settings.get("httpOptions"); +export function createHttpServer(requestListener: Http.RequestListener) { + const httpOptions = configuration().get("httpOptions"); - return createServer(injector, { - port: settings.get("httpPort"), + return createServer({ + port: constant("httpPort"), type: "http", token: Http.Server, server: () => Http.createServer(httpOptions, requestListener) diff --git a/packages/platform/platform-http/src/common/utils/createHttpsServer.spec.ts b/packages/platform/platform-http/src/common/utils/createHttpsServer.spec.ts index be4458710b4..922b9ff99ef 100644 --- a/packages/platform/platform-http/src/common/utils/createHttpsServer.spec.ts +++ b/packages/platform/platform-http/src/common/utils/createHttpsServer.spec.ts @@ -1,4 +1,4 @@ -import {InjectorService} from "@tsed/di"; +import {configuration, destroyInjector, injector, InjectorService} from "@tsed/di"; import Https from "https"; import {createHttpsServer} from "./createHttpsServer.js"; @@ -7,21 +7,21 @@ describe("createHttpsServer", () => { afterEach(() => { vi.resetAllMocks(); }); + afterEach(() => destroyInjector()); it("should create an instance of Https (Https port true)", async () => { - const injector = new InjectorService(); - injector.settings.set("httpsPort", true); + configuration().set("httpsPort", true); const fn: any = vi.fn(); - const listener = createHttpsServer(injector, fn)!; + const listener = createHttpsServer(fn)!; - expect(!!injector.get(Https.Server)).toEqual(true); + 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(injector().logger, "info").mockReturnValue(undefined); + vi.spyOn(injector().logger, "debug").mockReturnValue(undefined); 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) => { @@ -33,43 +33,40 @@ describe("createHttpsServer", () => { await listener(); expect(server.listen).toHaveBeenCalledWith(true, "0.0.0.0"); - expect(injector.logger.info).toHaveBeenCalledWith("Listen server on https://0.0.0.0:8089"); + expect(injector().logger.info).toHaveBeenCalledWith("Listen server on https://0.0.0.0:8089"); }); it("should create a raw object (Https port false)", () => { - const injector = new InjectorService(); - injector.settings.set("httpsPort", false); + configuration().set("httpsPort", false); const fn: any = vi.fn(); - const listener = createHttpsServer(injector, fn); + const listener = createHttpsServer(fn); - expect(injector.get(Https.Server)).toEqual(null); + expect(injector().get(Https.Server)).toEqual(null); expect(listener).toBeUndefined(); }); it("should create an instance of Https (https port 0)", () => { - const injector = new InjectorService(); - injector.settings.set("httpsPort", 0); + configuration().set("httpsPort", 0); const fn: any = vi.fn(); - const listener = createHttpsServer(injector, fn); + const listener = createHttpsServer(fn); - expect(!!injector.get(Https.Server)).toEqual(true); + expect(!!injector().get(Https.Server)).toEqual(true); expect(listener).toBeInstanceOf(Function); }); it("should create an instance of Https (https port + address)", () => { - const injector = new InjectorService(); - injector.settings.set("httpsPort", "0.0.0.0:8080"); + configuration().set("httpsPort", "0.0.0.0:8080"); const fn: any = vi.fn(); - const listener = createHttpsServer(injector, fn); + const listener = createHttpsServer(fn); - expect(!!injector.get(Https.Server)).toEqual(true); + expect(!!injector().get(Https.Server)).toEqual(true); expect(listener).toBeInstanceOf(Function); }); diff --git a/packages/platform/platform-http/src/common/utils/createHttpsServer.ts b/packages/platform/platform-http/src/common/utils/createHttpsServer.ts index 93c1ebbe7c4..a2d500a3ee7 100644 --- a/packages/platform/platform-http/src/common/utils/createHttpsServer.ts +++ b/packages/platform/platform-http/src/common/utils/createHttpsServer.ts @@ -1,17 +1,16 @@ -import {InjectorService} from "@tsed/di"; +import {configuration, constant, InjectorService} from "@tsed/di"; import Http from "http"; import Https from "https"; import {createServer} from "./createServer.js"; -export function createHttpsServer(injector: InjectorService, requestListener?: Http.RequestListener) { - const {settings} = injector; - const httpsOptions = settings.get("httpsOptions"); +export function createHttpsServer(requestListener?: Http.RequestListener) { + const httpsOptions = configuration().get("httpsOptions"); - return createServer(injector, { + return createServer({ type: "https", token: Https.Server, - port: settings.get("httpsPort"), + port: constant("httpsPort"), server: () => Https.createServer(httpsOptions, requestListener) }); } diff --git a/packages/platform/platform-http/src/common/utils/createInjector.ts b/packages/platform/platform-http/src/common/utils/createInjector.ts index 6b4524ba304..1fd18839101 100644 --- a/packages/platform/platform-http/src/common/utils/createInjector.ts +++ b/packages/platform/platform-http/src/common/utils/createInjector.ts @@ -42,7 +42,7 @@ export function createInjector({adapter, settings = {}}: CreateInjectorOptions) inj.invoke(PlatformAdapter); inj.alias(PlatformAdapter, "PlatformAdapter"); - setLoggerConfiguration(inj); + setLoggerConfiguration(); const instance = inj.get(PlatformAdapter)!; @@ -52,7 +52,7 @@ export function createInjector({adapter, settings = {}}: CreateInjectorOptions) inj.addProvider(token, provider); }); - inj.invoke(PlatformApplication); + DEFAULT_PROVIDERS.map((provider) => inj.get(provider.provide)); return inj; } diff --git a/packages/platform/platform-http/src/common/utils/createServer.ts b/packages/platform/platform-http/src/common/utils/createServer.ts index 6495b3272ad..984de44cd1e 100644 --- a/packages/platform/platform-http/src/common/utils/createServer.ts +++ b/packages/platform/platform-http/src/common/utils/createServer.ts @@ -1,5 +1,5 @@ import {getHostInfoFromPort, ReturnHostInfoFromPort} from "@tsed/core"; -import {InjectorService, ProviderScope, TokenProvider} from "@tsed/di"; +import {configuration, injector, logger, ProviderScope, TokenProvider} from "@tsed/di"; import Http from "http"; import Http2 from "http2"; import Https from "https"; @@ -9,33 +9,29 @@ import {listenServer} from "./listenServer.js"; export interface CreateServerOptions { token: TokenProvider; type: "http" | "https"; - port: string | false; + port: string | false | undefined; listen?: (hostInfo: ReturnHostInfoFromPort) => Promise; server: () => Http.Server | Https.Server | Http2.Http2Server; } export type CreateServerReturn = () => Promise; -export function createServer( - injector: InjectorService, - {token, type, port, server: get, listen}: CreateServerOptions -): undefined | CreateServerReturn { - const {settings} = injector; +export function createServer({token, type, port, server: get, listen}: CreateServerOptions): undefined | CreateServerReturn { const server = port !== false ? get() : null; - injector.addProvider(token, { + injector().addProvider(token, { scope: ProviderScope.SINGLETON, useValue: server }); - injector.invoke(token); + injector().invoke(token); if (server) { const hostInfo = getHostInfoFromPort(type, port); return async () => { const url = `${hostInfo.protocol}://${hostInfo.address}:${port}`; - injector.logger.debug(`Start server on ${url}`); + logger().debug(`Start server on ${url}`); await (listen ? listen(hostInfo) : listenServer(server, hostInfo)); @@ -46,8 +42,8 @@ export function createServer( hostInfo.port = address.port; } - injector.logger.info(`Listen server on ${hostInfo.toString()}`); - settings.set(`${type}Port`, `${hostInfo.address}:${hostInfo.port}`); + logger().info(`Listen server on ${hostInfo.toString()}`); + configuration().set(`${type}Port`, `${hostInfo.address}:${hostInfo.port}`); return server; }; diff --git a/packages/platform/platform-http/src/common/utils/registerPlatformAdapter.ts b/packages/platform/platform-http/src/common/utils/registerPlatformAdapter.ts deleted file mode 100644 index ae41bebcfbe..00000000000 --- a/packages/platform/platform-http/src/common/utils/registerPlatformAdapter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Type} from "@tsed/core"; - -import {PlatformTest} from "../../testing/PlatformTest.js"; -import {PlatformBuilder} from "../builder/PlatformBuilder.js"; -import {PlatformAdapter} from "../services/PlatformAdapter.js"; - -export function registerPlatformAdapter(adapter: Type>) { - PlatformTest.adapter = adapter; - PlatformBuilder.adapter = adapter; -} diff --git a/packages/platform/platform-http/src/testing/PlatformTest.ts b/packages/platform/platform-http/src/testing/PlatformTest.ts index a06faba558b..4b780a70a75 100644 --- a/packages/platform/platform-http/src/testing/PlatformTest.ts +++ b/packages/platform/platform-http/src/testing/PlatformTest.ts @@ -1,10 +1,11 @@ import {Type} from "@tsed/core"; -import {DITest, hasInjector, injector, InjectorService} from "@tsed/di"; +import {DITest, injector, InjectorService} from "@tsed/di"; import accepts from "accepts"; import type {IncomingMessage, RequestListener, ServerResponse} from "http"; import {PlatformBuilder} from "../common/builder/PlatformBuilder.js"; import {PlatformContext, PlatformContextOptions} from "../common/domain/PlatformContext.js"; +import {adapter as $adapter} from "../common/fn/adapter.js"; import {PlatformAdapter, PlatformBuilderSettings} from "../common/services/PlatformAdapter.js"; import {PlatformApplication} from "../common/services/PlatformApplication.js"; import {createInjector} from "../common/utils/createInjector.js"; @@ -15,8 +16,6 @@ import {FakeResponse} from "./FakeResponse.js"; * @platform */ export class PlatformTest extends DITest { - public static adapter: Type; - static async create(settings: Partial = {}) { PlatformTest.createInjector(getConfiguration(settings)); await DITest.createContainer(); @@ -39,10 +38,20 @@ export class PlatformTest extends DITest { * @param settings * @returns {Promise} */ - static bootstrap(mod: any, {listen, ...settings}: Partial = {}): () => Promise { + static bootstrap( + mod: any, + { + listen, + ...settings + }: Partial< + PlatformBuilderSettings & { + listen: boolean; + } + > = {} + ): () => Promise { return async function before(): Promise { let instance: PlatformBuilder; - const adapter: Type = settings.platform || settings.adapter || PlatformTest.adapter; + const adapter: Type = $adapter(settings.platform || settings.adapter); /* istanbul ignore next */ if (!adapter) { @@ -76,9 +85,7 @@ export class PlatformTest extends DITest { */ static inject(targets: any[], func: (...args: any[]) => Promise | T): () => Promise { return async (): Promise => { - if (!hasInjector()) { - await PlatformTest.create(); - } + await PlatformTest.create(); const inj: InjectorService = injector(); const deps = []; diff --git a/packages/platform/platform-http/vitest.config.mts b/packages/platform/platform-http/vitest.config.mts index f5429eb293b..10afad17e66 100644 --- a/packages/platform/platform-http/vitest.config.mts +++ b/packages/platform/platform-http/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.21, - branches: 95.14, - functions: 95.31, - lines: 97.21 + statements: 96.88, + branches: 95.5, + functions: 94.11, + lines: 96.88 } } } From ef4b43a51a10302c2074578605b4a25b55960538 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:16:18 +0100 Subject: [PATCH 26/32] refactor(platform-express): use functional API to inject/declare services --- .../src/components/PlatformExpress.ts | 36 +++++++++---------- .../services/PlatformExpressHandler.spec.ts | 26 ++++++++++---- .../platform-express/test/app/Server.ts | 1 + .../platform-express/vitest.config.mts | 4 +-- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/platform/platform-express/src/components/PlatformExpress.ts b/packages/platform/platform-express/src/components/PlatformExpress.ts index e7a3811e8c7..f0b9f0a11ca 100644 --- a/packages/platform/platform-express/src/components/PlatformExpress.ts +++ b/packages/platform/platform-express/src/components/PlatformExpress.ts @@ -1,7 +1,9 @@ import {catchAsyncError, Env, isFunction, Type} from "@tsed/core"; -import {InjectorService, runInContext} from "@tsed/di"; +import {constant, inject, logger, runInContext} from "@tsed/di"; import {PlatformExceptions} from "@tsed/platform-exceptions"; import { + adapter, + application, createContext, PlatformAdapter, PlatformBuilder, @@ -9,11 +11,9 @@ import { PlatformHandler, PlatformMulter, PlatformMulterSettings, - PlatformProvider, PlatformStaticsOptions } from "@tsed/platform-http"; import {PlatformHandlerMetadata, PlatformHandlerType, PlatformLayer} from "@tsed/platform-router"; -import type {PlatformViews} from "@tsed/platform-views"; import {OptionsJson, OptionsText, OptionsUrlencoded} from "body-parser"; import Express from "express"; import {IncomingMessage, ServerResponse} from "http"; @@ -56,7 +56,6 @@ declare global { * @platform * @express */ -@PlatformProvider() export class PlatformExpress extends PlatformAdapter { static readonly NAME = "express"; @@ -68,9 +67,8 @@ export class PlatformExpress extends PlatformAdapter { ]; #multer: typeof multer; - constructor(injector: InjectorService) { - super(injector); - + constructor() { + super(); import("multer").then(({default: multer}) => (this.#multer = multer)); } @@ -99,18 +97,17 @@ export class PlatformExpress extends PlatformAdapter { } async beforeLoadRoutes() { - const injector = this.injector; const {app} = this; // disable x-powered-by header - injector.settings.get("env") === Env.PROD && app.getApp().disable("x-powered-by"); + constant("env") === Env.PROD && app.getApp().disable("x-powered-by"); await this.configureViewsEngine(); } afterLoadRoutes() { const {app} = this; - const platformExceptions = this.injector.get(PlatformExceptions)!; + const platformExceptions = inject(PlatformExceptions)!; // NOT FOUND app.use((req: any, res: any, next: any) => { @@ -170,8 +167,8 @@ export class PlatformExpress extends PlatformAdapter { } useContext(): this { - const {app} = this; - const invoke = createContext(this.injector); + const invoke = createContext(); + const app = application(); app.use(async (request: any, response: any, next: any) => { const $ctx = invoke({request, response}); @@ -186,7 +183,7 @@ export class PlatformExpress extends PlatformAdapter { } createApp() { - const app = this.injector.settings.get("express.app") || Express(); + const app = constant("express.app") || Express(); return { app, @@ -225,7 +222,7 @@ export class PlatformExpress extends PlatformAdapter { } bodyParser(type: "json" | "text" | "urlencoded", additionalOptions: any = {}): any { - const opts = this.injector.settings.get(`express.bodyParser.${type}`); + const opts = constant(`express.bodyParser.${type}`); let parser: any = Express[type]; let options: OptionsJson & OptionsText & OptionsUrlencoded = {}; @@ -240,7 +237,7 @@ export class PlatformExpress extends PlatformAdapter { } options.verify = (req: IncomingMessage & {rawBody: Buffer}, _res: ServerResponse, buffer: Buffer) => { - const rawBody = this.injector.settings.get(`rawBody`); + const rawBody = constant(`rawBody`); if (rawBody) { req.rawBody = buffer; @@ -253,15 +250,14 @@ export class PlatformExpress extends PlatformAdapter { } private async configureViewsEngine() { - const injector = this.injector; const {app} = this; try { - const {exists, disabled} = this.injector.settings.get("views") || {}; + const {exists, disabled} = constant<{exists?: boolean; disabled?: boolean}>("views") || {}; if (exists && !disabled) { const {PlatformViews} = await import("@tsed/platform-views"); - const platformViews = injector.get(PlatformViews)!; + const platformViews = inject(PlatformViews)!; const express = app.getApp(); platformViews.getEngines().forEach(({extension, engine}) => { @@ -273,7 +269,7 @@ export class PlatformExpress extends PlatformAdapter { } } catch (error) { // istanbul ignore next - injector.logger.warn({ + logger().warn({ event: "PLATFORM_VIEWS_ERROR", message: "Unable to configure the PlatformViews service on your environment.", error @@ -281,3 +277,5 @@ export class PlatformExpress extends PlatformAdapter { } } } + +adapter(PlatformExpress); diff --git a/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts b/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts index d4d717bf821..a4de3f31398 100644 --- a/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts +++ b/packages/platform/platform-express/src/services/PlatformExpressHandler.spec.ts @@ -1,15 +1,29 @@ +import {DITest, inject} from "@tsed/di"; +import {PlatformRouters} from "@tsed/platform-router"; + import {PlatformExpressHandler} from "./PlatformExpressHandler.js"; vi.mock("@tsed/platform-http"); describe("PlatformExpressHandler", () => { + beforeEach(() => + DITest.create({ + imports: [ + { + token: PlatformRouters, + use: { + prebuild: vi.fn(), + hooks: { + on: vi.fn().mockReturnThis() + } + } + } + ] + }) + ); + afterEach(() => DITest.reset()); it("should call middleware", async () => { - const instance = new PlatformExpressHandler({ - hooks: { - on: vi.fn().mockReturnThis() - } - } as any); - + const instance = inject(PlatformExpressHandler); const response: any = {}; const $ctx: any = { getRequest: vi.fn().mockReturnThis(), diff --git a/packages/platform/platform-express/test/app/Server.ts b/packages/platform/platform-express/test/app/Server.ts index 5f4872f3d74..97cadc9331c 100644 --- a/packages/platform/platform-express/test/app/Server.ts +++ b/packages/platform/platform-express/test/app/Server.ts @@ -1,4 +1,5 @@ import "@tsed/ajv"; +import "@tsed/swagger"; import "../../src/index.js"; import {Configuration, Constant, Inject} from "@tsed/di"; diff --git a/packages/platform/platform-express/vitest.config.mts b/packages/platform/platform-express/vitest.config.mts index 62b9336d242..1df44539a97 100644 --- a/packages/platform/platform-express/vitest.config.mts +++ b/packages/platform/platform-express/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 96.61, + statements: 96.63, branches: 95, functions: 100, - lines: 96.61 + lines: 96.63 } } } From 4c9c32f8fae6f17c0c0a14138cf540fa114817b7 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:17:10 +0100 Subject: [PATCH 27/32] refactor(platform-koa): use functional API to inject/declare services --- .../src/components/PlatformKoa.ts | 27 +++++++++---------- .../platform/platform-koa/test/app/Server.ts | 3 ++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/platform/platform-koa/src/components/PlatformKoa.ts b/packages/platform/platform-koa/src/components/PlatformKoa.ts index a8cba1e2d03..e1ae74df406 100644 --- a/packages/platform/platform-koa/src/components/PlatformKoa.ts +++ b/packages/platform/platform-koa/src/components/PlatformKoa.ts @@ -1,15 +1,16 @@ import KoaRouter from "@koa/router"; import {catchAsyncError, isFunction, Type} from "@tsed/core"; -import {runInContext} from "@tsed/di"; +import {constant, inject, runInContext} from "@tsed/di"; import {PlatformExceptions} from "@tsed/platform-exceptions"; import { + adapter, + application, createContext, PlatformAdapter, PlatformBuilder, PlatformHandler, PlatformMulter, PlatformMulterSettings, - PlatformProvider, PlatformRequest, PlatformResponse, PlatformStaticsOptions @@ -54,7 +55,6 @@ KoaRouter.prototype.match = function match(...args: any[]) { * @platform * @koa */ -@PlatformProvider() export class PlatformKoa extends PlatformAdapter { static readonly NAME = "koa"; @@ -102,9 +102,7 @@ export class PlatformKoa extends PlatformAdapter { } mapLayers(layers: PlatformLayer[]) { - const {settings} = this.injector; - const {app} = this; - const options = settings.get("koa.router", {}); + const options = constant("koa.router", {}); const rawRouter = new KoaRouter(options) as any; layers.forEach((layer) => { @@ -118,7 +116,7 @@ export class PlatformKoa extends PlatformAdapter { } }); - app.getApp().use(rawRouter.routes()).use(rawRouter.allowedMethods()); + application().getApp().use(rawRouter.routes()).use(rawRouter.allowedMethods()); } mapHandler(handler: Function, metadata: PlatformHandlerMetadata) { @@ -140,11 +138,10 @@ export class PlatformKoa extends PlatformAdapter { } useContext(): this { - const {app} = this; - const invoke = createContext(this.injector); - const platformExceptions = this.injector.get(PlatformExceptions); + const invoke = createContext(); + const platformExceptions = inject(PlatformExceptions); - app.use((koaContext: Context, next: Next) => { + application().use((koaContext: Context, next: Next) => { const $ctx = invoke({ request: koaContext.request as any, response: koaContext.response as any, @@ -172,7 +169,7 @@ export class PlatformKoa extends PlatformAdapter { } createApp() { - const app = this.injector.settings.get("koa.app") || new Koa(); + const app = constant("koa.app") || new Koa(); koaQs(app, "extended"); return { @@ -191,8 +188,8 @@ export class PlatformKoa extends PlatformAdapter { return staticsMiddleware(options); } - bodyParser(type: "json" | "urlencoded" | "raw" | "text", additionalOptions: any = {}): any { - const opts = this.injector.settings.get(`koa.bodyParser`); + bodyParser(_: "json" | "urlencoded" | "raw" | "text", additionalOptions: any = {}): any { + const opts = constant(`koa.bodyParser`); let parser: any = koaBodyParser; let options: Options = {}; @@ -205,3 +202,5 @@ export class PlatformKoa extends PlatformAdapter { return parser({...options, ...additionalOptions}); } } + +adapter(PlatformKoa); diff --git a/packages/platform/platform-koa/test/app/Server.ts b/packages/platform/platform-koa/test/app/Server.ts index c1ccc8b357c..17ee2b31249 100644 --- a/packages/platform/platform-koa/test/app/Server.ts +++ b/packages/platform/platform-koa/test/app/Server.ts @@ -1,4 +1,5 @@ import "@tsed/ajv"; +import "@tsed/swagger"; import {Configuration, Inject} from "@tsed/di"; import {PlatformApplication} from "@tsed/platform-http"; @@ -31,7 +32,7 @@ export class Server { session( { key: "connect.sid" /** (string) cookie key (default is koa.sess) */, - /** (number || 'session') maxAge in ms (default is 1 days) */ + /** (number || 'session') maxAge in ms (default is 1 day) */ /** 'session' will result in a cookie that expires when session/browser is closed */ /** Warning: If a session cookie is stolen, this cookie will never expire */ maxAge: 86400000, From 6acfaeb212f7016f751b0d376f3032b27762c3f5 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 09:45:28 +0100 Subject: [PATCH 28/32] refactor(normalize-path): move normalize-path module to third-parties --- package.json | 7 +++---- packages/specs/swagger/tsconfig.json | 2 +- packages/third-parties/components-scan/tsconfig.json | 2 +- packages/third-parties/formio/tsconfig.json | 2 +- .../{utils => third-parties}/normalize-path/.npmignore | 0 .../{utils => third-parties}/normalize-path/package.json | 4 ++-- packages/{utils => third-parties}/normalize-path/readme.md | 0 .../{utils => third-parties}/normalize-path/src/index.ts | 0 .../normalize-path/src/normalizePath.spec.ts | 0 .../normalize-path/src/normalizePath.ts | 0 .../normalize-path/tsconfig.esm.json | 2 +- .../{utils => third-parties}/normalize-path/tsconfig.json | 2 +- .../normalize-path/tsconfig.spec.json | 2 +- .../normalize-path/tsconfig.tsbuildinfo | 0 .../normalize-path/vitest.config.mts | 0 tsconfig.json | 2 +- yarn.lock | 4 ++-- 17 files changed, 14 insertions(+), 15 deletions(-) rename packages/{utils => third-parties}/normalize-path/.npmignore (100%) rename packages/{utils => third-parties}/normalize-path/package.json (92%) rename packages/{utils => third-parties}/normalize-path/readme.md (100%) rename packages/{utils => third-parties}/normalize-path/src/index.ts (100%) rename packages/{utils => third-parties}/normalize-path/src/normalizePath.spec.ts (100%) rename packages/{utils => third-parties}/normalize-path/src/normalizePath.ts (100%) rename packages/{utils => third-parties}/normalize-path/tsconfig.esm.json (95%) rename packages/{utils => third-parties}/normalize-path/tsconfig.json (91%) rename packages/{utils => third-parties}/normalize-path/tsconfig.spec.json (98%) rename packages/{utils => third-parties}/normalize-path/tsconfig.tsbuildinfo (100%) rename packages/{utils => third-parties}/normalize-path/vitest.config.mts (100%) diff --git a/package.json b/package.json index 73a555c24e5..042d2196dff 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,13 @@ "test:ci": "yarn test:lint && yarn test:core && yarn test:specs && yarn test:platform && yarn test:integration && yarn test:graphql && yarn test:orm && yarn test:security && yarn test:third-parties", "test:lint": "eslint '**/*.{ts,js}'", "test:lint:fix": "eslint '**/*.{ts,js}' --fix", - "test:core": "lerna run test:ci --scope '@tsed/{core,di,hooks,platform-http,engines,normalize-path}' --stream --concurrency 2", - "test:platform": "lerna run test:ci --ignore '@tsed/platform-{express,koa}' --scope '@tsed/platform-*' --stream --concurrency 2", - "test:integration": "lerna run test:ci --scope '@tsed/platform-{express,koa}' --stream --concurrency 2", + "test:core": "lerna run test:ci --scope '@tsed/{core,di,hooks,engines}' --stream --concurrency 2", + "test:platform": "lerna run test:ci --scope '@tsed/platform-*' --stream --concurrency 2", "test:orm": "lerna run test:ci --scope '@tsed/{adapters,adapters-redis,mikro-orm,mongoose,objection,prisma}' --stream --concurrency 4", "test:graphql": "lerna run test:ci --scope '@tsed/{apollo,typegraphql}' --stream", "test:security": "lerna run test:ci --scope '@tsed/{jwks,oidc-provider,passport,oidc-provider-plugin-wildcard-redirect-uri}' --stream", "test:specs": "lerna run test --scope '@tsed/{ajv,exceptions,json-mapper,schema,swagger}' --stream --concurrency 2", - "test:third-parties": "lerna run test:ci --scope '@tsed/{agenda,bullmq,components-scan,event-emitter,formio,pulse,sse,socketio,stripe,temporal,terminus,vike,schema-formio,formio}' --stream --concurrency 1", + "test:third-parties": "lerna run test:ci --scope '@tsed/{normalize-path,agenda,bullmq,components-scan,event-emitter,formio,pulse,sse,socketio,stripe,temporal,terminus,vike,schema-formio,formio}' --stream --concurrency 1", "coverage": "merge-istanbul --out coverage/coverage-final.json \"**/packages/**/coverage/coverage-final.json\" && nyc report --reporter text --reporter html --reporter lcov -t coverage --report-dir coverage", "barrels": "lerna run barrels", "build": "monorepo build --verbose", diff --git a/packages/specs/swagger/tsconfig.json b/packages/specs/swagger/tsconfig.json index e92a8edb48b..1b737a96561 100644 --- a/packages/specs/swagger/tsconfig.json +++ b/packages/specs/swagger/tsconfig.json @@ -25,7 +25,7 @@ "path": "../schema/tsconfig.json" }, { - "path": "../../utils/normalize-path/tsconfig.json" + "path": "../../third-parties/normalize-path/tsconfig.json" }, { "path": "./tsconfig.esm.json" diff --git a/packages/third-parties/components-scan/tsconfig.json b/packages/third-parties/components-scan/tsconfig.json index 5c4ed14eb23..a88b978b9ec 100644 --- a/packages/third-parties/components-scan/tsconfig.json +++ b/packages/third-parties/components-scan/tsconfig.json @@ -13,7 +13,7 @@ "path": "../../di/tsconfig.json" }, { - "path": "../../utils/normalize-path/tsconfig.json" + "path": "../normalize-path/tsconfig.json" }, { "path": "./tsconfig.esm.json" diff --git a/packages/third-parties/formio/tsconfig.json b/packages/third-parties/formio/tsconfig.json index 30de59994e2..3cb7bf45ac1 100644 --- a/packages/third-parties/formio/tsconfig.json +++ b/packages/third-parties/formio/tsconfig.json @@ -22,7 +22,7 @@ "path": "../formio-types/tsconfig.json" }, { - "path": "../../utils/normalize-path/tsconfig.json" + "path": "../normalize-path/tsconfig.json" }, { "path": "./tsconfig.esm.json" diff --git a/packages/utils/normalize-path/.npmignore b/packages/third-parties/normalize-path/.npmignore similarity index 100% rename from packages/utils/normalize-path/.npmignore rename to packages/third-parties/normalize-path/.npmignore diff --git a/packages/utils/normalize-path/package.json b/packages/third-parties/normalize-path/package.json similarity index 92% rename from packages/utils/normalize-path/package.json rename to packages/third-parties/normalize-path/package.json index 786caab6ee7..8fdd50e1bcc 100644 --- a/packages/utils/normalize-path/package.json +++ b/packages/third-parties/normalize-path/package.json @@ -4,8 +4,8 @@ "type": "module", "version": "8.0.0-rc.5", "source": "./src/index.ts", - "main": "./lib/esm/index.js", - "module": "./lib/esm/index.js", + "main": "lib/esm/index.js", + "module": "lib/esm/index.js", "typings": "./lib/types/index.d.ts", "exports": { ".": { diff --git a/packages/utils/normalize-path/readme.md b/packages/third-parties/normalize-path/readme.md similarity index 100% rename from packages/utils/normalize-path/readme.md rename to packages/third-parties/normalize-path/readme.md diff --git a/packages/utils/normalize-path/src/index.ts b/packages/third-parties/normalize-path/src/index.ts similarity index 100% rename from packages/utils/normalize-path/src/index.ts rename to packages/third-parties/normalize-path/src/index.ts diff --git a/packages/utils/normalize-path/src/normalizePath.spec.ts b/packages/third-parties/normalize-path/src/normalizePath.spec.ts similarity index 100% rename from packages/utils/normalize-path/src/normalizePath.spec.ts rename to packages/third-parties/normalize-path/src/normalizePath.spec.ts diff --git a/packages/utils/normalize-path/src/normalizePath.ts b/packages/third-parties/normalize-path/src/normalizePath.ts similarity index 100% rename from packages/utils/normalize-path/src/normalizePath.ts rename to packages/third-parties/normalize-path/src/normalizePath.ts diff --git a/packages/utils/normalize-path/tsconfig.esm.json b/packages/third-parties/normalize-path/tsconfig.esm.json similarity index 95% rename from packages/utils/normalize-path/tsconfig.esm.json rename to packages/third-parties/normalize-path/tsconfig.esm.json index 8954049da4a..ccf3df0d458 100644 --- a/packages/utils/normalize-path/tsconfig.esm.json +++ b/packages/third-parties/normalize-path/tsconfig.esm.json @@ -1,7 +1,7 @@ { "extends": "@tsed/typescript/tsconfig.node.json", "compilerOptions": { - "baseUrl": ".", + "baseUrl": "./", "rootDir": "src", "outDir": "./lib/esm", "declarationDir": "./lib/types", diff --git a/packages/utils/normalize-path/tsconfig.json b/packages/third-parties/normalize-path/tsconfig.json similarity index 91% rename from packages/utils/normalize-path/tsconfig.json rename to packages/third-parties/normalize-path/tsconfig.json index ffa3a8cd7e1..cbf69c6b937 100644 --- a/packages/utils/normalize-path/tsconfig.json +++ b/packages/third-parties/normalize-path/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@tsed/typescript/tsconfig.node.json", "compilerOptions": { - "baseUrl": ".", + "baseUrl": "./", "noEmit": true }, "include": [], diff --git a/packages/utils/normalize-path/tsconfig.spec.json b/packages/third-parties/normalize-path/tsconfig.spec.json similarity index 98% rename from packages/utils/normalize-path/tsconfig.spec.json rename to packages/third-parties/normalize-path/tsconfig.spec.json index e1ebafb229c..a018c43134a 100644 --- a/packages/utils/normalize-path/tsconfig.spec.json +++ b/packages/third-parties/normalize-path/tsconfig.spec.json @@ -1,7 +1,7 @@ { "extends": "@tsed/typescript/tsconfig.node.json", "compilerOptions": { - "baseUrl": ".", + "baseUrl": "./", "rootDir": "../..", "declaration": false, "composite": false, diff --git a/packages/utils/normalize-path/tsconfig.tsbuildinfo b/packages/third-parties/normalize-path/tsconfig.tsbuildinfo similarity index 100% rename from packages/utils/normalize-path/tsconfig.tsbuildinfo rename to packages/third-parties/normalize-path/tsconfig.tsbuildinfo diff --git a/packages/utils/normalize-path/vitest.config.mts b/packages/third-parties/normalize-path/vitest.config.mts similarity index 100% rename from packages/utils/normalize-path/vitest.config.mts rename to packages/third-parties/normalize-path/vitest.config.mts diff --git a/tsconfig.json b/tsconfig.json index 73cbf6892b2..68b0cd8d16a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -66,7 +66,7 @@ "path": "./packages/platform/platform-views/tsconfig.json" }, { - "path": "./packages/utils/normalize-path/tsconfig.json" + "path": "./packages/third-parties/normalize-path/tsconfig.json" }, { "path": "./packages/third-parties/components-scan/tsconfig.json" diff --git a/yarn.lock b/yarn.lock index ed2faaceed9..b5119cc9fb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7205,9 +7205,9 @@ __metadata: languageName: node linkType: hard -"@tsed/normalize-path@workspace:*, @tsed/normalize-path@workspace:packages/utils/normalize-path": +"@tsed/normalize-path@workspace:*, @tsed/normalize-path@workspace:packages/third-parties/normalize-path": version: 0.0.0-use.local - resolution: "@tsed/normalize-path@workspace:packages/utils/normalize-path" + resolution: "@tsed/normalize-path@workspace:packages/third-parties/normalize-path" dependencies: "@tsed/barrels": "workspace:*" "@tsed/typescript": "workspace:*" From 501ac712ba8a71f22ee59e97f97510f1b6bf7661 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sat, 23 Nov 2024 12:02:41 +0100 Subject: [PATCH 29/32] refactor(platform-serverless): use functional API to inject/declare services --- .../src/PlatformServerlessTest.ts | 7 ++-- .../src/builder/PlatformServerless.ts | 35 ++++++++----------- .../src/builder/PlatformServerlessHandler.ts | 25 +++++-------- .../platform-serverless/vitest.config.mts | 8 ++--- 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts index 087bc1fa1e2..92ad53be273 100644 --- a/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts +++ b/packages/platform/platform-serverless-testing/src/PlatformServerlessTest.ts @@ -1,5 +1,5 @@ import {nameOf, Type} from "@tsed/core"; -import {destroyInjector, DITest, hasInjector} from "@tsed/di"; +import {destroyInjector, DITest} from "@tsed/di"; import type {PlatformBuilder, PlatformBuilderSettings} from "@tsed/platform-http"; import type { APIGatewayEventDefaultAuthorizerContext, @@ -193,8 +193,7 @@ export class PlatformServerlessTest extends DITest { if (PlatformServerlessTest.instance) { await PlatformServerlessTest.instance.stop(); } - if (hasInjector()) { - await destroyInjector(); - } + + await destroyInjector(); } } diff --git a/packages/platform/platform-serverless/src/builder/PlatformServerless.ts b/packages/platform/platform-serverless/src/builder/PlatformServerless.ts index f07fffc76f1..9a5e8ba4d4b 100644 --- a/packages/platform/platform-serverless/src/builder/PlatformServerless.ts +++ b/packages/platform/platform-serverless/src/builder/PlatformServerless.ts @@ -1,6 +1,7 @@ import {Env, Type} from "@tsed/core"; -import {createContainer, injector, InjectorService, setLoggerConfiguration} from "@tsed/di"; -import {$log, Logger} from "@tsed/logger"; +import {configuration, constant, createContainer, destroyInjector, injector, InjectorService, setLoggerConfiguration} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; +import {$log} from "@tsed/logger"; import {getOperationsRoutes, JsonEntityStore} from "@tsed/schema"; import type {Context, Handler} from "aws-lambda"; import type {HTTPMethod, Instance} from "find-my-way"; @@ -20,17 +21,15 @@ export interface PlatformServerlessSettings extends Partial */ export class PlatformServerless { readonly name: string = "PlatformServerless"; - - private _injector: InjectorService; private _router: Instance; private _promise: Promise; get injector(): InjectorService { - return this._injector; + return injector(); } get settings() { - return this.injector.settings; + return configuration(); } get promise() { @@ -88,7 +87,7 @@ export class PlatformServerless { } public callbacks(tokens: Type | Type[] = [], callbacks: any = {}): Record { - return this.settings + return configuration() .get("lambda", []) .concat(tokens) .reduce((callbacks, token) => { @@ -115,12 +114,11 @@ export class PlatformServerless { } public async ready() { - await this.injector.emit("$onReady"); + await $asyncEmit("$onReady"); } public async stop() { - await this.injector.emit("$onDestroy"); - return this.injector.destroy(); + await destroyInjector(); } public init() { @@ -148,8 +146,6 @@ export class PlatformServerless { context, responseStream, id: getRequestId(event, context), - logger: this.injector.logger as Logger, - injector: this.injector, endpoint: entity }); @@ -183,12 +179,11 @@ export class PlatformServerless { } protected createInjector(settings: any) { - this._injector = injector(); - this.injector.logger = $log; - this.injector.settings.set(settings); + injector().logger = $log; + injector().settings.set(settings); // istanbul ignore next - if (this.injector.settings.get("env") === Env.TEST && !settings?.logger?.level) { + if (constant("env") === Env.TEST && !settings?.logger?.level) { $log.stop(); } @@ -198,12 +193,10 @@ export class PlatformServerless { protected async loadInjector() { const container = createContainer(); - setLoggerConfiguration(this.injector); - - await this.injector.emit("$beforeInit"); + setLoggerConfiguration(); - await this.injector.load(container); + await injector().load(container); - await this.injector.emit("$afterInit"); + await $asyncEmit("$afterInit"); } } diff --git a/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts b/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts index 334d503dd32..7840676a35b 100644 --- a/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts +++ b/packages/platform/platform-serverless/src/builder/PlatformServerlessHandler.ts @@ -1,9 +1,9 @@ import {pipeline} from "node:stream/promises"; import {AnyPromiseResult, AnyToPromise, isSerializable, isStream} from "@tsed/core"; -import {BaseContext, Inject, Injectable, InjectorService, LazyInject, ProviderScope, runInContext, TokenProvider} from "@tsed/di"; +import {BaseContext, inject, injectable, lazyInject, ProviderScope, runInContext, TokenProvider} from "@tsed/di"; +import {$asyncEmit} from "@tsed/hooks"; import {serialize} from "@tsed/json-mapper"; -import type {PlatformExceptions} from "@tsed/platform-exceptions"; import {DeserializerPipe, PlatformParams, ValidationPipe} from "@tsed/platform-params"; import {ServerlessContext} from "../domain/ServerlessContext.js"; @@ -11,26 +11,15 @@ import type {ServerlessEvent} from "../domain/ServerlessEvent.js"; import {ServerlessResponseStream} from "../domain/ServerlessResponseStream.js"; import {setResponseHeaders} from "../utils/setResponseHeaders.js"; -@Injectable({ - scope: ProviderScope.SINGLETON, - imports: [DeserializerPipe, ValidationPipe] -}) export class PlatformServerlessHandler { - @Inject() - protected injector: InjectorService; - - @Inject() - protected params: PlatformParams; - - @LazyInject(() => import("@tsed/platform-exceptions")) - protected exceptionsManager: Promise; + protected params = inject(PlatformParams); createHandler(token: TokenProvider, propertyKey: string | symbol) { const promisedHandler = this.params.compileHandler({token, propertyKey}); return ($ctx: ServerlessContext) => { return runInContext($ctx, async () => { - await this.injector.emit("$onRequest", $ctx); + await $asyncEmit("$onRequest", $ctx); try { const resolver = new AnyToPromise(); @@ -40,7 +29,7 @@ export class PlatformServerlessHandler { this.processResult(result, $ctx); } catch (er) { $ctx.response.status(500).body(er); - const exceptions = await this.exceptionsManager; + const exceptions = await lazyInject(() => import("@tsed/platform-exceptions")); await exceptions.catch(er, $ctx as unknown as BaseContext); } @@ -53,7 +42,7 @@ export class PlatformServerlessHandler { private async flush($ctx: ServerlessContext) { const body: unknown = $ctx.isHttpEvent() && !$ctx.isAuthorizerEvent() ? await this.makeHttpResponse($ctx) : $ctx.response.getBody(); - await this.injector.emit("$onResponse", $ctx); + await $asyncEmit("$onResponse", $ctx); $ctx.logger.flush(); $ctx.destroy(); @@ -128,3 +117,5 @@ export class PlatformServerlessHandler { } } } + +injectable(PlatformServerlessHandler).scope(ProviderScope.SINGLETON).imports([DeserializerPipe, ValidationPipe]); diff --git a/packages/platform/platform-serverless/vitest.config.mts b/packages/platform/platform-serverless/vitest.config.mts index 5d52e5c80d0..680b0d8fbb8 100644 --- a/packages/platform/platform-serverless/vitest.config.mts +++ b/packages/platform/platform-serverless/vitest.config.mts @@ -10,10 +10,10 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 98.53, - branches: 96.36, - functions: 100, - lines: 98.53 + statements: 98.2, + branches: 96.34, + functions: 98.75, + lines: 98.2 } } } From d73117137fb833aae082bfafea6d86d18ea31dce Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 10:12:35 +0100 Subject: [PATCH 30/32] fix(bullmq): change hook to handle correct DI initialization --- .../bullmq/src/BullMQModule.spec.ts | 63 +++---- .../third-parties/bullmq/src/BullMQModule.ts | 53 +++--- .../src/dispatchers/JobDispatcher.spec.ts | 154 ++++++++++-------- .../bullmq/src/dispatchers/JobDispatcher.ts | 20 +-- .../bullmq/src/utils/createQueueProvider.ts | 21 +-- .../bullmq/src/utils/createWorkerProvider.ts | 21 ++- .../third-parties/bullmq/vitest.config.mts | 11 +- 7 files changed, 175 insertions(+), 168 deletions(-) diff --git a/packages/third-parties/bullmq/src/BullMQModule.spec.ts b/packages/third-parties/bullmq/src/BullMQModule.spec.ts index a2d409c73d3..73d3aba9cd3 100644 --- a/packages/third-parties/bullmq/src/BullMQModule.spec.ts +++ b/packages/third-parties/bullmq/src/BullMQModule.spec.ts @@ -4,6 +4,7 @@ import {catchAsyncError} from "@tsed/core"; import {PlatformTest} from "@tsed/platform-http/testing"; import {Queue, Worker} from "bullmq"; import {anything, instance, mock, verify, when} from "ts-mockito"; +import {beforeEach} from "vitest"; import {BullMQModule} from "./BullMQModule.js"; import {type BullMQConfig} from "./config/config.js"; @@ -62,18 +63,17 @@ describe("BullMQModule", () => { dispatcher = mock(JobDispatcher); when(dispatcher.dispatch(CustomCronJob)).thenResolve(); }); + beforeEach(() => { + queueConstructorSpy.mockClear(); + workerConstructorSpy.mockClear(); + }); afterEach(PlatformTest.reset); describe("configuration", () => { - beforeEach(() => { - queueConstructorSpy.mockClear(); - workerConstructorSpy.mockClear(); - }); - describe("merges config correctly", () => { - beforeEach(async () => { - await PlatformTest.create({ + beforeEach(() => + PlatformTest.create({ bullmq: { queues: ["default", "special"], connection: { @@ -114,8 +114,8 @@ describe("BullMQModule", () => { use: instance(dispatcher) } ] - }); - }); + }) + ); it("queue", () => { expect(queueConstructorSpy).toHaveBeenCalledTimes(2); @@ -161,10 +161,9 @@ describe("BullMQModule", () => { }); }); }); - describe("discover queues from decorators", () => { - beforeEach(async () => { - await PlatformTest.create({ + beforeEach(() => + PlatformTest.create({ bullmq: { queues: ["special"], connection: { @@ -205,8 +204,8 @@ describe("BullMQModule", () => { use: instance(dispatcher) } ] - }); - }); + }) + ); it("queue", () => { expect(queueConstructorSpy).toHaveBeenCalledTimes(2); @@ -252,7 +251,6 @@ describe("BullMQModule", () => { }); }); }); - describe("disableWorker", () => { const config = { queues: ["default", "foo", "bar"], @@ -260,8 +258,8 @@ describe("BullMQModule", () => { disableWorker: true } as BullMQConfig; - beforeEach(async () => { - await PlatformTest.create({ + beforeEach(() => + PlatformTest.create({ bullmq: config, imports: [ { @@ -269,25 +267,25 @@ describe("BullMQModule", () => { use: instance(dispatcher) } ] - }); - }); + }) + ); it("should not create any workers", () => { expect(workerConstructorSpy).toHaveBeenCalledTimes(0); }); }); - describe("without", () => { - it("skips initialization", async () => { - await PlatformTest.create({ + beforeEach(() => + PlatformTest.create({ imports: [ { token: JobDispatcher, use: instance(dispatcher) } ] - }); - + }) + ); + it("skips initialization", async () => { expect(queueConstructorSpy).not.toHaveBeenCalled(); verify(dispatcher.dispatch(anything())).never(); }); @@ -301,8 +299,8 @@ describe("BullMQModule", () => { workerQueues: ["default", "foo"] } as BullMQConfig; - beforeEach(async () => { - await PlatformTest.create({ + beforeEach(() => + PlatformTest.create({ bullmq: config, imports: [ { @@ -310,8 +308,8 @@ describe("BullMQModule", () => { use: instance(dispatcher) } ] - }); - }); + }) + ); describe("cronjobs", () => { it("should dispatch cron jobs automatically", () => { @@ -331,10 +329,6 @@ describe("BullMQModule", () => { expect(instance).toBeInstanceOf(Queue); }); - - it("should not allow direct injection of the queue", () => { - expect(PlatformTest.get(Queue)).not.toBeInstanceOf(Queue); - }); }); describe("workers", () => { @@ -354,10 +348,6 @@ describe("BullMQModule", () => { expect(PlatformTest.get("bullmq.worker.bar")).toBeUndefined(); }); - it("should not allow direct injection of the worker", () => { - expect(PlatformTest.get(Worker)).not.toBeInstanceOf(Worker); - }); - it("should run worker and execute processor", async () => { const bullMQModule = PlatformTest.get(BullMQModule); const worker = PlatformTest.get("bullmq.job.default.regular"); @@ -426,7 +416,6 @@ describe("BullMQModule", () => { }); }); }); - describe("with fallback controller", () => { beforeEach(async () => { @FallbackJobController("foo") diff --git a/packages/third-parties/bullmq/src/BullMQModule.ts b/packages/third-parties/bullmq/src/BullMQModule.ts index d9581c39118..c3ff4e8f13a 100644 --- a/packages/third-parties/bullmq/src/BullMQModule.ts +++ b/packages/third-parties/bullmq/src/BullMQModule.ts @@ -1,5 +1,16 @@ -import {DIContext, InjectorService, Module, OnDestroy, runInContext} from "@tsed/di"; -import type {BeforeInit} from "@tsed/platform-http"; +import { + constant, + DIContext, + inject, + injectable, + injectMany, + injector, + logger, + OnDestroy, + type OnInit, + ProviderType, + runInContext +} from "@tsed/di"; import {getComputedType} from "@tsed/schema"; import {Job, Queue, Worker} from "bullmq"; import {v4} from "uuid"; @@ -15,12 +26,10 @@ import {getFallbackJobToken, getJobToken} from "./utils/getJobToken.js"; import {mapQueueOptions} from "./utils/mapQueueOptions.js"; import {mapWorkerOptions} from "./utils/mapWorkerOptions.js"; -@Module() -export class BullMQModule implements BeforeInit, OnDestroy { - constructor( - private readonly injector: InjectorService, - private readonly dispatcher: JobDispatcher - ) { +export class BullMQModule implements OnInit, OnDestroy { + private readonly dispatcher = inject(JobDispatcher); + + constructor() { // build providers allow @Inject(queue) usage in JobController instance if (this.isEnabled()) { const queues = [...this.getUniqQueueNames()]; @@ -36,12 +45,12 @@ export class BullMQModule implements BeforeInit, OnDestroy { } get config() { - return this.injector.settings.get("bullmq"); + return constant("bullmq")!; } - $beforeInit() { + $onInit() { if (this.isEnabled()) { - this.injector.getMany(BullMQTypes.CRON).map((job) => this.dispatcher.dispatch(getComputedType(job))); + injectMany(BullMQTypes.CRON).map((job) => this.dispatcher.dispatch(getComputedType(job))); } } @@ -50,8 +59,8 @@ export class BullMQModule implements BeforeInit, OnDestroy { return; } - await Promise.all(this.injector.getMany(BullMQTypes.QUEUE).map((queue) => queue.close())); - await Promise.all(this.injector.getMany(BullMQTypes.WORKER).map((worker) => worker.close())); + await Promise.all(injectMany(BullMQTypes.QUEUE).map((queue) => queue.close())); + await Promise.all(injectMany(BullMQTypes.WORKER).map((worker) => worker.close())); } isEnabled() { @@ -65,14 +74,14 @@ export class BullMQModule implements BeforeInit, OnDestroy { private buildQueues(queues: string[]) { queues.forEach((queue) => { const opts = mapQueueOptions(queue, this.config); - createQueueProvider(this.injector, queue, opts); + createQueueProvider(queue, opts); }); } private buildWorkers(workers: string[]) { workers.forEach((worker) => { const opts = mapWorkerOptions(worker, this.config); - createWorkerProvider(this.injector, worker, this.onProcess, opts); + createWorkerProvider(worker, this.onProcess, opts); }); } @@ -82,7 +91,7 @@ export class BullMQModule implements BeforeInit, OnDestroy { */ private getUniqQueueNames() { return new Set( - this.injector + injector() .getProviders([BullMQTypes.JOB, BullMQTypes.CRON, BullMQTypes.FALLBACK_JOB]) .map((provider) => provider.store.get(BULLMQ)?.queue) .concat(this.config.queues!) @@ -91,18 +100,14 @@ export class BullMQModule implements BeforeInit, OnDestroy { } private getJob(name: string, queueName: string) { - return ( - this.injector.get(getJobToken(queueName, name)) || - this.injector.get(getFallbackJobToken(queueName)) || - this.injector.get(getFallbackJobToken()) - ); + return inject(getJobToken(queueName, name)) || inject(getFallbackJobToken(queueName)) || inject(getFallbackJobToken()); } private onProcess = async (job: Job) => { const jobService = this.getJob(job.name, job.queueName); if (!jobService) { - this.injector.logger.warn({ + logger().warn({ event: "BULLMQ_JOB_NOT_FOUND", message: `Job ${job.name} ${job.queueName} not found` }); @@ -110,8 +115,6 @@ export class BullMQModule implements BeforeInit, OnDestroy { } const $ctx = new DIContext({ - injector: this.injector, - logger: this.injector.logger, id: job.id || v4().split("-").join(""), additionalProps: { logType: "bullmq", @@ -144,3 +147,5 @@ export class BullMQModule implements BeforeInit, OnDestroy { } }; } + +injectable(BullMQModule).type(ProviderType.MODULE); diff --git a/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.spec.ts b/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.spec.ts index 679f69d273b..c9cc0caf71b 100644 --- a/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.spec.ts +++ b/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.spec.ts @@ -1,6 +1,6 @@ -import {InjectorService} from "@tsed/di"; -import {Queue} from "bullmq"; -import {anything, capture, instance, mock, objectContaining, spy, verify, when} from "ts-mockito"; +import {catchAsyncError} from "@tsed/core"; +import {DITest, inject, injectable, injector} from "@tsed/di"; +import {beforeEach} from "vitest"; import {JobMethods} from "../contracts/index.js"; import {JobController} from "../decorators/index.js"; @@ -27,45 +27,60 @@ class NotConfiguredQueueTestJob implements JobMethods { handle() {} } +function getFixture() { + const dispatcher = inject(JobDispatcher); + const queue = { + name: "default", + add: vi.fn() + }; + + const specialQueue = { + name: "special", + add: vi.fn() + }; + + injectable("bullmq.queue.default").value(queue); + injectable("bullmq.queue.special").value(specialQueue); + injectable("bullmq.job.default.example-job").value(new ExampleTestJob()); + injectable("bullmq.job.default.example-job-with-custom-id-from-job-methods").value(new ExampleJobWithCustomJobIdFromJobMethods()); + + vi.spyOn(injector(), "resolve"); + + return { + dispatcher, + queue, + specialQueue, + job: inject("bullmq.job.default.example-job-with-custom-id-from-job-methods") + }; +} + describe("JobDispatcher", () => { - let injector: InjectorService; - let queue: Queue; - let dispatcher: JobDispatcher; - beforeEach(() => { - injector = mock(InjectorService); - queue = mock(Queue); - when(queue.name).thenReturn("default"); - when(injector.get("bullmq.queue.default")).thenReturn(instance(queue)); - when(injector.get("bullmq.job.default.example-job")).thenReturn(new ExampleTestJob()); - - dispatcher = new JobDispatcher(instance(injector)); - }); + beforeEach(() => DITest.create()); + afterEach(() => DITest.reset()); it("should throw an exception when a queue is not configured", async () => { - when(injector.get("bullmq.queue.not-configured")).thenReturn(undefined); + const {dispatcher} = getFixture(); - await expect(dispatcher.dispatch(NotConfiguredQueueTestJob)).rejects.toThrow(new Error("Queue(not-configured) not defined")); - verify(injector.get("bullmq.queue.not-configured")).once(); - }); + const error = await catchAsyncError(() => dispatcher.dispatch(NotConfiguredQueueTestJob)); + await expect(error).toEqual(new Error("Queue(not-configured) not defined")); + + expect(injector().resolve).toHaveBeenCalledWith("bullmq.queue.not-configured", expect.anything()); + }); it("should dispatch job as type", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}); - verify( - queue.add( - "example-job", - objectContaining({msg: "hello test"}), - objectContaining({ - backoff: 69 - }) - ) - ).once(); + expect(queue.add).toHaveBeenCalledOnce(); + expect(queue.add).toHaveBeenCalledWith( + "example-job", + expect.objectContaining({msg: "hello test"}), + expect.objectContaining({backoff: 69}) + ); }); - it("should dispatch job as options", async () => { - const specialQueue = mock(Queue); - when(specialQueue.name).thenReturn("special"); - when(injector.get("bullmq.queue.special")).thenReturn(instance(specialQueue)); + const {dispatcher, specialQueue} = getFixture(); await dispatcher.dispatch( { @@ -75,76 +90,77 @@ describe("JobDispatcher", () => { {msg: "hello test"} ); - verify(specialQueue.add("some-name", objectContaining({msg: "hello test"}), objectContaining({}))).once(); + expect(specialQueue.add).toHaveBeenCalledOnce(); + expect(specialQueue.add).toHaveBeenCalledWith("some-name", expect.objectContaining({msg: "hello test"}), expect.anything()); }); - it("should dispatch job as string", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch("some-name", {msg: "hello test"}); - verify(queue.add("some-name", objectContaining({msg: "hello test"}), objectContaining({}))).once(); + expect(queue.add).toHaveBeenCalledOnce(); + expect(queue.add).toHaveBeenCalledWith("some-name", expect.objectContaining({msg: "hello test"}), expect.anything()); }); - it("should overwrite job options defined by the job", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {backoff: 42, jobId: "ffeeaa"}); - verify( - queue.add( - "example-job", - objectContaining({msg: "hello test"}), - objectContaining({ - backoff: 42, - jobId: "ffeeaa" - }) - ) - ).once(); + expect(queue.add).toHaveBeenCalledOnce(); + expect(queue.add).toHaveBeenCalledWith( + "example-job", + expect.objectContaining({msg: "hello test"}), + expect.objectContaining({backoff: 42, jobId: "ffeeaa"}) + ); }); - it("should keep existing options and add new ones", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch(ExampleTestJob, {msg: "hello test"}, {jobId: "ffeeaa"}); - verify( - queue.add( - "example-job", - objectContaining({msg: "hello test"}), - objectContaining({ - backoff: 69, - jobId: "ffeeaa" - }) - ) - ).once(); + expect(queue.add).toHaveBeenCalledOnce(); + expect(queue.add).toHaveBeenCalledWith( + "example-job", + expect.objectContaining({msg: "hello test"}), + expect.objectContaining({backoff: 69, jobId: "ffeeaa"}) + ); }); - describe("custom jobId", () => { - let job: ExampleJobWithCustomJobIdFromJobMethods; - beforeEach(() => { - job = new ExampleJobWithCustomJobIdFromJobMethods(); - when(injector.get("bullmq.job.default.example-job-with-custom-id-from-job-methods")).thenReturn(job); - }); - it("should allow setting the job id from within the job", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world"); - verify(queue.add("example-job-with-custom-id-from-job-methods", "hello world", anything())).once(); + expect(queue.add).toHaveBeenCalledOnce(); + expect(queue.add).toHaveBeenCalledWith("example-job-with-custom-id-from-job-methods", "hello world", expect.anything()); + + const [, , opts] = queue.add.mock.calls.at(-1)!; - const [, , opts] = capture(queue.add).last(); expect(opts).toMatchObject({ jobId: "HELLO WORLD" }); }); it("should pass the payload to the jobId method", async () => { - const spyJob = spy(job); + const {dispatcher, job} = getFixture(); + + vi.spyOn(job, "jobId"); + await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world"); - verify(spyJob.jobId("hello world")).once(); + expect(job.jobId).toHaveBeenCalledOnce(); + expect(job.jobId).toHaveBeenCalledWith("hello world"); }); it("should choose the jobId provided to the dispatcher even when the method is implemented", async () => { + const {dispatcher, queue} = getFixture(); + await dispatcher.dispatch(ExampleJobWithCustomJobIdFromJobMethods, "hello world", { jobId: "I don't think so" }); - const [, , opts] = capture(queue.add).last(); + const [, , opts] = queue.add.mock.calls.at(-1)!; + expect(opts).toMatchObject({ jobId: "I don't think so" }); diff --git a/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.ts b/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.ts index 355158ccf76..e663bec7e7f 100644 --- a/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.ts +++ b/packages/third-parties/bullmq/src/dispatchers/JobDispatcher.ts @@ -1,5 +1,5 @@ -import {Store, Type} from "@tsed/core"; -import {Injectable, InjectorService} from "@tsed/di"; +import {isClass, Store, Type} from "@tsed/core"; +import {inject, injectable} from "@tsed/di"; import {Job as BullMQJob, JobsOptions, Queue} from "bullmq"; import {BULLMQ} from "../constants/constants.js"; @@ -8,10 +8,7 @@ import {getJobToken} from "../utils/getJobToken.js"; import {getQueueToken} from "../utils/getQueueToken.js"; import type {JobDispatcherOptions} from "./JobDispatcherOptions.js"; -@Injectable() export class JobDispatcher { - constructor(private readonly injector: InjectorService) {} - public async dispatch( job: Type, payload?: Parameters[0], @@ -22,7 +19,7 @@ export class JobDispatcher { public async dispatch(job: Type | JobDispatcherOptions | string, payload: unknown, options: JobsOptions = {}): Promise { const {queueName, jobName, defaultJobOptions} = await this.resolveDispatchArgs(job, payload); - const queue = this.injector.get(getQueueToken(queueName)); + const queue = inject(getQueueToken(queueName)); if (!queue) { throw new Error(`Queue(${queueName}) not defined`); @@ -44,6 +41,7 @@ export class JobDispatcher { const store = Store.from(job).get(BULLMQ); queueName = store.queue; jobName = store.name; + defaultJobOptions = await this.retrieveJobOptionsFromClassBasedJob(store, payload); } else if (typeof job === "object") { // job is passed as JobDispatcherOptions @@ -63,13 +61,9 @@ export class JobDispatcher { } private async retrieveJobOptionsFromClassBasedJob(store: JobStore, payload: unknown): Promise { - const job = this.injector.get(getJobToken(store.queue, store.name)); - - if (!job) { - return store.opts; - } - + const job = inject(getJobToken(store.queue, store.name)); const jobId = await job.jobId?.(payload); + if (jobId === undefined) { return store.opts; } @@ -80,3 +74,5 @@ export class JobDispatcher { }; } } + +injectable(JobDispatcher); diff --git a/packages/third-parties/bullmq/src/utils/createQueueProvider.ts b/packages/third-parties/bullmq/src/utils/createQueueProvider.ts index 29aa7ae655c..faae9c91a96 100644 --- a/packages/third-parties/bullmq/src/utils/createQueueProvider.ts +++ b/packages/third-parties/bullmq/src/utils/createQueueProvider.ts @@ -1,19 +1,16 @@ -import {InjectorService} from "@tsed/di"; +import {inject, injectable} from "@tsed/di"; import {Queue, QueueOptions} from "bullmq"; -import {BullMQTypes} from "../constants/BullMQTypes.js"; import {getQueueToken} from "./getQueueToken.js"; -export function createQueueProvider(injector: InjectorService, queue: string, opts: QueueOptions) { +export function createQueueProvider(queue: string, opts: QueueOptions) { const token = getQueueToken(queue); - return injector - .add(token, { - type: BullMQTypes.QUEUE, - useValue: new Queue(queue, opts), - hooks: { - $onDestroy: (queue) => queue.close() - } - }) - .invoke(token); + injectable(token) + .factory(() => new Queue(queue, opts)) + .hooks({ + $onDestroy: (queue: Queue) => queue.close() + }); + + return inject(token); } diff --git a/packages/third-parties/bullmq/src/utils/createWorkerProvider.ts b/packages/third-parties/bullmq/src/utils/createWorkerProvider.ts index 4b9fa3aac51..6705363740d 100644 --- a/packages/third-parties/bullmq/src/utils/createWorkerProvider.ts +++ b/packages/third-parties/bullmq/src/utils/createWorkerProvider.ts @@ -1,19 +1,18 @@ -import {InjectorService} from "@tsed/di"; +import {inject, injectable} from "@tsed/di"; import {Job, Worker, WorkerOptions} from "bullmq"; import {BullMQTypes} from "../constants/BullMQTypes.js"; import {getWorkerToken} from "./getWorkerToken.js"; -export function createWorkerProvider(injector: InjectorService, worker: string, process: (job: Job) => any, opts: WorkerOptions) { +export function createWorkerProvider(worker: string, process: (job: Job) => any, opts: WorkerOptions) { const token = getWorkerToken(worker); - return injector - .add(token, { - type: BullMQTypes.WORKER, - useValue: new Worker(worker, process, opts), - hooks: { - $onDestroy: (worker) => worker.close() - } - }) - .invoke(token); + injectable(token) + .type(BullMQTypes.WORKER) + .value(new Worker(worker, process, opts)) + .hooks({ + $onDestroy: (worker: Worker) => worker.close() + }); + + return inject(token); } diff --git a/packages/third-parties/bullmq/vitest.config.mts b/packages/third-parties/bullmq/vitest.config.mts index 452b24fe8da..439f03ca5e3 100644 --- a/packages/third-parties/bullmq/vitest.config.mts +++ b/packages/third-parties/bullmq/vitest.config.mts @@ -9,11 +9,16 @@ export default defineConfig( ...presets.test, coverage: { ...presets.test.coverage, + exclude: [ + ...presets.test.coverage.exclude, + "**/contracts/**/*", + "**/config/**/*" + ], thresholds: { - statements: 98.92, - branches: 98.55, + statements: 100, + branches: 98.38, functions: 100, - lines: 98.92 + lines: 100 } } } From edd143dbcfdfc14513552f0f1ec052daaf8c7117 Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 09:42:55 +0100 Subject: [PATCH 31/32] chore: update coverage --- packages/orm/mikro-orm/vitest.config.mts | 8 ++++---- packages/orm/prisma/vitest.config.mts | 4 ++-- packages/platform/platform-router/vitest.config.mts | 4 ++-- .../modules/feature/controllers/FeatureController.ts | 3 +-- packages/security/jwks/vitest.config.mts | 10 +++++----- .../vitest.config.mts | 10 +++++----- packages/security/oidc-provider/vitest.config.mts | 10 +++++----- packages/security/passport/vitest.config.mts | 10 +++++----- packages/third-parties/agenda/vitest.config.mts | 10 +++++----- .../third-parties/components-scan/vitest.config.mts | 10 +++++----- packages/third-parties/event-emitter/vitest.config.mts | 10 +++++----- packages/third-parties/pulse/vitest.config.mts | 10 +++++----- .../third-parties/socketio/test/app/models/User.ts | 3 +-- packages/third-parties/socketio/vitest.config.mts | 10 +++++----- packages/third-parties/stripe/vitest.config.mts | 10 +++++----- packages/third-parties/terminus/vitest.config.mts | 10 +++++----- 16 files changed, 65 insertions(+), 67 deletions(-) diff --git a/packages/orm/mikro-orm/vitest.config.mts b/packages/orm/mikro-orm/vitest.config.mts index fc66778a331..3868d5b39aa 100644 --- a/packages/orm/mikro-orm/vitest.config.mts +++ b/packages/orm/mikro-orm/vitest.config.mts @@ -12,12 +12,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 97.75, - branches: 97.46, + statements: 97.78, + branches: 97.43, functions: 100, - lines: 97.75 + lines: 97.78 } } } } -); \ No newline at end of file +); diff --git a/packages/orm/prisma/vitest.config.mts b/packages/orm/prisma/vitest.config.mts index a79dbf60936..3a9b3813ae0 100644 --- a/packages/orm/prisma/vitest.config.mts +++ b/packages/orm/prisma/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 91.11, - branches: 92.15, + branches: 92.2, functions: 92.59, lines: 91.11 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-router/vitest.config.mts b/packages/platform/platform-router/vitest.config.mts index 3205093cb01..76524d4d8d5 100644 --- a/packages/platform/platform-router/vitest.config.mts +++ b/packages/platform/platform-router/vitest.config.mts @@ -11,11 +11,11 @@ export default defineConfig( ...presets.test.coverage, thresholds: { statements: 100, - branches: 94.24, + branches: 94.2, functions: 100, lines: 100 } } } } -); \ No newline at end of file +); diff --git a/packages/platform/platform-test-sdk/src/modules/feature/controllers/FeatureController.ts b/packages/platform/platform-test-sdk/src/modules/feature/controllers/FeatureController.ts index 88fbd9a42b2..b692271d9f6 100644 --- a/packages/platform/platform-test-sdk/src/modules/feature/controllers/FeatureController.ts +++ b/packages/platform/platform-test-sdk/src/modules/feature/controllers/FeatureController.ts @@ -1,6 +1,5 @@ import {Controller} from "@tsed/di"; -import {Get} from "@tsed/schema"; -import {Hidden} from "@tsed/swagger"; +import {Get, Hidden} from "@tsed/schema"; @Hidden() @Controller("/features") diff --git a/packages/security/jwks/vitest.config.mts b/packages/security/jwks/vitest.config.mts index d759e817941..d2598fb346b 100644 --- a/packages/security/jwks/vitest.config.mts +++ b/packages/security/jwks/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 100, + branches: 100, + functions: 100, + lines: 100 } } } } -); +); \ No newline at end of file diff --git a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts index d759e817941..6bb1b3f37a0 100644 --- a/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts +++ b/packages/security/oidc-provider-plugin-wildcard-redirect-uri/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 98.76, + branches: 86.36, + functions: 100, + lines: 98.76 } } } } -); +); \ No newline at end of file diff --git a/packages/security/oidc-provider/vitest.config.mts b/packages/security/oidc-provider/vitest.config.mts index d759e817941..4c03a1e8518 100644 --- a/packages/security/oidc-provider/vitest.config.mts +++ b/packages/security/oidc-provider/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 97.37, + branches: 92, + functions: 98.61, + lines: 97.37 } } } } -); +); \ No newline at end of file diff --git a/packages/security/passport/vitest.config.mts b/packages/security/passport/vitest.config.mts index d759e817941..702ef1c4a01 100644 --- a/packages/security/passport/vitest.config.mts +++ b/packages/security/passport/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 0, - branches: 0, - functions: 0, - lines: 0 + statements: 98.66, + branches: 93.58, + functions: 100, + lines: 98.66 } } } } -); +); \ No newline at end of file diff --git a/packages/third-parties/agenda/vitest.config.mts b/packages/third-parties/agenda/vitest.config.mts index 69d33025271..f8dc9b023ac 100644 --- a/packages/third-parties/agenda/vitest.config.mts +++ b/packages/third-parties/agenda/vitest.config.mts @@ -12,12 +12,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ 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 aa8bed784fe..950e19b9f86 100644 --- a/packages/third-parties/components-scan/vitest.config.mts +++ b/packages/third-parties/components-scan/vitest.config.mts @@ -14,13 +14,13 @@ export default defineConfig( "**/isTsEnv.ts" ], thresholds: { - statements: 100, - branches: 95.65, - functions: 100, - lines: 100, + statements: 0, + branches: 0, + functions: 0, + lines: 0, } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/event-emitter/vitest.config.mts b/packages/third-parties/event-emitter/vitest.config.mts index 00611ba4831..d759e817941 100644 --- a/packages/third-parties/event-emitter/vitest.config.mts +++ b/packages/third-parties/event-emitter/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 81.72, - branches: 100, - functions: 90, - lines: 81.72 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/pulse/vitest.config.mts b/packages/third-parties/pulse/vitest.config.mts index 69d33025271..f8dc9b023ac 100644 --- a/packages/third-parties/pulse/vitest.config.mts +++ b/packages/third-parties/pulse/vitest.config.mts @@ -12,12 +12,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/socketio/test/app/models/User.ts b/packages/third-parties/socketio/test/app/models/User.ts index 2cf189a243b..cce8a99839b 100644 --- a/packages/third-parties/socketio/test/app/models/User.ts +++ b/packages/third-parties/socketio/test/app/models/User.ts @@ -1,6 +1,5 @@ -import {Indexed, Model, ObjectID, Unique} from "@tsed/mongoose"; +import {ObjectID} from "@tsed/mongoose"; import {Allow, Email, Ignore, MinLength, Property, Required} from "@tsed/schema"; -import {Hidden} from "@tsed/swagger"; export interface IUser { name: string; diff --git a/packages/third-parties/socketio/vitest.config.mts b/packages/third-parties/socketio/vitest.config.mts index 5293c3bb219..d759e817941 100644 --- a/packages/third-parties/socketio/vitest.config.mts +++ b/packages/third-parties/socketio/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 99.59, - branches: 98.87, - functions: 100, - lines: 99.59 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/stripe/vitest.config.mts b/packages/third-parties/stripe/vitest.config.mts index d2598fb346b..d759e817941 100644 --- a/packages/third-parties/stripe/vitest.config.mts +++ b/packages/third-parties/stripe/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); diff --git a/packages/third-parties/terminus/vitest.config.mts b/packages/third-parties/terminus/vitest.config.mts index d2598fb346b..d759e817941 100644 --- a/packages/third-parties/terminus/vitest.config.mts +++ b/packages/third-parties/terminus/vitest.config.mts @@ -10,12 +10,12 @@ export default defineConfig( coverage: { ...presets.test.coverage, thresholds: { - statements: 100, - branches: 100, - functions: 100, - lines: 100 + statements: 0, + branches: 0, + functions: 0, + lines: 0 } } } } -); \ No newline at end of file +); From e2a2e85275dbba921c09d4287dc0933388938e8f Mon Sep 17 00:00:00 2001 From: Romain Lenzotti Date: Sun, 24 Nov 2024 12:50:11 +0100 Subject: [PATCH 32/32] ci: update test workflow --- .github/workflows/build.yml | 73 +------------------ .../di/src/common/services/InjectorService.ts | 2 +- 2 files changed, 3 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2635b9d9461..ac0c3686ff3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,71 +33,6 @@ jobs: - name: Run lint run: yarn test:lint - test-integration: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.12.2] - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "yarn" - - name: Install dependencies - run: yarn install --immutable --network-timeout 500000 - - name: Run build - run: yarn tsc --build - env: - FORCE_COLOR: true - - name: Run test - run: yarn test:integration - env: - FORCE_COLOR: true - - name: Upload vitest config files - uses: actions/upload-artifact@v4 - with: - name: vitest-config-integration - overwrite: true - path: | - team.json - packages/platform/platform-express/vitest.config.mts - packages/platform/platform-koa/vitest.config.mts - continue-on-error: true - - test-envs: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [20.12.2] - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "yarn" - - name: Install dependencies - run: yarn install --immutable --network-timeout 500000 - - name: Run build - run: yarn tsc --build - env: - FORCE_COLOR: true - - name: Run test - run: yarn test:integration - env: - FORCE_COLOR: true - test-core: runs-on: ubuntu-latest @@ -190,8 +125,6 @@ jobs: path: | team.json packages/platform/platform-*/vitest.config.mts - !packages/platform/platform-express/vitest.config.mts - !packages/platform/platform-koa/vitest.config.mts test-orm: runs-on: ubuntu-latest @@ -311,8 +244,7 @@ jobs: test-download-artifacts: runs-on: ubuntu-latest - needs: - [lint, test-core, test-specs, test-platform, test-integration, test-envs, test-orm, test-security, test-graphql, test-third-parties] + needs: [lint, test-core, test-specs, test-platform, test-orm, test-security, test-graphql, test-third-parties] if: github.event_name == 'pull_request' strategy: matrix: @@ -337,8 +269,7 @@ jobs: deploy-packages: runs-on: ubuntu-latest - needs: - [lint, test-core, test-specs, test-platform, test-integration, test-envs, test-orm, test-security, test-graphql, test-third-parties] + needs: [lint, test-core, test-specs, test-platform, test-orm, test-security, test-graphql, test-third-parties] if: github.event_name != 'pull_request' && contains(' refs/heads/production refs/heads/alpha diff --git a/packages/di/src/common/services/InjectorService.ts b/packages/di/src/common/services/InjectorService.ts index 39c9d24d25f..091a8bd4f68 100644 --- a/packages/di/src/common/services/InjectorService.ts +++ b/packages/di/src/common/services/InjectorService.ts @@ -341,7 +341,7 @@ export class InjectorService extends Container { } /** - * Boostrap injector from container and invokeToken configuration. + * Bootstrap injector from container, resolve configuration and providers. * * @param container */