@silkweave/nestjs 2.0.0 → 2.2.0
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/README.md +14 -2
- package/build/index.d.mts +37 -5
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +60 -16
- package/build/index.mjs.map +1 -1
- package/package.json +18 -11
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-check.log +0 -3
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-prepack.log +0 -18
- package/eslint.config.mjs +0 -93
- package/src/adapter/mcp.ts +0 -102
- package/src/decorator/mcp.ts +0 -39
- package/src/index.ts +0 -10
- package/src/lib/controllerDiscovery.ts +0 -201
- package/src/lib/executionContext.ts +0 -76
- package/src/lib/guards.ts +0 -70
- package/src/lib/metadata.ts +0 -38
- package/src/lib/rebind.ts +0 -117
- package/src/lib/reflect/classValidator.ts +0 -56
- package/src/lib/reflect/openapi.ts +0 -87
- package/src/lib/reflect/params.ts +0 -63
- package/src/lib/reflect/route.ts +0 -58
- package/src/lib/reflect/schema.ts +0 -270
- package/src/lib/reflect/swagger.ts +0 -39
- package/src/lib/silkweave.module.ts +0 -75
- package/src/lib/types.ts +0 -61
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/src/lib/guards.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { ForbiddenException, type CanActivate, type Type } from '@nestjs/common'
|
|
2
|
-
import { GUARDS_METADATA } from '@nestjs/common/constants.js'
|
|
3
|
-
import { ModuleRef, Reflector } from '@nestjs/core'
|
|
4
|
-
import { isObservable, lastValueFrom } from 'rxjs'
|
|
5
|
-
import { SilkweaveExecutionContext } from './executionContext.js'
|
|
6
|
-
|
|
7
|
-
type GuardRef = Type<CanActivate> | CanActivate
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Read `@UseGuards(...)` metadata for both the method and its class and merge
|
|
11
|
-
* the two lists. Method-level guards run AFTER class-level guards (matching
|
|
12
|
-
* Nest's own behavior).
|
|
13
|
-
*/
|
|
14
|
-
export function collectGuards(
|
|
15
|
-
reflector: Reflector,
|
|
16
|
-
classRef: Type<unknown>,
|
|
17
|
-
handler: (...args: unknown[]) => unknown
|
|
18
|
-
): GuardRef[] {
|
|
19
|
-
const classGuards = reflector.get<GuardRef[]>(GUARDS_METADATA, classRef) ?? []
|
|
20
|
-
const methodGuards = reflector.get<GuardRef[]>(GUARDS_METADATA, handler) ?? []
|
|
21
|
-
return [...classGuards, ...methodGuards]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function resolveGuard(ref: GuardRef, moduleRef: ModuleRef): Promise<CanActivate> {
|
|
25
|
-
if (typeof ref === 'function') {
|
|
26
|
-
try {
|
|
27
|
-
return await moduleRef.get(ref, { strict: false })
|
|
28
|
-
} catch {
|
|
29
|
-
return moduleRef.create(ref)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return ref
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Run the configured guards against a request. Throws `ForbiddenException`
|
|
37
|
-
* if any guard rejects, mirroring Nest's HTTP request-pipeline behavior.
|
|
38
|
-
*
|
|
39
|
-
* Pass `null` for `response` when running on top of a tRPC or MCP request that
|
|
40
|
-
* doesn't surface a raw response object; guards that introspect the response
|
|
41
|
-
* will receive `null`.
|
|
42
|
-
*
|
|
43
|
-
* `contextType` is reflected through `ExecutionContext.getType()` so guards can
|
|
44
|
-
* branch on the transport. It is `'http'` for REST/tRPC and for MCP-over-HTTP
|
|
45
|
-
* (where a header-bearing request stand-in is available); transports without any
|
|
46
|
-
* HTTP request (e.g. MCP stdio) pass `'rpc'`.
|
|
47
|
-
*/
|
|
48
|
-
export async function runGuards(
|
|
49
|
-
guards: GuardRef[],
|
|
50
|
-
moduleRef: ModuleRef,
|
|
51
|
-
reflector: Reflector,
|
|
52
|
-
classRef: Type<unknown>,
|
|
53
|
-
handler: (...args: unknown[]) => unknown,
|
|
54
|
-
request: unknown,
|
|
55
|
-
response: unknown,
|
|
56
|
-
contextType: 'http' | 'rpc' = 'http'
|
|
57
|
-
): Promise<void> {
|
|
58
|
-
if (guards.length === 0) { return }
|
|
59
|
-
const context = new SilkweaveExecutionContext([request, response], classRef, handler, contextType)
|
|
60
|
-
for (const ref of guards) {
|
|
61
|
-
const guard = await resolveGuard(ref, moduleRef)
|
|
62
|
-
const result = guard.canActivate(context)
|
|
63
|
-
const allowed = isObservable(result) ? await lastValueFrom(result) : await Promise.resolve(result)
|
|
64
|
-
if (!allowed) {
|
|
65
|
-
throw new ForbiddenException('Forbidden resource')
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Reflector kept in the signature for future use (e.g., per-guard metadata) - unused here.
|
|
69
|
-
void reflector
|
|
70
|
-
}
|
package/src/lib/metadata.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { z } from 'zod/v4'
|
|
2
|
-
|
|
3
|
-
/** Reflect-metadata key carrying `@Mcp` options on a controller method. */
|
|
4
|
-
export const MCP_METADATA = '__silkweave_mcp__'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Options for the `@Mcp()` method decorator. Every field is optional - an empty
|
|
8
|
-
* `@Mcp()` exposes the decorated controller route as an MCP tool with its name,
|
|
9
|
-
* description, and input schema fully reflected from the method's route
|
|
10
|
-
* (`@Get`/`@Post`/...), parameter decorators (`@Param`/`@Query`/`@Body`), and
|
|
11
|
-
* any `@nestjs/swagger` (`@ApiOperation`/`@ApiParam`/`@ApiProperty`) or
|
|
12
|
-
* `class-validator` metadata it carries.
|
|
13
|
-
*/
|
|
14
|
-
export interface McpMetadata {
|
|
15
|
-
/**
|
|
16
|
-
* MCP tool name override. When unset it is derived from the controller class
|
|
17
|
-
* and method name (e.g. `ChannelsController.findOne` → `ChannelsFindOne`).
|
|
18
|
-
*/
|
|
19
|
-
name?: string
|
|
20
|
-
/**
|
|
21
|
-
* Tool description override. When unset it falls back to the method's
|
|
22
|
-
* `@ApiOperation({ summary | description })`, then a generated default.
|
|
23
|
-
*/
|
|
24
|
-
description?: string
|
|
25
|
-
/**
|
|
26
|
-
* Zod raw-shape override merged over the reflected input fields (override
|
|
27
|
-
* wins per field). The escape hatch for shapes reflection can't express
|
|
28
|
-
* losslessly - discriminated unions, custom validators, `@Transform`, etc.
|
|
29
|
-
*/
|
|
30
|
-
input?: Record<string, z.ZodType>
|
|
31
|
-
/**
|
|
32
|
-
* Whether to apply the controller method's parameter-bound pipes
|
|
33
|
-
* (`@Param('id', ParseIntPipe)`) when re-binding the call. Default `'apply'`.
|
|
34
|
-
* Global/`ValidationPipe`, interceptors, and exception filters never run -
|
|
35
|
-
* the method is invoked directly, not through Nest's HTTP request pipeline.
|
|
36
|
-
*/
|
|
37
|
-
pipes?: 'apply' | 'skip'
|
|
38
|
-
}
|
package/src/lib/rebind.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import { PARAMTYPE } from './reflect/params.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Instruction for reconstructing one positional handler argument from the flat
|
|
6
|
-
* MCP tool input. Built at discovery time, consumed by {@link invokeRebound}.
|
|
7
|
-
*/
|
|
8
|
-
export type Binding =
|
|
9
|
-
| { kind: 'value'; field: string; source: 'path' | 'query' | 'body'; metatype?: unknown; pipes?: unknown[] }
|
|
10
|
-
| { kind: 'object'; source: 'query' | 'body'; fields: string[]; metatype?: unknown; pipes?: unknown[] }
|
|
11
|
-
| { kind: 'params'; fields: string[] }
|
|
12
|
-
| { kind: 'request' }
|
|
13
|
-
| { kind: 'response' }
|
|
14
|
-
| { kind: 'headers'; data?: string }
|
|
15
|
-
| { kind: 'ip' }
|
|
16
|
-
| { kind: 'host'; data?: string }
|
|
17
|
-
| { kind: 'missing' }
|
|
18
|
-
|
|
19
|
-
interface RequestLike {
|
|
20
|
-
headers?: Record<string, unknown>
|
|
21
|
-
ip?: unknown
|
|
22
|
-
hosts?: Record<string, unknown>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function newInstance(P: any): any | null {
|
|
26
|
-
try { return new P() } catch { return null }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function applyPipes(value: unknown, pipes: unknown[] | undefined, metatype: unknown, type: 'param' | 'query' | 'body', data: string | undefined): Promise<unknown> {
|
|
30
|
-
if (!pipes || pipes.length === 0) { return value }
|
|
31
|
-
let current = value
|
|
32
|
-
for (const p of pipes) {
|
|
33
|
-
const pipe = typeof p === 'function' ? newInstance(p) : p
|
|
34
|
-
if (pipe && typeof (pipe).transform === 'function') {
|
|
35
|
-
current = await (pipe).transform(current, { type, metatype, data })
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return current
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Pick a subset of keys (skipping `undefined`) into a fresh object. */
|
|
42
|
-
function pickFields(input: Record<string, unknown>, fields: string[]): Record<string, unknown> {
|
|
43
|
-
const obj: Record<string, unknown> = {}
|
|
44
|
-
for (const f of fields) {
|
|
45
|
-
if (input[f] !== undefined) { obj[f] = input[f] }
|
|
46
|
-
}
|
|
47
|
-
return obj
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Resolve a single positional argument from one binding. */
|
|
51
|
-
async function resolveArg(
|
|
52
|
-
b: Binding,
|
|
53
|
-
input: Record<string, unknown>,
|
|
54
|
-
request: RequestLike | undefined,
|
|
55
|
-
applyParamPipes: boolean
|
|
56
|
-
): Promise<unknown> {
|
|
57
|
-
switch (b.kind) {
|
|
58
|
-
case 'value': {
|
|
59
|
-
const raw = input[b.field]
|
|
60
|
-
const type = b.source === 'path' ? 'param' : b.source
|
|
61
|
-
return applyParamPipes ? applyPipes(raw, b.pipes, b.metatype, type, b.field) : raw
|
|
62
|
-
}
|
|
63
|
-
case 'object': {
|
|
64
|
-
const obj = pickFields(input, b.fields)
|
|
65
|
-
return applyParamPipes ? applyPipes(obj, b.pipes, b.metatype, b.source, undefined) : obj
|
|
66
|
-
}
|
|
67
|
-
case 'params':
|
|
68
|
-
return pickFields(input, b.fields)
|
|
69
|
-
case 'headers':
|
|
70
|
-
return b.data ? request?.headers?.[b.data.toLowerCase()] : (request?.headers ?? {})
|
|
71
|
-
case 'request':
|
|
72
|
-
return request ?? { headers: {} }
|
|
73
|
-
case 'ip':
|
|
74
|
-
return request?.ip
|
|
75
|
-
case 'host':
|
|
76
|
-
return b.data ? request?.hosts?.[b.data] : request?.hosts
|
|
77
|
-
default:
|
|
78
|
-
return undefined
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Reconstruct the controller method's positional arguments from the validated
|
|
84
|
-
* tool `input` (and the request stand-in for `@Req`/`@Headers`/...), then call
|
|
85
|
-
* it. `applyParamPipes` controls whether parameter-bound pipes run.
|
|
86
|
-
*/
|
|
87
|
-
export async function invokeRebound(
|
|
88
|
-
method: (...args: any[]) => any,
|
|
89
|
-
instance: object,
|
|
90
|
-
input: Record<string, unknown>,
|
|
91
|
-
bindings: Binding[],
|
|
92
|
-
request: RequestLike | undefined,
|
|
93
|
-
applyParamPipes: boolean
|
|
94
|
-
): Promise<unknown> {
|
|
95
|
-
const args: unknown[] = []
|
|
96
|
-
for (let i = 0; i < bindings.length; i += 1) {
|
|
97
|
-
args[i] = await resolveArg(bindings[i], input, request, applyParamPipes)
|
|
98
|
-
}
|
|
99
|
-
return await method.apply(instance, args)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Map a non-input parameter slot (`@Req`/`@Headers`/`@Ip`/...) to its runtime binding. */
|
|
103
|
-
export function specialBinding(paramtype: number, data: string | undefined): Binding | null {
|
|
104
|
-
switch (paramtype) {
|
|
105
|
-
case PARAMTYPE.REQUEST: return { kind: 'request' }
|
|
106
|
-
case PARAMTYPE.RESPONSE:
|
|
107
|
-
case PARAMTYPE.NEXT: return { kind: 'response' }
|
|
108
|
-
case PARAMTYPE.HEADERS: return { kind: 'headers', data }
|
|
109
|
-
case PARAMTYPE.IP: return { kind: 'ip' }
|
|
110
|
-
case PARAMTYPE.HOST: return { kind: 'host', data }
|
|
111
|
-
case PARAMTYPE.SESSION:
|
|
112
|
-
case PARAMTYPE.FILE:
|
|
113
|
-
case PARAMTYPE.FILES:
|
|
114
|
-
case PARAMTYPE.RAW_BODY: return { kind: 'missing' }
|
|
115
|
-
default: return null
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
let cached: any | null | undefined
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Lazily resolve the optional `class-validator` peer; `null` when not installed.
|
|
9
|
-
* Resolution is attempted both from this package and from the app's working
|
|
10
|
-
* directory - the latter is what finds it in a pnpm install where the optional
|
|
11
|
-
* peer lives in the consumer app rather than alongside `@silkweave/nestjs`. A
|
|
12
|
-
* pnpm-deduped install shares one physical copy, so the metadata singleton the
|
|
13
|
-
* app's decorators wrote to is the same one we read.
|
|
14
|
-
*/
|
|
15
|
-
function loadClassValidator(): any | null {
|
|
16
|
-
if (cached !== undefined) { return cached }
|
|
17
|
-
for (const base of [import.meta.url, join(process.cwd(), 'noop.js')]) {
|
|
18
|
-
try {
|
|
19
|
-
cached = createRequire(base)('class-validator')
|
|
20
|
-
return cached
|
|
21
|
-
} catch { /* try the next resolution base */ }
|
|
22
|
-
}
|
|
23
|
-
cached = null
|
|
24
|
-
return cached
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ValidationMeta {
|
|
28
|
-
/** `ValidationTypes` discriminator (e.g. `customValidation`, `conditionalValidation`). */
|
|
29
|
-
type?: string
|
|
30
|
-
/** Validator name - the actionable identity for built-ins (`isString`, `minLength`, ...). */
|
|
31
|
-
name?: string
|
|
32
|
-
constraints?: unknown[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read `class-validator` metadata for a DTO class, grouped by property name.
|
|
37
|
-
* Returns an empty map when `class-validator` is not installed or the class
|
|
38
|
-
* carries no validation decorators - so callers degrade gracefully to swagger /
|
|
39
|
-
* `design:type` reflection.
|
|
40
|
-
*/
|
|
41
|
-
export function classValidatorMetas(dtoType: any): Record<string, ValidationMeta[]> {
|
|
42
|
-
const cv = loadClassValidator()
|
|
43
|
-
if (!cv?.getMetadataStorage) { return {} }
|
|
44
|
-
let metas: Array<{ propertyName?: string; type?: string; name?: string; constraints?: unknown[] }>
|
|
45
|
-
try {
|
|
46
|
-
metas = cv.getMetadataStorage().getTargetValidationMetadatas(dtoType, null, false, false)
|
|
47
|
-
} catch {
|
|
48
|
-
return {}
|
|
49
|
-
}
|
|
50
|
-
const out: Record<string, ValidationMeta[]> = {}
|
|
51
|
-
for (const m of metas) {
|
|
52
|
-
if (!m.propertyName) { continue }
|
|
53
|
-
(out[m.propertyName] ??= []).push({ type: m.type, name: m.name, constraints: m.constraints })
|
|
54
|
-
}
|
|
55
|
-
return out
|
|
56
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import { type FieldDesc, mergeField, openapiSchemaToField } from './schema.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A minimal view of an OpenAPI document - the subset we read. Matches the shape
|
|
6
|
-
* `SwaggerModule.createDocument()` returns, but typed loosely so callers can
|
|
7
|
-
* pass any compatible object without a hard `@nestjs/swagger` dependency.
|
|
8
|
-
*/
|
|
9
|
-
export interface OpenApiDocument {
|
|
10
|
-
paths?: Record<string, Record<string, any>>
|
|
11
|
-
components?: { schemas?: Record<string, any> }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface OpenApiLookup {
|
|
15
|
-
doc: OpenApiDocument
|
|
16
|
-
/** `${METHOD} ${path}` → operation object. */
|
|
17
|
-
operations: Map<string, any>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Index a document's operations by `${METHOD} ${path}` for fast matching. */
|
|
21
|
-
export function buildOpenApiLookup(doc: OpenApiDocument): OpenApiLookup {
|
|
22
|
-
const operations = new Map<string, any>()
|
|
23
|
-
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
24
|
-
for (const [verb, op] of Object.entries(item ?? {})) {
|
|
25
|
-
operations.set(`${verb.toUpperCase()} ${path}`, op)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return { doc, operations }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Locate the operation for a route, tolerating a global path prefix on the document side. */
|
|
32
|
-
function findOperation(lookup: OpenApiLookup, method: string, openapiPath: string): any | undefined {
|
|
33
|
-
const exact = lookup.operations.get(`${method} ${openapiPath}`)
|
|
34
|
-
if (exact) { return exact }
|
|
35
|
-
// Fall back to a suffix match so a `setGlobalPrefix('api')` document still resolves.
|
|
36
|
-
for (const [key, op] of lookup.operations) {
|
|
37
|
-
const [verb, path] = key.split(' ')
|
|
38
|
-
if (verb === method && (path.endsWith(openapiPath) || openapiPath.endsWith(path))) { return op }
|
|
39
|
-
}
|
|
40
|
-
return undefined
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveRef(doc: OpenApiDocument, schema: any): any {
|
|
44
|
-
let current = schema
|
|
45
|
-
let guard = 0
|
|
46
|
-
while (current && typeof current === 'object' && typeof current['$ref'] === 'string' && guard < 10) {
|
|
47
|
-
const match = /^#\/components\/schemas\/(.+)$/.exec(current['$ref'])
|
|
48
|
-
if (!match) { break }
|
|
49
|
-
current = doc.components?.schemas?.[match[1]]
|
|
50
|
-
guard += 1
|
|
51
|
-
}
|
|
52
|
-
return current
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the per-field descriptors for a route from an ingested OpenAPI
|
|
57
|
-
* document. Merges `parameters` (path/query/header) and the JSON request-body
|
|
58
|
-
* schema's properties into a single field map. Returns `{}` when the operation
|
|
59
|
-
* isn't found - callers fall back to decorator reflection.
|
|
60
|
-
*/
|
|
61
|
-
export function openApiFields(lookup: OpenApiLookup, method: string, openapiPath: string): Record<string, FieldDesc> {
|
|
62
|
-
const op = findOperation(lookup, method, openapiPath)
|
|
63
|
-
if (!op) { return {} }
|
|
64
|
-
const out: Record<string, FieldDesc> = {}
|
|
65
|
-
|
|
66
|
-
for (const param of (op['parameters'] as Array<Record<string, any>> | undefined) ?? []) {
|
|
67
|
-
const name = typeof param['name'] === 'string' ? param['name'] : undefined
|
|
68
|
-
if (!name) { continue }
|
|
69
|
-
const schema = param['schema'] ? resolveRef(lookup.doc, param['schema']) : undefined
|
|
70
|
-
let field = schema ? openapiSchemaToField(schema) : {}
|
|
71
|
-
if (param['description']) { field = mergeField(field, { description: param['description'] }) }
|
|
72
|
-
if (typeof param['required'] === 'boolean') { field = mergeField(field, { required: param['required'] }) }
|
|
73
|
-
out[name] = field
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const bodySchema = resolveRef(lookup.doc, op['requestBody']?.['content']?.['application/json']?.['schema'])
|
|
77
|
-
if (bodySchema && typeof bodySchema === 'object' && bodySchema['properties']) {
|
|
78
|
-
const required = new Set<string>(Array.isArray(bodySchema['required']) ? bodySchema['required'] : [])
|
|
79
|
-
for (const [name, propSchema] of Object.entries(bodySchema['properties'] as Record<string, any>)) {
|
|
80
|
-
const field = openapiSchemaToField(resolveRef(lookup.doc, propSchema))
|
|
81
|
-
field.required = required.has(name)
|
|
82
|
-
out[name] = field
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return out
|
|
87
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `RouteParamtypes` numeric values from `@nestjs/common` (re-declared to avoid a
|
|
6
|
-
* value import of an internal enum). These are how `@Param`/`@Query`/`@Body`/...
|
|
7
|
-
* tag each handler argument in `ROUTE_ARGS_METADATA`.
|
|
8
|
-
*/
|
|
9
|
-
export const PARAMTYPE = {
|
|
10
|
-
REQUEST: 0,
|
|
11
|
-
RESPONSE: 1,
|
|
12
|
-
NEXT: 2,
|
|
13
|
-
BODY: 3,
|
|
14
|
-
QUERY: 4,
|
|
15
|
-
PARAM: 5,
|
|
16
|
-
HEADERS: 6,
|
|
17
|
-
SESSION: 7,
|
|
18
|
-
FILE: 8,
|
|
19
|
-
FILES: 9,
|
|
20
|
-
HOST: 10,
|
|
21
|
-
IP: 11,
|
|
22
|
-
RAW_BODY: 12
|
|
23
|
-
} as const
|
|
24
|
-
|
|
25
|
-
export interface ParamSlot {
|
|
26
|
-
/** `PARAMTYPE` value - which decorator tagged this argument. */
|
|
27
|
-
paramtype: number
|
|
28
|
-
/** Handler argument position. */
|
|
29
|
-
index: number
|
|
30
|
-
/** Decorator sub-key: `@Param('id')` → `'id'`; a bare `@Body()` → `undefined`. */
|
|
31
|
-
data?: string
|
|
32
|
-
/** Parameter-bound pipes (`@Param('id', ParseIntPipe)`). */
|
|
33
|
-
pipes: unknown[]
|
|
34
|
-
/** TypeScript-emitted constructor at this position (e.g. `String`, `CreateDto`). */
|
|
35
|
-
designType?: unknown
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Read and normalise the route-argument metadata for a controller method.
|
|
40
|
-
* Returns one {@link ParamSlot} per decorated handler argument, sorted by
|
|
41
|
-
* argument index.
|
|
42
|
-
*/
|
|
43
|
-
export function readParamSlots(classRef: any, methodName: string, proto: any): ParamSlot[] {
|
|
44
|
-
const raw = Reflect.getMetadata(ROUTE_ARGS_METADATA, classRef, methodName) as
|
|
45
|
-
Record<string, { index: number; data?: unknown; pipes?: unknown[] }> | undefined
|
|
46
|
-
if (!raw) { return [] }
|
|
47
|
-
const designTypes = (Reflect.getMetadata('design:paramtypes', proto, methodName) as unknown[] | undefined) ?? []
|
|
48
|
-
|
|
49
|
-
const slots: ParamSlot[] = []
|
|
50
|
-
for (const key of Object.keys(raw)) {
|
|
51
|
-
const entry = raw[key]
|
|
52
|
-
const paramtype = Number(key.split(':')[0])
|
|
53
|
-
if (Number.isNaN(paramtype)) { continue }
|
|
54
|
-
slots.push({
|
|
55
|
-
paramtype,
|
|
56
|
-
index: entry.index,
|
|
57
|
-
data: typeof entry.data === 'string' ? entry.data : undefined,
|
|
58
|
-
pipes: entry.pipes ?? [],
|
|
59
|
-
designType: designTypes[entry.index]
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
return slots.sort((a, b) => a.index - b.index)
|
|
63
|
-
}
|
package/src/lib/reflect/route.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { PATH_METADATA, METHOD_METADATA } from '@nestjs/common/constants.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `RequestMethod` numeric values (from `@nestjs/common`) mapped to verbs. We
|
|
6
|
-
* map our own table rather than importing the enum to avoid pulling a value
|
|
7
|
-
* import for a handful of constants.
|
|
8
|
-
*/
|
|
9
|
-
const REQUEST_METHOD: Record<number, string> = {
|
|
10
|
-
0: 'GET',
|
|
11
|
-
1: 'POST',
|
|
12
|
-
2: 'PUT',
|
|
13
|
-
3: 'DELETE',
|
|
14
|
-
4: 'PATCH',
|
|
15
|
-
6: 'OPTIONS',
|
|
16
|
-
7: 'HEAD'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface RouteInfo {
|
|
20
|
-
/** HTTP verb (`GET`/`POST`/...). `PATCH` and others outside the REST set fall back to `POST`-ish handling by callers. */
|
|
21
|
-
method: string
|
|
22
|
-
/** Full route template in Nest form, e.g. `sessions/:sessionId/channels/:channelId` (no leading slash). */
|
|
23
|
-
path: string
|
|
24
|
-
/** Full route template in OpenAPI form, e.g. `/sessions/{sessionId}/channels/{channelId}`. */
|
|
25
|
-
openapiPath: string
|
|
26
|
-
/** Names of the `:param` placeholders across controller + method path. */
|
|
27
|
-
pathParams: string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function normalizeSegment(value: unknown): string {
|
|
31
|
-
if (typeof value !== 'string') { return '' }
|
|
32
|
-
return value.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function joinPath(...segments: string[]): string {
|
|
36
|
-
return segments.map(normalizeSegment).filter(Boolean).join('/')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolve the composed route (controller prefix + method path), HTTP verb, and
|
|
41
|
-
* path-param names for a decorated controller method, reading Nest's own
|
|
42
|
-
* `PATH_METADATA`/`METHOD_METADATA` reflection.
|
|
43
|
-
*/
|
|
44
|
-
export function reflectRoute(classRef: any, method: (...args: any[]) => any): RouteInfo {
|
|
45
|
-
const classPath = Reflect.getMetadata(PATH_METADATA, classRef) as string | string[] | undefined
|
|
46
|
-
const methodPath = Reflect.getMetadata(PATH_METADATA, method) as string | string[] | undefined
|
|
47
|
-
const verbCode = Reflect.getMetadata(METHOD_METADATA, method) as number | undefined
|
|
48
|
-
|
|
49
|
-
const classSeg = Array.isArray(classPath) ? (classPath[0] ?? '') : (classPath ?? '')
|
|
50
|
-
const methodSeg = Array.isArray(methodPath) ? (methodPath[0] ?? '') : (methodPath ?? '')
|
|
51
|
-
const path = joinPath(classSeg, methodSeg)
|
|
52
|
-
const httpMethod = REQUEST_METHOD[verbCode ?? 0] ?? 'GET'
|
|
53
|
-
|
|
54
|
-
const pathParams = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1])
|
|
55
|
-
const openapiPath = `/${path.replace(/:([A-Za-z0-9_]+)/g, '{$1}')}`
|
|
56
|
-
|
|
57
|
-
return { method: httpMethod, path, openapiPath, pathParams }
|
|
58
|
-
}
|