@keq-request/nestjs 5.0.0-beta.13 → 5.0.0-beta.14
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 +19 -19
- package/CHANGELOG.md +19 -0
- package/__tests__/keq-module.spec.ts +161 -113
- package/dist/index.d.mts +74 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +74 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -119
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +99 -120
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/decorators/inject-keq-consumer.decorator.ts +26 -0
- package/src/index.ts +3 -0
- package/src/interfaces/index.ts +7 -2
- package/src/interfaces/keq-middleware-consumer.interface.ts +11 -5
- package/src/keq-consumer.ts +34 -0
- package/src/keq-middleware-collector.ts +150 -0
- package/src/keq-middleware-consumer.ts +30 -176
- package/src/keq.module.ts +8 -93
- package/src/interfaces/keq-middleware-module.interface.ts +0 -6
- package/src/utils/has-configure-keq-middleware.ts +0 -10
|
@@ -1,179 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import picomatch from 'picomatch'
|
|
6
|
-
import { KEQ_ROUTES } from './constants.js'
|
|
7
|
-
import type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import type { KeqMiddleware } from 'keq'
|
|
3
|
+
import { KeqRequest } from 'keq'
|
|
4
|
+
import type { KeqMiddlewareConfigProxy } from './interfaces/keq-middleware-consumer.interface.js'
|
|
8
5
|
import type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return this
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
return proxy
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {
|
|
42
|
-
for (const entry of this.entries) {
|
|
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
|
-
|
|
50
|
-
const isGlobal = entry.routes.length === 0
|
|
51
|
-
|| entry.routes.some((r) => r === KEQ_ROUTES.ALL)
|
|
52
|
-
|
|
53
|
-
if (isGlobal) {
|
|
54
|
-
for (const mw of resolved) keqRequest.use(mw)
|
|
55
|
-
} else {
|
|
56
|
-
for (const routeTarget of entry.routes) {
|
|
57
|
-
if (routeTarget === KEQ_ROUTES.ALL) continue
|
|
58
|
-
|
|
59
|
-
if (this.isKeqModuleClass(routeTarget)) {
|
|
60
|
-
const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)
|
|
61
|
-
if (targetRequest) {
|
|
62
|
-
for (const mw of resolved) targetRequest.use(mw)
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
const route = this.buildRoute(routeTarget as KeqRouteInfo)
|
|
66
|
-
keqRequest.useRouter().route(route, resolved)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private isKeqModuleClass(route: unknown): route is KeqModuleClass {
|
|
74
|
-
return (
|
|
75
|
-
typeof route === 'function'
|
|
76
|
-
&& 'KEQ_REQUEST' in route
|
|
77
|
-
&& typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {
|
|
82
|
-
return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype
|
|
83
|
-
}
|
|
84
|
-
|
|
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 {
|
|
102
|
-
if (this.isNestMiddlewareClass(m)) {
|
|
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
|
-
}
|
|
110
|
-
return async (ctx, next) => {
|
|
111
|
-
await instance.use(ctx, next)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return m
|
|
115
|
-
}
|
|
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
|
-
|
|
145
|
-
private buildRoute(info: KeqRouteInfo): KeqRoute {
|
|
146
|
-
const routes: KeqRoute[] = []
|
|
147
|
-
if (info.host) {
|
|
148
|
-
const host = info.host
|
|
149
|
-
routes.push((ctx) => ctx.request.url.host === host)
|
|
150
|
-
}
|
|
151
|
-
if (info.method) {
|
|
152
|
-
const method = info.method.toLowerCase()
|
|
153
|
-
routes.push((ctx) => ctx.request.method.toLowerCase() === method)
|
|
154
|
-
}
|
|
155
|
-
if (info.pathname) {
|
|
156
|
-
const isMatch: (test: string) => boolean = picomatch(info.pathname)
|
|
157
|
-
routes.push((ctx) => isMatch(ctx.request.url.pathname))
|
|
158
|
-
}
|
|
159
|
-
if (routes.length === 1) return routes[0]
|
|
160
|
-
return composeRoute(routes)
|
|
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
|
|
6
|
+
import { KeqMiddlewareCollector } from './keq-middleware-collector.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 全局中间件消费者,提供 `.apply().forRoutes()` API。
|
|
11
|
+
*
|
|
12
|
+
* `forRoutes()` 返回 `KeqMiddlewareConsumer` 自身,支持链式注册:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* consumer
|
|
16
|
+
* .apply(mw1).forRoutes(KEQ_ROUTES.ALL)
|
|
17
|
+
* .apply(mw2).forRoutes({ pathname: '/api/*' })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 模块级别的中间件请使用 `@InjectKeqConsumer(GeneratedModule)` 注入
|
|
21
|
+
* `KeqConsumer<GeneratedModule>`,其 `forRoutes()` 直接作用于目标模块。
|
|
22
|
+
*/
|
|
23
|
+
@Injectable()
|
|
24
|
+
export class KeqMiddlewareConsumer {
|
|
25
|
+
constructor(private readonly keqRequest: KeqRequest) {}
|
|
26
|
+
|
|
27
|
+
apply(
|
|
28
|
+
...middlewares: Array<KeqNestMiddleware | KeqMiddleware>
|
|
29
|
+
): KeqMiddlewareConfigProxy {
|
|
30
|
+
const collector = new KeqMiddlewareCollector(this.keqRequest, this)
|
|
31
|
+
return collector.apply(middlewares)
|
|
178
32
|
}
|
|
179
33
|
}
|
package/src/keq.module.ts
CHANGED
|
@@ -1,99 +1,14 @@
|
|
|
1
|
-
import { Global, Module
|
|
2
|
-
import { ModulesContainer } from '@nestjs/core'
|
|
1
|
+
import { Global, Module } from '@nestjs/common'
|
|
3
2
|
import { KeqRequest } from 'keq'
|
|
4
|
-
import {
|
|
5
|
-
import { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'
|
|
3
|
+
import { KeqMiddlewareConsumer } from './keq-middleware-consumer.js'
|
|
6
4
|
|
|
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
|
-
}
|
|
70
5
|
|
|
71
6
|
@Global()
|
|
72
7
|
@Module({
|
|
73
|
-
providers: [
|
|
74
|
-
|
|
8
|
+
providers: [
|
|
9
|
+
{ provide: KeqRequest, useFactory: () => new KeqRequest() },
|
|
10
|
+
KeqMiddlewareConsumer,
|
|
11
|
+
],
|
|
12
|
+
exports: [KeqRequest, KeqMiddlewareConsumer],
|
|
75
13
|
})
|
|
76
|
-
export class KeqModule
|
|
77
|
-
constructor(
|
|
78
|
-
private readonly modulesContainer: ModulesContainer,
|
|
79
|
-
private readonly keqRequest: KeqRequest,
|
|
80
|
-
) {
|
|
81
|
-
validateConfigureKeqMiddlewareImports(modulesContainer)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
onModuleInit(): void {
|
|
85
|
-
for (const [, moduleWrapper] of this.modulesContainer) {
|
|
86
|
-
const instance = moduleWrapper.instance
|
|
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)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
14
|
+
export class KeqModule {}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {
|
|
5
|
-
return (
|
|
6
|
-
instance !== null
|
|
7
|
-
&& instance !== undefined
|
|
8
|
-
&& typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'
|
|
9
|
-
)
|
|
10
|
-
}
|