@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.
- package/__tests__/openapi-generator.test.ts +194 -0
- package/__tests__/openapi-module.test.ts +115 -0
- package/__tests__/route-scanner.test.ts +120 -0
- package/__tests__/schema-converter.test.ts +73 -0
- package/dist/decorators/api-operation.d.ts +2 -0
- package/dist/decorators/api-operation.js +6 -0
- package/dist/decorators/api-property.d.ts +2 -0
- package/dist/decorators/api-property.js +6 -0
- package/dist/decorators/api-response.d.ts +4 -0
- package/dist/decorators/api-response.js +6 -0
- package/dist/decorators/api-tags.d.ts +1 -0
- package/dist/decorators/api-tags.js +6 -0
- package/dist/generator/openapi-generator.d.ts +25 -0
- package/dist/generator/openapi-generator.js +166 -0
- package/dist/generator/route-scanner.d.ts +19 -0
- package/dist/generator/route-scanner.js +40 -0
- package/dist/generator/schema-converter.d.ts +5 -0
- package/dist/generator/schema-converter.js +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/interfaces/openapi-options.d.ts +17 -0
- package/dist/interfaces/openapi-options.js +1 -0
- package/dist/metadata/openapi-storage.d.ts +40 -0
- package/dist/metadata/openapi-storage.js +51 -0
- package/dist/openapi.module.d.ts +6 -0
- package/dist/openapi.module.js +103 -0
- package/package.json +27 -0
- package/src/decorators/api-operation.ts +16 -0
- package/src/decorators/api-property.ts +16 -0
- package/src/decorators/api-response.ts +14 -0
- package/src/decorators/api-tags.ts +7 -0
- package/src/generator/openapi-generator.ts +213 -0
- package/src/generator/route-scanner.ts +92 -0
- package/src/generator/schema-converter.ts +21 -0
- package/src/index.ts +14 -0
- package/src/interfaces/openapi-options.ts +11 -0
- package/src/metadata/openapi-storage.ts +108 -0
- package/src/openapi.module.ts +91 -0
- 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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|