@silkweave/nestjs 1.12.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 +108 -163
- package/build/index.d.mts +250 -267
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +736 -511
- package/build/index.mjs.map +1 -1
- package/package.json +22 -16
- 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/adapter/rest.ts +0 -173
- package/src/adapter/trpc.ts +0 -58
- package/src/adapter/typegen.ts +0 -55
- package/src/decorator/action.ts +0 -38
- package/src/decorator/actions.ts +0 -25
- package/src/index.ts +0 -14
- package/src/lib/discovery.ts +0 -131
- package/src/lib/executionContext.ts +0 -76
- package/src/lib/filter.ts +0 -26
- package/src/lib/guards.ts +0 -70
- package/src/lib/metadata.ts +0 -58
- package/src/lib/openapi.ts +0 -116
- package/src/lib/silkweave.module.ts +0 -75
- package/src/lib/swagger.ts +0 -74
- package/src/lib/types.ts +0 -58
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/build/index.mjs
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { authMiddleware, mcpCors, mcpTransport, oauthRoutes, protectedResourceMetadata, sideloadResource } from "@silkweave/mcp";
|
|
2
3
|
import express from "express";
|
|
3
|
-
import { ForbiddenException,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { buildLogLevels } from "@silkweave/logger";
|
|
4
|
+
import { ForbiddenException, Inject, Injectable, Module, SetMetadata } from "@nestjs/common";
|
|
5
|
+
import { ApplicationConfig, DiscoveryModule, DiscoveryService, HttpAdapterHost, MetadataScanner, ModuleRef, Reflector } from "@nestjs/core";
|
|
6
|
+
import { createAction, createContext } from "@silkweave/core";
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { renderTypegen } from "@silkweave/typegen";
|
|
11
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, resolve } from "node:path";
|
|
13
|
-
import { DiscoveryModule, DiscoveryService, HttpAdapterHost, MetadataScanner, ModuleRef, Reflector } from "@nestjs/core";
|
|
14
|
-
import { camelCase, kebabCase } from "change-case";
|
|
15
|
-
import { GUARDS_METADATA } from "@nestjs/common/constants.js";
|
|
8
|
+
import { GUARDS_METADATA, METHOD_METADATA, PATH_METADATA, ROUTE_ARGS_METADATA } from "@nestjs/common/constants.js";
|
|
9
|
+
import { join } from "node:path";
|
|
16
10
|
//#region \0rolldown/runtime.js
|
|
17
11
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
18
12
|
//#endregion
|
|
@@ -72,309 +66,46 @@ function mcp(options = {}) {
|
|
|
72
66
|
};
|
|
73
67
|
}
|
|
74
68
|
//#endregion
|
|
75
|
-
//#region src/adapter/rest.ts
|
|
76
|
-
const CONSOLE_LEVEL_MAP = {
|
|
77
|
-
emergency: "error",
|
|
78
|
-
alert: "error",
|
|
79
|
-
critical: "error",
|
|
80
|
-
error: "error",
|
|
81
|
-
warning: "warn",
|
|
82
|
-
notice: "info",
|
|
83
|
-
info: "info",
|
|
84
|
-
debug: "log"
|
|
85
|
-
};
|
|
86
|
-
function createRestLogger() {
|
|
87
|
-
return {
|
|
88
|
-
...buildLogLevels((level, data) => {
|
|
89
|
-
console[CONSOLE_LEVEL_MAP[level]](data);
|
|
90
|
-
}),
|
|
91
|
-
progress: () => {}
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function actionNameToPath(name) {
|
|
95
|
-
return `/${name.replace(/\./g, "/")}`;
|
|
96
|
-
}
|
|
97
|
-
async function readJsonBody(req) {
|
|
98
|
-
return new Promise((resolve, reject) => {
|
|
99
|
-
const chunks = [];
|
|
100
|
-
req.on("data", (c) => chunks.push(c));
|
|
101
|
-
req.on("end", () => {
|
|
102
|
-
if (chunks.length === 0) return resolve({});
|
|
103
|
-
try {
|
|
104
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
105
|
-
} catch (err) {
|
|
106
|
-
reject(err instanceof Error ? err : /* @__PURE__ */ new Error("Unable to parse JSON body"));
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
req.on("error", reject);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function parseQuery(url) {
|
|
113
|
-
if (!url) return {};
|
|
114
|
-
const qIndex = url.indexOf("?");
|
|
115
|
-
if (qIndex === -1) return {};
|
|
116
|
-
const params = {};
|
|
117
|
-
const search = new URLSearchParams(url.slice(qIndex + 1));
|
|
118
|
-
for (const [k, v] of search.entries()) params[k] = v;
|
|
119
|
-
return params;
|
|
120
|
-
}
|
|
121
|
-
function sendJson(res, body, status) {
|
|
122
|
-
if (res.headersSent) return;
|
|
123
|
-
res.statusCode = status;
|
|
124
|
-
res.setHeader("Content-Type", "application/json");
|
|
125
|
-
res.end(JSON.stringify(body));
|
|
126
|
-
}
|
|
127
|
-
function handleRestError(err, res) {
|
|
128
|
-
if (err instanceof z.ZodError) {
|
|
129
|
-
sendJson(res, {
|
|
130
|
-
error: "validation_error",
|
|
131
|
-
issues: err.issues
|
|
132
|
-
}, 400);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (err instanceof SilkweaveError) {
|
|
136
|
-
sendJson(res, {
|
|
137
|
-
error: err.code,
|
|
138
|
-
message: err.message
|
|
139
|
-
}, err.statusCode);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (err instanceof HttpException) {
|
|
143
|
-
const body = err.getResponse();
|
|
144
|
-
sendJson(res, typeof body === "string" ? {
|
|
145
|
-
error: err.name,
|
|
146
|
-
message: body
|
|
147
|
-
} : body, err.getStatus());
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
console.error(err);
|
|
151
|
-
sendJson(res, {
|
|
152
|
-
error: "internal",
|
|
153
|
-
message: "Internal error"
|
|
154
|
-
}, 500);
|
|
155
|
-
}
|
|
156
|
-
function createActionHandler(action, context, auth, logger) {
|
|
157
|
-
return async (req, res) => {
|
|
158
|
-
try {
|
|
159
|
-
const reqLike = req;
|
|
160
|
-
let authInfo;
|
|
161
|
-
if (auth) {
|
|
162
|
-
const result = await validateToken(typeof reqLike.headers.authorization === "string" ? reqLike.headers.authorization : void 0, auth, context.fork({ request: req }));
|
|
163
|
-
if (result.error) {
|
|
164
|
-
for (const [k, v] of Object.entries(result.error.headers)) res.setHeader(k, v);
|
|
165
|
-
sendJson(res, result.error.body, result.error.statusCode);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
authInfo = result.auth;
|
|
169
|
-
}
|
|
170
|
-
const query = reqLike.query ?? parseQuery(req.url);
|
|
171
|
-
const body = methodHasBody(actionMethod(action)) ? reqLike.body ?? await readJsonBody(req) : void 0;
|
|
172
|
-
const raw = resolveActionInput(action, {
|
|
173
|
-
params: reqLike.params,
|
|
174
|
-
query,
|
|
175
|
-
body
|
|
176
|
-
});
|
|
177
|
-
const input = action.input.parse(raw);
|
|
178
|
-
sendJson(res, await action.run(input, context.fork({
|
|
179
|
-
logger,
|
|
180
|
-
request: req,
|
|
181
|
-
response: res,
|
|
182
|
-
...authInfo ? { auth: authInfo } : {}
|
|
183
|
-
})), 200);
|
|
184
|
-
} catch (err) {
|
|
185
|
-
handleRestError(err, res);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
|
|
191
|
-
* as an individual route on Nest's HTTP adapter:
|
|
192
|
-
*
|
|
193
|
-
* - HTTP verb comes from `method`, else `GET` for `kind: 'query'`, else `POST`.
|
|
194
|
-
* - Path comes from `path` (joined to `basePath`, may contain `:param`
|
|
195
|
-
* placeholders), else `${basePath}/${actionName-with-slashes}`.
|
|
196
|
-
* - Input is merged from the request body, the `queryParams` query-string
|
|
197
|
-
* fields, and any `:param` path placeholders (see `resolveActionInput`).
|
|
198
|
-
*
|
|
199
|
-
* Routes show up in Nest's `RoutesResolver` logger. They are registered
|
|
200
|
-
* directly on the HTTP adapter (not as Nest controllers), so `@nestjs/swagger`'s
|
|
201
|
-
* controller scanner does not see them - use `addSilkweaveActions()` to add them
|
|
202
|
-
* to the OpenAPI document. Works on `@nestjs/platform-express` out of the box.
|
|
203
|
-
* For `@nestjs/platform-fastify`, register `@fastify/express` first.
|
|
204
|
-
*/
|
|
205
|
-
function rest(options = {}) {
|
|
206
|
-
const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
|
|
207
|
-
return {
|
|
208
|
-
name: "rest",
|
|
209
|
-
basePath,
|
|
210
|
-
register({ httpAdapter, baseContext, actions }) {
|
|
211
|
-
const logger = createRestLogger();
|
|
212
|
-
for (const action of actions) {
|
|
213
|
-
validateActionRouting(action);
|
|
214
|
-
const path = action.path ? `${basePath}/${action.path.replace(/^\//, "")}` : `${basePath}${actionNameToPath(action.name)}`;
|
|
215
|
-
const method = actionMethod(action).toLowerCase();
|
|
216
|
-
const handler = createActionHandler(action, baseContext, options.auth, logger);
|
|
217
|
-
httpAdapter[method](path, handler);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
//#endregion
|
|
223
|
-
//#region src/adapter/trpc.ts
|
|
224
|
-
/**
|
|
225
|
-
* tRPC adapter for `@silkweave/nestjs`. Builds a tRPC router from discovered
|
|
226
|
-
* `@Action` methods and mounts the resulting express middleware at
|
|
227
|
-
* `basePath` on Nest's HTTP adapter.
|
|
228
|
-
*
|
|
229
|
-
* Action names with dots (e.g. `users.list` from `@Actions('users')`) collapse
|
|
230
|
-
* to camelCase procedure keys (`usersList`).
|
|
231
|
-
*
|
|
232
|
-
* Works on `@nestjs/platform-express`. On `@nestjs/platform-fastify`, register
|
|
233
|
-
* `@fastify/express` first so Nest can serve Express-style middleware.
|
|
234
|
-
*/
|
|
235
|
-
function trpc(options = {}) {
|
|
236
|
-
return {
|
|
237
|
-
name: "trpc",
|
|
238
|
-
register({ httpAdapter, baseContext, actions }) {
|
|
239
|
-
const basePath = (options.basePath ?? "/trpc").replace(/\/$/, "") || "/";
|
|
240
|
-
const router = buildRouter(actions);
|
|
241
|
-
const logger = createActionLogger();
|
|
242
|
-
const createContext = async (opts) => {
|
|
243
|
-
const resolved = await resolveAuth(options.auth, opts.req.headers.authorization, baseContext.fork({ request: opts.req }));
|
|
244
|
-
if (resolved.kind === "error") {
|
|
245
|
-
for (const [key, value] of Object.entries(resolved.error.headers)) opts.res.setHeader(key, value);
|
|
246
|
-
opts.res.statusCode = resolved.error.statusCode;
|
|
247
|
-
opts.res.setHeader("Content-Type", "application/json");
|
|
248
|
-
opts.res.end(JSON.stringify(resolved.error.body));
|
|
249
|
-
throw new Error("Unauthorized");
|
|
250
|
-
}
|
|
251
|
-
return { silkweaveContext: baseContext.fork({
|
|
252
|
-
logger,
|
|
253
|
-
request: opts.req,
|
|
254
|
-
response: opts.res,
|
|
255
|
-
...resolved.authInfo ? { auth: resolved.authInfo } : {}
|
|
256
|
-
}) };
|
|
257
|
-
};
|
|
258
|
-
const middleware = createExpressMiddleware({
|
|
259
|
-
router,
|
|
260
|
-
createContext
|
|
261
|
-
});
|
|
262
|
-
httpAdapter.use(basePath, middleware);
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/adapter/typegen.ts
|
|
268
|
-
/**
|
|
269
|
-
* Typegen adapter for `@silkweave/nestjs`. Discovers every `@Action`-decorated
|
|
270
|
-
* method (regardless of `transports` filtering) and writes a single `.d.ts`
|
|
271
|
-
* file with REST input/output interfaces and/or a tRPC `AppRouter` type alias.
|
|
272
|
-
*
|
|
273
|
-
* Designed for the monorepo pattern where the server (this app) is the source
|
|
274
|
-
* of truth for types and consumer apps import from `path` - e.g.
|
|
275
|
-
* `path: '../app/src/types/silkweave.ts'`.
|
|
276
|
-
*/
|
|
277
|
-
function typegen(options) {
|
|
278
|
-
const enabled = options.enabled ?? process.env["NODE_ENV"] !== "production";
|
|
279
|
-
return {
|
|
280
|
-
name: "typegen",
|
|
281
|
-
allActions: true,
|
|
282
|
-
register({ actions }) {
|
|
283
|
-
if (!enabled) return;
|
|
284
|
-
const output = renderTypegen(actions, options.format ?? "all");
|
|
285
|
-
const target = resolve(options.path);
|
|
286
|
-
(async () => {
|
|
287
|
-
try {
|
|
288
|
-
await mkdir(dirname(target), { recursive: true });
|
|
289
|
-
await writeFile(target, output, "utf-8");
|
|
290
|
-
console.info(`@silkweave/nestjs typegen: wrote ${actions.length} action types to ${target}`);
|
|
291
|
-
} catch (err) {
|
|
292
|
-
console.error("@silkweave/nestjs typegen:", err);
|
|
293
|
-
}
|
|
294
|
-
})();
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
//#endregion
|
|
299
69
|
//#region src/lib/metadata.ts
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const ACTION_RESPONSE_KEY = "__silkweave_response__";
|
|
70
|
+
/** Reflect-metadata key carrying `@Mcp` options on a controller method. */
|
|
71
|
+
const MCP_METADATA = "__silkweave_mcp__";
|
|
303
72
|
//#endregion
|
|
304
|
-
//#region src/decorator/
|
|
73
|
+
//#region src/decorator/mcp.ts
|
|
305
74
|
/**
|
|
306
|
-
* Method decorator that
|
|
75
|
+
* Method decorator that exposes an existing NestJS controller route as an MCP
|
|
76
|
+
* tool. It is **additive** - the route keeps serving HTTP exactly as before;
|
|
77
|
+
* `@Mcp()` just opts the method into MCP discovery.
|
|
307
78
|
*
|
|
308
|
-
* The
|
|
309
|
-
*
|
|
310
|
-
*
|
|
79
|
+
* The tool's name, description, and input schema are reflected from the
|
|
80
|
+
* method's own metadata:
|
|
81
|
+
* - **fields** from the parameter decorators (`@Param`/`@Query`/`@Body`) - a
|
|
82
|
+
* `@Param('id')` becomes an `id` field; a whole-DTO `@Body() dto: CreateDto`
|
|
83
|
+
* is flattened to its properties,
|
|
84
|
+
* - **types/constraints/descriptions** from `@nestjs/swagger`
|
|
85
|
+
* (`@ApiParam`/`@ApiQuery`/`@ApiProperty`/`@ApiOperation`) and, when present,
|
|
86
|
+
* `class-validator` decorators on the DTOs,
|
|
87
|
+
* - optionally refined by an OpenAPI document passed to `SilkweaveModule`.
|
|
311
88
|
*
|
|
312
|
-
*
|
|
313
|
-
* and
|
|
314
|
-
* `auth`). The class instance is a normal Nest provider, so other services can
|
|
315
|
-
* be injected via the constructor.
|
|
89
|
+
* On a tool call the input is split back into the method's positional arguments
|
|
90
|
+
* and the method is invoked directly (with `@UseGuards` guards applied first).
|
|
316
91
|
*
|
|
317
92
|
* @example
|
|
318
93
|
* ```ts
|
|
319
|
-
* @
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* })
|
|
329
|
-
* list(input: { limit?: number }, ctx: SilkweaveContext) {
|
|
330
|
-
* return this.db.listUsers(input.limit)
|
|
94
|
+
* @Controller('sessions/:sessionId/channels')
|
|
95
|
+
* export class ChannelsController {
|
|
96
|
+
* @Get(':channelId')
|
|
97
|
+
* @ApiOperation({ summary: 'Get a specific channel by ID' })
|
|
98
|
+
* @ApiParam({ name: 'sessionId', description: 'Session ID' })
|
|
99
|
+
* @ApiParam({ name: 'channelId', description: 'Channel ID' })
|
|
100
|
+
* @Mcp()
|
|
101
|
+
* findOne(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) {
|
|
102
|
+
* return this.service.get(sessionId, channelId)
|
|
331
103
|
* }
|
|
332
104
|
* }
|
|
333
105
|
* ```
|
|
334
106
|
*/
|
|
335
|
-
function
|
|
336
|
-
return SetMetadata(
|
|
337
|
-
}
|
|
338
|
-
//#endregion
|
|
339
|
-
//#region src/decorator/actions.ts
|
|
340
|
-
/**
|
|
341
|
-
* Class decorator that groups a provider's `@Action` methods under a common
|
|
342
|
-
* prefix. The prefix is joined to each method's action name with a dot
|
|
343
|
-
* (e.g. `@Actions('users')` + method `list` → action name `users.list`).
|
|
344
|
-
*
|
|
345
|
-
* The class itself remains a normal Nest provider - add `@Injectable()`
|
|
346
|
-
* separately so it can be resolved by the DI container.
|
|
347
|
-
*
|
|
348
|
-
* Accepts either a prefix string (shorthand) or a full options object:
|
|
349
|
-
* ```ts
|
|
350
|
-
* @Actions('users')
|
|
351
|
-
* @Actions({ prefix: 'users', transports: ['rest', 'trpc'] })
|
|
352
|
-
* ```
|
|
353
|
-
*/
|
|
354
|
-
function Actions(prefixOrOptions = {}) {
|
|
355
|
-
return SetMetadata(ACTIONS_METADATA, typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions);
|
|
356
|
-
}
|
|
357
|
-
//#endregion
|
|
358
|
-
//#region src/lib/filter.ts
|
|
359
|
-
/**
|
|
360
|
-
* Compile a `transports` allowlist + optional user `isEnabled` into a single
|
|
361
|
-
* `(ctx) => boolean` callback compatible with `Action.isEnabled`.
|
|
362
|
-
*
|
|
363
|
-
* - If `transports` is omitted, the action runs on every adapter.
|
|
364
|
-
* - If `transports` is set, the action is gated on `ctx.get<string>('adapter')`
|
|
365
|
-
* matching one of the listed transports. Each adapter in `@silkweave/nestjs`
|
|
366
|
-
* forks its context with `{ adapter: 'rest' | 'trpc' | 'mcp' }`.
|
|
367
|
-
* - If both `transports` and `userIsEnabled` are set, they are AND-combined.
|
|
368
|
-
*/
|
|
369
|
-
function buildIsEnabled(transports, userIsEnabled) {
|
|
370
|
-
if (!transports && !userIsEnabled) return;
|
|
371
|
-
return (ctx) => {
|
|
372
|
-
if (transports) {
|
|
373
|
-
const adapter = ctx.getOptional("adapter");
|
|
374
|
-
if (adapter && !transports.includes(adapter)) return false;
|
|
375
|
-
}
|
|
376
|
-
return userIsEnabled ? userIsEnabled(ctx) : true;
|
|
377
|
-
};
|
|
107
|
+
function Mcp(options = {}) {
|
|
108
|
+
return SetMetadata(MCP_METADATA, options);
|
|
378
109
|
}
|
|
379
110
|
//#endregion
|
|
380
111
|
//#region ../../node_modules/.pnpm/rxjs@7.8.2/node_modules/rxjs/dist/cjs/internal/util/isFunction.js
|
|
@@ -9030,6 +8761,26 @@ var SilkweaveExecutionContext = class {
|
|
|
9030
8761
|
//#endregion
|
|
9031
8762
|
//#region src/lib/guards.ts
|
|
9032
8763
|
/**
|
|
8764
|
+
* Collect the app's global guards that match an opt-in allow-list of classes.
|
|
8765
|
+
*
|
|
8766
|
+
* Reads both registration styles Nest exposes via `ApplicationConfig`:
|
|
8767
|
+
* `useGlobalGuards(new X())` instances (`getGlobalGuards()`) and
|
|
8768
|
+
* `{ provide: APP_GUARD, useClass }` DI guards (`getGlobalRequestGuards()`,
|
|
8769
|
+
* which yields `InstanceWrapper`s - we read `.instance` off each).
|
|
8770
|
+
*
|
|
8771
|
+
* The allow-list is intentionally explicit-by-class: a blanket "run every
|
|
8772
|
+
* global" would also fire unrelated globals (e.g. a `ThrottlerGuard` that
|
|
8773
|
+
* assumes a writable response) on every tool call. An empty allow-list runs
|
|
8774
|
+
* no globals, preserving the prior behavior.
|
|
8775
|
+
*
|
|
8776
|
+
* Call this at tool-call time, not at discovery time: `APP_GUARD` instances
|
|
8777
|
+
* aren't populated until `app.init()` finishes.
|
|
8778
|
+
*/
|
|
8779
|
+
function collectGlobalGuards(appConfig, allowList) {
|
|
8780
|
+
if (allowList.length === 0) return [];
|
|
8781
|
+
return [...appConfig.getGlobalGuards(), ...appConfig.getGlobalRequestGuards().map((w) => w.instance)].filter((g) => g != null).filter((g) => allowList.some((c) => g instanceof c));
|
|
8782
|
+
}
|
|
8783
|
+
/**
|
|
9033
8784
|
* Read `@UseGuards(...)` metadata for both the method and its class and merge
|
|
9034
8785
|
* the two lists. Method-level guards run AFTER class-level guards (matching
|
|
9035
8786
|
* Nest's own behavior).
|
|
@@ -9069,6 +8820,532 @@ async function runGuards(guards, moduleRef, reflector, classRef, handler, reques
|
|
|
9069
8820
|
}
|
|
9070
8821
|
}
|
|
9071
8822
|
//#endregion
|
|
8823
|
+
//#region src/lib/reflect/params.ts
|
|
8824
|
+
/**
|
|
8825
|
+
* `RouteParamtypes` numeric values from `@nestjs/common` (re-declared to avoid a
|
|
8826
|
+
* value import of an internal enum). These are how `@Param`/`@Query`/`@Body`/...
|
|
8827
|
+
* tag each handler argument in `ROUTE_ARGS_METADATA`.
|
|
8828
|
+
*/
|
|
8829
|
+
const PARAMTYPE = {
|
|
8830
|
+
REQUEST: 0,
|
|
8831
|
+
RESPONSE: 1,
|
|
8832
|
+
NEXT: 2,
|
|
8833
|
+
BODY: 3,
|
|
8834
|
+
QUERY: 4,
|
|
8835
|
+
PARAM: 5,
|
|
8836
|
+
HEADERS: 6,
|
|
8837
|
+
SESSION: 7,
|
|
8838
|
+
FILE: 8,
|
|
8839
|
+
FILES: 9,
|
|
8840
|
+
HOST: 10,
|
|
8841
|
+
IP: 11,
|
|
8842
|
+
RAW_BODY: 12
|
|
8843
|
+
};
|
|
8844
|
+
/**
|
|
8845
|
+
* Read and normalise the route-argument metadata for a controller method.
|
|
8846
|
+
* Returns one {@link ParamSlot} per decorated handler argument, sorted by
|
|
8847
|
+
* argument index.
|
|
8848
|
+
*/
|
|
8849
|
+
function readParamSlots(classRef, methodName, proto) {
|
|
8850
|
+
const raw = Reflect.getMetadata(ROUTE_ARGS_METADATA, classRef, methodName);
|
|
8851
|
+
if (!raw) return [];
|
|
8852
|
+
const designTypes = Reflect.getMetadata("design:paramtypes", proto, methodName) ?? [];
|
|
8853
|
+
const slots = [];
|
|
8854
|
+
for (const key of Object.keys(raw)) {
|
|
8855
|
+
const entry = raw[key];
|
|
8856
|
+
const paramtype = Number(key.split(":")[0]);
|
|
8857
|
+
if (Number.isNaN(paramtype)) continue;
|
|
8858
|
+
slots.push({
|
|
8859
|
+
paramtype,
|
|
8860
|
+
index: entry.index,
|
|
8861
|
+
data: typeof entry.data === "string" ? entry.data : void 0,
|
|
8862
|
+
pipes: entry.pipes ?? [],
|
|
8863
|
+
designType: designTypes[entry.index]
|
|
8864
|
+
});
|
|
8865
|
+
}
|
|
8866
|
+
return slots.sort((a, b) => a.index - b.index);
|
|
8867
|
+
}
|
|
8868
|
+
//#endregion
|
|
8869
|
+
//#region src/lib/rebind.ts
|
|
8870
|
+
function newInstance(P) {
|
|
8871
|
+
try {
|
|
8872
|
+
return new P();
|
|
8873
|
+
} catch {
|
|
8874
|
+
return null;
|
|
8875
|
+
}
|
|
8876
|
+
}
|
|
8877
|
+
async function applyPipes(value, pipes, metatype, type, data) {
|
|
8878
|
+
if (!pipes || pipes.length === 0) return value;
|
|
8879
|
+
let current = value;
|
|
8880
|
+
for (const p of pipes) {
|
|
8881
|
+
const pipe = typeof p === "function" ? newInstance(p) : p;
|
|
8882
|
+
if (pipe && typeof pipe.transform === "function") current = await pipe.transform(current, {
|
|
8883
|
+
type,
|
|
8884
|
+
metatype,
|
|
8885
|
+
data
|
|
8886
|
+
});
|
|
8887
|
+
}
|
|
8888
|
+
return current;
|
|
8889
|
+
}
|
|
8890
|
+
/** Pick a subset of keys (skipping `undefined`) into a fresh object. */
|
|
8891
|
+
function pickFields(input, fields) {
|
|
8892
|
+
const obj = {};
|
|
8893
|
+
for (const f of fields) if (input[f] !== void 0) obj[f] = input[f];
|
|
8894
|
+
return obj;
|
|
8895
|
+
}
|
|
8896
|
+
/** Resolve a single positional argument from one binding. */
|
|
8897
|
+
async function resolveArg(b, input, request, applyParamPipes) {
|
|
8898
|
+
switch (b.kind) {
|
|
8899
|
+
case "value": {
|
|
8900
|
+
const raw = input[b.field];
|
|
8901
|
+
const type = b.source === "path" ? "param" : b.source;
|
|
8902
|
+
return applyParamPipes ? applyPipes(raw, b.pipes, b.metatype, type, b.field) : raw;
|
|
8903
|
+
}
|
|
8904
|
+
case "object": {
|
|
8905
|
+
const obj = pickFields(input, b.fields);
|
|
8906
|
+
return applyParamPipes ? applyPipes(obj, b.pipes, b.metatype, b.source, void 0) : obj;
|
|
8907
|
+
}
|
|
8908
|
+
case "params": return pickFields(input, b.fields);
|
|
8909
|
+
case "headers": return b.data ? request?.headers?.[b.data.toLowerCase()] : request?.headers ?? {};
|
|
8910
|
+
case "request": return request ?? { headers: {} };
|
|
8911
|
+
case "ip": return request?.ip;
|
|
8912
|
+
case "host": return b.data ? request?.hosts?.[b.data] : request?.hosts;
|
|
8913
|
+
default: return;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
/**
|
|
8917
|
+
* Reconstruct the controller method's positional arguments from the validated
|
|
8918
|
+
* tool `input` (and the request stand-in for `@Req`/`@Headers`/...), then call
|
|
8919
|
+
* it. `applyParamPipes` controls whether parameter-bound pipes run.
|
|
8920
|
+
*/
|
|
8921
|
+
async function invokeRebound(method, instance, input, bindings, request, applyParamPipes) {
|
|
8922
|
+
const args = [];
|
|
8923
|
+
for (let i = 0; i < bindings.length; i += 1) args[i] = await resolveArg(bindings[i], input, request, applyParamPipes);
|
|
8924
|
+
return await method.apply(instance, args);
|
|
8925
|
+
}
|
|
8926
|
+
/** Map a non-input parameter slot (`@Req`/`@Headers`/`@Ip`/...) to its runtime binding. */
|
|
8927
|
+
function specialBinding(paramtype, data) {
|
|
8928
|
+
switch (paramtype) {
|
|
8929
|
+
case PARAMTYPE.REQUEST: return { kind: "request" };
|
|
8930
|
+
case PARAMTYPE.RESPONSE:
|
|
8931
|
+
case PARAMTYPE.NEXT: return { kind: "response" };
|
|
8932
|
+
case PARAMTYPE.HEADERS: return {
|
|
8933
|
+
kind: "headers",
|
|
8934
|
+
data
|
|
8935
|
+
};
|
|
8936
|
+
case PARAMTYPE.IP: return { kind: "ip" };
|
|
8937
|
+
case PARAMTYPE.HOST: return {
|
|
8938
|
+
kind: "host",
|
|
8939
|
+
data
|
|
8940
|
+
};
|
|
8941
|
+
case PARAMTYPE.SESSION:
|
|
8942
|
+
case PARAMTYPE.FILE:
|
|
8943
|
+
case PARAMTYPE.FILES:
|
|
8944
|
+
case PARAMTYPE.RAW_BODY: return { kind: "missing" };
|
|
8945
|
+
default: return null;
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
//#endregion
|
|
8949
|
+
//#region src/lib/reflect/classValidator.ts
|
|
8950
|
+
let cached;
|
|
8951
|
+
/**
|
|
8952
|
+
* Lazily resolve the optional `class-validator` peer; `null` when not installed.
|
|
8953
|
+
* Resolution is attempted both from this package and from the app's working
|
|
8954
|
+
* directory - the latter is what finds it in a pnpm install where the optional
|
|
8955
|
+
* peer lives in the consumer app rather than alongside `@silkweave/nestjs`. A
|
|
8956
|
+
* pnpm-deduped install shares one physical copy, so the metadata singleton the
|
|
8957
|
+
* app's decorators wrote to is the same one we read.
|
|
8958
|
+
*/
|
|
8959
|
+
function loadClassValidator() {
|
|
8960
|
+
if (cached !== void 0) return cached;
|
|
8961
|
+
for (const base of [import.meta.url, join(process.cwd(), "noop.js")]) try {
|
|
8962
|
+
cached = createRequire(base)("class-validator");
|
|
8963
|
+
return cached;
|
|
8964
|
+
} catch {}
|
|
8965
|
+
cached = null;
|
|
8966
|
+
return cached;
|
|
8967
|
+
}
|
|
8968
|
+
/**
|
|
8969
|
+
* Read `class-validator` metadata for a DTO class, grouped by property name.
|
|
8970
|
+
* Returns an empty map when `class-validator` is not installed or the class
|
|
8971
|
+
* carries no validation decorators - so callers degrade gracefully to swagger /
|
|
8972
|
+
* `design:type` reflection.
|
|
8973
|
+
*/
|
|
8974
|
+
function classValidatorMetas(dtoType) {
|
|
8975
|
+
const cv = loadClassValidator();
|
|
8976
|
+
if (!cv?.getMetadataStorage) return {};
|
|
8977
|
+
let metas;
|
|
8978
|
+
try {
|
|
8979
|
+
metas = cv.getMetadataStorage().getTargetValidationMetadatas(dtoType, null, false, false);
|
|
8980
|
+
} catch {
|
|
8981
|
+
return {};
|
|
8982
|
+
}
|
|
8983
|
+
const out = {};
|
|
8984
|
+
for (const m of metas) {
|
|
8985
|
+
if (!m.propertyName) continue;
|
|
8986
|
+
(out[m.propertyName] ??= []).push({
|
|
8987
|
+
type: m.type,
|
|
8988
|
+
name: m.name,
|
|
8989
|
+
constraints: m.constraints
|
|
8990
|
+
});
|
|
8991
|
+
}
|
|
8992
|
+
return out;
|
|
8993
|
+
}
|
|
8994
|
+
//#endregion
|
|
8995
|
+
//#region src/lib/reflect/schema.ts
|
|
8996
|
+
/** `@nestjs/swagger` reflect-metadata keys. Read directly so swagger stays an optional peer. */
|
|
8997
|
+
const API_MODEL_PROPERTIES = "swagger/apiModelProperties";
|
|
8998
|
+
const API_MODEL_PROPERTIES_ARRAY = "swagger/apiModelPropertiesArray";
|
|
8999
|
+
/** Strip `undefined` values so a later source never clobbers an earlier one with a hole. */
|
|
9000
|
+
function defined(obj) {
|
|
9001
|
+
const out = {};
|
|
9002
|
+
for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
|
|
9003
|
+
return out;
|
|
9004
|
+
}
|
|
9005
|
+
/** Merge `over` onto `base` - defined keys of `over` win. */
|
|
9006
|
+
function mergeField(base, over) {
|
|
9007
|
+
return {
|
|
9008
|
+
...base,
|
|
9009
|
+
...defined(over)
|
|
9010
|
+
};
|
|
9011
|
+
}
|
|
9012
|
+
function enumToZod(values) {
|
|
9013
|
+
const strings = values.filter((v) => typeof v === "string");
|
|
9014
|
+
if (strings.length === values.length && strings.length > 0) return z.enum(strings);
|
|
9015
|
+
const literals = values.map((v) => z.literal(v));
|
|
9016
|
+
if (literals.length === 0) return z.unknown();
|
|
9017
|
+
if (literals.length === 1) return literals[0];
|
|
9018
|
+
return z.union(literals);
|
|
9019
|
+
}
|
|
9020
|
+
function baseToZod(d) {
|
|
9021
|
+
switch (d.type) {
|
|
9022
|
+
case "string": {
|
|
9023
|
+
let s = z.string();
|
|
9024
|
+
if (d.minLength != null) s = s.min(d.minLength);
|
|
9025
|
+
if (d.maxLength != null) s = s.max(d.maxLength);
|
|
9026
|
+
return s;
|
|
9027
|
+
}
|
|
9028
|
+
case "integer":
|
|
9029
|
+
case "number": {
|
|
9030
|
+
let n = z.number();
|
|
9031
|
+
if (d.type === "integer") n = n.int();
|
|
9032
|
+
if (d.min != null) n = n.min(d.min);
|
|
9033
|
+
if (d.max != null) n = n.max(d.max);
|
|
9034
|
+
return n;
|
|
9035
|
+
}
|
|
9036
|
+
case "boolean": return z.boolean();
|
|
9037
|
+
case "array": return z.array(d.items ? fieldToZod({
|
|
9038
|
+
...d.items,
|
|
9039
|
+
required: true
|
|
9040
|
+
}) : z.unknown());
|
|
9041
|
+
case "object": return z.record(z.string(), z.unknown());
|
|
9042
|
+
default: return z.unknown();
|
|
9043
|
+
}
|
|
9044
|
+
}
|
|
9045
|
+
/** Convert a merged {@link FieldDesc} to a Zod schema. */
|
|
9046
|
+
function fieldToZod(d) {
|
|
9047
|
+
let schema = d.enum?.length ? enumToZod(d.enum) : baseToZod(d);
|
|
9048
|
+
if (d.description) schema = schema.describe(d.description);
|
|
9049
|
+
if (d.default !== void 0) schema = schema.default(d.default);
|
|
9050
|
+
else if (d.required === false) schema = schema.optional();
|
|
9051
|
+
return schema;
|
|
9052
|
+
}
|
|
9053
|
+
/** Normalise a TS-enum object or an array literal to a flat value list. */
|
|
9054
|
+
function normalizeEnum(value) {
|
|
9055
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string" || typeof v === "number");
|
|
9056
|
+
if (value && typeof value === "object") return Object.values(value).filter((v) => typeof v === "string" || typeof v === "number");
|
|
9057
|
+
}
|
|
9058
|
+
/** Map a swagger/`design:type` type token (constructor, `[Type]`, or string) to a base type. */
|
|
9059
|
+
function typeTokenToBase(type) {
|
|
9060
|
+
if (type == null) return;
|
|
9061
|
+
if (type === String) return "string";
|
|
9062
|
+
if (type === Number) return "number";
|
|
9063
|
+
if (type === Boolean) return "boolean";
|
|
9064
|
+
if (type === Array) return "array";
|
|
9065
|
+
if (type === Date) return "string";
|
|
9066
|
+
if (Array.isArray(type)) return "array";
|
|
9067
|
+
if (typeof type === "string") {
|
|
9068
|
+
const t = type.toLowerCase();
|
|
9069
|
+
if (t === "string" || t === "number" || t === "integer" || t === "boolean" || t === "array" || t === "object") return t;
|
|
9070
|
+
}
|
|
9071
|
+
}
|
|
9072
|
+
/** From the constructor TypeScript emits via `emitDecoratorMetadata`. */
|
|
9073
|
+
function designTypeToField(ctor) {
|
|
9074
|
+
const type = typeTokenToBase(ctor);
|
|
9075
|
+
const f = {};
|
|
9076
|
+
if (type) f.type = type;
|
|
9077
|
+
return f;
|
|
9078
|
+
}
|
|
9079
|
+
/** From an `@ApiParam`/`@ApiQuery` entry stored under `swagger/apiParameters`. */
|
|
9080
|
+
function swaggerParamToField(p) {
|
|
9081
|
+
const f = {};
|
|
9082
|
+
if (p["description"]) f.description = p["description"];
|
|
9083
|
+
if (typeof p["required"] === "boolean") f.required = p["required"];
|
|
9084
|
+
const type = typeTokenToBase(p["type"]);
|
|
9085
|
+
if (type) f.type = type;
|
|
9086
|
+
if (p["isArray"]) f.type = "array";
|
|
9087
|
+
const en = normalizeEnum(p["enum"]);
|
|
9088
|
+
if (en) f.enum = en;
|
|
9089
|
+
if (p["schema"] && typeof p["schema"] === "object") return mergeField(f, openapiSchemaToField(p["schema"]));
|
|
9090
|
+
return f;
|
|
9091
|
+
}
|
|
9092
|
+
/** From an `@ApiProperty` options object stored under `swagger/apiModelProperties`. */
|
|
9093
|
+
function apiPropertyToField(o) {
|
|
9094
|
+
const f = {};
|
|
9095
|
+
if (o["description"]) f.description = o["description"];
|
|
9096
|
+
if (typeof o["required"] === "boolean") f.required = o["required"];
|
|
9097
|
+
const type = typeTokenToBase(o["type"]);
|
|
9098
|
+
if (type) f.type = type;
|
|
9099
|
+
if (o["isArray"]) {
|
|
9100
|
+
f.items = { type: type && type !== "array" ? type : "unknown" };
|
|
9101
|
+
f.type = "array";
|
|
9102
|
+
}
|
|
9103
|
+
const en = normalizeEnum(o["enum"]);
|
|
9104
|
+
if (en) f.enum = en;
|
|
9105
|
+
if (o["minimum"] != null) f.min = o["minimum"];
|
|
9106
|
+
if (o["maximum"] != null) f.max = o["maximum"];
|
|
9107
|
+
if (o["minLength"] != null) f.minLength = o["minLength"];
|
|
9108
|
+
if (o["maxLength"] != null) f.maxLength = o["maxLength"];
|
|
9109
|
+
if (o["format"]) f.format = o["format"];
|
|
9110
|
+
if (o["default"] !== void 0) f.default = o["default"];
|
|
9111
|
+
return f;
|
|
9112
|
+
}
|
|
9113
|
+
/** `class-validator` decorator type → field base type. */
|
|
9114
|
+
const CV_TYPE = {
|
|
9115
|
+
isString: "string",
|
|
9116
|
+
isInt: "integer",
|
|
9117
|
+
isBoolean: "boolean",
|
|
9118
|
+
isArray: "array"
|
|
9119
|
+
};
|
|
9120
|
+
/** `class-validator` decorator type → numeric-constraint field key. */
|
|
9121
|
+
const CV_NUMERIC = {
|
|
9122
|
+
min: "min",
|
|
9123
|
+
max: "max",
|
|
9124
|
+
minLength: "minLength",
|
|
9125
|
+
maxLength: "maxLength"
|
|
9126
|
+
};
|
|
9127
|
+
/**
|
|
9128
|
+
* Fold one `class-validator` metadata entry into the field descriptor. Built-in
|
|
9129
|
+
* validators record their identity in `name` (`isString`, `minLength`, ...) with
|
|
9130
|
+
* `type: 'customValidation'`; `@IsOptional` uses `name: 'isOptional'`. We key off
|
|
9131
|
+
* `name`, falling back to `type`.
|
|
9132
|
+
*/
|
|
9133
|
+
function applyValidationMeta(f, meta) {
|
|
9134
|
+
const key = meta.name ?? meta.type;
|
|
9135
|
+
if (!key) return;
|
|
9136
|
+
if (key in CV_TYPE) {
|
|
9137
|
+
f.type = CV_TYPE[key];
|
|
9138
|
+
return;
|
|
9139
|
+
}
|
|
9140
|
+
const c0 = meta.constraints?.[0];
|
|
9141
|
+
if (key in CV_NUMERIC) {
|
|
9142
|
+
if (typeof c0 === "number") f[CV_NUMERIC[key]] = c0;
|
|
9143
|
+
return;
|
|
9144
|
+
}
|
|
9145
|
+
if (key === "isNumber") {
|
|
9146
|
+
if (!f.type) f.type = "number";
|
|
9147
|
+
return;
|
|
9148
|
+
}
|
|
9149
|
+
if (key === "isEmail") {
|
|
9150
|
+
if (!f.type) f.type = "string";
|
|
9151
|
+
f.format = "email";
|
|
9152
|
+
return;
|
|
9153
|
+
}
|
|
9154
|
+
if (key === "isDate" || key === "isDateString") {
|
|
9155
|
+
f.type = "string";
|
|
9156
|
+
f.format = "date-time";
|
|
9157
|
+
return;
|
|
9158
|
+
}
|
|
9159
|
+
if (key === "isEnum") {
|
|
9160
|
+
const e = normalizeEnum(c0);
|
|
9161
|
+
if (e) f.enum = e;
|
|
9162
|
+
return;
|
|
9163
|
+
}
|
|
9164
|
+
if (key === "isOptional") f.required = false;
|
|
9165
|
+
}
|
|
9166
|
+
/** From an array of `class-validator` validation-metadata entries for one property. */
|
|
9167
|
+
function classValidatorToField(metas) {
|
|
9168
|
+
const f = {};
|
|
9169
|
+
for (const meta of metas) applyValidationMeta(f, meta);
|
|
9170
|
+
return f;
|
|
9171
|
+
}
|
|
9172
|
+
/** From an OpenAPI Schema Object (used by both the doc and `@ApiParam({ schema })`). */
|
|
9173
|
+
function openapiSchemaToField(schema) {
|
|
9174
|
+
const f = {};
|
|
9175
|
+
const type = typeof schema["type"] === "string" ? schema["type"].toLowerCase() : void 0;
|
|
9176
|
+
if (type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "array" || type === "object") f.type = type;
|
|
9177
|
+
if (schema["description"]) f.description = schema["description"];
|
|
9178
|
+
const en = normalizeEnum(schema["enum"]);
|
|
9179
|
+
if (en) f.enum = en;
|
|
9180
|
+
if (schema["minimum"] != null) f.min = schema["minimum"];
|
|
9181
|
+
if (schema["maximum"] != null) f.max = schema["maximum"];
|
|
9182
|
+
if (schema["minLength"] != null) f.minLength = schema["minLength"];
|
|
9183
|
+
if (schema["maxLength"] != null) f.maxLength = schema["maxLength"];
|
|
9184
|
+
if (schema["format"]) f.format = schema["format"];
|
|
9185
|
+
if (schema["default"] !== void 0) f.default = schema["default"];
|
|
9186
|
+
if (schema["items"] && typeof schema["items"] === "object") f.items = openapiSchemaToField(schema["items"]);
|
|
9187
|
+
return f;
|
|
9188
|
+
}
|
|
9189
|
+
/**
|
|
9190
|
+
* Reflect a whole-DTO class (`@Body() dto: CreateDto`) into per-property
|
|
9191
|
+
* {@link FieldDesc}s. Property names are the union of `@ApiProperty` and
|
|
9192
|
+
* `class-validator` decorated fields; each field merges `design:type` (base),
|
|
9193
|
+
* then `class-validator` constraints, then `@ApiProperty` (highest). Properties
|
|
9194
|
+
* default to required unless a source marks them optional.
|
|
9195
|
+
*/
|
|
9196
|
+
function reflectDtoFields(dtoType) {
|
|
9197
|
+
const proto = dtoType?.prototype;
|
|
9198
|
+
if (!proto) return {};
|
|
9199
|
+
const cvMetas = classValidatorMetas(dtoType);
|
|
9200
|
+
const names = /* @__PURE__ */ new Set();
|
|
9201
|
+
const swaggerArray = Reflect.getMetadata(API_MODEL_PROPERTIES_ARRAY, proto) ?? [];
|
|
9202
|
+
for (const entry of swaggerArray) names.add(entry.replace(/^:/, ""));
|
|
9203
|
+
for (const name of Object.keys(cvMetas)) names.add(name);
|
|
9204
|
+
const out = {};
|
|
9205
|
+
for (const name of names) {
|
|
9206
|
+
let f = designTypeToField(Reflect.getMetadata("design:type", proto, name));
|
|
9207
|
+
if (cvMetas[name]) f = mergeField(f, classValidatorToField(cvMetas[name]));
|
|
9208
|
+
const apiProp = Reflect.getMetadata(API_MODEL_PROPERTIES, proto, name);
|
|
9209
|
+
if (apiProp) {
|
|
9210
|
+
const fromApi = apiPropertyToField(apiProp);
|
|
9211
|
+
if (fromApi.required === void 0 && apiProp["required"] === void 0) fromApi.required = true;
|
|
9212
|
+
f = mergeField(f, fromApi);
|
|
9213
|
+
}
|
|
9214
|
+
if (f.required === void 0) f.required = true;
|
|
9215
|
+
out[name] = f;
|
|
9216
|
+
}
|
|
9217
|
+
return out;
|
|
9218
|
+
}
|
|
9219
|
+
//#endregion
|
|
9220
|
+
//#region src/lib/reflect/openapi.ts
|
|
9221
|
+
/** Index a document's operations by `${METHOD} ${path}` for fast matching. */
|
|
9222
|
+
function buildOpenApiLookup(doc) {
|
|
9223
|
+
const operations = /* @__PURE__ */ new Map();
|
|
9224
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) for (const [verb, op] of Object.entries(item ?? {})) operations.set(`${verb.toUpperCase()} ${path}`, op);
|
|
9225
|
+
return {
|
|
9226
|
+
doc,
|
|
9227
|
+
operations
|
|
9228
|
+
};
|
|
9229
|
+
}
|
|
9230
|
+
/** Locate the operation for a route, tolerating a global path prefix on the document side. */
|
|
9231
|
+
function findOperation(lookup, method, openapiPath) {
|
|
9232
|
+
const exact = lookup.operations.get(`${method} ${openapiPath}`);
|
|
9233
|
+
if (exact) return exact;
|
|
9234
|
+
for (const [key, op] of lookup.operations) {
|
|
9235
|
+
const [verb, path] = key.split(" ");
|
|
9236
|
+
if (verb === method && (path.endsWith(openapiPath) || openapiPath.endsWith(path))) return op;
|
|
9237
|
+
}
|
|
9238
|
+
}
|
|
9239
|
+
function resolveRef(doc, schema) {
|
|
9240
|
+
let current = schema;
|
|
9241
|
+
let guard = 0;
|
|
9242
|
+
while (current && typeof current === "object" && typeof current["$ref"] === "string" && guard < 10) {
|
|
9243
|
+
const match = /^#\/components\/schemas\/(.+)$/.exec(current["$ref"]);
|
|
9244
|
+
if (!match) break;
|
|
9245
|
+
current = doc.components?.schemas?.[match[1]];
|
|
9246
|
+
guard += 1;
|
|
9247
|
+
}
|
|
9248
|
+
return current;
|
|
9249
|
+
}
|
|
9250
|
+
/**
|
|
9251
|
+
* Resolve the per-field descriptors for a route from an ingested OpenAPI
|
|
9252
|
+
* document. Merges `parameters` (path/query/header) and the JSON request-body
|
|
9253
|
+
* schema's properties into a single field map. Returns `{}` when the operation
|
|
9254
|
+
* isn't found - callers fall back to decorator reflection.
|
|
9255
|
+
*/
|
|
9256
|
+
function openApiFields(lookup, method, openapiPath) {
|
|
9257
|
+
const op = findOperation(lookup, method, openapiPath);
|
|
9258
|
+
if (!op) return {};
|
|
9259
|
+
const out = {};
|
|
9260
|
+
for (const param of op["parameters"] ?? []) {
|
|
9261
|
+
const name = typeof param["name"] === "string" ? param["name"] : void 0;
|
|
9262
|
+
if (!name) continue;
|
|
9263
|
+
const schema = param["schema"] ? resolveRef(lookup.doc, param["schema"]) : void 0;
|
|
9264
|
+
let field = schema ? openapiSchemaToField(schema) : {};
|
|
9265
|
+
if (param["description"]) field = mergeField(field, { description: param["description"] });
|
|
9266
|
+
if (typeof param["required"] === "boolean") field = mergeField(field, { required: param["required"] });
|
|
9267
|
+
out[name] = field;
|
|
9268
|
+
}
|
|
9269
|
+
const bodySchema = resolveRef(lookup.doc, op["requestBody"]?.["content"]?.["application/json"]?.["schema"]);
|
|
9270
|
+
if (bodySchema && typeof bodySchema === "object" && bodySchema["properties"]) {
|
|
9271
|
+
const required = new Set(Array.isArray(bodySchema["required"]) ? bodySchema["required"] : []);
|
|
9272
|
+
for (const [name, propSchema] of Object.entries(bodySchema["properties"])) {
|
|
9273
|
+
const field = openapiSchemaToField(resolveRef(lookup.doc, propSchema));
|
|
9274
|
+
field.required = required.has(name);
|
|
9275
|
+
out[name] = field;
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
return out;
|
|
9279
|
+
}
|
|
9280
|
+
//#endregion
|
|
9281
|
+
//#region src/lib/reflect/route.ts
|
|
9282
|
+
/**
|
|
9283
|
+
* `RequestMethod` numeric values (from `@nestjs/common`) mapped to verbs. We
|
|
9284
|
+
* map our own table rather than importing the enum to avoid pulling a value
|
|
9285
|
+
* import for a handful of constants.
|
|
9286
|
+
*/
|
|
9287
|
+
const REQUEST_METHOD = {
|
|
9288
|
+
0: "GET",
|
|
9289
|
+
1: "POST",
|
|
9290
|
+
2: "PUT",
|
|
9291
|
+
3: "DELETE",
|
|
9292
|
+
4: "PATCH",
|
|
9293
|
+
6: "OPTIONS",
|
|
9294
|
+
7: "HEAD"
|
|
9295
|
+
};
|
|
9296
|
+
function normalizeSegment(value) {
|
|
9297
|
+
if (typeof value !== "string") return "";
|
|
9298
|
+
return value.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
9299
|
+
}
|
|
9300
|
+
function joinPath(...segments) {
|
|
9301
|
+
return segments.map(normalizeSegment).filter(Boolean).join("/");
|
|
9302
|
+
}
|
|
9303
|
+
/**
|
|
9304
|
+
* Resolve the composed route (controller prefix + method path), HTTP verb, and
|
|
9305
|
+
* path-param names for a decorated controller method, reading Nest's own
|
|
9306
|
+
* `PATH_METADATA`/`METHOD_METADATA` reflection.
|
|
9307
|
+
*/
|
|
9308
|
+
function reflectRoute(classRef, method) {
|
|
9309
|
+
const classPath = Reflect.getMetadata(PATH_METADATA, classRef);
|
|
9310
|
+
const methodPath = Reflect.getMetadata(PATH_METADATA, method);
|
|
9311
|
+
const verbCode = Reflect.getMetadata(METHOD_METADATA, method);
|
|
9312
|
+
const path = joinPath(Array.isArray(classPath) ? classPath[0] ?? "" : classPath ?? "", Array.isArray(methodPath) ? methodPath[0] ?? "" : methodPath ?? "");
|
|
9313
|
+
const httpMethod = REQUEST_METHOD[verbCode ?? 0] ?? "GET";
|
|
9314
|
+
const pathParams = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
|
|
9315
|
+
return {
|
|
9316
|
+
method: httpMethod,
|
|
9317
|
+
path,
|
|
9318
|
+
openapiPath: `/${path.replace(/:([A-Za-z0-9_]+)/g, "{$1}")}`,
|
|
9319
|
+
pathParams
|
|
9320
|
+
};
|
|
9321
|
+
}
|
|
9322
|
+
//#endregion
|
|
9323
|
+
//#region src/lib/reflect/swagger.ts
|
|
9324
|
+
/** `@nestjs/swagger` reflect-metadata keys (read directly - swagger is an optional peer). */
|
|
9325
|
+
const API_OPERATION = "swagger/apiOperation";
|
|
9326
|
+
const API_PARAMETERS = "swagger/apiParameters";
|
|
9327
|
+
/**
|
|
9328
|
+
* Read operation-level `@nestjs/swagger` metadata off a controller method:
|
|
9329
|
+
* `@ApiOperation` (summary/description) and the `@ApiParam`/`@ApiQuery` array
|
|
9330
|
+
* (each describing one path/query field). Returns empty data when swagger
|
|
9331
|
+
* decorators are absent.
|
|
9332
|
+
*/
|
|
9333
|
+
function reflectOperation(method) {
|
|
9334
|
+
const operation = Reflect.getMetadata(API_OPERATION, method);
|
|
9335
|
+
const description = operation ? typeof operation["summary"] === "string" && operation["summary"] ? operation["summary"] : typeof operation["description"] === "string" ? operation["description"] : void 0 : void 0;
|
|
9336
|
+
const parameters = Reflect.getMetadata(API_PARAMETERS, method) ?? [];
|
|
9337
|
+
const params = {};
|
|
9338
|
+
for (const p of parameters) {
|
|
9339
|
+
const name = typeof p["name"] === "string" ? p["name"] : void 0;
|
|
9340
|
+
if (!name) continue;
|
|
9341
|
+
params[name] = swaggerParamToField(p);
|
|
9342
|
+
}
|
|
9343
|
+
return {
|
|
9344
|
+
description,
|
|
9345
|
+
params
|
|
9346
|
+
};
|
|
9347
|
+
}
|
|
9348
|
+
//#endregion
|
|
9072
9349
|
//#region \0@oxc-project+runtime@0.127.0/helpers/decorateMetadata.js
|
|
9073
9350
|
function __decorateMetadata(k, v) {
|
|
9074
9351
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
@@ -9082,61 +9359,69 @@ function __decorate(decorators, target, key, desc) {
|
|
|
9082
9359
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
9083
9360
|
}
|
|
9084
9361
|
//#endregion
|
|
9085
|
-
//#region src/lib/
|
|
9086
|
-
var _ref$1, _ref2$1, _ref3, _ref4;
|
|
9087
|
-
let
|
|
9088
|
-
constructor(discovery, scanner, reflector, moduleRef) {
|
|
9362
|
+
//#region src/lib/controllerDiscovery.ts
|
|
9363
|
+
var _ref$1, _ref2$1, _ref3, _ref4, _ref5;
|
|
9364
|
+
let ControllerDiscovery = class ControllerDiscovery {
|
|
9365
|
+
constructor(discovery, scanner, reflector, moduleRef, appConfig) {
|
|
9089
9366
|
this.discovery = discovery;
|
|
9090
9367
|
this.scanner = scanner;
|
|
9091
9368
|
this.reflector = reflector;
|
|
9092
9369
|
this.moduleRef = moduleRef;
|
|
9370
|
+
this.appConfig = appConfig;
|
|
9093
9371
|
}
|
|
9094
9372
|
/**
|
|
9095
|
-
* Walk every Nest provider, find methods annotated with `@
|
|
9096
|
-
*
|
|
9097
|
-
*
|
|
9098
|
-
*
|
|
9099
|
-
*
|
|
9100
|
-
*
|
|
9101
|
-
* SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
|
|
9102
|
-
* so DI-injected dependencies remain available.
|
|
9373
|
+
* Walk every Nest provider/controller, find methods annotated with `@Mcp`,
|
|
9374
|
+
* and build a core `Action` per method whose input schema is reflected from
|
|
9375
|
+
* the route + parameter decorators (+ optional OpenAPI document) and whose
|
|
9376
|
+
* `run` re-binds the validated input back into the method's positional
|
|
9377
|
+
* arguments (with `@UseGuards` guards - and any opted-in `globalGuards` -
|
|
9378
|
+
* applied first).
|
|
9103
9379
|
*/
|
|
9104
|
-
discover() {
|
|
9380
|
+
discover(openapi, globalGuards = []) {
|
|
9381
|
+
const lookup = openapi ? buildOpenApiLookup(openapi) : void 0;
|
|
9105
9382
|
const discovered = [];
|
|
9106
|
-
const
|
|
9107
|
-
for (const wrapper of wrappers) {
|
|
9383
|
+
for (const wrapper of this.discovery.getProviders().concat(this.discovery.getControllers())) {
|
|
9108
9384
|
const { instance } = wrapper;
|
|
9109
9385
|
if (!instance || typeof instance !== "object") continue;
|
|
9110
9386
|
const proto = Object.getPrototypeOf(instance);
|
|
9111
9387
|
if (!proto) continue;
|
|
9112
9388
|
const classRef = instance.constructor;
|
|
9113
|
-
const classMeta = this.reflector.get("__silkweave_actions__", classRef) ?? {};
|
|
9114
9389
|
for (const methodName of this.scanner.getAllMethodNames(proto)) {
|
|
9115
9390
|
const method = proto[methodName];
|
|
9116
9391
|
if (typeof method !== "function") continue;
|
|
9117
|
-
const meta = this.reflector.get(
|
|
9392
|
+
const meta = this.reflector.get(MCP_METADATA, method);
|
|
9118
9393
|
if (!meta) continue;
|
|
9119
9394
|
discovered.push({
|
|
9120
9395
|
instance,
|
|
9121
9396
|
classRef,
|
|
9122
9397
|
method,
|
|
9123
9398
|
methodName,
|
|
9124
|
-
meta
|
|
9125
|
-
classMeta
|
|
9399
|
+
meta
|
|
9126
9400
|
});
|
|
9127
9401
|
}
|
|
9128
9402
|
}
|
|
9129
|
-
return discovered.map((d) => this.toAction(d));
|
|
9130
|
-
}
|
|
9131
|
-
toAction(d) {
|
|
9132
|
-
const
|
|
9133
|
-
const
|
|
9134
|
-
const
|
|
9403
|
+
return discovered.map((d) => this.toAction(d, lookup, globalGuards));
|
|
9404
|
+
}
|
|
9405
|
+
toAction(d, lookup, globalGuards) {
|
|
9406
|
+
const proto = Object.getPrototypeOf(d.instance);
|
|
9407
|
+
const route = reflectRoute(d.classRef, d.method);
|
|
9408
|
+
const slots = readParamSlots(d.classRef, d.methodName, proto);
|
|
9409
|
+
const operation = reflectOperation(d.method);
|
|
9410
|
+
const docFields = lookup ? openApiFields(lookup, route.method, route.openapiPath) : {};
|
|
9411
|
+
const { shape, bindings } = this.buildInput(d, route.pathParams, slots, operation.params, docFields);
|
|
9412
|
+
const base = d.classRef.name.replace(/Controller$/, "");
|
|
9413
|
+
const name = d.meta.name ?? `${base}.${d.methodName}`;
|
|
9414
|
+
const description = d.meta.description ?? operation.description ?? `${d.methodName} (${route.method} /${route.path})`;
|
|
9415
|
+
const applyParamPipes = d.meta.pipes !== "skip";
|
|
9135
9416
|
const guards = collectGuards(this.reflector, d.classRef, d.method);
|
|
9136
|
-
const
|
|
9137
|
-
const reflector = this
|
|
9138
|
-
const
|
|
9139
|
-
|
|
9417
|
+
const pathFields = pathParamFields(bindings);
|
|
9418
|
+
const { moduleRef, reflector, appConfig } = this;
|
|
9419
|
+
const classRef = d.classRef;
|
|
9420
|
+
const method = d.method;
|
|
9421
|
+
const instance = d.instance;
|
|
9422
|
+
const applyGuards = async (context, input) => {
|
|
9423
|
+
const all = [...collectGlobalGuards(appConfig, globalGuards), ...guards];
|
|
9424
|
+
if (all.length === 0) return;
|
|
9140
9425
|
const request = context.getOptional("request");
|
|
9141
9426
|
const response = context.getOptional("response") ?? null;
|
|
9142
9427
|
const hasRequest = request != null;
|
|
@@ -9145,151 +9430,140 @@ let ActionDiscovery = class ActionDiscovery {
|
|
|
9145
9430
|
params: {},
|
|
9146
9431
|
query: {}
|
|
9147
9432
|
};
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
if (!d.meta.chunk) throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`);
|
|
9152
|
-
const method = d.method;
|
|
9153
|
-
const instance = d.instance;
|
|
9154
|
-
return createAction({
|
|
9155
|
-
name,
|
|
9156
|
-
description: d.meta.description,
|
|
9157
|
-
input: d.meta.input,
|
|
9158
|
-
chunk: d.meta.chunk,
|
|
9159
|
-
kind: d.meta.kind ?? "mutation",
|
|
9160
|
-
method: d.meta.method,
|
|
9161
|
-
path: d.meta.path,
|
|
9162
|
-
queryParams: d.meta.queryParams,
|
|
9163
|
-
args: d.meta.args,
|
|
9164
|
-
isEnabled,
|
|
9165
|
-
toolResult: d.meta.toolResult,
|
|
9166
|
-
run: async function* (input, context) {
|
|
9167
|
-
await applyGuards(context);
|
|
9168
|
-
yield* method.call(instance, input, context);
|
|
9169
|
-
}
|
|
9170
|
-
});
|
|
9171
|
-
}
|
|
9433
|
+
populatePathParams(guardRequest, pathFields, input);
|
|
9434
|
+
await runGuards(all, moduleRef, reflector, classRef, method, guardRequest, response, hasRequest ? "http" : "rpc");
|
|
9435
|
+
};
|
|
9172
9436
|
return createAction({
|
|
9173
9437
|
name,
|
|
9174
|
-
description
|
|
9175
|
-
input:
|
|
9176
|
-
|
|
9177
|
-
kind: d.meta.kind ?? "mutation",
|
|
9178
|
-
method: d.meta.method,
|
|
9179
|
-
path: d.meta.path,
|
|
9180
|
-
queryParams: d.meta.queryParams,
|
|
9181
|
-
args: d.meta.args,
|
|
9182
|
-
isEnabled,
|
|
9183
|
-
toolResult: d.meta.toolResult,
|
|
9438
|
+
description,
|
|
9439
|
+
input: z.object(shape),
|
|
9440
|
+
isEnabled: (ctx) => ctx.getOptional("adapter") === "mcp",
|
|
9184
9441
|
run: async (input, context) => {
|
|
9185
|
-
await applyGuards(context);
|
|
9186
|
-
return await
|
|
9442
|
+
await applyGuards(context, input);
|
|
9443
|
+
return await invokeRebound(method, instance, input, bindings, context.getOptional("request"), applyParamPipes) ?? {};
|
|
9187
9444
|
}
|
|
9188
9445
|
});
|
|
9189
9446
|
}
|
|
9447
|
+
/** Build the merged Zod input shape and the per-argument re-bind plan. */
|
|
9448
|
+
buildInput(d, pathParams, slots, operationParams, docFields) {
|
|
9449
|
+
const proto = Object.getPrototypeOf(d.instance);
|
|
9450
|
+
const designTypes = Reflect.getMetadata("design:paramtypes", proto, d.methodName) ?? [];
|
|
9451
|
+
const fields = {};
|
|
9452
|
+
const maxIndex = slots.reduce((m, s) => Math.max(m, s.index), -1);
|
|
9453
|
+
const bindings = Array.from({ length: maxIndex + 1 }, () => ({ kind: "missing" }));
|
|
9454
|
+
const addField = (name, desc) => {
|
|
9455
|
+
fields[name] = name in fields ? mergeField(fields[name], desc) : desc;
|
|
9456
|
+
};
|
|
9457
|
+
for (const slot of slots) {
|
|
9458
|
+
const { binding, fields: contributed } = contributeSlot(slot, pathParams, designTypes);
|
|
9459
|
+
bindings[slot.index] = binding;
|
|
9460
|
+
for (const [name, desc] of Object.entries(contributed)) addField(name, desc);
|
|
9461
|
+
}
|
|
9462
|
+
for (const [name, desc] of Object.entries(operationParams)) if (name in fields) fields[name] = mergeField(fields[name], desc);
|
|
9463
|
+
for (const [name, desc] of Object.entries(docFields)) if (name in fields) fields[name] = mergeField(fields[name], desc);
|
|
9464
|
+
const shape = {};
|
|
9465
|
+
for (const [name, desc] of Object.entries(fields)) shape[name] = fieldToZod(desc);
|
|
9466
|
+
Object.assign(shape, d.meta.input ?? {});
|
|
9467
|
+
return {
|
|
9468
|
+
shape,
|
|
9469
|
+
bindings
|
|
9470
|
+
};
|
|
9471
|
+
}
|
|
9190
9472
|
};
|
|
9191
|
-
|
|
9473
|
+
ControllerDiscovery = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [
|
|
9192
9474
|
typeof (_ref$1 = typeof DiscoveryService !== "undefined" && DiscoveryService) === "function" ? _ref$1 : Object,
|
|
9193
9475
|
typeof (_ref2$1 = typeof MetadataScanner !== "undefined" && MetadataScanner) === "function" ? _ref2$1 : Object,
|
|
9194
9476
|
typeof (_ref3 = typeof Reflector !== "undefined" && Reflector) === "function" ? _ref3 : Object,
|
|
9195
|
-
typeof (_ref4 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref4 : Object
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9477
|
+
typeof (_ref4 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref4 : Object,
|
|
9478
|
+
typeof (_ref5 = typeof ApplicationConfig !== "undefined" && ApplicationConfig) === "function" ? _ref5 : Object
|
|
9479
|
+
])], ControllerDiscovery);
|
|
9480
|
+
/** Input field names that originate from the URL path (`@Param`), per the re-bind plan. */
|
|
9481
|
+
function pathParamFields(bindings) {
|
|
9482
|
+
const fields = [];
|
|
9483
|
+
for (const b of bindings) if (b.kind === "params") fields.push(...b.fields);
|
|
9484
|
+
else if (b.kind === "value" && b.source === "path") fields.push(b.field);
|
|
9485
|
+
return fields;
|
|
9202
9486
|
}
|
|
9203
|
-
/**
|
|
9204
|
-
|
|
9205
|
-
|
|
9487
|
+
/**
|
|
9488
|
+
* Fill `request.params` with the reflected path fields from the validated input,
|
|
9489
|
+
* matching what Express would populate over REST (raw string values). Only adds
|
|
9490
|
+
* keys that are absent, so a real REST request's params are never overwritten.
|
|
9491
|
+
*/
|
|
9492
|
+
function populatePathParams(request, pathFields, input) {
|
|
9493
|
+
if (pathFields.length === 0 || typeof request !== "object" || request === null) return;
|
|
9494
|
+
const params = request.params ??= {};
|
|
9495
|
+
for (const field of pathFields) if (!(field in params) && input[field] !== void 0) params[field] = String(input[field]);
|
|
9206
9496
|
}
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
return
|
|
9497
|
+
function designTypeAt(designTypes, index) {
|
|
9498
|
+
const ctor = designTypes[index];
|
|
9499
|
+
if (ctor === String) return { type: "string" };
|
|
9500
|
+
if (ctor === Number) return { type: "number" };
|
|
9501
|
+
if (ctor === Boolean) return { type: "boolean" };
|
|
9502
|
+
return {};
|
|
9211
9503
|
}
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
if (
|
|
9215
|
-
|
|
9216
|
-
|
|
9504
|
+
/** A `@Param('id')` scalar or a bare `@Param()` covering all path params. */
|
|
9505
|
+
function paramContribution(slot, pathParams) {
|
|
9506
|
+
if (slot.data) return {
|
|
9507
|
+
binding: {
|
|
9508
|
+
kind: "value",
|
|
9509
|
+
field: slot.data,
|
|
9510
|
+
source: "path",
|
|
9511
|
+
metatype: slot.designType,
|
|
9512
|
+
pipes: slot.pipes
|
|
9513
|
+
},
|
|
9514
|
+
fields: { [slot.data]: {
|
|
9515
|
+
type: "string",
|
|
9516
|
+
required: true
|
|
9517
|
+
} }
|
|
9217
9518
|
};
|
|
9218
|
-
}
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9519
|
+
const fields = {};
|
|
9520
|
+
for (const p of pathParams) fields[p] = {
|
|
9521
|
+
type: "string",
|
|
9522
|
+
required: true
|
|
9523
|
+
};
|
|
9524
|
+
return {
|
|
9525
|
+
binding: {
|
|
9526
|
+
kind: "params",
|
|
9527
|
+
fields: pathParams
|
|
9528
|
+
},
|
|
9529
|
+
fields
|
|
9229
9530
|
};
|
|
9230
|
-
for (const [key, schema] of Object.entries(json.properties ?? {})) if (pathParams.has(key)) split.parameters.push({
|
|
9231
|
-
name: key,
|
|
9232
|
-
in: "path",
|
|
9233
|
-
required: true,
|
|
9234
|
-
schema
|
|
9235
|
-
});
|
|
9236
|
-
else if (!hasBody || queryKeys.has(key)) split.parameters.push({
|
|
9237
|
-
name: key,
|
|
9238
|
-
in: "query",
|
|
9239
|
-
required: required.has(key),
|
|
9240
|
-
schema
|
|
9241
|
-
});
|
|
9242
|
-
else {
|
|
9243
|
-
split.bodyProps[key] = schema;
|
|
9244
|
-
if (required.has(key)) split.bodyRequired.push(key);
|
|
9245
|
-
}
|
|
9246
|
-
return split;
|
|
9247
9531
|
}
|
|
9248
|
-
/**
|
|
9249
|
-
function
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
...response ? { content: { "application/json": { schema: response } } } : {}
|
|
9260
|
-
} }
|
|
9532
|
+
/** A `@Query('x')`/`@Body('x')` scalar or a whole-DTO `@Query()`/`@Body()`. */
|
|
9533
|
+
function bodyOrQueryContribution(slot, source, requiredScalar, designTypes) {
|
|
9534
|
+
if (slot.data) return {
|
|
9535
|
+
binding: {
|
|
9536
|
+
kind: "value",
|
|
9537
|
+
field: slot.data,
|
|
9538
|
+
source,
|
|
9539
|
+
metatype: slot.designType,
|
|
9540
|
+
pipes: slot.pipes
|
|
9541
|
+
},
|
|
9542
|
+
fields: { [slot.data]: mergeField(designTypeAt(designTypes, slot.index), { required: requiredScalar }) }
|
|
9261
9543
|
};
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9544
|
+
const dtoFields = reflectDtoFields(slot.designType);
|
|
9545
|
+
return {
|
|
9546
|
+
binding: {
|
|
9547
|
+
kind: "object",
|
|
9548
|
+
source,
|
|
9549
|
+
fields: Object.keys(dtoFields),
|
|
9550
|
+
metatype: slot.designType,
|
|
9551
|
+
pipes: slot.pipes
|
|
9552
|
+
},
|
|
9553
|
+
fields: dtoFields
|
|
9270
9554
|
};
|
|
9271
|
-
return operation;
|
|
9272
9555
|
}
|
|
9273
|
-
/**
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
for (const action of actions) {
|
|
9285
|
-
const method = actionMethod(action).toLowerCase();
|
|
9286
|
-
const route = actionRoute(action, basePath);
|
|
9287
|
-
paths[route] = {
|
|
9288
|
-
...paths[route] ?? {},
|
|
9289
|
-
[method]: buildOperation(action, tag)
|
|
9290
|
-
};
|
|
9291
|
-
}
|
|
9292
|
-
return paths;
|
|
9556
|
+
/** Map one parameter slot to its input-field contribution and re-bind instruction. */
|
|
9557
|
+
function contributeSlot(slot, pathParams, designTypes) {
|
|
9558
|
+
switch (slot.paramtype) {
|
|
9559
|
+
case PARAMTYPE.PARAM: return paramContribution(slot, pathParams);
|
|
9560
|
+
case PARAMTYPE.QUERY: return bodyOrQueryContribution(slot, "query", false, designTypes);
|
|
9561
|
+
case PARAMTYPE.BODY: return bodyOrQueryContribution(slot, "body", true, designTypes);
|
|
9562
|
+
default: return {
|
|
9563
|
+
binding: specialBinding(slot.paramtype, slot.data) ?? { kind: "missing" },
|
|
9564
|
+
fields: {}
|
|
9565
|
+
};
|
|
9566
|
+
}
|
|
9293
9567
|
}
|
|
9294
9568
|
//#endregion
|
|
9295
9569
|
//#region src/lib/types.ts
|
|
@@ -9318,14 +9592,14 @@ let SilkweaveModule = _SilkweaveModule = class SilkweaveModule {
|
|
|
9318
9592
|
providers: [{
|
|
9319
9593
|
provide: SILKWEAVE_MODULE_OPTIONS,
|
|
9320
9594
|
useValue: options
|
|
9321
|
-
},
|
|
9595
|
+
}, ControllerDiscovery],
|
|
9322
9596
|
exports: []
|
|
9323
9597
|
};
|
|
9324
9598
|
}
|
|
9325
9599
|
configure(_consumer) {
|
|
9326
9600
|
const httpAdapter = this.httpAdapterHost.httpAdapter;
|
|
9327
9601
|
if (!httpAdapter) throw new Error("@silkweave/nestjs: HttpAdapterHost.httpAdapter is not available.");
|
|
9328
|
-
const allActions = this.discovery.discover();
|
|
9602
|
+
const allActions = this.discovery.discover(this.options.openapi, this.options.globalGuards);
|
|
9329
9603
|
for (const adapter of this.options.adapters) {
|
|
9330
9604
|
const baseContext = createContext({
|
|
9331
9605
|
...this.options.context ?? {},
|
|
@@ -9346,60 +9620,11 @@ SilkweaveModule = _SilkweaveModule = __decorate([
|
|
|
9346
9620
|
__decorateParam(0, Inject(SILKWEAVE_MODULE_OPTIONS)),
|
|
9347
9621
|
__decorateMetadata("design:paramtypes", [
|
|
9348
9622
|
Object,
|
|
9349
|
-
typeof (_ref = typeof
|
|
9623
|
+
typeof (_ref = typeof ControllerDiscovery !== "undefined" && ControllerDiscovery) === "function" ? _ref : Object,
|
|
9350
9624
|
typeof (_ref2 = typeof HttpAdapterHost !== "undefined" && HttpAdapterHost) === "function" ? _ref2 : Object
|
|
9351
9625
|
])
|
|
9352
9626
|
], SilkweaveModule);
|
|
9353
9627
|
//#endregion
|
|
9354
|
-
|
|
9355
|
-
/**
|
|
9356
|
-
* Merge every REST-exposed Silkweave `@Action` into a NestJS Swagger
|
|
9357
|
-
* `OpenAPIObject`.
|
|
9358
|
-
*
|
|
9359
|
-
* `@nestjs/swagger` builds its document by scanning **controllers**, but
|
|
9360
|
-
* Silkweave registers action routes directly on the HTTP adapter (so they sit
|
|
9361
|
-
* ahead of controllers in the request pipeline) - which means the scanner never
|
|
9362
|
-
* sees them. This helper closes that gap: it discovers the actions through the
|
|
9363
|
-
* same `ActionDiscovery` provider the `rest()` adapter uses, builds OpenAPI
|
|
9364
|
-
* paths with the same routing logic (`buildActionPaths`), and merges them into
|
|
9365
|
-
* the document. The result stays in sync with the live routes without any
|
|
9366
|
-
* dynamic controllers.
|
|
9367
|
-
*
|
|
9368
|
-
* Call it between `SwaggerModule.createDocument()` and `SwaggerModule.setup()`:
|
|
9369
|
-
*
|
|
9370
|
-
* @example
|
|
9371
|
-
* ```ts
|
|
9372
|
-
* const document = SwaggerModule.createDocument(app, config)
|
|
9373
|
-
* addSilkweaveActions(app, document)
|
|
9374
|
-
* SwaggerModule.setup('api/docs', app, document)
|
|
9375
|
-
* ```
|
|
9376
|
-
*/
|
|
9377
|
-
function addSilkweaveActions(app, document, options = {}) {
|
|
9378
|
-
const discovery = app.get(ActionDiscovery);
|
|
9379
|
-
const moduleOptions = app.get(SILKWEAVE_MODULE_OPTIONS);
|
|
9380
|
-
const restAdapter = moduleOptions.adapters.find((adapter) => adapter.name === "rest");
|
|
9381
|
-
const basePath = options.basePath ?? restAdapter?.basePath ?? "/api";
|
|
9382
|
-
const allActions = discovery.discover();
|
|
9383
|
-
const paths = buildActionPaths(options.includeDisabled ? allActions : allActions.filter((action) => isRestEnabled(action, moduleOptions)), {
|
|
9384
|
-
basePath,
|
|
9385
|
-
tag: options.tag
|
|
9386
|
-
});
|
|
9387
|
-
document.paths ??= {};
|
|
9388
|
-
for (const [route, item] of Object.entries(paths)) document.paths[route] = {
|
|
9389
|
-
...document.paths[route] ?? {},
|
|
9390
|
-
...item
|
|
9391
|
-
};
|
|
9392
|
-
return document;
|
|
9393
|
-
}
|
|
9394
|
-
/** Whether an action would be registered by the `rest()` adapter (adapter: 'rest'). */
|
|
9395
|
-
function isRestEnabled(action, moduleOptions) {
|
|
9396
|
-
if (!action.isEnabled) return true;
|
|
9397
|
-
return action.isEnabled(createContext({
|
|
9398
|
-
...moduleOptions.context ?? {},
|
|
9399
|
-
adapter: "rest"
|
|
9400
|
-
}));
|
|
9401
|
-
}
|
|
9402
|
-
//#endregion
|
|
9403
|
-
export { ACTIONS_METADATA, ACTION_METADATA, ACTION_RESPONSE_KEY, Action, ActionDiscovery, Actions, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, addSilkweaveActions, buildActionPaths, buildIsEnabled, collectGuards, mcp, rest, runGuards, trpc, typegen };
|
|
9628
|
+
export { ControllerDiscovery, MCP_METADATA, Mcp, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, apiPropertyToField, buildOpenApiLookup, classValidatorToField, collectGlobalGuards, collectGuards, designTypeToField, fieldToZod, invokeRebound, mcp, mergeField, normalizeEnum, openApiFields, openapiSchemaToField, reflectDtoFields, runGuards, specialBinding, swaggerParamToField, typeTokenToBase };
|
|
9404
9629
|
|
|
9405
9630
|
//# sourceMappingURL=index.mjs.map
|