@cinnabun/openapi 0.0.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.
Files changed (39) hide show
  1. package/__tests__/openapi-generator.test.ts +194 -0
  2. package/__tests__/openapi-module.test.ts +115 -0
  3. package/__tests__/route-scanner.test.ts +120 -0
  4. package/__tests__/schema-converter.test.ts +73 -0
  5. package/dist/decorators/api-operation.d.ts +2 -0
  6. package/dist/decorators/api-operation.js +6 -0
  7. package/dist/decorators/api-property.d.ts +2 -0
  8. package/dist/decorators/api-property.js +6 -0
  9. package/dist/decorators/api-response.d.ts +4 -0
  10. package/dist/decorators/api-response.js +6 -0
  11. package/dist/decorators/api-tags.d.ts +1 -0
  12. package/dist/decorators/api-tags.js +6 -0
  13. package/dist/generator/openapi-generator.d.ts +25 -0
  14. package/dist/generator/openapi-generator.js +166 -0
  15. package/dist/generator/route-scanner.d.ts +19 -0
  16. package/dist/generator/route-scanner.js +40 -0
  17. package/dist/generator/schema-converter.d.ts +5 -0
  18. package/dist/generator/schema-converter.js +9 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +9 -0
  21. package/dist/interfaces/openapi-options.d.ts +17 -0
  22. package/dist/interfaces/openapi-options.js +1 -0
  23. package/dist/metadata/openapi-storage.d.ts +40 -0
  24. package/dist/metadata/openapi-storage.js +51 -0
  25. package/dist/openapi.module.d.ts +6 -0
  26. package/dist/openapi.module.js +103 -0
  27. package/package.json +27 -0
  28. package/src/decorators/api-operation.ts +16 -0
  29. package/src/decorators/api-property.ts +16 -0
  30. package/src/decorators/api-response.ts +14 -0
  31. package/src/decorators/api-tags.ts +7 -0
  32. package/src/generator/openapi-generator.ts +213 -0
  33. package/src/generator/route-scanner.ts +92 -0
  34. package/src/generator/schema-converter.ts +21 -0
  35. package/src/index.ts +14 -0
  36. package/src/interfaces/openapi-options.ts +11 -0
  37. package/src/metadata/openapi-storage.ts +108 -0
  38. package/src/openapi.module.ts +91 -0
  39. package/tsconfig.json +18 -0
