@fluojs/openapi 1.0.0-beta.1

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.
@@ -0,0 +1,400 @@
1
+ import { metadataSymbol } from '@fluojs/core/internal';
2
+
3
+ /**
4
+ * User-facing operation metadata accepted by `@ApiOperation(...)`.
5
+ */
6
+
7
+ /**
8
+ * User-facing response metadata accepted by `@ApiResponse(...)`.
9
+ */
10
+
11
+ /**
12
+ * User-facing parameter metadata accepted by `@ApiParam(...)`, `@ApiQuery(...)`, `@ApiHeader(...)`, and `@ApiCookie(...)`.
13
+ */
14
+
15
+ /**
16
+ * User-facing request-body metadata accepted by `@ApiBody(...)`.
17
+ */
18
+
19
+ /**
20
+ * Normalized operation metadata stored on controller methods.
21
+ */
22
+
23
+ /**
24
+ * Stored OpenAPI security requirement metadata for a single operation.
25
+ */
26
+
27
+ /**
28
+ * Normalized response metadata stored on controller methods.
29
+ */
30
+
31
+ /**
32
+ * Normalized parameter metadata stored on controller methods.
33
+ */
34
+
35
+ /**
36
+ * Normalized request-body metadata stored on controller methods.
37
+ */
38
+
39
+ /**
40
+ * Aggregated OpenAPI metadata snapshot for a controller method.
41
+ */
42
+
43
+ const openApiControllerTagsKey = Symbol.for('fluo.openapi.controller-tags');
44
+ const openApiMethodOperationKey = Symbol.for('fluo.openapi.method-operation');
45
+ const openApiMethodResponsesKey = Symbol.for('fluo.openapi.method-responses');
46
+ const openApiMethodParametersKey = Symbol.for('fluo.openapi.method-parameters');
47
+ const openApiMethodRequestBodyKey = Symbol.for('fluo.openapi.method-request-body');
48
+ const openApiMethodSecurityKey = Symbol.for('fluo.openapi.method-security');
49
+ const openApiMethodSecurityRequirementsKey = Symbol.for('fluo.openapi.method-security-requirements');
50
+ const openApiMethodExcludeEndpointKey = Symbol.for('fluo.openapi.method-exclude-endpoint');
51
+ function getMetadataBag(target) {
52
+ void metadataSymbol;
53
+ return target[metadataSymbol];
54
+ }
55
+ function cloneUnknown(value) {
56
+ if (value === null || value === undefined) {
57
+ return value;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ return value.map(entry => cloneUnknown(entry));
61
+ }
62
+ if (typeof value !== 'object') {
63
+ return value;
64
+ }
65
+ const clone = {};
66
+ for (const key of Reflect.ownKeys(value)) {
67
+ clone[key] = cloneUnknown(value[key]);
68
+ }
69
+ return clone;
70
+ }
71
+ function cloneApiOperationMetadata(operation) {
72
+ if (!operation) {
73
+ return undefined;
74
+ }
75
+ return {
76
+ deprecated: operation.deprecated,
77
+ description: operation.description,
78
+ summary: operation.summary
79
+ };
80
+ }
81
+ function cloneApiSecurityRequirementMetadata(requirement) {
82
+ const clone = {};
83
+ for (const [scheme, scopes] of Object.entries(requirement)) {
84
+ clone[scheme] = [...scopes];
85
+ }
86
+ return clone;
87
+ }
88
+ function cloneApiResponseMetadata(response) {
89
+ return {
90
+ description: response.description,
91
+ schema: cloneUnknown(response.schema),
92
+ status: response.status,
93
+ type: response.type
94
+ };
95
+ }
96
+ function cloneApiParameterMetadata(parameter) {
97
+ return {
98
+ description: parameter.description,
99
+ in: parameter.in,
100
+ name: parameter.name,
101
+ required: parameter.required,
102
+ schema: cloneUnknown(parameter.schema)
103
+ };
104
+ }
105
+ function cloneApiBodyMetadata(requestBody) {
106
+ return {
107
+ ...(requestBody.content !== undefined ? {
108
+ content: cloneUnknown(requestBody.content)
109
+ } : {}),
110
+ ...(requestBody.description !== undefined ? {
111
+ description: requestBody.description
112
+ } : {}),
113
+ ...(requestBody.required !== undefined ? {
114
+ required: requestBody.required
115
+ } : {}),
116
+ ...(requestBody.schema !== undefined ? {
117
+ schema: cloneUnknown(requestBody.schema)
118
+ } : {})
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Read tags registered via `@ApiTag` on a controller class.
124
+ *
125
+ * @param target Controller class token.
126
+ * @returns A defensive copy of registered tags, or `undefined` when no tags are present.
127
+ */
128
+ export function getControllerTags(target) {
129
+ const bag = getMetadataBag(target);
130
+ const tags = bag?.[openApiControllerTagsKey];
131
+ return tags ? [...tags] : undefined;
132
+ }
133
+
134
+ /**
135
+ * Read OpenAPI metadata registered for a controller method.
136
+ *
137
+ * @param target Controller class token.
138
+ * @param propertyKey Controller method key to inspect.
139
+ * @returns A defensive metadata snapshot, or `undefined` when the method has no OpenAPI metadata.
140
+ */
141
+ export function getMethodApiMetadata(target, propertyKey) {
142
+ const bag = getMetadataBag(target);
143
+ const operationMap = bag?.[openApiMethodOperationKey];
144
+ const responsesMap = bag?.[openApiMethodResponsesKey];
145
+ const parametersMap = bag?.[openApiMethodParametersKey];
146
+ const requestBodyMap = bag?.[openApiMethodRequestBodyKey];
147
+ const securityMap = bag?.[openApiMethodSecurityKey];
148
+ const securityRequirementsMap = bag?.[openApiMethodSecurityRequirementsKey];
149
+ const excludeEndpointMap = bag?.[openApiMethodExcludeEndpointKey];
150
+ const operation = operationMap?.get(propertyKey);
151
+ const responses = responsesMap?.get(propertyKey);
152
+ const parameters = parametersMap?.get(propertyKey);
153
+ const requestBody = requestBodyMap?.get(propertyKey);
154
+ const security = securityMap?.get(propertyKey);
155
+ const securityRequirements = securityRequirementsMap?.get(propertyKey);
156
+ const excludeEndpoint = excludeEndpointMap?.get(propertyKey);
157
+ if (!operation && !responses && !parameters && !requestBody && !security && !securityRequirements && !excludeEndpoint) {
158
+ return undefined;
159
+ }
160
+ return {
161
+ operation: cloneApiOperationMetadata(operation),
162
+ responses: (responses ?? []).map(response => cloneApiResponseMetadata(response)),
163
+ parameters: parameters?.map(parameter => cloneApiParameterMetadata(parameter)),
164
+ requestBody: requestBody ? cloneApiBodyMetadata(requestBody) : undefined,
165
+ security: security ? [...security] : undefined,
166
+ securityRequirements: securityRequirements?.map(requirement => cloneApiSecurityRequirementMetadata(requirement)),
167
+ excludeEndpoint
168
+ };
169
+ }
170
+ /**
171
+ * Attach an OpenAPI tag to a controller class.
172
+ *
173
+ * Multiple tags can be declared by stacking `@ApiTag(...)` decorators.
174
+ *
175
+ * @param tag Tag label appended to the controller-level tag list.
176
+ * @returns A class decorator that stores controller tag metadata.
177
+ */
178
+ export function ApiTag(tag) {
179
+ return (_value, context) => {
180
+ const bag = context.metadata;
181
+ const existing = bag[openApiControllerTagsKey] ?? [];
182
+ bag[openApiControllerTagsKey] = [...existing, tag];
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Describe a controller method's OpenAPI operation metadata.
188
+ *
189
+ * @param options Operation metadata such as summary, description, and deprecation flag.
190
+ * @returns A method decorator that stores operation metadata.
191
+ */
192
+ export function ApiOperation(options) {
193
+ return (_value, context) => {
194
+ const bag = context.metadata;
195
+ let map = bag[openApiMethodOperationKey];
196
+ if (!map) {
197
+ map = new Map();
198
+ bag[openApiMethodOperationKey] = map;
199
+ }
200
+ map.set(context.name, {
201
+ deprecated: options.deprecated,
202
+ description: options.description,
203
+ summary: options.summary
204
+ });
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Exclude a controller method from generated OpenAPI `paths`.
210
+ *
211
+ * @returns A method decorator that marks the endpoint as excluded.
212
+ */
213
+ export function ApiExcludeEndpoint() {
214
+ return (_value, context) => {
215
+ const bag = context.metadata;
216
+ let map = bag[openApiMethodExcludeEndpointKey];
217
+ if (!map) {
218
+ map = new Map();
219
+ bag[openApiMethodExcludeEndpointKey] = map;
220
+ }
221
+ map.set(context.name, true);
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Add a security requirement to a controller method in the generated OpenAPI document.
227
+ *
228
+ * @param name Security scheme name (for example `bearerAuth`, `oauth2`, `apiKey`).
229
+ * @param scopes Optional OAuth scopes associated with this security requirement.
230
+ * @returns A method decorator that appends security metadata.
231
+ */
232
+ export function ApiSecurity(name, scopes = []) {
233
+ return (_value, context) => {
234
+ const bag = context.metadata;
235
+ let securityMap = bag[openApiMethodSecurityKey];
236
+ if (!securityMap) {
237
+ securityMap = new Map();
238
+ bag[openApiMethodSecurityKey] = securityMap;
239
+ }
240
+ const existingSecurityNames = securityMap.get(context.name) ?? [];
241
+ if (!existingSecurityNames.includes(name)) {
242
+ securityMap.set(context.name, [...existingSecurityNames, name]);
243
+ }
244
+ let requirementsMap = bag[openApiMethodSecurityRequirementsKey];
245
+ if (!requirementsMap) {
246
+ requirementsMap = new Map();
247
+ bag[openApiMethodSecurityRequirementsKey] = requirementsMap;
248
+ }
249
+ const existingRequirements = requirementsMap.get(context.name) ?? [];
250
+ requirementsMap.set(context.name, [...existingRequirements, {
251
+ [name]: [...scopes]
252
+ }]);
253
+ };
254
+ }
255
+ function registerMethodParameter(parameter) {
256
+ return (_value, context) => {
257
+ const bag = context.metadata;
258
+ let map = bag[openApiMethodParametersKey];
259
+ if (!map) {
260
+ map = new Map();
261
+ bag[openApiMethodParametersKey] = map;
262
+ }
263
+ const existing = map.get(context.name) ?? [];
264
+ map.set(context.name, [...existing, cloneApiParameterMetadata(parameter)]);
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Declare a path parameter for a controller method.
270
+ *
271
+ * @param name Parameter name.
272
+ * @param options Optional parameter metadata such as description, required, and schema.
273
+ * @returns A method decorator that appends path-parameter metadata.
274
+ */
275
+ export function ApiParam(name, options = {}) {
276
+ return registerMethodParameter({
277
+ description: options.description,
278
+ in: 'path',
279
+ name,
280
+ required: options.required ?? true,
281
+ schema: options.schema
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Declare a query parameter for a controller method.
287
+ *
288
+ * @param name Parameter name.
289
+ * @param options Optional parameter metadata such as description, required, and schema.
290
+ * @returns A method decorator that appends query-parameter metadata.
291
+ */
292
+ export function ApiQuery(name, options = {}) {
293
+ return registerMethodParameter({
294
+ description: options.description,
295
+ in: 'query',
296
+ name,
297
+ required: options.required,
298
+ schema: options.schema
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Declare a header parameter for a controller method.
304
+ *
305
+ * @param name Parameter name.
306
+ * @param options Optional parameter metadata such as description, required, and schema.
307
+ * @returns A method decorator that appends header-parameter metadata.
308
+ */
309
+ export function ApiHeader(name, options = {}) {
310
+ return registerMethodParameter({
311
+ description: options.description,
312
+ in: 'header',
313
+ name,
314
+ required: options.required,
315
+ schema: options.schema
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Declare a cookie parameter for a controller method.
321
+ *
322
+ * @param name Parameter name.
323
+ * @param options Optional parameter metadata such as description, required, and schema.
324
+ * @returns A method decorator that appends cookie-parameter metadata.
325
+ */
326
+ export function ApiCookie(name, options = {}) {
327
+ return registerMethodParameter({
328
+ description: options.description,
329
+ in: 'cookie',
330
+ name,
331
+ required: options.required,
332
+ schema: options.schema
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Declare an explicit request body for a controller method.
338
+ *
339
+ * @param options Request-body metadata and schema/content declarations.
340
+ * @returns A method decorator that stores request-body metadata.
341
+ */
342
+ export function ApiBody(options) {
343
+ return (_value, context) => {
344
+ const bag = context.metadata;
345
+ let map = bag[openApiMethodRequestBodyKey];
346
+ if (!map) {
347
+ map = new Map();
348
+ bag[openApiMethodRequestBodyKey] = map;
349
+ }
350
+ map.set(context.name, cloneApiBodyMetadata(options));
351
+ };
352
+ }
353
+ function normalizeApiResponseOptions(statusOrOptions, options) {
354
+ if (typeof statusOrOptions === 'number') {
355
+ return {
356
+ status: statusOrOptions,
357
+ ...options
358
+ };
359
+ }
360
+ return statusOrOptions;
361
+ }
362
+
363
+ /** Declare an expected HTTP response for a controller method. */
364
+
365
+ /** Declare an expected HTTP response for a controller method. */
366
+
367
+ /**
368
+ * Declare an expected HTTP response for a controller method.
369
+ *
370
+ * @param statusOrOptions Either a numeric status code or full response-options object.
371
+ * @param options Optional response metadata when the first argument is numeric status.
372
+ * @returns A method decorator that appends response metadata for the method.
373
+ */
374
+ export function ApiResponse(statusOrOptions, options) {
375
+ const normalized = normalizeApiResponseOptions(statusOrOptions, options);
376
+ return (_value, context) => {
377
+ const bag = context.metadata;
378
+ let map = bag[openApiMethodResponsesKey];
379
+ if (!map) {
380
+ map = new Map();
381
+ bag[openApiMethodResponsesKey] = map;
382
+ }
383
+ const existing = map.get(context.name) ?? [];
384
+ map.set(context.name, [...existing, cloneApiResponseMetadata({
385
+ description: normalized.description,
386
+ schema: normalized.schema,
387
+ status: normalized.status,
388
+ type: normalized.type
389
+ })]);
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Mark a controller method as requiring Bearer token authentication in the OpenAPI spec.
395
+ *
396
+ * @returns A method decorator equivalent to `ApiSecurity('bearerAuth')`.
397
+ */
398
+ export function ApiBearerAuth() {
399
+ return ApiSecurity('bearerAuth');
400
+ }
@@ -0,0 +1,20 @@
1
+ import type { HandlerDescriptor } from '@fluojs/http';
2
+ /**
3
+ * Mutable registry used to snapshot handler descriptors before document generation.
4
+ */
5
+ export declare class OpenApiHandlerRegistry {
6
+ private descriptors;
7
+ /**
8
+ * Replace the current handler-descriptor snapshot.
9
+ *
10
+ * @param descriptors Handler descriptors to retain for later document generation.
11
+ */
12
+ setDescriptors(descriptors: readonly HandlerDescriptor[]): void;
13
+ /**
14
+ * Read the registered handler-descriptor snapshot.
15
+ *
16
+ * @returns A defensive copy of the current handler descriptors.
17
+ */
18
+ getDescriptors(): HandlerDescriptor[];
19
+ }
20
+ //# sourceMappingURL=handler-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-registry.d.ts","sourceRoot":"","sources":["../src/handler-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD;;GAEG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,WAAW,CAA2B;IAE9C;;;;OAIG;IACH,cAAc,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,GAAG,IAAI;IAI/D;;;;OAIG;IACH,cAAc,IAAI,iBAAiB,EAAE;CAGtC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Mutable registry used to snapshot handler descriptors before document generation.
3
+ */
4
+ export class OpenApiHandlerRegistry {
5
+ descriptors = [];
6
+
7
+ /**
8
+ * Replace the current handler-descriptor snapshot.
9
+ *
10
+ * @param descriptors Handler descriptors to retain for later document generation.
11
+ */
12
+ setDescriptors(descriptors) {
13
+ this.descriptors = [...descriptors];
14
+ }
15
+
16
+ /**
17
+ * Read the registered handler-descriptor snapshot.
18
+ *
19
+ * @returns A defensive copy of the current handler descriptors.
20
+ */
21
+ getDescriptors() {
22
+ return [...this.descriptors];
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ export * from './decorators.js';
2
+ export * from './handler-registry.js';
3
+ export * from './openapi-module.js';
4
+ export * from './schema-builder.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './decorators.js';
2
+ export * from './handler-registry.js';
3
+ export * from './openapi-module.js';
4
+ export * from './schema-builder.js';
@@ -0,0 +1,63 @@
1
+ import { type HandlerDescriptor, type HandlerSource } from '@fluojs/http';
2
+ import { type AsyncModuleOptions, type Constructor } from '@fluojs/core';
3
+ import { type ModuleType } from '@fluojs/runtime';
4
+ import { type DefaultErrorResponsesPolicy, type OpenApiDocument, type OpenApiSecuritySchemeObject } from './schema-builder.js';
5
+ /**
6
+ * Public options for `OpenApiModule.forRoot(...)` and `OpenApiModule.forRootAsync(...)`.
7
+ *
8
+ * @remarks
9
+ * Keep README examples for full controller/module workflows. These options are
10
+ * intended to document the runtime hooks that shape the generated document.
11
+ */
12
+ export interface OpenApiModuleOptions {
13
+ defaultErrorResponsesPolicy?: DefaultErrorResponsesPolicy;
14
+ title: string;
15
+ version: string;
16
+ ui?: boolean;
17
+ descriptors?: readonly HandlerDescriptor[];
18
+ sources?: readonly HandlerSource[];
19
+ securitySchemes?: Record<string, OpenApiSecuritySchemeObject>;
20
+ extraModels?: Constructor[];
21
+ documentTransform?: (document: OpenApiDocument) => OpenApiDocument;
22
+ }
23
+ /**
24
+ * Runtime module entrypoint for serving OpenAPI JSON and optional Swagger UI.
25
+ */
26
+ export declare class OpenApiModule {
27
+ /**
28
+ * Registers OpenAPI providers using static options.
29
+ *
30
+ * @param options Static module options used to build and serve the OpenAPI document.
31
+ * @returns A runtime module type that can be imported in `@Module({ imports: [...] })`.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * OpenApiModule.forRoot({
36
+ * title: 'Public API',
37
+ * version: '1.0.0',
38
+ * ui: true,
39
+ * });
40
+ * ```
41
+ */
42
+ static forRoot(options: OpenApiModuleOptions): ModuleType;
43
+ /**
44
+ * Registers OpenAPI providers using an async DI factory.
45
+ *
46
+ * @param options Async options factory plus optional DI `inject` token list.
47
+ * @returns A runtime module type that resolves options at bootstrap time.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * OpenApiModule.forRootAsync({
52
+ * inject: [ConfigService],
53
+ * useFactory: (config) => ({
54
+ * title: config.get('APP_NAME'),
55
+ * version: config.get('APP_VERSION'),
56
+ * }),
57
+ * });
58
+ * ```
59
+ */
60
+ static forRootAsync(options: AsyncModuleOptions<OpenApiModuleOptions>): ModuleType;
61
+ private static createModule;
62
+ }
63
+ //# sourceMappingURL=openapi-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-module.d.ts","sourceRoot":"","sources":["../src/openapi-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,cAAc,CAAC;AAChH,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,2BAA2B,EACjC,MAAM,qBAAqB,CAAC;AAO7B;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,WAAW,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC3C,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IAC9D,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,eAAe,CAAC;CACpE;AAsED;;GAEG;AACH,qBAAa,aAAa;IACxB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,UAAU;IAOzD;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,GAAG,UAAU;IAQlF,OAAO,CAAC,MAAM,CAAC,YAAY;CAqE5B"}