@buenojs/bueno 0.8.3 → 0.8.5
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/README.md +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Scanner
|
|
3
|
+
*
|
|
4
|
+
* Scans controllers and extracts OpenAPI operation metadata from decorators.
|
|
5
|
+
* Combines HTTP method decorators, parameter documentation, and responses
|
|
6
|
+
* into OpenAPI operation objects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Constructor,
|
|
11
|
+
OpenAPIMediaType,
|
|
12
|
+
OpenAPIOperation,
|
|
13
|
+
OpenAPIPaths,
|
|
14
|
+
OpenAPIResponse,
|
|
15
|
+
OpenAPISchema,
|
|
16
|
+
} from './types';
|
|
17
|
+
import { getApiMetadata, getApiMethodMetadata } from './metadata';
|
|
18
|
+
// Read controller path and route list from the modules metadata stores
|
|
19
|
+
import { getMetadata, getPrototypeMetadata } from '../modules/metadata';
|
|
20
|
+
import { SchemaGenerator } from './schema-generator';
|
|
21
|
+
|
|
22
|
+
interface RouteInfo {
|
|
23
|
+
method: string;
|
|
24
|
+
path: string;
|
|
25
|
+
handler: string | symbol;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RouteScanner - Scans controllers and extracts OpenAPI operations
|
|
30
|
+
*/
|
|
31
|
+
export class RouteScanner {
|
|
32
|
+
constructor(private schemaGenerator: SchemaGenerator) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scan all controllers and generate OpenAPI paths
|
|
36
|
+
*/
|
|
37
|
+
scanControllers(controllers: Constructor[]): OpenAPIPaths {
|
|
38
|
+
const paths: OpenAPIPaths = {};
|
|
39
|
+
|
|
40
|
+
for (const controller of controllers) {
|
|
41
|
+
// Skip controllers marked with @ApiExcludeController
|
|
42
|
+
if (getApiMetadata<boolean>(controller, 'api:exclude')) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get controller base path from @Controller decorator (stored in modules metadata)
|
|
47
|
+
const basePath = getMetadata<string>(controller, 'path') ?? '';
|
|
48
|
+
|
|
49
|
+
// Get routes from prototype metadata (stored by @Get, @Post, etc. in modules metadata)
|
|
50
|
+
const routes =
|
|
51
|
+
getPrototypeMetadata<RouteInfo[]>(controller.prototype, 'routes') ?? [];
|
|
52
|
+
|
|
53
|
+
// Get class-level tags and security from OpenAPI metadata
|
|
54
|
+
const classLevelTags = getApiMetadata<string[]>(controller, 'api:tags') ?? [];
|
|
55
|
+
const classLevelSecurity =
|
|
56
|
+
getApiMetadata<Record<string, string[]>[]>(controller, 'api:security') ?? [];
|
|
57
|
+
|
|
58
|
+
// Process each route in the controller
|
|
59
|
+
for (const route of routes) {
|
|
60
|
+
const prototype = controller.prototype;
|
|
61
|
+
const handlerKey = route.handler;
|
|
62
|
+
|
|
63
|
+
// Skip endpoints marked with @ApiExcludeEndpoint (per-method metadata)
|
|
64
|
+
if (getApiMethodMetadata<boolean>(prototype, `api:exclude:${String(handlerKey)}`)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fullPath = this.convertPathToOpenAPI(basePath + route.path);
|
|
69
|
+
|
|
70
|
+
// Ensure path exists in paths object
|
|
71
|
+
if (!paths[fullPath]) {
|
|
72
|
+
paths[fullPath] = {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate the operation
|
|
76
|
+
const operation = this.generateOperation(
|
|
77
|
+
controller,
|
|
78
|
+
route,
|
|
79
|
+
classLevelTags,
|
|
80
|
+
classLevelSecurity,
|
|
81
|
+
basePath,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Add operation to the path (always write — overwrite if duplicate method)
|
|
85
|
+
(paths[fullPath] as Record<string, OpenAPIOperation>)[
|
|
86
|
+
route.method.toLowerCase()
|
|
87
|
+
] = operation;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return paths;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate an OpenAPI operation for a single route
|
|
96
|
+
*/
|
|
97
|
+
private generateOperation(
|
|
98
|
+
controller: Constructor,
|
|
99
|
+
route: RouteInfo,
|
|
100
|
+
classLevelTags: string[],
|
|
101
|
+
classLevelSecurity: Record<string, string[]>[],
|
|
102
|
+
basePath: string,
|
|
103
|
+
): OpenAPIOperation {
|
|
104
|
+
const prototype = controller.prototype;
|
|
105
|
+
const handlerKey = String(route.handler);
|
|
106
|
+
|
|
107
|
+
// Helper to read per-method OpenAPI metadata — stored under "api:<key>:<handler>"
|
|
108
|
+
const getMethodMeta = <T>(key: string): T | undefined =>
|
|
109
|
+
getApiMethodMetadata<T>(prototype, `${key}:${handlerKey}`);
|
|
110
|
+
|
|
111
|
+
// Get operation metadata from decorators
|
|
112
|
+
const operationMeta = getMethodMeta<{
|
|
113
|
+
summary?: string;
|
|
114
|
+
description?: string;
|
|
115
|
+
operationId?: string;
|
|
116
|
+
deprecated?: boolean;
|
|
117
|
+
}>('api:operation');
|
|
118
|
+
|
|
119
|
+
const responseMeta =
|
|
120
|
+
getMethodMeta<
|
|
121
|
+
Array<{
|
|
122
|
+
status: number | 'default';
|
|
123
|
+
description: string;
|
|
124
|
+
type?: Constructor | Constructor[];
|
|
125
|
+
schema?: OpenAPISchema;
|
|
126
|
+
}>
|
|
127
|
+
>('api:responses') ?? [];
|
|
128
|
+
|
|
129
|
+
const paramMeta =
|
|
130
|
+
getMethodMeta<
|
|
131
|
+
Array<{
|
|
132
|
+
name: string;
|
|
133
|
+
type?: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
required?: boolean;
|
|
136
|
+
example?: unknown;
|
|
137
|
+
schema?: OpenAPISchema;
|
|
138
|
+
}>
|
|
139
|
+
>('api:params') ?? [];
|
|
140
|
+
|
|
141
|
+
const queryMeta =
|
|
142
|
+
getMethodMeta<
|
|
143
|
+
Array<{
|
|
144
|
+
name: string;
|
|
145
|
+
type?: string;
|
|
146
|
+
description?: string;
|
|
147
|
+
required?: boolean;
|
|
148
|
+
example?: unknown;
|
|
149
|
+
schema?: OpenAPISchema;
|
|
150
|
+
}>
|
|
151
|
+
>('api:query') ?? [];
|
|
152
|
+
|
|
153
|
+
const headerMeta =
|
|
154
|
+
getMethodMeta<
|
|
155
|
+
Array<{
|
|
156
|
+
name: string;
|
|
157
|
+
description?: string;
|
|
158
|
+
required?: boolean;
|
|
159
|
+
schema?: OpenAPISchema;
|
|
160
|
+
}>
|
|
161
|
+
>('api:headers') ?? [];
|
|
162
|
+
|
|
163
|
+
const bodyMeta = getMethodMeta<{
|
|
164
|
+
type?: Constructor;
|
|
165
|
+
description?: string;
|
|
166
|
+
required?: boolean;
|
|
167
|
+
schema?: OpenAPISchema;
|
|
168
|
+
}>('api:body');
|
|
169
|
+
|
|
170
|
+
const methodLevelTags = getMethodMeta<string[]>('api:tags') ?? [];
|
|
171
|
+
|
|
172
|
+
const methodLevelSecurity =
|
|
173
|
+
getMethodMeta<Record<string, string[]>[]>('api:security') ?? [];
|
|
174
|
+
|
|
175
|
+
// Build parameters array
|
|
176
|
+
const parameters = [
|
|
177
|
+
...paramMeta.map((p) => ({
|
|
178
|
+
name: p.name,
|
|
179
|
+
in: 'path' as const,
|
|
180
|
+
description: p.description,
|
|
181
|
+
required: p.required ?? true,
|
|
182
|
+
example: p.example,
|
|
183
|
+
schema: p.schema,
|
|
184
|
+
})),
|
|
185
|
+
...queryMeta.map((q) => ({
|
|
186
|
+
name: q.name,
|
|
187
|
+
in: 'query' as const,
|
|
188
|
+
description: q.description,
|
|
189
|
+
required: q.required ?? false,
|
|
190
|
+
example: q.example,
|
|
191
|
+
schema: q.schema,
|
|
192
|
+
})),
|
|
193
|
+
...headerMeta.map((h) => ({
|
|
194
|
+
name: h.name,
|
|
195
|
+
in: 'header' as const,
|
|
196
|
+
description: h.description,
|
|
197
|
+
required: h.required,
|
|
198
|
+
schema: h.schema,
|
|
199
|
+
})),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
// Build responses object (properly typed)
|
|
203
|
+
const responses: Record<string, OpenAPIResponse> = {};
|
|
204
|
+
|
|
205
|
+
for (const resp of responseMeta) {
|
|
206
|
+
const statusCode = String(resp.status);
|
|
207
|
+
const response: OpenAPIResponse = { description: resp.description };
|
|
208
|
+
|
|
209
|
+
// Add schema if provided
|
|
210
|
+
if (resp.type || resp.schema) {
|
|
211
|
+
let schema: OpenAPISchema = resp.schema ?? {};
|
|
212
|
+
|
|
213
|
+
if (resp.type) {
|
|
214
|
+
if (Array.isArray(resp.type)) {
|
|
215
|
+
const itemSchema = this.schemaGenerator.generateSchema(resp.type[0]);
|
|
216
|
+
schema = { type: 'array', items: itemSchema };
|
|
217
|
+
} else {
|
|
218
|
+
schema = this.schemaGenerator.generateSchema(resp.type);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const mediaType: OpenAPIMediaType = { schema };
|
|
223
|
+
response.content = { 'application/json': mediaType };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
responses[statusCode] = response;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If no responses documented, add a default 200
|
|
230
|
+
if (Object.keys(responses).length === 0) {
|
|
231
|
+
responses['200'] = { description: 'Success' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build request body if present
|
|
235
|
+
const requestBody = bodyMeta
|
|
236
|
+
? {
|
|
237
|
+
description: bodyMeta.description,
|
|
238
|
+
required: bodyMeta.required ?? true,
|
|
239
|
+
content: {
|
|
240
|
+
'application/json': {
|
|
241
|
+
schema:
|
|
242
|
+
bodyMeta.schema ??
|
|
243
|
+
(bodyMeta.type
|
|
244
|
+
? this.schemaGenerator.generateSchema(bodyMeta.type)
|
|
245
|
+
: { type: 'object' as const }),
|
|
246
|
+
} satisfies OpenAPIMediaType,
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
250
|
+
|
|
251
|
+
// Combine tags (class-level + method-level, deduplicated)
|
|
252
|
+
const tags = [...new Set([...classLevelTags, ...methodLevelTags])];
|
|
253
|
+
|
|
254
|
+
// Combine security
|
|
255
|
+
const security = [...classLevelSecurity, ...methodLevelSecurity];
|
|
256
|
+
|
|
257
|
+
// Build the operation
|
|
258
|
+
const operation: OpenAPIOperation = {
|
|
259
|
+
operationId:
|
|
260
|
+
operationMeta?.operationId ??
|
|
261
|
+
`${route.method.toLowerCase()}_${basePath.replace(/\//g, '_')}_${handlerKey}`,
|
|
262
|
+
summary: operationMeta?.summary,
|
|
263
|
+
description: operationMeta?.description,
|
|
264
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
265
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
266
|
+
requestBody,
|
|
267
|
+
responses,
|
|
268
|
+
deprecated: operationMeta?.deprecated,
|
|
269
|
+
security: security.length > 0 ? security : undefined,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return operation;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Convert a route pattern from :param format to {param} format (OpenAPI style)
|
|
277
|
+
* Handles optional params (:param?) and regex params (:param<regex>)
|
|
278
|
+
*/
|
|
279
|
+
private convertPathToOpenAPI(pattern: string): string {
|
|
280
|
+
return pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)\??(?:<[^>]+>)?/g, '{$1}');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get the schema generator instance
|
|
285
|
+
*/
|
|
286
|
+
getSchemaGenerator(): SchemaGenerator {
|
|
287
|
+
return this.schemaGenerator;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Converts TypeScript class types and @ApiProperty decorators into OpenAPI schemas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ApiPropertyOptions, Constructor, OpenAPISchema } from './types';
|
|
8
|
+
import { getApiPropertyMetadata, getApiPropertyKeys } from './metadata';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SchemaGenerator - Converts TypeScript types to OpenAPI schemas
|
|
12
|
+
*/
|
|
13
|
+
export class SchemaGenerator {
|
|
14
|
+
private schemas = new Map<string, OpenAPISchema>();
|
|
15
|
+
private typeNames = new Map<Constructor, string>();
|
|
16
|
+
private typeCounter = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate an OpenAPI schema from a TypeScript type
|
|
20
|
+
* Supports classes, interfaces (via decorators), primitives, arrays
|
|
21
|
+
*/
|
|
22
|
+
generateSchema(type: Constructor | string | Function): OpenAPISchema {
|
|
23
|
+
// Handle string type names (primitives like 'string', 'number')
|
|
24
|
+
if (typeof type === 'string') {
|
|
25
|
+
return this.generatePrimitiveSchema(type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle function types (classes, constructors)
|
|
29
|
+
if (typeof type === 'function') {
|
|
30
|
+
const typeName = this.getTypeName(type);
|
|
31
|
+
|
|
32
|
+
// Check if already cached
|
|
33
|
+
if (this.schemas.has(typeName)) {
|
|
34
|
+
return { $ref: `#/components/schemas/${typeName}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle built-in types
|
|
38
|
+
if (this.isBuiltInType(type)) {
|
|
39
|
+
return this.generatePrimitiveSchema(type);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle arrays (Array constructor)
|
|
43
|
+
if (type === Array) {
|
|
44
|
+
return { type: 'array', items: { type: 'object' } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate object schema from class
|
|
48
|
+
return this.generateObjectSchema(type);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default fallback
|
|
52
|
+
return { type: 'object' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate schema for an object/class type
|
|
57
|
+
* Reads @ApiProperty metadata from class properties
|
|
58
|
+
*/
|
|
59
|
+
private generateObjectSchema(type: Function): OpenAPISchema {
|
|
60
|
+
const typeName = this.getTypeName(type);
|
|
61
|
+
const properties: Record<string, OpenAPISchema> = {};
|
|
62
|
+
const required: string[] = [];
|
|
63
|
+
|
|
64
|
+
// Get all property keys that have @ApiProperty metadata
|
|
65
|
+
const propertyKeys = getApiPropertyKeys((type as Constructor).prototype);
|
|
66
|
+
|
|
67
|
+
for (const key of propertyKeys) {
|
|
68
|
+
const propName = typeof key === 'symbol' ? key.toString() : key;
|
|
69
|
+
const propOptions = getApiPropertyMetadata<ApiPropertyOptions>(
|
|
70
|
+
(type as Constructor).prototype,
|
|
71
|
+
key,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (propOptions) {
|
|
75
|
+
properties[propName] = this.generatePropertySchema(propOptions);
|
|
76
|
+
|
|
77
|
+
// Track required properties
|
|
78
|
+
if (propOptions.required !== false) {
|
|
79
|
+
required.push(propName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const schema: OpenAPISchema = {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: Object.keys(properties).length > 0 ? properties : undefined,
|
|
87
|
+
required: required.length > 0 ? required : undefined,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Cache the schema
|
|
91
|
+
this.schemas.set(typeName, schema);
|
|
92
|
+
|
|
93
|
+
// Return a reference to the cached schema
|
|
94
|
+
return { $ref: `#/components/schemas/${typeName}` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate schema from an @ApiProperty options object
|
|
99
|
+
*/
|
|
100
|
+
private generatePropertySchema(options: ApiPropertyOptions): OpenAPISchema {
|
|
101
|
+
const schema: OpenAPISchema = {};
|
|
102
|
+
|
|
103
|
+
// Type mapping
|
|
104
|
+
if (options.type) {
|
|
105
|
+
if (typeof options.type === 'string') {
|
|
106
|
+
// Map string type names
|
|
107
|
+
const typeSchema = this.mapStringType(options.type);
|
|
108
|
+
Object.assign(schema, typeSchema);
|
|
109
|
+
} else if (typeof options.type === 'function') {
|
|
110
|
+
// Recurse for class types
|
|
111
|
+
const nested = this.generateSchema(options.type);
|
|
112
|
+
Object.assign(schema, nested);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// String validations
|
|
117
|
+
if (options.minLength !== undefined) schema.minLength = options.minLength;
|
|
118
|
+
if (options.maxLength !== undefined) schema.maxLength = options.maxLength;
|
|
119
|
+
if (options.pattern !== undefined) schema.pattern = options.pattern;
|
|
120
|
+
|
|
121
|
+
// Numeric validations
|
|
122
|
+
if (options.minimum !== undefined) schema.minimum = options.minimum;
|
|
123
|
+
if (options.maximum !== undefined) schema.maximum = options.maximum;
|
|
124
|
+
|
|
125
|
+
// Array validations
|
|
126
|
+
if (options.minItems !== undefined) schema.minItems = options.minItems;
|
|
127
|
+
if (options.maxItems !== undefined) schema.maxItems = options.maxItems;
|
|
128
|
+
if (options.items !== undefined) schema.items = options.items;
|
|
129
|
+
|
|
130
|
+
// Enum
|
|
131
|
+
if (options.enum !== undefined) {
|
|
132
|
+
schema.enum = options.enum;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format
|
|
136
|
+
if (options.format !== undefined) schema.format = options.format;
|
|
137
|
+
|
|
138
|
+
// Metadata
|
|
139
|
+
if (options.title !== undefined) schema.title = options.title;
|
|
140
|
+
if (options.description !== undefined) schema.description = options.description;
|
|
141
|
+
if (options.example !== undefined) schema.example = options.example;
|
|
142
|
+
if (options.default !== undefined) schema.default = options.default;
|
|
143
|
+
if (options.nullable !== undefined) schema.nullable = options.nullable;
|
|
144
|
+
if (options.readOnly !== undefined) schema.readOnly = options.readOnly;
|
|
145
|
+
if (options.writeOnly !== undefined) schema.writeOnly = options.writeOnly;
|
|
146
|
+
|
|
147
|
+
return schema;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate schema for a primitive TypeScript type
|
|
152
|
+
*/
|
|
153
|
+
private generatePrimitiveSchema(type: string | Function): OpenAPISchema {
|
|
154
|
+
if (typeof type === 'string') {
|
|
155
|
+
return this.mapStringType(type);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Map constructor to primitive type
|
|
159
|
+
if (type === String) return { type: 'string' };
|
|
160
|
+
if (type === Number) return { type: 'number' };
|
|
161
|
+
if (type === Boolean) return { type: 'boolean' };
|
|
162
|
+
if (type === Date) return { type: 'string', format: 'date-time' };
|
|
163
|
+
if (type === Array) return { type: 'array', items: {} };
|
|
164
|
+
|
|
165
|
+
return { type: 'object' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Map string type names to OpenAPI schema
|
|
170
|
+
*/
|
|
171
|
+
private mapStringType(type: string): OpenAPISchema {
|
|
172
|
+
switch (type.toLowerCase()) {
|
|
173
|
+
case 'string':
|
|
174
|
+
return { type: 'string' };
|
|
175
|
+
case 'number':
|
|
176
|
+
return { type: 'number' };
|
|
177
|
+
case 'integer':
|
|
178
|
+
return { type: 'integer' };
|
|
179
|
+
case 'boolean':
|
|
180
|
+
return { type: 'boolean' };
|
|
181
|
+
case 'date':
|
|
182
|
+
return { type: 'string', format: 'date' };
|
|
183
|
+
case 'datetime':
|
|
184
|
+
case 'date-time':
|
|
185
|
+
return { type: 'string', format: 'date-time' };
|
|
186
|
+
case 'email':
|
|
187
|
+
return { type: 'string', format: 'email' };
|
|
188
|
+
case 'uuid':
|
|
189
|
+
return { type: 'string', format: 'uuid' };
|
|
190
|
+
case 'url':
|
|
191
|
+
case 'uri':
|
|
192
|
+
return { type: 'string', format: 'uri' };
|
|
193
|
+
case 'object':
|
|
194
|
+
return { type: 'object' };
|
|
195
|
+
case 'array':
|
|
196
|
+
return { type: 'array', items: {} };
|
|
197
|
+
default:
|
|
198
|
+
return { type: 'string' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if type is a built-in TypeScript type
|
|
204
|
+
*/
|
|
205
|
+
private isBuiltInType(type: Function): boolean {
|
|
206
|
+
return (
|
|
207
|
+
type === String ||
|
|
208
|
+
type === Number ||
|
|
209
|
+
type === Boolean ||
|
|
210
|
+
type === Date ||
|
|
211
|
+
type === Array ||
|
|
212
|
+
type === Object
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get the name for a type (for referencing in components.schemas)
|
|
218
|
+
*/
|
|
219
|
+
private getTypeName(type: Function): string {
|
|
220
|
+
// Check if we've already assigned a name
|
|
221
|
+
if (this.typeNames.has(type as Constructor)) {
|
|
222
|
+
return this.typeNames.get(type as Constructor)!;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Use the constructor name if available
|
|
226
|
+
let name = type.name;
|
|
227
|
+
|
|
228
|
+
// Fallback to generic name if no name
|
|
229
|
+
if (!name || name === 'Object' || name === 'Function') {
|
|
230
|
+
name = `Schema_${++this.typeCounter}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.typeNames.set(type as Constructor, name);
|
|
234
|
+
return name;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get all generated schemas (for components.schemas)
|
|
239
|
+
*/
|
|
240
|
+
getSchemas(): Record<string, OpenAPISchema> {
|
|
241
|
+
const result: Record<string, OpenAPISchema> = {};
|
|
242
|
+
for (const [name, schema] of this.schemas) {
|
|
243
|
+
result[name] = schema;
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Clear cached schemas
|
|
250
|
+
*/
|
|
251
|
+
clear(): void {
|
|
252
|
+
this.schemas.clear();
|
|
253
|
+
this.typeNames.clear();
|
|
254
|
+
this.typeCounter = 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swagger Module
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities to set up Swagger UI and integrate OpenAPI document generation
|
|
5
|
+
* with a Bueno Application.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Application } from '../modules';
|
|
9
|
+
import type { Constructor, OpenAPIDocument, SwaggerOptions } from './types';
|
|
10
|
+
import { DocumentBuilder } from './document-builder';
|
|
11
|
+
import { RouteScanner } from './route-scanner';
|
|
12
|
+
import { SchemaGenerator } from './schema-generator';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SwaggerModule - Static utilities for OpenAPI integration
|
|
16
|
+
*/
|
|
17
|
+
export class SwaggerModule {
|
|
18
|
+
/**
|
|
19
|
+
* Create an OpenAPI document from a Bueno Application and controllers
|
|
20
|
+
*
|
|
21
|
+
* @param app - The Application instance
|
|
22
|
+
* @param config - Base OpenAPI document configuration
|
|
23
|
+
* @param controllers - Array of controller classes to scan
|
|
24
|
+
* @returns Complete OpenAPI document with paths and schemas
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const document = SwaggerModule.createDocument(app, {
|
|
29
|
+
* openapi: '3.1.0',
|
|
30
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
31
|
+
* paths: {},
|
|
32
|
+
* }, [UserController, PostController]);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
static createDocument(
|
|
36
|
+
app: Application,
|
|
37
|
+
config: OpenAPIDocument,
|
|
38
|
+
controllers: Constructor[],
|
|
39
|
+
): OpenAPIDocument {
|
|
40
|
+
const schemaGenerator = new SchemaGenerator();
|
|
41
|
+
const scanner = new RouteScanner(schemaGenerator);
|
|
42
|
+
|
|
43
|
+
// Scan controllers to get paths and schemas
|
|
44
|
+
const paths = scanner.scanControllers(controllers);
|
|
45
|
+
const schemas = schemaGenerator.getSchemas();
|
|
46
|
+
|
|
47
|
+
// Merge with the provided config
|
|
48
|
+
return {
|
|
49
|
+
...config,
|
|
50
|
+
paths: {
|
|
51
|
+
...config.paths,
|
|
52
|
+
...paths,
|
|
53
|
+
},
|
|
54
|
+
components: {
|
|
55
|
+
...config.components,
|
|
56
|
+
schemas: {
|
|
57
|
+
...(config.components?.schemas ?? {}),
|
|
58
|
+
...schemas,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Setup Swagger UI and JSON endpoint
|
|
66
|
+
*
|
|
67
|
+
* Registers two routes:
|
|
68
|
+
* - GET {path} - Swagger UI HTML page
|
|
69
|
+
* - GET {path}-json - OpenAPI JSON document
|
|
70
|
+
*
|
|
71
|
+
* @param path - Base path for Swagger UI (e.g., '/api-docs')
|
|
72
|
+
* @param app - The Application instance
|
|
73
|
+
* @param document - OpenAPI document to serve
|
|
74
|
+
* @param options - Swagger UI configuration options
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const document = SwaggerModule.createDocument(app, config, controllers);
|
|
79
|
+
* SwaggerModule.setup('/api-docs', app, document, {
|
|
80
|
+
* title: 'My API Documentation',
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
static setup(
|
|
85
|
+
path: string,
|
|
86
|
+
app: Application,
|
|
87
|
+
document: OpenAPIDocument,
|
|
88
|
+
options?: SwaggerOptions,
|
|
89
|
+
): void {
|
|
90
|
+
// Register JSON endpoint
|
|
91
|
+
app.router.get(`${path}-json`, (ctx) => {
|
|
92
|
+
return ctx.json(document);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Register Swagger UI endpoint
|
|
96
|
+
app.router.get(path, (ctx) => {
|
|
97
|
+
const html = this.generateSwaggerUI(path, document, options);
|
|
98
|
+
return ctx.html(html);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate Swagger UI HTML
|
|
104
|
+
*/
|
|
105
|
+
private static generateSwaggerUI(
|
|
106
|
+
jsonPath: string,
|
|
107
|
+
document: OpenAPIDocument,
|
|
108
|
+
options?: SwaggerOptions,
|
|
109
|
+
): string {
|
|
110
|
+
const title = options?.title ?? 'API Documentation';
|
|
111
|
+
const customCss = options?.customCss ?? '';
|
|
112
|
+
const customSiteTitle = options?.customSiteTitle ?? title;
|
|
113
|
+
const favicon = options?.customfavIcon ?? '';
|
|
114
|
+
|
|
115
|
+
return `<!DOCTYPE html>
|
|
116
|
+
<html lang="en">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="UTF-8">
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
120
|
+
<title>${customSiteTitle}</title>
|
|
121
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
122
|
+
<style>
|
|
123
|
+
html {
|
|
124
|
+
box-sizing: border-box;
|
|
125
|
+
overflow: -moz-scrollbars-vertical;
|
|
126
|
+
overflow-y: scroll;
|
|
127
|
+
}
|
|
128
|
+
*, *:before, *:after {
|
|
129
|
+
box-sizing: inherit;
|
|
130
|
+
}
|
|
131
|
+
body {
|
|
132
|
+
margin: 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
font-family: sans-serif;
|
|
135
|
+
}
|
|
136
|
+
${customCss}
|
|
137
|
+
</style>
|
|
138
|
+
${favicon ? `<link rel="icon" href="${favicon}">` : ''}
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
<div id="swagger-ui"></div>
|
|
142
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
143
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
144
|
+
<script>
|
|
145
|
+
window.onload = function() {
|
|
146
|
+
SwaggerUIBundle({
|
|
147
|
+
url: '${jsonPath}-json',
|
|
148
|
+
dom_id: '#swagger-ui',
|
|
149
|
+
deepLinking: true,
|
|
150
|
+
presets: [
|
|
151
|
+
SwaggerUIBundle.presets.apis,
|
|
152
|
+
SwaggerUIStandalonePreset
|
|
153
|
+
],
|
|
154
|
+
plugins: [
|
|
155
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
156
|
+
],
|
|
157
|
+
layout: 'StandaloneLayout',
|
|
158
|
+
defaultModelsExpandDepth: 1,
|
|
159
|
+
defaultModelExpandDepth: 1,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
</script>
|
|
163
|
+
</body>
|
|
164
|
+
</html>`;
|
|
165
|
+
}
|
|
166
|
+
}
|