@@ -0,0 +1,166 @@
1
+ import { convertZodSchema } from "./schema-converter.js";
2
+ export function generateSpec(options, routes) {
3
+ const spec = {
4
+ openapi: "3.1.0",
5
+ info: {
6
+ title: options.title,
7
+ version: options.version,
8
+ },
9
+ paths: {},
10
+ components: {
11
+ schemas: {},
12
+ },
13
+ };
14
+ if (options.description) {
15
+ spec.info.description = options.description;
16
+ }
17
+ if (options.servers?.length) {
18
+ spec.servers = options.servers;
19
+ }
20
+ // Collect all tags from options + routes
21
+ const tagSet = new Map();
22
+ if (options.tags) {
23
+ for (const t of options.tags) {
24
+ tagSet.set(t.name, t.description);
25
+ }
26
+ }
27
+ for (const route of routes) {
28
+ for (const tag of route.tags) {
29
+ if (!tagSet.has(tag))
30
+ tagSet.set(tag, undefined);
31
+ }
32
+ }
33
+ if (tagSet.size > 0) {
34
+ spec.tags = Array.from(tagSet.entries()).map(([name, description]) => description ? { name, description } : { name });
35
+ }
36
+ // Bearer auth
37
+ if (options.bearerAuth) {
38
+ spec.components.securitySchemes = {
39
+ bearerAuth: {
40
+ type: "http",
41
+ scheme: "bearer",
42
+ bearerFormat: "JWT",
43
+ },
44
+ };
45
+ spec.security = [{ bearerAuth: [] }];
46
+ }
47
+ // Build paths
48
+ for (const route of routes) {
49
+ const openApiPath = convertPathParams(route.fullPath);
50
+ const method = route.httpMethod.toLowerCase();
51
+ if (!spec.paths[openApiPath]) {
52
+ spec.paths[openApiPath] = {};
53
+ }
54
+ const operation = {};
55
+ // Tags
56
+ if (route.tags.length > 0) {
57
+ operation.tags = route.tags;
58
+ }
59
+ // Operation metadata
60
+ if (route.operation) {
61
+ if (route.operation.summary)
62
+ operation.summary = route.operation.summary;
63
+ if (route.operation.description)
64
+ operation.description = route.operation.description;
65
+ if (route.operation.deprecated)
66
+ operation.deprecated = true;
67
+ }
68
+ // Parameters from @Param and @Query
69
+ const parameters = buildParameters(route);
70
+ if (parameters.length > 0) {
71
+ operation.parameters = parameters;
72
+ }
73
+ // Request body from validation schema
74
+ if (route.validationSchema?.body) {
75
+ const schemaName = `${route.controllerName}_${route.methodKey}_Body`;
76
+ const { jsonSchema, componentName } = convertZodSchema(route.validationSchema.body, schemaName);
77
+ spec.components.schemas[componentName] = jsonSchema;
78
+ operation.requestBody = {
79
+ required: true,
80
+ content: {
81
+ "application/json": {
82
+ schema: { $ref: `#/components/schemas/${componentName}` },
83
+ },
84
+ },
85
+ };
86
+ }
87
+ // Responses
88
+ operation.responses = buildResponses(route);
89
+ spec.paths[openApiPath][method] = operation;
90
+ }
91
+ return spec;
92
+ }
93
+ function convertPathParams(path) {
94
+ // Convert Express-style :param to OpenAPI {param}
95
+ return path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}");
96
+ }
97
+ function buildParameters(route) {
98
+ const parameters = [];
99
+ for (const param of route.params) {
100
+ if (param.source === "param" && param.name) {
101
+ parameters.push({
102
+ name: param.name,
103
+ in: "path",
104
+ required: true,
105
+ schema: { type: "string" },
106
+ });
107
+ }
108
+ else if (param.source === "query" && param.name) {
109
+ parameters.push({
110
+ name: param.name,
111
+ in: "query",
112
+ required: false,
113
+ schema: { type: "string" },
114
+ });
115
+ }
116
+ }
117
+ // Also add query schema from validation if available
118
+ if (route.validationSchema?.query) {
119
+ const { jsonSchema } = convertZodSchema(route.validationSchema.query);
120
+ if (jsonSchema.type === "object" &&
121
+ jsonSchema.properties &&
122
+ typeof jsonSchema.properties === "object") {
123
+ const required = jsonSchema.required ?? [];
124
+ for (const [name, propSchema] of Object.entries(jsonSchema.properties)) {
125
+ // Skip if already added from @Query decorator
126
+ if (parameters.some((p) => p.name === name && p.in === "query"))
127
+ continue;
128
+ parameters.push({
129
+ name,
130
+ in: "query",
131
+ required: required.includes(name),
132
+ schema: propSchema,
133
+ });
134
+ }
135
+ }
136
+ }
137
+ return parameters;
138
+ }
139
+ function buildResponses(route) {
140
+ const responses = {};
141
+ if (route.responses.length > 0) {
142
+ for (const resp of route.responses) {
143
+ responses[String(resp.status)] = {
144
+ description: resp.description,
145
+ };
146
+ }
147
+ }
148
+ else {
149
+ // Default response based on HTTP code or method
150
+ const code = route.httpCode ?? getDefaultStatusCode(route.httpMethod);
151
+ responses[String(code)] = {
152
+ description: "Successful response",
153
+ };
154
+ }
155
+ return responses;
156
+ }
157
+ function getDefaultStatusCode(method) {
158
+ switch (method) {
159
+ case "POST":
160
+ return 201;
161
+ case "DELETE":
162
+ return 204;
163
+ default:
164
+ return 200;
165
+ }
166
+ }
@@ -0,0 +1,19 @@
1
+ import { type ParamMetadata, type HttpMethod } from "@cinnabun/core";
2
+ import { type ApiOperationMetadata, type ApiResponseMetadata } from "../metadata/openapi-storage.js";
3
+ export interface ScannedRoute {
4
+ httpMethod: HttpMethod;
5
+ fullPath: string;
6
+ controllerName: string;
7
+ methodKey: string;
8
+ params: ParamMetadata[];
9
+ validationSchema?: {
10
+ body?: any;
11
+ query?: any;
12
+ params?: any;
13
+ };
14
+ httpCode?: number;
15
+ operation?: ApiOperationMetadata;
16
+ responses: ApiResponseMetadata[];
17
+ tags: string[];
18
+ }
19
+ export declare function scanRoutes(excludeControllers?: Function[]): ScannedRoute[];
@@ -0,0 +1,40 @@
1
+ import { metadataStorage, } from "@cinnabun/core";
2
+ import { openApiMetadataStorage, } from "../metadata/openapi-storage.js";
3
+ export function scanRoutes(excludeControllers) {
4
+ const routes = [];
5
+ const excludeNames = new Set((excludeControllers ?? []).map((c) => c.name));
6
+ for (const route of metadataStorage.routes) {
7
+ const controllerName = route.target.name;
8
+ // Skip excluded controllers (e.g. OpenApiController itself)
9
+ if (excludeNames.has(controllerName))
10
+ continue;
11
+ const basePath = metadataStorage.getControllerPath(route.target);
12
+ const fullPath = normalizePath(basePath + route.path);
13
+ const params = metadataStorage.getParamsFor(route.target, route.methodKey);
14
+ const validationSchema = metadataStorage.getValidationSchema(route.target, route.methodKey);
15
+ const httpCode = metadataStorage.getHttpCode(route.target, route.methodKey);
16
+ const operation = openApiMetadataStorage.getOperation(route.target, route.methodKey);
17
+ const responses = openApiMetadataStorage.getResponses(route.target, route.methodKey);
18
+ const tags = openApiMetadataStorage.getTags(route.target);
19
+ routes.push({
20
+ httpMethod: route.httpMethod,
21
+ fullPath,
22
+ controllerName,
23
+ methodKey: route.methodKey,
24
+ params,
25
+ validationSchema,
26
+ httpCode,
27
+ operation,
28
+ responses,
29
+ tags,
30
+ });
31
+ }
32
+ return routes;
33
+ }
34
+ function normalizePath(path) {
35
+ // Ensure leading slash, collapse double slashes
36
+ let normalized = "/" + path.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
37
+ if (normalized === "")
38
+ normalized = "/";
39
+ return normalized;
40
+ }
@@ -0,0 +1,5 @@
1
+ export interface ConvertedSchema {
2
+ jsonSchema: Record<string, unknown>;
3
+ componentName: string;
4
+ }
5
+ export declare function convertZodSchema(schema: any, name?: string): ConvertedSchema;
@@ -0,0 +1,9 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ export function convertZodSchema(schema, name) {
3
+ const raw = zodToJsonSchema(schema, { target: "openApi3" });
4
+ const jsonSchema = { ...raw };
5
+ // Remove $schema key — not needed inside OpenAPI spec
6
+ delete jsonSchema.$schema;
7
+ const componentName = name ?? "Schema";
8
+ return { jsonSchema, componentName };
9
+ }
@@ -0,0 +1,14 @@
1
+ export { OpenApiModule } from "./openapi.module.js";
2
+ export type { OpenApiModuleOptions } from "./interfaces/openapi-options.js";
3
+ export { ApiOperation } from "./decorators/api-operation.js";
4
+ export { ApiResponse } from "./decorators/api-response.js";
5
+ export { ApiTags } from "./decorators/api-tags.js";
6
+ export { ApiProperty } from "./decorators/api-property.js";
7
+ export type { ApiOperationMetadata, ApiResponseMetadata, ApiPropertyMetadata } from "./metadata/openapi-storage.js";
8
+ export { openApiMetadataStorage } from "./metadata/openapi-storage.js";
9
+ export { scanRoutes } from "./generator/route-scanner.js";
10
+ export type { ScannedRoute } from "./generator/route-scanner.js";
11
+ export { generateSpec } from "./generator/openapi-generator.js";
12
+ export type { OpenApiSpec } from "./generator/openapi-generator.js";
13
+ export { convertZodSchema } from "./generator/schema-converter.js";
14
+ export type { ConvertedSchema } from "./generator/schema-converter.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { OpenApiModule } from "./openapi.module.js";
2
+ export { ApiOperation } from "./decorators/api-operation.js";
3
+ export { ApiResponse } from "./decorators/api-response.js";
4
+ export { ApiTags } from "./decorators/api-tags.js";
5
+ export { ApiProperty } from "./decorators/api-property.js";
6
+ export { openApiMetadataStorage } from "./metadata/openapi-storage.js";
7
+ export { scanRoutes } from "./generator/route-scanner.js";
8
+ export { generateSpec } from "./generator/openapi-generator.js";
9
+ export { convertZodSchema } from "./generator/schema-converter.js";
@@ -0,0 +1,17 @@
1
+ export interface OpenApiModuleOptions {
2
+ title: string;
3
+ description?: string;
4
+ version: string;
5
+ basePath?: string;
6
+ docsPath?: string;
7
+ jsonPath?: string;
8
+ servers?: {
9
+ url: string;
10
+ description?: string;
11
+ }[];
12
+ tags?: {
13
+ name: string;
14
+ description?: string;
15
+ }[];
16
+ bearerAuth?: boolean;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ export interface ApiOperationMetadata {
2
+ summary?: string;
3
+ description?: string;
4
+ deprecated?: boolean;
5
+ }
6
+ export interface ApiResponseMetadata {
7
+ status: number;
8
+ description: string;
9
+ type?: any;
10
+ }
11
+ export interface ApiPropertyMetadata {
12
+ type?: string;
13
+ required?: boolean;
14
+ example?: unknown;
15
+ description?: string;
16
+ }
17
+ export interface CurrentUserParamMetadata {
18
+ target: Function;
19
+ methodKey: string;
20
+ parameterIndex: number;
21
+ }
22
+ declare class OpenApiMetadataStorage {
23
+ private operations;
24
+ private responses;
25
+ private tags;
26
+ private properties;
27
+ private key;
28
+ private classKey;
29
+ setOperation(target: Function, methodKey: string, metadata: ApiOperationMetadata): void;
30
+ getOperation(target: Function, methodKey: string): ApiOperationMetadata | undefined;
31
+ addResponse(target: Function, methodKey: string, metadata: ApiResponseMetadata): void;
32
+ getResponses(target: Function, methodKey: string): ApiResponseMetadata[];
33
+ setTags(target: Function, tags: string[]): void;
34
+ getTags(target: Function): string[];
35
+ setProperty(target: Function, propertyKey: string, metadata: ApiPropertyMetadata): void;
36
+ getProperties(target: Function): Map<string, ApiPropertyMetadata> | undefined;
37
+ reset(): void;
38
+ }
39
+ export declare const openApiMetadataStorage: OpenApiMetadataStorage;
40
+ export {};
@@ -0,0 +1,51 @@
1
+ class OpenApiMetadataStorage {
2
+ operations = new Map();
3
+ responses = new Map();
4
+ tags = new Map();
5
+ properties = new Map();
6
+ key(target, methodKey) {
7
+ return `${target.name}.${methodKey}`;
8
+ }
9
+ classKey(target, propertyKey) {
10
+ return `${target.name}.${propertyKey}`;
11
+ }
12
+ setOperation(target, methodKey, metadata) {
13
+ this.operations.set(this.key(target, methodKey), metadata);
14
+ }
15
+ getOperation(target, methodKey) {
16
+ return this.operations.get(this.key(target, methodKey));
17
+ }
18
+ addResponse(target, methodKey, metadata) {
19
+ const k = this.key(target, methodKey);
20
+ const existing = this.responses.get(k) ?? [];
21
+ existing.push(metadata);
22
+ this.responses.set(k, existing);
23
+ }
24
+ getResponses(target, methodKey) {
25
+ return this.responses.get(this.key(target, methodKey)) ?? [];
26
+ }
27
+ setTags(target, tags) {
28
+ this.tags.set(target, tags);
29
+ }
30
+ getTags(target) {
31
+ return this.tags.get(target) ?? [];
32
+ }
33
+ setProperty(target, propertyKey, metadata) {
34
+ const k = target.name ?? target.constructor?.name;
35
+ if (!this.properties.has(k)) {
36
+ this.properties.set(k, new Map());
37
+ }
38
+ this.properties.get(k).set(propertyKey, metadata);
39
+ }
40
+ getProperties(target) {
41
+ const k = target.name ?? target.constructor?.name;
42
+ return this.properties.get(k);
43
+ }
44
+ reset() {
45
+ this.operations.clear();
46
+ this.responses.clear();
47
+ this.tags.clear();
48
+ this.properties.clear();
49
+ }
50
+ }
51
+ export const openApiMetadataStorage = new OpenApiMetadataStorage();
@@ -0,0 +1,6 @@
1
+ import type { OpenApiModuleOptions } from "./interfaces/openapi-options.js";
2
+ export declare class OpenApiModule {
3
+ static forRoot(options: OpenApiModuleOptions): Function;
4
+ static getOptions(): OpenApiModuleOptions;
5
+ static resetCache(): void;
6
+ }
@@ -0,0 +1,103 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var DynamicOpenApiController_1;
11
+ import { Module, RestController, GetMapping } from "@cinnabun/core";
12
+ import { scanRoutes } from "./generator/route-scanner.js";
13
+ import { generateSpec } from "./generator/openapi-generator.js";
14
+ let moduleOptions = null;
15
+ let cachedSpec = null;
16
+ export class OpenApiModule {
17
+ static forRoot(options) {
18
+ moduleOptions = options;
19
+ cachedSpec = null;
20
+ const docsPath = options.docsPath ?? "/api-docs";
21
+ const jsonPath = options.jsonPath ?? `${docsPath}/json`;
22
+ const basePath = docsPath.replace(/\/$/, "") || "/";
23
+ const specRoute = jsonPath.startsWith(basePath)
24
+ ? jsonPath.slice(basePath.length) || "/json"
25
+ : "/json";
26
+ // Dynamically create controller with configured paths
27
+ let DynamicOpenApiController = DynamicOpenApiController_1 = class DynamicOpenApiController {
28
+ getSpec() {
29
+ if (!cachedSpec) {
30
+ const routes = scanRoutes([DynamicOpenApiController_1]);
31
+ cachedSpec = generateSpec(options, routes);
32
+ }
33
+ return cachedSpec;
34
+ }
35
+ getSwaggerUI() {
36
+ const html = buildSwaggerUIHtml(options.title, jsonPath);
37
+ return new Response(html, {
38
+ headers: { "Content-Type": "text/html; charset=utf-8" },
39
+ });
40
+ }
41
+ };
42
+ __decorate([
43
+ GetMapping(specRoute.startsWith("/") ? specRoute : `/${specRoute}`),
44
+ __metadata("design:type", Function),
45
+ __metadata("design:paramtypes", []),
46
+ __metadata("design:returntype", void 0)
47
+ ], DynamicOpenApiController.prototype, "getSpec", null);
48
+ __decorate([
49
+ GetMapping("/"),
50
+ __metadata("design:type", Function),
51
+ __metadata("design:paramtypes", []),
52
+ __metadata("design:returntype", void 0)
53
+ ], DynamicOpenApiController.prototype, "getSwaggerUI", null);
54
+ DynamicOpenApiController = DynamicOpenApiController_1 = __decorate([
55
+ RestController(docsPath)
56
+ ], DynamicOpenApiController);
57
+ let OpenApiDynamicModule = class OpenApiDynamicModule {
58
+ };
59
+ OpenApiDynamicModule = __decorate([
60
+ Module({
61
+ controllers: [DynamicOpenApiController],
62
+ providers: [],
63
+ exports: [],
64
+ })
65
+ ], OpenApiDynamicModule);
66
+ return OpenApiDynamicModule;
67
+ }
68
+ static getOptions() {
69
+ if (!moduleOptions) {
70
+ throw new Error("OpenApiModule not initialized. Call OpenApiModule.forRoot() first.");
71
+ }
72
+ return moduleOptions;
73
+ }
74
+ static resetCache() {
75
+ cachedSpec = null;
76
+ }
77
+ }
78
+ function buildSwaggerUIHtml(title, specUrl) {
79
+ return `<!DOCTYPE html>
80
+ <html lang="en">
81
+ <head>
82
+ <meta charset="UTF-8">
83
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
84
+ <title>${title} - API Docs</title>
85
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
86
+ </head>
87
+ <body>
88
+ <div id="swagger-ui"></div>
89
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
90
+ <script>
91
+ SwaggerUIBundle({
92
+ url: '${specUrl}',
93
+ dom_id: '#swagger-ui',
94
+ presets: [
95
+ SwaggerUIBundle.presets.apis,
96
+ SwaggerUIBundle.SwaggerUIStandalonePreset
97
+ ],
98
+ layout: 'BaseLayout'
99
+ });
100
+ </script>
101
+ </body>
102
+ </html>`;
103
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@cinnabun/openapi",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "bun test",
12
+ "build": "tsc"
13
+ },
14
+ "peerDependencies": {
15
+ "@cinnabun/core": "^0.0.3"
16
+ },
17
+ "dependencies": {
18
+ "zod-to-json-schema": "^3.24.1"
19
+ },
20
+ "devDependencies": {
21
+ "@cinnabun/core": "workspace:*",
22
+ "@types/bun": "latest",
23
+ "reflect-metadata": "^0.2.2",
24
+ "typescript": "^5.9.3",
25
+ "zod": "^3.24.0"
26
+ }
27
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ openApiMetadataStorage,
3
+ type ApiOperationMetadata,
4
+ } from "../metadata/openapi-storage.js";
5
+
6
+ export function ApiOperation(
7
+ metadata: ApiOperationMetadata,
8
+ ): MethodDecorator {
9
+ return (target: any, propertyKey: string | symbol) => {
10
+ openApiMetadataStorage.setOperation(
11
+ target.constructor,
12
+ propertyKey as string,
13
+ metadata,
14
+ );
15
+ };
16
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ openApiMetadataStorage,
3
+ type ApiPropertyMetadata,
4
+ } from "../metadata/openapi-storage.js";
5
+
6
+ export function ApiProperty(
7
+ metadata: ApiPropertyMetadata = {},
8
+ ): PropertyDecorator {
9
+ return (target: any, propertyKey: string | symbol) => {
10
+ openApiMetadataStorage.setProperty(
11
+ target.constructor,
12
+ propertyKey as string,
13
+ metadata,
14
+ );
15
+ };
16
+ }
@@ -0,0 +1,14 @@
1
+ import { openApiMetadataStorage } from "../metadata/openapi-storage.js";
2
+
3
+ export function ApiResponse(
4
+ status: number,
5
+ metadata: { description: string; type?: any },
6
+ ): MethodDecorator {
7
+ return (target: any, propertyKey: string | symbol) => {
8
+ openApiMetadataStorage.addResponse(
9
+ target.constructor,
10
+ propertyKey as string,
11
+ { status, ...metadata },
12
+ );
13
+ };
14
+ }
@@ -0,0 +1,7 @@
1
+ import { openApiMetadataStorage } from "../metadata/openapi-storage.js";
2
+
3
+ export function ApiTags(...tags: string[]): ClassDecorator {
4
+ return (target: any) => {
5
+ openApiMetadataStorage.setTags(target, tags);
6
+ };
7
+ }