@silkweave/nestjs 1.10.0 → 1.12.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/.turbo/turbo-build.log +16 -17
- package/.turbo/turbo-prepack.log +7 -8
- package/README.md +43 -8
- package/build/index.d.mts +93 -13
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +197 -20
- package/build/index.mjs.map +1 -1
- package/package.json +15 -8
- package/src/adapter/rest.ts +25 -13
- package/src/index.ts +2 -0
- package/src/lib/discovery.ts +19 -5
- package/src/lib/guards.ts +9 -3
- package/src/lib/metadata.ts +7 -1
- package/src/lib/openapi.ts +116 -0
- package/src/lib/swagger.ts +74 -0
- package/src/lib/types.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
package/build/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { authMiddleware, mcpCors, mcpTransport, oauthRoutes, protectedResourceMe
|
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { ForbiddenException, HttpException, Inject, Injectable, Module, SetMetadata } from "@nestjs/common";
|
|
4
4
|
import { validateToken } from "@silkweave/auth";
|
|
5
|
-
import { SilkweaveError, createAction, createContext } from "@silkweave/core";
|
|
5
|
+
import { SilkweaveError, actionMethod, createAction, createContext, methodHasBody, pathParamNames, resolveActionInput, validateActionRouting } from "@silkweave/core";
|
|
6
6
|
import { buildLogLevels } from "@silkweave/logger";
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
8
|
import { buildRouter, createActionLogger, resolveAuth } from "@silkweave/trpc";
|
|
@@ -11,7 +11,7 @@ import { renderTypegen } from "@silkweave/typegen";
|
|
|
11
11
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
12
|
import { dirname, resolve } from "node:path";
|
|
13
13
|
import { DiscoveryModule, DiscoveryService, HttpAdapterHost, MetadataScanner, ModuleRef, Reflector } from "@nestjs/core";
|
|
14
|
-
import { kebabCase } from "change-case";
|
|
14
|
+
import { camelCase, kebabCase } from "change-case";
|
|
15
15
|
import { GUARDS_METADATA } from "@nestjs/common/constants.js";
|
|
16
16
|
//#region \0rolldown/runtime.js
|
|
17
17
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
@@ -167,7 +167,13 @@ function createActionHandler(action, context, auth, logger) {
|
|
|
167
167
|
}
|
|
168
168
|
authInfo = result.auth;
|
|
169
169
|
}
|
|
170
|
-
const
|
|
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
|
+
});
|
|
171
177
|
const input = action.input.parse(raw);
|
|
172
178
|
sendJson(res, await action.run(input, context.fork({
|
|
173
179
|
logger,
|
|
@@ -184,22 +190,29 @@ function createActionHandler(action, context, auth, logger) {
|
|
|
184
190
|
* REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
|
|
185
191
|
* as an individual route on Nest's HTTP adapter:
|
|
186
192
|
*
|
|
187
|
-
* -
|
|
188
|
-
* -
|
|
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`).
|
|
189
198
|
*
|
|
190
|
-
* Routes show up in Nest's `RoutesResolver` logger
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
204
|
*/
|
|
194
205
|
function rest(options = {}) {
|
|
206
|
+
const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
|
|
195
207
|
return {
|
|
196
208
|
name: "rest",
|
|
209
|
+
basePath,
|
|
197
210
|
register({ httpAdapter, baseContext, actions }) {
|
|
198
|
-
const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
|
|
199
211
|
const logger = createRestLogger();
|
|
200
212
|
for (const action of actions) {
|
|
201
|
-
|
|
202
|
-
const
|
|
213
|
+
validateActionRouting(action);
|
|
214
|
+
const path = action.path ? `${basePath}/${action.path.replace(/^\//, "")}` : `${basePath}${actionNameToPath(action.name)}`;
|
|
215
|
+
const method = actionMethod(action).toLowerCase();
|
|
203
216
|
const handler = createActionHandler(action, baseContext, options.auth, logger);
|
|
204
217
|
httpAdapter[method](path, handler);
|
|
205
218
|
}
|
|
@@ -9035,16 +9048,21 @@ async function resolveGuard(ref, moduleRef) {
|
|
|
9035
9048
|
return ref;
|
|
9036
9049
|
}
|
|
9037
9050
|
/**
|
|
9038
|
-
* Run the configured guards against
|
|
9051
|
+
* Run the configured guards against a request. Throws `ForbiddenException`
|
|
9039
9052
|
* if any guard rejects, mirroring Nest's HTTP request-pipeline behavior.
|
|
9040
9053
|
*
|
|
9041
9054
|
* Pass `null` for `response` when running on top of a tRPC or MCP request that
|
|
9042
9055
|
* doesn't surface a raw response object; guards that introspect the response
|
|
9043
9056
|
* will receive `null`.
|
|
9057
|
+
*
|
|
9058
|
+
* `contextType` is reflected through `ExecutionContext.getType()` so guards can
|
|
9059
|
+
* branch on the transport. It is `'http'` for REST/tRPC and for MCP-over-HTTP
|
|
9060
|
+
* (where a header-bearing request stand-in is available); transports without any
|
|
9061
|
+
* HTTP request (e.g. MCP stdio) pass `'rpc'`.
|
|
9044
9062
|
*/
|
|
9045
|
-
async function runGuards(guards, moduleRef, reflector, classRef, handler, request, response) {
|
|
9063
|
+
async function runGuards(guards, moduleRef, reflector, classRef, handler, request, response, contextType = "http") {
|
|
9046
9064
|
if (guards.length === 0) return;
|
|
9047
|
-
const context = new SilkweaveExecutionContext([request, response], classRef, handler,
|
|
9065
|
+
const context = new SilkweaveExecutionContext([request, response], classRef, handler, contextType);
|
|
9048
9066
|
for (const ref of guards) {
|
|
9049
9067
|
const result = (await resolveGuard(ref, moduleRef)).canActivate(context);
|
|
9050
9068
|
if (!((0, import_cjs.isObservable)(result) ? await (0, import_cjs.lastValueFrom)(result) : await Promise.resolve(result))) throw new ForbiddenException("Forbidden resource");
|
|
@@ -9078,9 +9096,10 @@ let ActionDiscovery = class ActionDiscovery {
|
|
|
9078
9096
|
* a list of core `Action` objects ready to feed into `silkweave().actions()`.
|
|
9079
9097
|
*
|
|
9080
9098
|
* Action invocation is wrapped to (a) run `@UseGuards` guards declared on the
|
|
9081
|
-
* method or its class against the incoming
|
|
9082
|
-
* `ctx.get('request')
|
|
9083
|
-
*
|
|
9099
|
+
* method or its class against the incoming request (read from
|
|
9100
|
+
* `ctx.get('request')`, populated by REST/tRPC and by MCP-over-HTTP from the
|
|
9101
|
+
* SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
|
|
9102
|
+
* so DI-injected dependencies remain available.
|
|
9084
9103
|
*/
|
|
9085
9104
|
discover() {
|
|
9086
9105
|
const discovered = [];
|
|
@@ -9119,8 +9138,14 @@ let ActionDiscovery = class ActionDiscovery {
|
|
|
9119
9138
|
const applyGuards = async (context) => {
|
|
9120
9139
|
if (guards.length === 0) return;
|
|
9121
9140
|
const request = context.getOptional("request");
|
|
9122
|
-
const response = context.getOptional("response");
|
|
9123
|
-
|
|
9141
|
+
const response = context.getOptional("response") ?? null;
|
|
9142
|
+
const hasRequest = request != null;
|
|
9143
|
+
const guardRequest = hasRequest ? request : {
|
|
9144
|
+
headers: {},
|
|
9145
|
+
params: {},
|
|
9146
|
+
query: {}
|
|
9147
|
+
};
|
|
9148
|
+
await runGuards(guards, moduleRef, reflector, d.classRef, d.method, guardRequest, response, hasRequest ? "http" : "rpc");
|
|
9124
9149
|
};
|
|
9125
9150
|
if (d.method.constructor?.name === "AsyncGeneratorFunction") {
|
|
9126
9151
|
if (!d.meta.chunk) throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`);
|
|
@@ -9132,6 +9157,9 @@ let ActionDiscovery = class ActionDiscovery {
|
|
|
9132
9157
|
input: d.meta.input,
|
|
9133
9158
|
chunk: d.meta.chunk,
|
|
9134
9159
|
kind: d.meta.kind ?? "mutation",
|
|
9160
|
+
method: d.meta.method,
|
|
9161
|
+
path: d.meta.path,
|
|
9162
|
+
queryParams: d.meta.queryParams,
|
|
9135
9163
|
args: d.meta.args,
|
|
9136
9164
|
isEnabled,
|
|
9137
9165
|
toolResult: d.meta.toolResult,
|
|
@@ -9147,6 +9175,9 @@ let ActionDiscovery = class ActionDiscovery {
|
|
|
9147
9175
|
input: d.meta.input,
|
|
9148
9176
|
output: d.meta.output,
|
|
9149
9177
|
kind: d.meta.kind ?? "mutation",
|
|
9178
|
+
method: d.meta.method,
|
|
9179
|
+
path: d.meta.path,
|
|
9180
|
+
queryParams: d.meta.queryParams,
|
|
9150
9181
|
args: d.meta.args,
|
|
9151
9182
|
isEnabled,
|
|
9152
9183
|
toolResult: d.meta.toolResult,
|
|
@@ -9164,6 +9195,103 @@ ActionDiscovery = __decorate([Injectable(), __decorateMetadata("design:paramtype
|
|
|
9164
9195
|
typeof (_ref4 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref4 : Object
|
|
9165
9196
|
])], ActionDiscovery);
|
|
9166
9197
|
//#endregion
|
|
9198
|
+
//#region src/lib/openapi.ts
|
|
9199
|
+
/** Convert a route template's `:param` placeholders to OpenAPI `{param}` form. */
|
|
9200
|
+
function toOpenApiPath(template) {
|
|
9201
|
+
return template.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
9202
|
+
}
|
|
9203
|
+
/** The OpenAPI path key for an action - mirrors the `rest()` adapter's routing. */
|
|
9204
|
+
function actionRoute(action, basePath) {
|
|
9205
|
+
return toOpenApiPath(`${basePath}/${action.path ? action.path.replace(/^\//, "") : action.name.replace(/\./g, "/")}`.replace(/\/{2,}/g, "/"));
|
|
9206
|
+
}
|
|
9207
|
+
/** `z.toJSONSchema` tags a top-level `$schema`; drop it for a tidy OpenAPI doc. */
|
|
9208
|
+
function toJsonSchema(schema) {
|
|
9209
|
+
const { $schema: _schema, ...rest } = z.toJSONSchema(schema);
|
|
9210
|
+
return rest;
|
|
9211
|
+
}
|
|
9212
|
+
function responseSchema(action) {
|
|
9213
|
+
if (action.output) return toJsonSchema(action.output);
|
|
9214
|
+
if (action.chunk) return {
|
|
9215
|
+
type: "array",
|
|
9216
|
+
items: toJsonSchema(action.chunk)
|
|
9217
|
+
};
|
|
9218
|
+
}
|
|
9219
|
+
/** Split an action's input fields into OpenAPI path/query parameters and a body, matching the `rest()` adapter. */
|
|
9220
|
+
function splitInputSources(action, hasBody) {
|
|
9221
|
+
const json = z.toJSONSchema(action.input);
|
|
9222
|
+
const required = new Set(json.required ?? []);
|
|
9223
|
+
const pathParams = new Set(pathParamNames(action.path));
|
|
9224
|
+
const queryKeys = new Set((action.queryParams ?? []).map(String));
|
|
9225
|
+
const split = {
|
|
9226
|
+
parameters: [],
|
|
9227
|
+
bodyProps: {},
|
|
9228
|
+
bodyRequired: []
|
|
9229
|
+
};
|
|
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
|
+
}
|
|
9248
|
+
/** Build the OpenAPI operation object for a single action. */
|
|
9249
|
+
function buildOperation(action, tag) {
|
|
9250
|
+
const hasBody = methodHasBody(actionMethod(action));
|
|
9251
|
+
const { parameters, bodyProps, bodyRequired } = splitInputSources(action, hasBody);
|
|
9252
|
+
const response = responseSchema(action);
|
|
9253
|
+
const operation = {
|
|
9254
|
+
tags: [tag],
|
|
9255
|
+
summary: action.description,
|
|
9256
|
+
operationId: camelCase(action.name),
|
|
9257
|
+
responses: { 200: {
|
|
9258
|
+
description: "Successful response",
|
|
9259
|
+
...response ? { content: { "application/json": { schema: response } } } : {}
|
|
9260
|
+
} }
|
|
9261
|
+
};
|
|
9262
|
+
if (parameters.length) operation.parameters = parameters;
|
|
9263
|
+
if (hasBody && Object.keys(bodyProps).length) operation.requestBody = {
|
|
9264
|
+
required: bodyRequired.length > 0,
|
|
9265
|
+
content: { "application/json": { schema: {
|
|
9266
|
+
type: "object",
|
|
9267
|
+
properties: bodyProps,
|
|
9268
|
+
required: bodyRequired
|
|
9269
|
+
} } }
|
|
9270
|
+
};
|
|
9271
|
+
return operation;
|
|
9272
|
+
}
|
|
9273
|
+
/**
|
|
9274
|
+
* Build an OpenAPI `paths` fragment for a set of Silkweave actions, mirroring
|
|
9275
|
+
* exactly how the `rest()` adapter routes them: the same HTTP verb (`method` ??
|
|
9276
|
+
* `kind`), the same `path`/`name`-derived route, and the same path/query/body
|
|
9277
|
+
* field split (`pathParamNames` + `queryParams`). Schemas are inlined from the
|
|
9278
|
+
* Zod input/output via `z.toJSONSchema`; no shared components are emitted.
|
|
9279
|
+
*/
|
|
9280
|
+
function buildActionPaths(actions, options = {}) {
|
|
9281
|
+
const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
|
|
9282
|
+
const tag = options.tag ?? "Actions";
|
|
9283
|
+
const paths = {};
|
|
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;
|
|
9293
|
+
}
|
|
9294
|
+
//#endregion
|
|
9167
9295
|
//#region src/lib/types.ts
|
|
9168
9296
|
const SILKWEAVE_MODULE_OPTIONS = "__silkweave_module_options__";
|
|
9169
9297
|
//#endregion
|
|
@@ -9223,6 +9351,55 @@ SilkweaveModule = _SilkweaveModule = __decorate([
|
|
|
9223
9351
|
])
|
|
9224
9352
|
], SilkweaveModule);
|
|
9225
9353
|
//#endregion
|
|
9226
|
-
|
|
9354
|
+
//#region src/lib/swagger.ts
|
|
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 };
|
|
9227
9404
|
|
|
9228
9405
|
//# sourceMappingURL=index.mjs.map
|