@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/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- export * from './adapter/mcp.js'
2
- export * from './adapter/rest.js'
3
- export * from './adapter/trpc.js'
4
- export * from './adapter/typegen.js'
5
- export * from './decorator/action.js'
6
- export * from './decorator/actions.js'
7
- export * from './lib/discovery.js'
8
- export * from './lib/filter.js'
9
- export * from './lib/guards.js'
10
- export * from './lib/metadata.js'
11
- export * from './lib/openapi.js'
12
- export * from './lib/silkweave.module.js'
13
- export * from './lib/swagger.js'
14
- export * from './lib/types.js'
@@ -1,131 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
- import { Injectable, type Type } from '@nestjs/common'
3
- import { DiscoveryService, MetadataScanner, ModuleRef, Reflector } from '@nestjs/core'
4
- import { createAction, type Action, type ActionKind, type SilkweaveContext, type StreamingActionInput } from '@silkweave/core'
5
- import { kebabCase } from 'change-case'
6
- import { buildIsEnabled } from './filter.js'
7
- import { collectGuards, runGuards } from './guards.js'
8
- import { ACTION_METADATA, ACTIONS_METADATA, type ActionMetadata, type ActionsClassMetadata } from './metadata.js'
9
-
10
- interface DiscoveredAction {
11
- instance: object
12
- classRef: Type<unknown>
13
- method: (...args: unknown[]) => unknown
14
- methodName: string
15
- meta: ActionMetadata
16
- classMeta: ActionsClassMetadata
17
- }
18
-
19
- @Injectable()
20
- export class ActionDiscovery {
21
- constructor(
22
- private readonly discovery: DiscoveryService,
23
- private readonly scanner: MetadataScanner,
24
- private readonly reflector: Reflector,
25
- private readonly moduleRef: ModuleRef
26
- ) { }
27
-
28
- /**
29
- * Walk every Nest provider, find methods annotated with `@Action`, and build
30
- * a list of core `Action` objects ready to feed into `silkweave().actions()`.
31
- *
32
- * Action invocation is wrapped to (a) run `@UseGuards` guards declared on the
33
- * method or its class against the incoming request (read from
34
- * `ctx.get('request')`, populated by REST/tRPC and by MCP-over-HTTP from the
35
- * SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
36
- * so DI-injected dependencies remain available.
37
- */
38
- discover(): Action[] {
39
- const discovered: DiscoveredAction[] = []
40
- const wrappers = this.discovery.getProviders()
41
- for (const wrapper of wrappers) {
42
- const { instance } = wrapper
43
- if (!instance || typeof instance !== 'object') { continue }
44
- const proto = Object.getPrototypeOf(instance) as object | null
45
- if (!proto) { continue }
46
- const classRef = instance.constructor as Type<unknown>
47
- const classMeta = (this.reflector.get<ActionsClassMetadata>(ACTIONS_METADATA, classRef) ?? {})
48
- for (const methodName of this.scanner.getAllMethodNames(proto)) {
49
- const method = (proto as Record<string, unknown>)[methodName] as ((...args: unknown[]) => unknown) | undefined
50
- if (typeof method !== 'function') { continue }
51
- const meta = this.reflector.get<ActionMetadata>(ACTION_METADATA, method)
52
- if (!meta) { continue }
53
- discovered.push({ instance, classRef, method, methodName, meta, classMeta })
54
- }
55
- }
56
- return discovered.map((d) => this.toAction(d))
57
- }
58
-
59
- private toAction(d: DiscoveredAction): Action {
60
- const baseName = d.meta.name ?? kebabCase(d.methodName)
61
- const name = d.classMeta.prefix ? `${d.classMeta.prefix}.${baseName}` : baseName
62
- const transports = d.meta.transports ?? d.classMeta.transports
63
- const isEnabled = buildIsEnabled(transports, d.meta.isEnabled)
64
- const guards = collectGuards(this.reflector, d.classRef, d.method)
65
- const moduleRef = this.moduleRef
66
- const reflector = this.reflector
67
-
68
- const applyGuards = async (context: SilkweaveContext): Promise<void> => {
69
- if (guards.length === 0) { return }
70
- // REST and tRPC populate `request`/`response`; MCP-over-HTTP populates
71
- // `request` (a `{ headers, url, params, query }` stand-in built from the
72
- // SDK's `extra.requestInfo`). Transports with no HTTP request at all (e.g.
73
- // MCP stdio) get a header-less stand-in so guards reading `req.headers`
74
- // degrade gracefully (deny) instead of dereferencing `undefined`.
75
- const request = context.getOptional<unknown>('request')
76
- const response = context.getOptional<unknown>('response') ?? null
77
- const hasRequest = request != null
78
- const guardRequest = hasRequest ? request : { headers: {}, params: {}, query: {} }
79
- await runGuards(guards, moduleRef, reflector, d.classRef, d.method, guardRequest, response, hasRequest ? 'http' : 'rpc')
80
- }
81
-
82
- // Cast at the createAction boundary to bridge dual-zod-version installs
83
- // (zod@3.25 + zod@4.x can both be present transitively). Runtime is fine -
84
- // they share the same /v4 surface - but the structural types are distinct.
85
- const streaming = d.method.constructor?.name === 'AsyncGeneratorFunction'
86
-
87
- if (streaming) {
88
- if (!d.meta.chunk) {
89
- throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`)
90
- }
91
- const method = d.method
92
- const instance = d.instance
93
- return createAction({
94
- name,
95
- description: d.meta.description,
96
- input: d.meta.input,
97
- chunk: d.meta.chunk,
98
- kind: d.meta.kind ?? 'mutation',
99
- method: d.meta.method,
100
- path: d.meta.path,
101
- queryParams: d.meta.queryParams,
102
- args: d.meta.args,
103
- isEnabled,
104
- toolResult: d.meta.toolResult,
105
- run: async function* (input: object, context: SilkweaveContext) {
106
- await applyGuards(context)
107
- yield* (method.call(instance, input, context) as AsyncGenerator<object, void, void>)
108
- }
109
- } as StreamingActionInput<object, unknown, string, ActionKind>) as Action
110
- }
111
-
112
- return createAction({
113
- name,
114
- description: d.meta.description,
115
- input: d.meta.input,
116
- output: d.meta.output,
117
- kind: d.meta.kind ?? 'mutation',
118
- method: d.meta.method,
119
- path: d.meta.path,
120
- queryParams: d.meta.queryParams,
121
- args: d.meta.args,
122
- isEnabled,
123
- toolResult: d.meta.toolResult,
124
- run: async (input: object, context: SilkweaveContext): Promise<object> => {
125
- await applyGuards(context)
126
- const result = await (d.method.call(d.instance, input, context) as Promise<object>)
127
- return result
128
- }
129
- })
130
- }
131
- }
@@ -1,76 +0,0 @@
1
- import type { ArgumentsHost, ContextType, ExecutionContext, Type } from '@nestjs/common'
2
-
3
- /** Subset of `HttpArgumentsHost` we need - re-declared inline to avoid deep `@nestjs/common/interfaces` imports. */
4
- interface HttpHost {
5
- getRequest<T = unknown>(): T
6
- getResponse<T = unknown>(): T
7
- getNext<T = unknown>(): T
8
- }
9
-
10
- interface RpcHost {
11
- getData<T = unknown>(): T
12
- getContext<T = unknown>(): T
13
- }
14
-
15
- interface WsHost {
16
- getClient<T = unknown>(): T
17
- getData<T = unknown>(): T
18
- getPattern(): string
19
- }
20
-
21
- /**
22
- * Minimal `ExecutionContext` impl Nest guards can consume. We only need
23
- * `switchToHttp()` to work for our HTTP-backed transports; the RPC/WS shims are
24
- * stubbed so guards that introspect type can still call them without crashing.
25
- */
26
- export class SilkweaveExecutionContext implements ExecutionContext, ArgumentsHost {
27
- constructor(
28
- private readonly args: unknown[],
29
- private readonly classRef: Type<unknown>,
30
- private readonly handler: (...handlerArgs: unknown[]) => unknown,
31
- private readonly contextType = 'http'
32
- ) { }
33
-
34
- getType<T extends string = ContextType>(): T {
35
- return this.contextType as T
36
- }
37
-
38
- getClass<T = unknown>(): Type<T> {
39
- return this.classRef as Type<T>
40
- }
41
-
42
- getHandler(): (...handlerArgs: unknown[]) => unknown {
43
- return this.handler
44
- }
45
-
46
- getArgs<T extends unknown[] = unknown[]>(): T {
47
- return this.args as T
48
- }
49
-
50
- getArgByIndex<T = unknown>(index: number): T {
51
- return this.args[index] as T
52
- }
53
-
54
- switchToHttp(): HttpHost {
55
- return {
56
- getRequest: <T = unknown>(): T => this.args[0] as T,
57
- getResponse: <T = unknown>(): T => this.args[1] as T,
58
- getNext: <T = unknown>(): T => this.args[2] as T
59
- }
60
- }
61
-
62
- switchToRpc(): RpcHost {
63
- return {
64
- getData: <T = unknown>(): T => this.args[0] as T,
65
- getContext: <T = unknown>(): T => this.args[1] as T
66
- }
67
- }
68
-
69
- switchToWs(): WsHost {
70
- return {
71
- getClient: <T = unknown>(): T => this.args[0] as T,
72
- getData: <T = unknown>(): T => this.args[1] as T,
73
- getPattern: (): string => ''
74
- }
75
- }
76
- }
package/src/lib/filter.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { SilkweaveContext } from '@silkweave/core'
2
- import type { Transport } from './metadata.js'
3
-
4
- /**
5
- * Compile a `transports` allowlist + optional user `isEnabled` into a single
6
- * `(ctx) => boolean` callback compatible with `Action.isEnabled`.
7
- *
8
- * - If `transports` is omitted, the action runs on every adapter.
9
- * - If `transports` is set, the action is gated on `ctx.get<string>('adapter')`
10
- * matching one of the listed transports. Each adapter in `@silkweave/nestjs`
11
- * forks its context with `{ adapter: 'rest' | 'trpc' | 'mcp' }`.
12
- * - If both `transports` and `userIsEnabled` are set, they are AND-combined.
13
- */
14
- export function buildIsEnabled(
15
- transports: Transport[] | undefined,
16
- userIsEnabled: ((ctx: SilkweaveContext) => boolean) | undefined
17
- ): ((ctx: SilkweaveContext) => boolean) | undefined {
18
- if (!transports && !userIsEnabled) { return undefined }
19
- return (ctx) => {
20
- if (transports) {
21
- const adapter = ctx.getOptional<string>('adapter')
22
- if (adapter && !transports.includes(adapter as Transport)) { return false }
23
- }
24
- return userIsEnabled ? userIsEnabled(ctx) : true
25
- }
26
- }
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
- }
@@ -1,58 +0,0 @@
1
- import { type Action, type ActionKind, type HttpMethod, type SilkweaveContext } from '@silkweave/core'
2
- import type z from 'zod/v4'
3
-
4
- export const ACTION_METADATA = '__silkweave_action__'
5
- export const ACTIONS_METADATA = '__silkweave_actions__'
6
-
7
- export type Transport = 'rest' | 'trpc' | 'mcp'
8
-
9
- export interface ActionMetadata<I extends object = object, O extends object = object, C = unknown> {
10
- /** Action name. Defaults to the kebab-cased method name. */
11
- name?: string
12
- /** Human-readable description. Becomes the MCP tool description and REST OpenAPI summary. */
13
- description: string
14
- /** Zod object schema for the action's input. */
15
- input: z.ZodType<I> & { shape: Record<string, z.ZodTypeAny> }
16
- /** Optional Zod object schema for the action's output (used by tRPC type inference). */
17
- output?: z.ZodType<O> & { shape: Record<string, z.ZodTypeAny> }
18
- /**
19
- * Zod schema for individual chunks yielded by a streaming (async-generator)
20
- * action method. Required when the decorated method is an `async function*`.
21
- * Mirrors `Action.chunk` in @silkweave/core; typegen/trpc use it to expose
22
- * the action as a tRPC subscription.
23
- */
24
- chunk?: z.ZodType<C>
25
- /** `'query'` (GET in REST, `.query()` in tRPC) or `'mutation'` (POST in REST, `.mutation()` in tRPC). Default: `'mutation'`. */
26
- kind?: ActionKind
27
- /** HTTP verb for the REST route. Defaults to `POST` (or `GET` when `kind` is `'query'`). Overrides the `kind`-derived default. */
28
- method?: HttpMethod
29
- /** REST route path, optionally with `:param` placeholders (e.g. `'spaces/:spaceId/users'`). Each placeholder must be a key of `input`. Defaults to the action name with dots as slashes. */
30
- path?: string
31
- /** Input fields read from the URL query string instead of the request body (e.g. `['offset', 'limit']`). Each must be a key of `input`. */
32
- queryParams?: (keyof I)[]
33
- /** Allowlist of transports that should expose this action. Default: all registered transports. */
34
- transports?: Transport[]
35
- /** Dynamic enable check (in addition to `transports`). AND-combined with the transports filter. */
36
- isEnabled?: (context: SilkweaveContext) => boolean
37
- /** Custom MCP `CallToolResult` formatter. See `@silkweave/mcp`'s `smartToolResult`. */
38
- toolResult?: Action<I, O>['toolResult']
39
- /** CLI positional-argument keys (unused for HTTP-only adapters, kept for parity with `createAction`). */
40
- args?: (keyof I)[]
41
- /** Custom MCP tool name override. If unset, derived from action name as PascalCase. */
42
- mcpToolName?: string
43
- }
44
-
45
- export interface ActionsClassMetadata {
46
- /** Prefix joined to the method-level action name with a dot. e.g. `Actions('users')` + method `list` → `users.list`. */
47
- prefix?: string
48
- /** Class-level transports allowlist. Method-level `transports` overrides; otherwise inherits this. */
49
- transports?: Transport[]
50
- }
51
-
52
- export interface ResultToolResult<O extends object = object> {
53
- toolResult?: Action<object, O>['toolResult']
54
- }
55
-
56
- export type AnyActionMetadata = ActionMetadata<object, object> & ResultToolResult<object>
57
-
58
- export const ACTION_RESPONSE_KEY = '__silkweave_response__'
@@ -1,116 +0,0 @@
1
- import { type Action, actionMethod, methodHasBody, pathParamNames } from '@silkweave/core'
2
- import { camelCase } from 'change-case'
3
- import { z } from 'zod/v4'
4
-
5
- export interface ActionPathsOptions {
6
- /** URL prefix the `rest()` adapter mounts on. Default `'/api'`. */
7
- basePath?: string
8
- /** OpenAPI tag the actions are grouped under. Default `'Actions'`. */
9
- tag?: string
10
- }
11
-
12
- interface JsonSchemaObject {
13
- properties?: Record<string, unknown>
14
- required?: string[]
15
- }
16
-
17
- /** Convert a route template's `:param` placeholders to OpenAPI `{param}` form. */
18
- function toOpenApiPath(template: string): string {
19
- return template.replace(/:([A-Za-z0-9_]+)/g, '{$1}')
20
- }
21
-
22
- /** The OpenAPI path key for an action - mirrors the `rest()` adapter's routing. */
23
- function actionRoute(action: Action, basePath: string): string {
24
- const sub = action.path ? action.path.replace(/^\//, '') : action.name.replace(/\./g, '/')
25
- return toOpenApiPath(`${basePath}/${sub}`.replace(/\/{2,}/g, '/'))
26
- }
27
-
28
- /** `z.toJSONSchema` tags a top-level `$schema`; drop it for a tidy OpenAPI doc. */
29
- function toJsonSchema(schema: z.ZodType): Record<string, unknown> {
30
- const { $schema: _schema, ...rest } = z.toJSONSchema(schema) as Record<string, unknown>
31
- return rest
32
- }
33
-
34
- function responseSchema(action: Action): unknown {
35
- if (action.output) { return toJsonSchema(action.output) }
36
- if (action.chunk) { return { type: 'array', items: toJsonSchema(action.chunk) } }
37
- return undefined
38
- }
39
-
40
- interface SourceSplit {
41
- parameters: Array<Record<string, unknown>>
42
- bodyProps: Record<string, unknown>
43
- bodyRequired: string[]
44
- }
45
-
46
- /** Split an action's input fields into OpenAPI path/query parameters and a body, matching the `rest()` adapter. */
47
- function splitInputSources(action: Action, hasBody: boolean): SourceSplit {
48
- const json = z.toJSONSchema(action.input) as JsonSchemaObject
49
- const required = new Set(json.required ?? [])
50
- const pathParams = new Set(pathParamNames(action.path))
51
- const queryKeys = new Set((action.queryParams ?? []).map(String))
52
-
53
- const split: SourceSplit = { parameters: [], bodyProps: {}, bodyRequired: [] }
54
- for (const [key, schema] of Object.entries(json.properties ?? {})) {
55
- if (pathParams.has(key)) {
56
- split.parameters.push({ name: key, in: 'path', required: true, schema })
57
- } else if (!hasBody || queryKeys.has(key)) {
58
- split.parameters.push({ name: key, in: 'query', required: required.has(key), schema })
59
- } else {
60
- split.bodyProps[key] = schema
61
- if (required.has(key)) { split.bodyRequired.push(key) }
62
- }
63
- }
64
- return split
65
- }
66
-
67
- /** Build the OpenAPI operation object for a single action. */
68
- function buildOperation(action: Action, tag: string): Record<string, unknown> {
69
- const hasBody = methodHasBody(actionMethod(action))
70
- const { parameters, bodyProps, bodyRequired } = splitInputSources(action, hasBody)
71
- const response = responseSchema(action)
72
-
73
- const operation: Record<string, unknown> = {
74
- tags: [tag],
75
- summary: action.description,
76
- operationId: camelCase(action.name),
77
- responses: {
78
- 200: {
79
- description: 'Successful response',
80
- ...(response ? { content: { 'application/json': { schema: response } } } : {})
81
- }
82
- }
83
- }
84
- if (parameters.length) { operation.parameters = parameters }
85
- if (hasBody && Object.keys(bodyProps).length) {
86
- operation.requestBody = {
87
- required: bodyRequired.length > 0,
88
- content: { 'application/json': { schema: { type: 'object', properties: bodyProps, required: bodyRequired } } }
89
- }
90
- }
91
- return operation
92
- }
93
-
94
- /**
95
- * Build an OpenAPI `paths` fragment for a set of Silkweave actions, mirroring
96
- * exactly how the `rest()` adapter routes them: the same HTTP verb (`method` ??
97
- * `kind`), the same `path`/`name`-derived route, and the same path/query/body
98
- * field split (`pathParamNames` + `queryParams`). Schemas are inlined from the
99
- * Zod input/output via `z.toJSONSchema`; no shared components are emitted.
100
- */
101
- export function buildActionPaths(
102
- actions: Action[],
103
- options: ActionPathsOptions = {}
104
- ): Record<string, Record<string, unknown>> {
105
- const basePath = (options.basePath ?? '/api').replace(/\/$/, '')
106
- const tag = options.tag ?? 'Actions'
107
- const paths: Record<string, Record<string, unknown>> = {}
108
-
109
- for (const action of actions) {
110
- const method = actionMethod(action).toLowerCase()
111
- const route = actionRoute(action, basePath)
112
- paths[route] = { ...(paths[route] ?? {}), [method]: buildOperation(action, tag) }
113
- }
114
-
115
- return paths
116
- }
@@ -1,75 +0,0 @@
1
- import { Inject, Module, type DynamicModule, type MiddlewareConsumer, type NestModule } from '@nestjs/common'
2
- import { DiscoveryModule, HttpAdapterHost } from '@nestjs/core'
3
- import { createContext } from '@silkweave/core'
4
- import { ActionDiscovery } from './discovery.js'
5
- import { SILKWEAVE_MODULE_OPTIONS, type SilkweaveModuleOptions } from './types.js'
6
-
7
- /**
8
- * Root module for `@silkweave/nestjs`.
9
- *
10
- * Discovers every `@Action`-decorated method via `DiscoveryService` and
11
- * registers the configured adapters (`rest()`, `trpc()`, `mcp()`) directly on
12
- * Nest's HTTP adapter inside `configure()`. Because `configure()` runs during
13
- * `registerModules` - before Nest's `registerRouter()` step - Silkweave's
14
- * routes always sit ahead of every controller in the Express stack. There is
15
- * no slot middleware, no race with Nest's 404 catch-all, and every route
16
- * shows up in Nest's `RoutesResolver` logger.
17
- *
18
- * @example
19
- * ```ts
20
- * @Module({
21
- * imports: [
22
- * SilkweaveModule.forRoot({
23
- * silkweave: { name: 'app', description: 'My App', version: '1.0.0' },
24
- * adapters: [
25
- * rest({ basePath: '/api' }),
26
- * trpc({ basePath: '/trpc' }),
27
- * mcp({ basePath: '/mcp' })
28
- * ]
29
- * })
30
- * ]
31
- * })
32
- * export class AppModule {}
33
- * ```
34
- */
35
- @Module({})
36
- export class SilkweaveModule implements NestModule {
37
- constructor(
38
- @Inject(SILKWEAVE_MODULE_OPTIONS) private readonly options: SilkweaveModuleOptions,
39
- private readonly discovery: ActionDiscovery,
40
- private readonly httpAdapterHost: HttpAdapterHost
41
- ) { }
42
-
43
- static forRoot(options: SilkweaveModuleOptions): DynamicModule {
44
- return {
45
- module: SilkweaveModule,
46
- global: true,
47
- imports: [DiscoveryModule],
48
- providers: [
49
- { provide: SILKWEAVE_MODULE_OPTIONS, useValue: options },
50
- ActionDiscovery
51
- ],
52
- exports: []
53
- }
54
- }
55
-
56
- configure(_consumer: MiddlewareConsumer): void {
57
- const httpAdapter = this.httpAdapterHost.httpAdapter
58
- if (!httpAdapter) {
59
- throw new Error('@silkweave/nestjs: HttpAdapterHost.httpAdapter is not available.')
60
- }
61
- const allActions = this.discovery.discover()
62
- for (const adapter of this.options.adapters) {
63
- const baseContext = createContext({ ...(this.options.context ?? {}), adapter: adapter.name })
64
- const actions = adapter.allActions
65
- ? allActions
66
- : allActions.filter((a) => !a.isEnabled || a.isEnabled(baseContext))
67
- adapter.register({
68
- httpAdapter,
69
- silkweaveOptions: this.options.silkweave,
70
- baseContext,
71
- actions
72
- })
73
- }
74
- }
75
- }
@@ -1,74 +0,0 @@
1
- import { type INestApplication } from '@nestjs/common'
2
- import type { OpenAPIObject } from '@nestjs/swagger'
3
- import { type Action, createContext } from '@silkweave/core'
4
- import { ActionDiscovery } from './discovery.js'
5
- import { buildActionPaths } from './openapi.js'
6
- import { SILKWEAVE_MODULE_OPTIONS, type SilkweaveModuleOptions } from './types.js'
7
-
8
- export interface SilkweaveSwaggerOptions {
9
- /**
10
- * URL prefix the `rest()` adapter mounts on. Defaults to the configured
11
- * `rest()` adapter's `basePath`, falling back to `'/api'`.
12
- */
13
- basePath?: string
14
- /** OpenAPI tag the actions are grouped under. Default `'Actions'`. */
15
- tag?: string
16
- /**
17
- * Include actions that are *not* enabled on the REST transport (gated out via
18
- * `transports` / `isEnabled`). Default `false` - the document mirrors the
19
- * routes the `rest()` adapter actually registers.
20
- */
21
- includeDisabled?: boolean
22
- }
23
-
24
- /**
25
- * Merge every REST-exposed Silkweave `@Action` into a NestJS Swagger
26
- * `OpenAPIObject`.
27
- *
28
- * `@nestjs/swagger` builds its document by scanning **controllers**, but
29
- * Silkweave registers action routes directly on the HTTP adapter (so they sit
30
- * ahead of controllers in the request pipeline) - which means the scanner never
31
- * sees them. This helper closes that gap: it discovers the actions through the
32
- * same `ActionDiscovery` provider the `rest()` adapter uses, builds OpenAPI
33
- * paths with the same routing logic (`buildActionPaths`), and merges them into
34
- * the document. The result stays in sync with the live routes without any
35
- * dynamic controllers.
36
- *
37
- * Call it between `SwaggerModule.createDocument()` and `SwaggerModule.setup()`:
38
- *
39
- * @example
40
- * ```ts
41
- * const document = SwaggerModule.createDocument(app, config)
42
- * addSilkweaveActions(app, document)
43
- * SwaggerModule.setup('api/docs', app, document)
44
- * ```
45
- */
46
- export function addSilkweaveActions(
47
- app: INestApplication,
48
- document: OpenAPIObject,
49
- options: SilkweaveSwaggerOptions = {}
50
- ): OpenAPIObject {
51
- const discovery = app.get(ActionDiscovery)
52
- const moduleOptions = app.get<SilkweaveModuleOptions>(SILKWEAVE_MODULE_OPTIONS)
53
- const restAdapter = moduleOptions.adapters.find((adapter) => adapter.name === 'rest')
54
- const basePath = options.basePath ?? restAdapter?.basePath ?? '/api'
55
-
56
- const allActions = discovery.discover()
57
- const actions = options.includeDisabled
58
- ? allActions
59
- : allActions.filter((action) => isRestEnabled(action, moduleOptions))
60
-
61
- const paths = buildActionPaths(actions, { basePath, tag: options.tag })
62
-
63
- document.paths ??= {}
64
- for (const [route, item] of Object.entries(paths)) {
65
- document.paths[route] = { ...(document.paths[route] ?? {}), ...item }
66
- }
67
- return document
68
- }
69
-
70
- /** Whether an action would be registered by the `rest()` adapter (adapter: 'rest'). */
71
- function isRestEnabled(action: Action, moduleOptions: SilkweaveModuleOptions): boolean {
72
- if (!action.isEnabled) { return true }
73
- return action.isEnabled(createContext({ ...(moduleOptions.context ?? {}), adapter: 'rest' }))
74
- }