@keq-request/nestjs 5.0.0-beta.11 → 5.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +18 -18
- package/CHANGELOG.md +22 -0
- package/__tests__/keq-module.spec.ts +155 -0
- package/dist/index.d.mts +22 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +22 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -24
- package/dist/index.mjs.map +1 -1
- package/jest.config.cts +12 -2
- package/package.json +5 -3
- package/src/interfaces/keq-middleware-consumer.interface.ts +2 -1
- package/src/keq-middleware-consumer.ts +92 -8
- package/src/keq.module.ts +78 -9
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
> @keq-request/nestjs@5.0.0-beta.
|
|
2
|
+
> @keq-request/nestjs@5.0.0-beta.13 build /home/runner/work/keq/keq/packages/nestjs
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m [34mtsdown v0.21.10[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.17[39m
|
|
6
6
|
[34mℹ[39m config file: [4m/home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts[24m
|
|
7
|
-
(node:
|
|
7
|
+
(node:2744) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts?no-cache=63d674a9-ded8-4584-ad0b-ff9b06a87e93 is not specified and it doesn't parse as CommonJS.
|
|
8
8
|
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
|
|
9
9
|
To eliminate this warning, add "type": "module" to /home/runner/work/keq/keq/packages/nestjs/package.json.
|
|
10
10
|
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
@@ -12,25 +12,25 @@ To eliminate this warning, add "type": "module" to /home/runner/work/keq/keq/pac
|
|
|
12
12
|
[34mℹ[39m target: [34mnode20[39m
|
|
13
13
|
[34mℹ[39m tsconfig: [34mtsconfig.lib.json[39m
|
|
14
14
|
[34mℹ[39m Build start
|
|
15
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.js[22m [
|
|
16
|
-
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.js.map [
|
|
17
|
-
[34mℹ[39m [33m[CJS][39m 2 files, total:
|
|
15
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.js[22m [2m10.52 kB[22m [2m│ gzip: 3.95 kB[22m
|
|
16
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.js.map [2m15.05 kB[22m [2m│ gzip: 5.17 kB[22m
|
|
17
|
+
[34mℹ[39m [33m[CJS][39m 2 files, total: 25.57 kB
|
|
18
18
|
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `tsdown:deps`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
19
19
|
|
|
20
|
-
[34mℹ[39m [
|
|
21
|
-
[34mℹ[39m [
|
|
22
|
-
[34mℹ[39m [
|
|
20
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m 9.19 kB[22m [2m│ gzip: 3.46 kB[22m
|
|
21
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m14.98 kB[22m [2m│ gzip: 5.14 kB[22m
|
|
22
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m 1.08 kB[22m [2m│ gzip: 0.47 kB[22m
|
|
23
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 2.96 kB[22m [2m│ gzip: 1.38 kB[22m
|
|
24
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 28.22 kB
|
|
25
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
26
|
+
|
|
27
|
+
[32m✔[39m Build complete in [32m5612ms[39m
|
|
28
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.ts.map [2m1.08 kB[22m [2m│ gzip: 0.47 kB[22m
|
|
29
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.ts[22m[39m [2m2.96 kB[22m [2m│ gzip: 1.37 kB[22m
|
|
30
|
+
[34mℹ[39m [33m[CJS][39m 2 files, total: 4.04 kB
|
|
23
31
|
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugins. Here is a breakdown:
|
|
24
32
|
- rolldown-plugin-dts:generate (48%)
|
|
25
|
-
- tsdown:deps (
|
|
33
|
+
- tsdown:deps (43%)
|
|
26
34
|
See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
27
35
|
|
|
28
|
-
[32m✔[39m Build complete in [
|
|
29
|
-
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
30
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m4.54 kB[22m [2m│ gzip: 1.57 kB[22m
|
|
31
|
-
|
|
32
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m7.21 kB[22m [2m│ gzip: 2.36 kB[22m
|
|
33
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m0.99 kB[22m [2m│ gzip: 0.43 kB[22m
|
|
34
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m1.86 kB[22m [2m│ gzip: 0.66 kB[22m
|
|
35
|
-
[34mℹ[39m [34m[ESM][39m 4 files, total: 14.60 kB
|
|
36
|
-
[32m✔[39m Build complete in [32m5201ms[39m
|
|
36
|
+
[32m✔[39m Build complete in [32m5614ms[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @keq-request/nestjs
|
|
2
2
|
|
|
3
|
+
## 5.0.0-beta.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 59f4773: **Fix:** ensure `configureKeqMiddleware()` works correctly in pnpm monorepos and validate module imports at startup.
|
|
8
|
+
|
|
9
|
+
- Fix middleware not being applied when pnpm creates multiple `@keq-request/nestjs` instances due to peerDependency version differences.
|
|
10
|
+
- Detect missing `KeqModule` imports in modules that implement `configureKeqMiddleware()` at startup, throwing a clear error instead of silently skipping middleware.
|
|
11
|
+
- Prevent middleware from being applied more than once when multiple `KeqModule` instances exist.
|
|
12
|
+
- Throw a descriptive error when a NestJS middleware class passed to `consumer.apply()` is not registered as a provider.
|
|
13
|
+
- keq@5.0.0-beta.13
|
|
14
|
+
|
|
15
|
+
## 5.0.0-beta.12
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- fbc851f: **Feat:** Add `exclude()` method to `KeqMiddlewareConfigProxy`, allowing middleware to skip specific routes. Supports `KeqRouteInfo` with `host`, `method`, and `pathname` patterns, following the NestJS `MiddlewareConfigProxy.exclude()` convention.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- keq@5.0.0-beta.12
|
|
24
|
+
|
|
3
25
|
## 5.0.0-beta.11
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { expect, test } from '@jest/globals'
|
|
3
|
+
import { Module } from '@nestjs/common'
|
|
4
|
+
import type { ModulesContainer } from '@nestjs/core'
|
|
5
|
+
import { KeqModule, validateConfigureKeqMiddlewareImports } from '../src/keq.module.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 模拟 NestJS Module wrapper 的运行时结构。
|
|
9
|
+
* NestJS 在扫描阶段会将 DynamicModule 的 imports 解析为 Set<Module>,
|
|
10
|
+
* 因此使用 imports Set 而非静态 Reflect.getMetadata 来校验。
|
|
11
|
+
*/
|
|
12
|
+
function createMockModule(metatype: unknown, importsModules: Array<{ metatype: unknown }> = []) {
|
|
13
|
+
return {
|
|
14
|
+
metatype,
|
|
15
|
+
imports: new Set(importsModules),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mockModulesContainer(
|
|
20
|
+
modules: Array<ReturnType<typeof createMockModule>>,
|
|
21
|
+
): ModulesContainer {
|
|
22
|
+
const map = new Map()
|
|
23
|
+
for (const mod of modules) {
|
|
24
|
+
map.set(Symbol('module'), mod)
|
|
25
|
+
}
|
|
26
|
+
return map as unknown as ModulesContainer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
test('throws when configureKeqMiddleware is declared without KeqModule in imports', () => {
|
|
31
|
+
@Module({ imports: [] })
|
|
32
|
+
class BadModule {
|
|
33
|
+
configureKeqMiddleware(): void {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const container = mockModulesContainer([
|
|
37
|
+
createMockModule(KeqModule),
|
|
38
|
+
createMockModule(BadModule),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
|
|
42
|
+
/\[KeqModule\] 'BadModule' implements configureKeqMiddleware\(\)/,
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('passes when configureKeqMiddleware is declared with KeqModule in imports', () => {
|
|
47
|
+
@Module({ imports: [KeqModule] })
|
|
48
|
+
class GoodModule {
|
|
49
|
+
configureKeqMiddleware(): void {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const keqModuleWrapper = createMockModule(KeqModule)
|
|
53
|
+
|
|
54
|
+
const container = mockModulesContainer([
|
|
55
|
+
keqModuleWrapper,
|
|
56
|
+
createMockModule(GoodModule, [keqModuleWrapper]),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('passes when module does not implement configureKeqMiddleware', () => {
|
|
63
|
+
@Module({ imports: [] })
|
|
64
|
+
class PlainModule {}
|
|
65
|
+
|
|
66
|
+
const container = mockModulesContainer([
|
|
67
|
+
createMockModule(PlainModule),
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('passes when KeqModule is imported as DynamicModule', () => {
|
|
74
|
+
@Module({ imports: [{ module: KeqModule }] })
|
|
75
|
+
class DynamicImportModule {
|
|
76
|
+
configureKeqMiddleware(): void {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// NestJS 在扫描阶段解析 DynamicModule,将 KeqModule wrapper 加入 imports Set
|
|
80
|
+
const keqModuleWrapper = createMockModule(KeqModule)
|
|
81
|
+
|
|
82
|
+
const container = mockModulesContainer([
|
|
83
|
+
keqModuleWrapper,
|
|
84
|
+
createMockModule(DynamicImportModule, [keqModuleWrapper]),
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('passes when KeqModule is imported via forwardRef', () => {
|
|
91
|
+
const { forwardRef } = require('@nestjs/common')
|
|
92
|
+
@Module({ imports: [forwardRef(() => KeqModule)] })
|
|
93
|
+
class ForwardRefModule {
|
|
94
|
+
configureKeqMiddleware(): void {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// NestJS 在扫描阶段解析 forwardRef,将 KeqModule wrapper 加入 imports Set
|
|
98
|
+
const keqModuleWrapper = createMockModule(KeqModule)
|
|
99
|
+
|
|
100
|
+
const container = mockModulesContainer([
|
|
101
|
+
keqModuleWrapper,
|
|
102
|
+
createMockModule(ForwardRefModule, [keqModuleWrapper]),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('passes when metatype is null', () => {
|
|
109
|
+
const container = mockModulesContainer([
|
|
110
|
+
createMockModule(null as unknown as undefined),
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('throws when module has configureKeqMiddleware but KeqModule is not in container', () => {
|
|
117
|
+
// KeqModule 未在 ModulesContainer 中注册时,任何实现了 configureKeqMiddleware
|
|
118
|
+
// 的模块都无法通过校验,因为缺少 KeqModule 来注册中间件
|
|
119
|
+
@Module({ imports: [] })
|
|
120
|
+
class SomeModule {
|
|
121
|
+
configureKeqMiddleware(): void {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const container = mockModulesContainer([
|
|
125
|
+
createMockModule(SomeModule),
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
|
|
129
|
+
/\[KeqModule\] 'SomeModule'/,
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('throws for one module while another is valid', () => {
|
|
134
|
+
@Module({ imports: [] })
|
|
135
|
+
class BadModule {
|
|
136
|
+
configureKeqMiddleware(): void {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Module({ imports: [KeqModule] })
|
|
140
|
+
class GoodModule {
|
|
141
|
+
configureKeqMiddleware(): void {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const keqModuleWrapper = createMockModule(KeqModule)
|
|
145
|
+
|
|
146
|
+
const container = mockModulesContainer([
|
|
147
|
+
keqModuleWrapper,
|
|
148
|
+
createMockModule(GoodModule, [keqModuleWrapper]),
|
|
149
|
+
createMockModule(BadModule),
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
|
|
153
|
+
/'BadModule'/,
|
|
154
|
+
)
|
|
155
|
+
})
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { OnModuleInit, Type } from "@nestjs/common";
|
|
2
|
-
import {
|
|
2
|
+
import { ModulesContainer } from "@nestjs/core";
|
|
3
3
|
import { KeqExecutionContext, KeqMiddleware, KeqNext, KeqRequest } from "keq";
|
|
4
4
|
|
|
5
5
|
//#region src/keq.module.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
|
|
8
|
+
* 是否显式导入了 `KeqModule`。
|
|
9
|
+
*
|
|
10
|
+
* NestJS 初始化分为两个严格串行的阶段:
|
|
11
|
+
* 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
|
|
12
|
+
* DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
|
|
13
|
+
* 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
|
|
14
|
+
*
|
|
15
|
+
* 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
|
|
16
|
+
* 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
|
|
17
|
+
*
|
|
18
|
+
* 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
|
|
19
|
+
* - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
|
|
20
|
+
* - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
|
|
21
|
+
*/
|
|
22
|
+
declare function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void;
|
|
6
23
|
declare class KeqModule implements OnModuleInit {
|
|
7
24
|
private readonly modulesContainer;
|
|
8
|
-
private readonly moduleRef;
|
|
9
25
|
private readonly keqRequest;
|
|
10
|
-
constructor(modulesContainer: ModulesContainer,
|
|
26
|
+
constructor(modulesContainer: ModulesContainer, keqRequest: KeqRequest);
|
|
11
27
|
onModuleInit(): void;
|
|
12
28
|
}
|
|
13
29
|
//#endregion
|
|
@@ -27,10 +43,11 @@ interface KeqRouteInfo {
|
|
|
27
43
|
method?: string;
|
|
28
44
|
pathname?: string;
|
|
29
45
|
}
|
|
30
|
-
interface KeqModuleClass {
|
|
46
|
+
interface KeqModuleClass extends Type<any> {
|
|
31
47
|
readonly KEQ_REQUEST: symbol;
|
|
32
48
|
}
|
|
33
49
|
interface KeqMiddlewareConfigProxy {
|
|
50
|
+
exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy;
|
|
34
51
|
forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer;
|
|
35
52
|
}
|
|
36
53
|
interface KeqMiddlewareConsumer {
|
|
@@ -48,5 +65,5 @@ interface KeqModuleOptions {
|
|
|
48
65
|
isolate?: boolean;
|
|
49
66
|
}
|
|
50
67
|
//#endregion
|
|
51
|
-
export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo };
|
|
68
|
+
export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo, validateConfigureKeqMiddlewareImports };
|
|
52
69
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;;;AAoCA;;;;;AAkCA;;;;;;;;;iBAlCgB,qCAAA,CAAsC,gBAAA,EAAkB,gBAAA;AAAA,cAuC3D,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,UAAA;cADA,gBAAA,EAAkB,gBAAA,EAClB,UAAA,EAAY,UAAA;EAK/B,YAAA,CAAA;AAAA;;;cCnFW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA,SAAuB,IAAA;EAAA,SAC7B,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { OnModuleInit, Type } from "@nestjs/common";
|
|
2
|
-
import {
|
|
2
|
+
import { ModulesContainer } from "@nestjs/core";
|
|
3
3
|
import { KeqExecutionContext, KeqMiddleware, KeqNext, KeqRequest } from "keq";
|
|
4
4
|
|
|
5
5
|
//#region src/keq.module.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
|
|
8
|
+
* 是否显式导入了 `KeqModule`。
|
|
9
|
+
*
|
|
10
|
+
* NestJS 初始化分为两个严格串行的阶段:
|
|
11
|
+
* 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
|
|
12
|
+
* DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
|
|
13
|
+
* 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
|
|
14
|
+
*
|
|
15
|
+
* 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
|
|
16
|
+
* 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
|
|
17
|
+
*
|
|
18
|
+
* 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
|
|
19
|
+
* - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
|
|
20
|
+
* - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
|
|
21
|
+
*/
|
|
22
|
+
declare function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void;
|
|
6
23
|
declare class KeqModule implements OnModuleInit {
|
|
7
24
|
private readonly modulesContainer;
|
|
8
|
-
private readonly moduleRef;
|
|
9
25
|
private readonly keqRequest;
|
|
10
|
-
constructor(modulesContainer: ModulesContainer,
|
|
26
|
+
constructor(modulesContainer: ModulesContainer, keqRequest: KeqRequest);
|
|
11
27
|
onModuleInit(): void;
|
|
12
28
|
}
|
|
13
29
|
//#endregion
|
|
@@ -27,10 +43,11 @@ interface KeqRouteInfo {
|
|
|
27
43
|
method?: string;
|
|
28
44
|
pathname?: string;
|
|
29
45
|
}
|
|
30
|
-
interface KeqModuleClass {
|
|
46
|
+
interface KeqModuleClass extends Type<any> {
|
|
31
47
|
readonly KEQ_REQUEST: symbol;
|
|
32
48
|
}
|
|
33
49
|
interface KeqMiddlewareConfigProxy {
|
|
50
|
+
exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy;
|
|
34
51
|
forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer;
|
|
35
52
|
}
|
|
36
53
|
interface KeqMiddlewareConsumer {
|
|
@@ -48,5 +65,5 @@ interface KeqModuleOptions {
|
|
|
48
65
|
isolate?: boolean;
|
|
49
66
|
}
|
|
50
67
|
//#endregion
|
|
51
|
-
export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo };
|
|
68
|
+
export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo, validateConfigureKeqMiddlewareImports };
|
|
52
69
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;;;AAoCA;;;;;AAkCA;;;;;;;;;iBAlCgB,qCAAA,CAAsC,gBAAA,EAAkB,gBAAA;AAAA,cAuC3D,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,UAAA;cADA,gBAAA,EAAkB,gBAAA,EAClB,UAAA,EAAY,UAAA;EAK/B,YAAA,CAAA;AAAA;;;cCnFW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA,SAAuB,IAAA;EAAA,SAC7B,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
|
package/dist/index.js
CHANGED
|
@@ -37,20 +37,33 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
37
37
|
middlewares,
|
|
38
38
|
routes: []
|
|
39
39
|
};
|
|
40
|
+
const excludedRouteInfos = [];
|
|
40
41
|
this.entries.push(entry);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const proxy = {
|
|
43
|
+
exclude: (...routes) => {
|
|
44
|
+
excludedRouteInfos.push(...routes);
|
|
45
|
+
return proxy;
|
|
46
|
+
},
|
|
47
|
+
forRoutes: (...routes) => {
|
|
48
|
+
entry.routes = routes;
|
|
49
|
+
if (excludedRouteInfos.length > 0) entry.excludedRouteInfos = excludedRouteInfos;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return proxy;
|
|
45
54
|
}
|
|
46
|
-
applyTo(keqRequest,
|
|
55
|
+
applyTo(keqRequest, modulesContainer) {
|
|
47
56
|
for (const entry of this.entries) {
|
|
48
|
-
|
|
57
|
+
let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
|
|
58
|
+
if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
|
|
59
|
+
const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
|
|
60
|
+
resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
|
|
61
|
+
}
|
|
49
62
|
if (entry.routes.length === 0 || entry.routes.some((r) => r === KEQ_ROUTES.ALL)) for (const mw of resolved) keqRequest.use(mw);
|
|
50
63
|
else for (const routeTarget of entry.routes) {
|
|
51
64
|
if (routeTarget === KEQ_ROUTES.ALL) continue;
|
|
52
65
|
if (this.isKeqModuleClass(routeTarget)) {
|
|
53
|
-
const targetRequest =
|
|
66
|
+
const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
|
|
54
67
|
if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
|
|
55
68
|
} else {
|
|
56
69
|
const route = this.buildRoute(routeTarget);
|
|
@@ -65,15 +78,41 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
65
78
|
isNestMiddlewareClass(m) {
|
|
66
79
|
return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
|
|
67
80
|
}
|
|
68
|
-
|
|
81
|
+
findMiddlewareInstance(middlewareClass, modulesContainer) {
|
|
82
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
83
|
+
const provider = moduleWrapper.providers.get(middlewareClass);
|
|
84
|
+
if (provider?.instance) return provider.instance;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
resolveMiddleware(m, modulesContainer) {
|
|
69
88
|
if (this.isNestMiddlewareClass(m)) {
|
|
70
|
-
const instance =
|
|
89
|
+
const instance = this.findMiddlewareInstance(m, modulesContainer);
|
|
90
|
+
if (!instance) throw new Error(`[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. Ensure the middleware class is added to the providers array of a @Module() decorator.`);
|
|
71
91
|
return async (ctx, next) => {
|
|
72
92
|
await instance.use(ctx, next);
|
|
73
93
|
};
|
|
74
94
|
}
|
|
75
95
|
return m;
|
|
76
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
|
|
99
|
+
*
|
|
100
|
+
* 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
|
|
101
|
+
* 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
|
|
102
|
+
*
|
|
103
|
+
* @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
|
|
104
|
+
* @param modulesContainer - NestJS 全局模块容器
|
|
105
|
+
* @returns KeqRequest 实例,未找到时返回 undefined
|
|
106
|
+
*/
|
|
107
|
+
findInModulesContainer(routeTarget, modulesContainer) {
|
|
108
|
+
const token = routeTarget.KEQ_REQUEST;
|
|
109
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
110
|
+
if (moduleWrapper.metatype !== routeTarget) continue;
|
|
111
|
+
const provider = moduleWrapper.providers.get(token);
|
|
112
|
+
if (!provider) continue;
|
|
113
|
+
if (provider.instance) return provider.instance;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
77
116
|
buildRoute(info) {
|
|
78
117
|
const routes = [];
|
|
79
118
|
if (info.host) {
|
|
@@ -91,6 +130,17 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
91
130
|
if (routes.length === 1) return routes[0];
|
|
92
131
|
return (0, keq.composeRoute)(routes);
|
|
93
132
|
}
|
|
133
|
+
wrapWithExclusion(middleware, excludedRoutes) {
|
|
134
|
+
const excluded = async (ctx, next) => {
|
|
135
|
+
if (await this.checkExcluded(ctx, excludedRoutes)) return next();
|
|
136
|
+
return middleware(ctx, next);
|
|
137
|
+
};
|
|
138
|
+
return excluded;
|
|
139
|
+
}
|
|
140
|
+
async checkExcluded(ctx, excludedRoutes) {
|
|
141
|
+
for (const route of excludedRoutes) if (await route(ctx)) return true;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
94
144
|
};
|
|
95
145
|
//#endregion
|
|
96
146
|
//#region src/utils/has-configure-keq-middleware.ts
|
|
@@ -112,21 +162,63 @@ function __decorate(decorators, target, key, desc) {
|
|
|
112
162
|
}
|
|
113
163
|
//#endregion
|
|
114
164
|
//#region src/keq.module.ts
|
|
115
|
-
var _ref, _ref2
|
|
165
|
+
var _ref, _ref2;
|
|
166
|
+
/**
|
|
167
|
+
* 全局标记,用于跨 KeqModule 实例去重
|
|
168
|
+
*
|
|
169
|
+
* 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
|
|
170
|
+
* `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
|
|
171
|
+
* 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
|
|
172
|
+
*
|
|
173
|
+
* 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
|
|
174
|
+
* 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
|
|
175
|
+
* 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
|
|
176
|
+
* 只被处理一次,无论有多少个 `KeqModule` 实例。
|
|
177
|
+
*/
|
|
178
|
+
const CONFIGURED_SYMBOL = Symbol.for("__keq_middleware_configured__");
|
|
179
|
+
/**
|
|
180
|
+
* 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
|
|
181
|
+
* 是否显式导入了 `KeqModule`。
|
|
182
|
+
*
|
|
183
|
+
* NestJS 初始化分为两个严格串行的阶段:
|
|
184
|
+
* 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
|
|
185
|
+
* DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
|
|
186
|
+
* 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
|
|
187
|
+
*
|
|
188
|
+
* 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
|
|
189
|
+
* 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
|
|
190
|
+
*
|
|
191
|
+
* 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
|
|
192
|
+
* - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
|
|
193
|
+
* - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
|
|
194
|
+
*/
|
|
195
|
+
function validateConfigureKeqMiddlewareImports(modulesContainer) {
|
|
196
|
+
let keqModuleMetatype;
|
|
197
|
+
for (const [, moduleWrapper] of modulesContainer) if (moduleWrapper.metatype?.name === "KeqModule") {
|
|
198
|
+
keqModuleMetatype = moduleWrapper.metatype;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
202
|
+
const metatype = moduleWrapper.metatype;
|
|
203
|
+
if (typeof metatype?.prototype?.configureKeqMiddleware !== "function") continue;
|
|
204
|
+
if (![...moduleWrapper.imports].some((importedModule) => importedModule.metatype === keqModuleMetatype)) throw new Error(`[KeqModule] '${metatype.name}' implements configureKeqMiddleware() but does not explicitly import KeqModule. Add 'imports: [KeqModule]' to its @Module() decorator to ensure middleware is registered before the module's onModuleInit() runs.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
116
207
|
let KeqModule = class KeqModule {
|
|
117
|
-
constructor(modulesContainer,
|
|
208
|
+
constructor(modulesContainer, keqRequest) {
|
|
118
209
|
this.modulesContainer = modulesContainer;
|
|
119
|
-
this.moduleRef = moduleRef;
|
|
120
210
|
this.keqRequest = keqRequest;
|
|
211
|
+
validateConfigureKeqMiddlewareImports(modulesContainer);
|
|
121
212
|
}
|
|
122
213
|
onModuleInit() {
|
|
123
214
|
for (const [, moduleWrapper] of this.modulesContainer) {
|
|
124
215
|
const instance = moduleWrapper.instance;
|
|
125
|
-
if (hasConfigureKeqMiddleware(instance))
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
216
|
+
if (!hasConfigureKeqMiddleware(instance)) continue;
|
|
217
|
+
if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue;
|
|
218
|
+
moduleWrapper.metatype[CONFIGURED_SYMBOL] = true;
|
|
219
|
+
const consumer = new KeqMiddlewareConsumerImpl();
|
|
220
|
+
instance.configureKeqMiddleware(consumer);
|
|
221
|
+
consumer.applyTo(this.keqRequest, this.modulesContainer);
|
|
130
222
|
}
|
|
131
223
|
}
|
|
132
224
|
};
|
|
@@ -139,11 +231,7 @@ KeqModule = __decorate([
|
|
|
139
231
|
}],
|
|
140
232
|
exports: [keq.KeqRequest]
|
|
141
233
|
}),
|
|
142
|
-
__decorateMetadata("design:paramtypes", [
|
|
143
|
-
typeof (_ref = typeof _nestjs_core.ModulesContainer !== "undefined" && _nestjs_core.ModulesContainer) === "function" ? _ref : Object,
|
|
144
|
-
typeof (_ref2 = typeof _nestjs_core.ModuleRef !== "undefined" && _nestjs_core.ModuleRef) === "function" ? _ref2 : Object,
|
|
145
|
-
typeof (_ref3 = typeof keq.KeqRequest !== "undefined" && keq.KeqRequest) === "function" ? _ref3 : Object
|
|
146
|
-
])
|
|
234
|
+
__decorateMetadata("design:paramtypes", [typeof (_ref = typeof _nestjs_core.ModulesContainer !== "undefined" && _nestjs_core.ModulesContainer) === "function" ? _ref : Object, typeof (_ref2 = typeof keq.KeqRequest !== "undefined" && keq.KeqRequest) === "function" ? _ref2 : Object])
|
|
147
235
|
], KeqModule);
|
|
148
236
|
//#endregion
|
|
149
237
|
exports.KEQ_ROUTES = KEQ_ROUTES;
|
|
@@ -153,5 +241,6 @@ Object.defineProperty(exports, "KeqModule", {
|
|
|
153
241
|
return KeqModule;
|
|
154
242
|
}
|
|
155
243
|
});
|
|
244
|
+
exports.validateConfigureKeqMiddlewareImports = validateConfigureKeqMiddlewareImports;
|
|
156
245
|
|
|
157
246
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["KeqRequest"],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModuleRef } from '@nestjs/core'\nimport type { KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n this.entries.push(entry)\n return {\n forRoutes: (...routes) => {\n entry.routes = routes\n return this\n },\n }\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private resolveMiddleware(m: Type<KeqNestMiddleware> | KeqMiddleware, moduleRef: ModuleRef): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = moduleRef.get<KeqNestMiddleware>(m, { strict: false })\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit } from '@nestjs/common'\nimport { ModulesContainer, ModuleRef } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly moduleRef: ModuleRef,\n private readonly keqRequest: KeqRequest,\n ) {}\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (hasConfigureKeqMiddleware(instance)) {\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.moduleRef)\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["KeqRequest"],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModulesContainer } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private findMiddlewareInstance(\n middlewareClass: Type<KeqNestMiddleware>,\n modulesContainer: ModulesContainer,\n ): KeqNestMiddleware | undefined {\n for (const [, moduleWrapper] of modulesContainer) {\n const provider = moduleWrapper.providers.get(middlewareClass)\n if (provider?.instance) {\n return provider.instance as KeqNestMiddleware\n }\n }\n return undefined\n }\n\n private resolveMiddleware(\n m: Type<KeqNestMiddleware> | KeqMiddleware,\n modulesContainer: ModulesContainer,\n ): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = this.findMiddlewareInstance(m, modulesContainer)\n if (!instance) {\n throw new Error(\n `[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `\n + 'Ensure the middleware class is added to the providers array of a @Module() decorator.',\n )\n }\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n /**\n * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例\n *\n * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。\n * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。\n *\n * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性\n * @param modulesContainer - NestJS 全局模块容器\n * @returns KeqRequest 实例,未找到时返回 undefined\n */\n private findInModulesContainer(\n routeTarget: KeqModuleClass,\n modulesContainer: ModulesContainer,\n ): KeqRequest | undefined {\n const token = routeTarget.KEQ_REQUEST\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype !== routeTarget) continue\n\n const provider = moduleWrapper.providers.get(token)\n if (!provider) continue\n\n if (provider.instance) {\n return provider.instance as KeqRequest\n }\n }\n return undefined\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit, Type } from '@nestjs/common'\nimport { ModulesContainer } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n/**\n * 全局标记,用于跨 KeqModule 实例去重\n *\n * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个\n * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,\n * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。\n *\n * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有\n * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol\n * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类\n * 只被处理一次,无论有多少个 `KeqModule` 实例。\n */\nconst CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')\n\n/**\n * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块\n * 是否显式导入了 `KeqModule`。\n *\n * NestJS 初始化分为两个严格串行的阶段:\n * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,\n * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set\n * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit\n *\n * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,\n * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。\n *\n * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:\n * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中\n * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败\n */\nexport function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {\n // 从 ModulesContainer 中查找 KeqModule 的 metatype,\n // 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题\n let keqModuleMetatype: Type<any> | undefined\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype?.name === 'KeqModule') {\n keqModuleMetatype = moduleWrapper.metatype\n break\n }\n }\n\n for (const [, moduleWrapper] of modulesContainer) {\n const metatype = moduleWrapper.metatype\n if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue\n\n // 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),\n // 替代 Reflect.getMetadata('imports', metatype),解决:\n // 1. DynamicModule 的 imports 无法通过静态元数据检测\n // 2. 重复包实例导致的类引用不匹配\n const hasKeqModule = [...moduleWrapper.imports].some(\n (importedModule) => importedModule.metatype === keqModuleMetatype,\n )\n\n if (!hasKeqModule) {\n throw new Error(\n `[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `\n + 'but does not explicitly import KeqModule. '\n + 'Add \\'imports: [KeqModule]\\' to its @Module() decorator to ensure '\n + 'middleware is registered before the module\\'s onModuleInit() runs.',\n )\n }\n }\n}\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly keqRequest: KeqRequest,\n ) {\n validateConfigureKeqMiddlewareImports(modulesContainer)\n }\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (!hasConfigureKeqMiddleware(instance)) continue\n\n // 跨 KeqModule 实例去重:\n // moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key\n if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue\n moduleWrapper.metatype[CONFIGURED_SYMBOL] = true\n\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.modulesContainer)\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Global, Module } from "@nestjs/common";
|
|
2
|
-
import {
|
|
2
|
+
import { ModulesContainer } from "@nestjs/core";
|
|
3
3
|
import { KeqRequest, composeRoute } from "keq";
|
|
4
4
|
import picomatch from "picomatch";
|
|
5
5
|
//#region src/constants.ts
|
|
@@ -13,20 +13,33 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
13
13
|
middlewares,
|
|
14
14
|
routes: []
|
|
15
15
|
};
|
|
16
|
+
const excludedRouteInfos = [];
|
|
16
17
|
this.entries.push(entry);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const proxy = {
|
|
19
|
+
exclude: (...routes) => {
|
|
20
|
+
excludedRouteInfos.push(...routes);
|
|
21
|
+
return proxy;
|
|
22
|
+
},
|
|
23
|
+
forRoutes: (...routes) => {
|
|
24
|
+
entry.routes = routes;
|
|
25
|
+
if (excludedRouteInfos.length > 0) entry.excludedRouteInfos = excludedRouteInfos;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return proxy;
|
|
21
30
|
}
|
|
22
|
-
applyTo(keqRequest,
|
|
31
|
+
applyTo(keqRequest, modulesContainer) {
|
|
23
32
|
for (const entry of this.entries) {
|
|
24
|
-
|
|
33
|
+
let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
|
|
34
|
+
if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
|
|
35
|
+
const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
|
|
36
|
+
resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
|
|
37
|
+
}
|
|
25
38
|
if (entry.routes.length === 0 || entry.routes.some((r) => r === KEQ_ROUTES.ALL)) for (const mw of resolved) keqRequest.use(mw);
|
|
26
39
|
else for (const routeTarget of entry.routes) {
|
|
27
40
|
if (routeTarget === KEQ_ROUTES.ALL) continue;
|
|
28
41
|
if (this.isKeqModuleClass(routeTarget)) {
|
|
29
|
-
const targetRequest =
|
|
42
|
+
const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
|
|
30
43
|
if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
|
|
31
44
|
} else {
|
|
32
45
|
const route = this.buildRoute(routeTarget);
|
|
@@ -41,15 +54,41 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
41
54
|
isNestMiddlewareClass(m) {
|
|
42
55
|
return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
|
|
43
56
|
}
|
|
44
|
-
|
|
57
|
+
findMiddlewareInstance(middlewareClass, modulesContainer) {
|
|
58
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
59
|
+
const provider = moduleWrapper.providers.get(middlewareClass);
|
|
60
|
+
if (provider?.instance) return provider.instance;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
resolveMiddleware(m, modulesContainer) {
|
|
45
64
|
if (this.isNestMiddlewareClass(m)) {
|
|
46
|
-
const instance =
|
|
65
|
+
const instance = this.findMiddlewareInstance(m, modulesContainer);
|
|
66
|
+
if (!instance) throw new Error(`[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. Ensure the middleware class is added to the providers array of a @Module() decorator.`);
|
|
47
67
|
return async (ctx, next) => {
|
|
48
68
|
await instance.use(ctx, next);
|
|
49
69
|
};
|
|
50
70
|
}
|
|
51
71
|
return m;
|
|
52
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
|
|
75
|
+
*
|
|
76
|
+
* 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
|
|
77
|
+
* 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
|
|
78
|
+
*
|
|
79
|
+
* @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
|
|
80
|
+
* @param modulesContainer - NestJS 全局模块容器
|
|
81
|
+
* @returns KeqRequest 实例,未找到时返回 undefined
|
|
82
|
+
*/
|
|
83
|
+
findInModulesContainer(routeTarget, modulesContainer) {
|
|
84
|
+
const token = routeTarget.KEQ_REQUEST;
|
|
85
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
86
|
+
if (moduleWrapper.metatype !== routeTarget) continue;
|
|
87
|
+
const provider = moduleWrapper.providers.get(token);
|
|
88
|
+
if (!provider) continue;
|
|
89
|
+
if (provider.instance) return provider.instance;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
53
92
|
buildRoute(info) {
|
|
54
93
|
const routes = [];
|
|
55
94
|
if (info.host) {
|
|
@@ -67,6 +106,17 @@ var KeqMiddlewareConsumerImpl = class {
|
|
|
67
106
|
if (routes.length === 1) return routes[0];
|
|
68
107
|
return composeRoute(routes);
|
|
69
108
|
}
|
|
109
|
+
wrapWithExclusion(middleware, excludedRoutes) {
|
|
110
|
+
const excluded = async (ctx, next) => {
|
|
111
|
+
if (await this.checkExcluded(ctx, excludedRoutes)) return next();
|
|
112
|
+
return middleware(ctx, next);
|
|
113
|
+
};
|
|
114
|
+
return excluded;
|
|
115
|
+
}
|
|
116
|
+
async checkExcluded(ctx, excludedRoutes) {
|
|
117
|
+
for (const route of excludedRoutes) if (await route(ctx)) return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
70
120
|
};
|
|
71
121
|
//#endregion
|
|
72
122
|
//#region src/utils/has-configure-keq-middleware.ts
|
|
@@ -88,21 +138,63 @@ function __decorate(decorators, target, key, desc) {
|
|
|
88
138
|
}
|
|
89
139
|
//#endregion
|
|
90
140
|
//#region src/keq.module.ts
|
|
91
|
-
var _ref, _ref2
|
|
141
|
+
var _ref, _ref2;
|
|
142
|
+
/**
|
|
143
|
+
* 全局标记,用于跨 KeqModule 实例去重
|
|
144
|
+
*
|
|
145
|
+
* 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
|
|
146
|
+
* `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
|
|
147
|
+
* 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
|
|
148
|
+
*
|
|
149
|
+
* 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
|
|
150
|
+
* 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
|
|
151
|
+
* 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
|
|
152
|
+
* 只被处理一次,无论有多少个 `KeqModule` 实例。
|
|
153
|
+
*/
|
|
154
|
+
const CONFIGURED_SYMBOL = Symbol.for("__keq_middleware_configured__");
|
|
155
|
+
/**
|
|
156
|
+
* 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
|
|
157
|
+
* 是否显式导入了 `KeqModule`。
|
|
158
|
+
*
|
|
159
|
+
* NestJS 初始化分为两个严格串行的阶段:
|
|
160
|
+
* 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
|
|
161
|
+
* DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
|
|
162
|
+
* 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
|
|
163
|
+
*
|
|
164
|
+
* 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
|
|
165
|
+
* 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
|
|
166
|
+
*
|
|
167
|
+
* 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
|
|
168
|
+
* - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
|
|
169
|
+
* - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
|
|
170
|
+
*/
|
|
171
|
+
function validateConfigureKeqMiddlewareImports(modulesContainer) {
|
|
172
|
+
let keqModuleMetatype;
|
|
173
|
+
for (const [, moduleWrapper] of modulesContainer) if (moduleWrapper.metatype?.name === "KeqModule") {
|
|
174
|
+
keqModuleMetatype = moduleWrapper.metatype;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
178
|
+
const metatype = moduleWrapper.metatype;
|
|
179
|
+
if (typeof metatype?.prototype?.configureKeqMiddleware !== "function") continue;
|
|
180
|
+
if (![...moduleWrapper.imports].some((importedModule) => importedModule.metatype === keqModuleMetatype)) throw new Error(`[KeqModule] '${metatype.name}' implements configureKeqMiddleware() but does not explicitly import KeqModule. Add 'imports: [KeqModule]' to its @Module() decorator to ensure middleware is registered before the module's onModuleInit() runs.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
92
183
|
let KeqModule = class KeqModule {
|
|
93
|
-
constructor(modulesContainer,
|
|
184
|
+
constructor(modulesContainer, keqRequest) {
|
|
94
185
|
this.modulesContainer = modulesContainer;
|
|
95
|
-
this.moduleRef = moduleRef;
|
|
96
186
|
this.keqRequest = keqRequest;
|
|
187
|
+
validateConfigureKeqMiddlewareImports(modulesContainer);
|
|
97
188
|
}
|
|
98
189
|
onModuleInit() {
|
|
99
190
|
for (const [, moduleWrapper] of this.modulesContainer) {
|
|
100
191
|
const instance = moduleWrapper.instance;
|
|
101
|
-
if (hasConfigureKeqMiddleware(instance))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
if (!hasConfigureKeqMiddleware(instance)) continue;
|
|
193
|
+
if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue;
|
|
194
|
+
moduleWrapper.metatype[CONFIGURED_SYMBOL] = true;
|
|
195
|
+
const consumer = new KeqMiddlewareConsumerImpl();
|
|
196
|
+
instance.configureKeqMiddleware(consumer);
|
|
197
|
+
consumer.applyTo(this.keqRequest, this.modulesContainer);
|
|
106
198
|
}
|
|
107
199
|
}
|
|
108
200
|
};
|
|
@@ -115,13 +207,9 @@ KeqModule = __decorate([
|
|
|
115
207
|
}],
|
|
116
208
|
exports: [KeqRequest]
|
|
117
209
|
}),
|
|
118
|
-
__decorateMetadata("design:paramtypes", [
|
|
119
|
-
typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object,
|
|
120
|
-
typeof (_ref2 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref2 : Object,
|
|
121
|
-
typeof (_ref3 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref3 : Object
|
|
122
|
-
])
|
|
210
|
+
__decorateMetadata("design:paramtypes", [typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object, typeof (_ref2 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref2 : Object])
|
|
123
211
|
], KeqModule);
|
|
124
212
|
//#endregion
|
|
125
|
-
export { KEQ_ROUTES, KeqModule };
|
|
213
|
+
export { KEQ_ROUTES, KeqModule, validateConfigureKeqMiddlewareImports };
|
|
126
214
|
|
|
127
215
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModuleRef } from '@nestjs/core'\nimport type { KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n this.entries.push(entry)\n return {\n forRoutes: (...routes) => {\n entry.routes = routes\n return this\n },\n }\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private resolveMiddleware(m: Type<KeqNestMiddleware> | KeqMiddleware, moduleRef: ModuleRef): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = moduleRef.get<KeqNestMiddleware>(m, { strict: false })\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit } from '@nestjs/common'\nimport { ModulesContainer, ModuleRef } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly moduleRef: ModuleRef,\n private readonly keqRequest: KeqRequest,\n ) {}\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (hasConfigureKeqMiddleware(instance)) {\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.moduleRef)\n }\n }\n }\n}\n"],"mappings":";;;;;AAAA,MAAa,aAAa,EACxB,KAAK,OAAO,iBAAiB,EAC9B;;;ACaD,IAAa,4BAAb,MAAwE;CACtE,UAAqC,EAAE;CAEvC,MAAM,GAAG,aAAuF;EAC9F,MAAM,QAAyB;GAAE;GAAa,QAAQ,EAAE;GAAE;AAC1D,OAAK,QAAQ,KAAK,MAAM;AACxB,SAAO,EACL,YAAY,GAAG,WAAW;AACxB,SAAM,SAAS;AACf,UAAO;KAEV;;CAGH,QAAQ,YAAwB,WAA4B;AAC1D,OAAK,MAAM,SAAS,KAAK,SAAS;GAChC,MAAM,WAAW,MAAM,YAAY,KAAK,MAAM,KAAK,kBAAkB,GAAG,UAAU,CAAC;AAInF,OAHiB,MAAM,OAAO,WAAW,KACpC,MAAM,OAAO,MAAM,MAAM,MAAM,WAAW,IAAI,CAGjD,MAAK,MAAM,MAAM,SAAU,YAAW,IAAI,GAAG;OAE7C,MAAK,MAAM,eAAe,MAAM,QAAQ;AACtC,QAAI,gBAAgB,WAAW,IAAK;AAEpC,QAAI,KAAK,iBAAiB,YAAY,EAAE;KACtC,MAAM,gBAAgB,UAAU,IAAgB,YAAY,aAAa,EAAE,QAAQ,OAAO,CAAC;AAC3F,SAAI,cACF,MAAK,MAAM,MAAM,SAAU,eAAc,IAAI,GAAG;WAE7C;KACL,MAAM,QAAQ,KAAK,WAAW,YAA4B;AAC1D,gBAAW,WAAW,CAAC,MAAM,OAAO,SAAS;;;;;CAOvD,iBAAyB,OAAyC;AAChE,SACE,OAAO,UAAU,cACd,iBAAiB,SACjB,OAAQ,MAAkC,gBAAgB;;CAIjE,sBAA8B,GAA0E;AACtG,SAAO,OAAO,MAAM,cAAc,EAAE,cAAc,KAAA,KAAa,SAAS,EAAE;;CAG5E,kBAA0B,GAA4C,WAAqC;AACzG,MAAI,KAAK,sBAAsB,EAAE,EAAE;GACjC,MAAM,WAAW,UAAU,IAAuB,GAAG,EAAE,QAAQ,OAAO,CAAC;AACvE,UAAO,OAAO,KAAK,SAAS;AAC1B,UAAM,SAAS,IAAI,KAAK,KAAK;;;AAGjC,SAAO;;CAGT,WAAmB,MAA8B;EAC/C,MAAM,SAAqB,EAAE;AAC7B,MAAI,KAAK,MAAM;GACb,MAAM,OAAO,KAAK;AAClB,UAAO,MAAM,QAAQ,IAAI,QAAQ,IAAI,SAAS,KAAK;;AAErD,MAAI,KAAK,QAAQ;GACf,MAAM,SAAS,KAAK,OAAO,aAAa;AACxC,UAAO,MAAM,QAAQ,IAAI,QAAQ,OAAO,aAAa,KAAK,OAAO;;AAEnE,MAAI,KAAK,UAAU;GACjB,MAAM,UAAqC,UAAU,KAAK,SAAS;AACnE,UAAO,MAAM,QAAQ,QAAQ,IAAI,QAAQ,IAAI,SAAS,CAAC;;AAEzD,MAAI,OAAO,WAAW,EAAG,QAAO,OAAO;AACvC,SAAO,aAAa,OAAO;;;;;ACzF/B,SAAgB,0BAA0B,UAAoD;AAC5F,QACE,aAAa,QACV,aAAa,KAAA,KACb,OAAQ,SAAqC,2BAA2B;;;;;;;;;;;;;;;;;;ACKxE,IAAA,YAAA,MAAM,UAAkC;CAC7C,YACE,kBACA,WACA,YACA;AAHiB,OAAA,mBAAA;AACA,OAAA,YAAA;AACA,OAAA,aAAA;;CAGnB,eAAqB;AACnB,OAAK,MAAM,GAAG,kBAAkB,KAAK,kBAAkB;GACrD,MAAM,WAAW,cAAc;AAC/B,OAAI,0BAA0B,SAAS,EAAE;IACvC,MAAM,WAAW,IAAI,2BAA2B;AAChD,aAAS,uBAAuB,SAAS;AACzC,aAAS,QAAQ,KAAK,YAAY,KAAK,UAAU;;;;;;CAlBxD,QAAQ;CACR,OAAO;EACN,WAAW,CAAC;GAAE,SAAS;GAAY,kBAAkB,IAAI,YAAY;GAAE,CAAC;EACxE,SAAS,CAAC,WAAW;EACtB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModulesContainer } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private findMiddlewareInstance(\n middlewareClass: Type<KeqNestMiddleware>,\n modulesContainer: ModulesContainer,\n ): KeqNestMiddleware | undefined {\n for (const [, moduleWrapper] of modulesContainer) {\n const provider = moduleWrapper.providers.get(middlewareClass)\n if (provider?.instance) {\n return provider.instance as KeqNestMiddleware\n }\n }\n return undefined\n }\n\n private resolveMiddleware(\n m: Type<KeqNestMiddleware> | KeqMiddleware,\n modulesContainer: ModulesContainer,\n ): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = this.findMiddlewareInstance(m, modulesContainer)\n if (!instance) {\n throw new Error(\n `[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `\n + 'Ensure the middleware class is added to the providers array of a @Module() decorator.',\n )\n }\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n /**\n * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例\n *\n * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。\n * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。\n *\n * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性\n * @param modulesContainer - NestJS 全局模块容器\n * @returns KeqRequest 实例,未找到时返回 undefined\n */\n private findInModulesContainer(\n routeTarget: KeqModuleClass,\n modulesContainer: ModulesContainer,\n ): KeqRequest | undefined {\n const token = routeTarget.KEQ_REQUEST\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype !== routeTarget) continue\n\n const provider = moduleWrapper.providers.get(token)\n if (!provider) continue\n\n if (provider.instance) {\n return provider.instance as KeqRequest\n }\n }\n return undefined\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit, Type } from '@nestjs/common'\nimport { ModulesContainer } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n/**\n * 全局标记,用于跨 KeqModule 实例去重\n *\n * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个\n * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,\n * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。\n *\n * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有\n * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol\n * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类\n * 只被处理一次,无论有多少个 `KeqModule` 实例。\n */\nconst CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')\n\n/**\n * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块\n * 是否显式导入了 `KeqModule`。\n *\n * NestJS 初始化分为两个严格串行的阶段:\n * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,\n * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set\n * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit\n *\n * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,\n * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。\n *\n * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:\n * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中\n * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败\n */\nexport function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {\n // 从 ModulesContainer 中查找 KeqModule 的 metatype,\n // 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题\n let keqModuleMetatype: Type<any> | undefined\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype?.name === 'KeqModule') {\n keqModuleMetatype = moduleWrapper.metatype\n break\n }\n }\n\n for (const [, moduleWrapper] of modulesContainer) {\n const metatype = moduleWrapper.metatype\n if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue\n\n // 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),\n // 替代 Reflect.getMetadata('imports', metatype),解决:\n // 1. DynamicModule 的 imports 无法通过静态元数据检测\n // 2. 重复包实例导致的类引用不匹配\n const hasKeqModule = [...moduleWrapper.imports].some(\n (importedModule) => importedModule.metatype === keqModuleMetatype,\n )\n\n if (!hasKeqModule) {\n throw new Error(\n `[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `\n + 'but does not explicitly import KeqModule. '\n + 'Add \\'imports: [KeqModule]\\' to its @Module() decorator to ensure '\n + 'middleware is registered before the module\\'s onModuleInit() runs.',\n )\n }\n }\n}\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly keqRequest: KeqRequest,\n ) {\n validateConfigureKeqMiddlewareImports(modulesContainer)\n }\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (!hasConfigureKeqMiddleware(instance)) continue\n\n // 跨 KeqModule 实例去重:\n // moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key\n if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue\n moduleWrapper.metatype[CONFIGURED_SYMBOL] = true\n\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.modulesContainer)\n }\n }\n}\n"],"mappings":"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"}
|
package/jest.config.cts
CHANGED
|
@@ -5,6 +5,16 @@ import { compilerOptions } from './tsconfig.json'
|
|
|
5
5
|
|
|
6
6
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
7
7
|
export default async (): Promise<Config> => ({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
transform: {
|
|
9
|
+
'^.+\\.tsx?$': ['ts-jest', {
|
|
10
|
+
tsconfig: {
|
|
11
|
+
noEmitOnError: false,
|
|
12
|
+
},
|
|
13
|
+
}],
|
|
14
|
+
},
|
|
15
|
+
testMatch: [
|
|
16
|
+
'<rootDir>/__tests__/**/*.spec.ts',
|
|
17
|
+
'<rootDir>/src/**/*.spec.ts',
|
|
18
|
+
],
|
|
19
|
+
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>', useESM: true }),
|
|
10
20
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keq-request/nestjs",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.13",
|
|
4
4
|
"description": "Request API write by Typescript for flexibility, readability, and a low learning curve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"request",
|
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@nestjs/common": "^11.1.19",
|
|
41
41
|
"@nestjs/core": "^11.1.19",
|
|
42
|
+
"@nestjs/testing": "^11.1.27",
|
|
42
43
|
"@types/node": "^20.19.39",
|
|
43
44
|
"@types/picomatch": "^4.0.0",
|
|
44
|
-
"
|
|
45
|
+
"reflect-metadata": "^0.2.2",
|
|
46
|
+
"keq": "5.0.0-beta.13"
|
|
45
47
|
},
|
|
46
48
|
"peerDependencies": {
|
|
47
49
|
"@nestjs/common": "^11.0.0",
|
|
48
50
|
"@nestjs/core": "^11.0.0",
|
|
49
|
-
"keq": "^5.0.0-beta.
|
|
51
|
+
"keq": "^5.0.0-beta.13"
|
|
50
52
|
},
|
|
51
53
|
"engines": {
|
|
52
54
|
"node": ">=20.0.0"
|
|
@@ -10,11 +10,12 @@ export interface KeqRouteInfo {
|
|
|
10
10
|
pathname?: string
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface KeqModuleClass {
|
|
13
|
+
export interface KeqModuleClass extends Type<any> {
|
|
14
14
|
readonly KEQ_REQUEST: symbol
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface KeqMiddlewareConfigProxy {
|
|
18
|
+
exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy
|
|
18
19
|
forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Type } from '@nestjs/common'
|
|
2
|
-
import type {
|
|
3
|
-
import type { KeqMiddleware, KeqRoute } from 'keq'
|
|
2
|
+
import type { ModulesContainer } from '@nestjs/core'
|
|
3
|
+
import type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'
|
|
4
4
|
import { composeRoute, KeqRequest } from 'keq'
|
|
5
5
|
import picomatch from 'picomatch'
|
|
6
6
|
import { KEQ_ROUTES } from './constants.js'
|
|
@@ -11,6 +11,7 @@ import type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interfa
|
|
|
11
11
|
interface MiddlewareEntry {
|
|
12
12
|
middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>
|
|
13
13
|
routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>
|
|
14
|
+
excludedRouteInfos?: KeqRouteInfo[]
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
@@ -18,18 +19,34 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
|
18
19
|
|
|
19
20
|
apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {
|
|
20
21
|
const entry: MiddlewareEntry = { middlewares, routes: [] }
|
|
22
|
+
const excludedRouteInfos: KeqRouteInfo[] = []
|
|
21
23
|
this.entries.push(entry)
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
const proxy: KeqMiddlewareConfigProxy = {
|
|
26
|
+
exclude: (...routes) => {
|
|
27
|
+
excludedRouteInfos.push(...routes)
|
|
28
|
+
return proxy
|
|
29
|
+
},
|
|
23
30
|
forRoutes: (...routes) => {
|
|
24
31
|
entry.routes = routes
|
|
32
|
+
if (excludedRouteInfos.length > 0) {
|
|
33
|
+
entry.excludedRouteInfos = excludedRouteInfos
|
|
34
|
+
}
|
|
25
35
|
return this
|
|
26
36
|
},
|
|
27
37
|
}
|
|
38
|
+
return proxy
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
applyTo(keqRequest: KeqRequest,
|
|
41
|
+
applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {
|
|
31
42
|
for (const entry of this.entries) {
|
|
32
|
-
|
|
43
|
+
let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))
|
|
44
|
+
|
|
45
|
+
if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
|
|
46
|
+
const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))
|
|
47
|
+
resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const isGlobal = entry.routes.length === 0
|
|
34
51
|
|| entry.routes.some((r) => r === KEQ_ROUTES.ALL)
|
|
35
52
|
|
|
@@ -40,7 +57,7 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
|
40
57
|
if (routeTarget === KEQ_ROUTES.ALL) continue
|
|
41
58
|
|
|
42
59
|
if (this.isKeqModuleClass(routeTarget)) {
|
|
43
|
-
const targetRequest =
|
|
60
|
+
const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)
|
|
44
61
|
if (targetRequest) {
|
|
45
62
|
for (const mw of resolved) targetRequest.use(mw)
|
|
46
63
|
}
|
|
@@ -65,9 +82,31 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
|
65
82
|
return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
private
|
|
85
|
+
private findMiddlewareInstance(
|
|
86
|
+
middlewareClass: Type<KeqNestMiddleware>,
|
|
87
|
+
modulesContainer: ModulesContainer,
|
|
88
|
+
): KeqNestMiddleware | undefined {
|
|
89
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
90
|
+
const provider = moduleWrapper.providers.get(middlewareClass)
|
|
91
|
+
if (provider?.instance) {
|
|
92
|
+
return provider.instance as KeqNestMiddleware
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private resolveMiddleware(
|
|
99
|
+
m: Type<KeqNestMiddleware> | KeqMiddleware,
|
|
100
|
+
modulesContainer: ModulesContainer,
|
|
101
|
+
): KeqMiddleware {
|
|
69
102
|
if (this.isNestMiddlewareClass(m)) {
|
|
70
|
-
const instance =
|
|
103
|
+
const instance = this.findMiddlewareInstance(m, modulesContainer)
|
|
104
|
+
if (!instance) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `
|
|
107
|
+
+ 'Ensure the middleware class is added to the providers array of a @Module() decorator.',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
71
110
|
return async (ctx, next) => {
|
|
72
111
|
await instance.use(ctx, next)
|
|
73
112
|
}
|
|
@@ -75,6 +114,34 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
|
75
114
|
return m
|
|
76
115
|
}
|
|
77
116
|
|
|
117
|
+
/**
|
|
118
|
+
* 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
|
|
119
|
+
*
|
|
120
|
+
* 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
|
|
121
|
+
* 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
|
|
122
|
+
*
|
|
123
|
+
* @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
|
|
124
|
+
* @param modulesContainer - NestJS 全局模块容器
|
|
125
|
+
* @returns KeqRequest 实例,未找到时返回 undefined
|
|
126
|
+
*/
|
|
127
|
+
private findInModulesContainer(
|
|
128
|
+
routeTarget: KeqModuleClass,
|
|
129
|
+
modulesContainer: ModulesContainer,
|
|
130
|
+
): KeqRequest | undefined {
|
|
131
|
+
const token = routeTarget.KEQ_REQUEST
|
|
132
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
133
|
+
if (moduleWrapper.metatype !== routeTarget) continue
|
|
134
|
+
|
|
135
|
+
const provider = moduleWrapper.providers.get(token)
|
|
136
|
+
if (!provider) continue
|
|
137
|
+
|
|
138
|
+
if (provider.instance) {
|
|
139
|
+
return provider.instance as KeqRequest
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
78
145
|
private buildRoute(info: KeqRouteInfo): KeqRoute {
|
|
79
146
|
const routes: KeqRoute[] = []
|
|
80
147
|
if (info.host) {
|
|
@@ -92,4 +159,21 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
|
|
|
92
159
|
if (routes.length === 1) return routes[0]
|
|
93
160
|
return composeRoute(routes)
|
|
94
161
|
}
|
|
162
|
+
|
|
163
|
+
private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {
|
|
164
|
+
const excluded: KeqMiddleware = async (ctx, next) => {
|
|
165
|
+
if (await this.checkExcluded(ctx, excludedRoutes)) {
|
|
166
|
+
return next()
|
|
167
|
+
}
|
|
168
|
+
return middleware(ctx, next)
|
|
169
|
+
}
|
|
170
|
+
return excluded
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {
|
|
174
|
+
for (const route of excludedRoutes) {
|
|
175
|
+
if (await route(ctx)) return true
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
95
179
|
}
|
package/src/keq.module.ts
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
|
-
import { Global, Module, OnModuleInit } from '@nestjs/common'
|
|
2
|
-
import { ModulesContainer
|
|
1
|
+
import { Global, Module, OnModuleInit, Type } from '@nestjs/common'
|
|
2
|
+
import { ModulesContainer } from '@nestjs/core'
|
|
3
3
|
import { KeqRequest } from 'keq'
|
|
4
4
|
import { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'
|
|
5
5
|
import { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 全局标记,用于跨 KeqModule 实例去重
|
|
9
|
+
*
|
|
10
|
+
* 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
|
|
11
|
+
* `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
|
|
12
|
+
* 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
|
|
13
|
+
*
|
|
14
|
+
* 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
|
|
15
|
+
* 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
|
|
16
|
+
* 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
|
|
17
|
+
* 只被处理一次,无论有多少个 `KeqModule` 实例。
|
|
18
|
+
*/
|
|
19
|
+
const CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
|
|
23
|
+
* 是否显式导入了 `KeqModule`。
|
|
24
|
+
*
|
|
25
|
+
* NestJS 初始化分为两个严格串行的阶段:
|
|
26
|
+
* 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
|
|
27
|
+
* DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
|
|
28
|
+
* 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
|
|
29
|
+
*
|
|
30
|
+
* 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
|
|
31
|
+
* 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
|
|
32
|
+
*
|
|
33
|
+
* 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
|
|
34
|
+
* - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
|
|
35
|
+
* - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
|
|
36
|
+
*/
|
|
37
|
+
export function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {
|
|
38
|
+
// 从 ModulesContainer 中查找 KeqModule 的 metatype,
|
|
39
|
+
// 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题
|
|
40
|
+
let keqModuleMetatype: Type<any> | undefined
|
|
41
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
42
|
+
if (moduleWrapper.metatype?.name === 'KeqModule') {
|
|
43
|
+
keqModuleMetatype = moduleWrapper.metatype
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [, moduleWrapper] of modulesContainer) {
|
|
49
|
+
const metatype = moduleWrapper.metatype
|
|
50
|
+
if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue
|
|
51
|
+
|
|
52
|
+
// 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),
|
|
53
|
+
// 替代 Reflect.getMetadata('imports', metatype),解决:
|
|
54
|
+
// 1. DynamicModule 的 imports 无法通过静态元数据检测
|
|
55
|
+
// 2. 重复包实例导致的类引用不匹配
|
|
56
|
+
const hasKeqModule = [...moduleWrapper.imports].some(
|
|
57
|
+
(importedModule) => importedModule.metatype === keqModuleMetatype,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (!hasKeqModule) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `
|
|
63
|
+
+ 'but does not explicitly import KeqModule. '
|
|
64
|
+
+ 'Add \'imports: [KeqModule]\' to its @Module() decorator to ensure '
|
|
65
|
+
+ 'middleware is registered before the module\'s onModuleInit() runs.',
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
7
70
|
|
|
8
71
|
@Global()
|
|
9
72
|
@Module({
|
|
@@ -13,18 +76,24 @@ import { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.
|
|
|
13
76
|
export class KeqModule implements OnModuleInit {
|
|
14
77
|
constructor(
|
|
15
78
|
private readonly modulesContainer: ModulesContainer,
|
|
16
|
-
private readonly moduleRef: ModuleRef,
|
|
17
79
|
private readonly keqRequest: KeqRequest,
|
|
18
|
-
) {
|
|
80
|
+
) {
|
|
81
|
+
validateConfigureKeqMiddlewareImports(modulesContainer)
|
|
82
|
+
}
|
|
19
83
|
|
|
20
84
|
onModuleInit(): void {
|
|
21
85
|
for (const [, moduleWrapper] of this.modulesContainer) {
|
|
22
86
|
const instance = moduleWrapper.instance
|
|
23
|
-
if (hasConfigureKeqMiddleware(instance))
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
if (!hasConfigureKeqMiddleware(instance)) continue
|
|
88
|
+
|
|
89
|
+
// 跨 KeqModule 实例去重:
|
|
90
|
+
// moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key
|
|
91
|
+
if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue
|
|
92
|
+
moduleWrapper.metatype[CONFIGURED_SYMBOL] = true
|
|
93
|
+
|
|
94
|
+
const consumer = new KeqMiddlewareConsumerImpl()
|
|
95
|
+
instance.configureKeqMiddleware(consumer)
|
|
96
|
+
consumer.applyTo(this.keqRequest, this.modulesContainer)
|
|
28
97
|
}
|
|
29
98
|
}
|
|
30
99
|
}
|