@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.
Files changed (218) hide show
  1. package/README.md +136 -16
  2. package/dist/cli/{index.js → bin.js} +3036 -1421
  3. package/dist/container/index.js +250 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/health/index.js +364 -0
  8. package/dist/i18n/index.js +345 -0
  9. package/dist/index.js +11043 -6482
  10. package/dist/jobs/index.js +819 -0
  11. package/dist/lock/index.js +367 -0
  12. package/dist/logger/index.js +281 -0
  13. package/dist/metrics/index.js +289 -0
  14. package/dist/middleware/index.js +77 -0
  15. package/dist/migrations/index.js +571 -0
  16. package/dist/modules/index.js +3346 -0
  17. package/dist/notification/index.js +484 -0
  18. package/dist/observability/index.js +331 -0
  19. package/dist/openapi/index.js +776 -0
  20. package/dist/orm/index.js +1356 -0
  21. package/dist/router/index.js +886 -0
  22. package/dist/rpc/index.js +691 -0
  23. package/dist/schema/index.js +400 -0
  24. package/dist/telemetry/index.js +595 -0
  25. package/dist/template/index.js +640 -0
  26. package/dist/templates/index.js +640 -0
  27. package/dist/testing/index.js +1111 -0
  28. package/dist/types/index.js +60 -0
  29. package/package.json +121 -27
  30. package/src/cache/index.ts +2 -1
  31. package/src/cli/bin.ts +2 -2
  32. package/src/cli/commands/build.ts +183 -165
  33. package/src/cli/commands/dev.ts +96 -89
  34. package/src/cli/commands/generate.ts +142 -111
  35. package/src/cli/commands/help.ts +20 -16
  36. package/src/cli/commands/index.ts +3 -6
  37. package/src/cli/commands/migration.ts +124 -105
  38. package/src/cli/commands/new.ts +392 -438
  39. package/src/cli/commands/start.ts +81 -79
  40. package/src/cli/core/args.ts +68 -50
  41. package/src/cli/core/console.ts +89 -95
  42. package/src/cli/core/index.ts +4 -4
  43. package/src/cli/core/prompt.ts +65 -62
  44. package/src/cli/core/spinner.ts +23 -20
  45. package/src/cli/index.ts +46 -38
  46. package/src/cli/templates/database/index.ts +61 -0
  47. package/src/cli/templates/database/mysql.ts +14 -0
  48. package/src/cli/templates/database/none.ts +16 -0
  49. package/src/cli/templates/database/postgresql.ts +14 -0
  50. package/src/cli/templates/database/sqlite.ts +14 -0
  51. package/src/cli/templates/deploy.ts +29 -26
  52. package/src/cli/templates/docker.ts +41 -30
  53. package/src/cli/templates/frontend/index.ts +63 -0
  54. package/src/cli/templates/frontend/none.ts +17 -0
  55. package/src/cli/templates/frontend/react.ts +140 -0
  56. package/src/cli/templates/frontend/solid.ts +134 -0
  57. package/src/cli/templates/frontend/svelte.ts +131 -0
  58. package/src/cli/templates/frontend/vue.ts +130 -0
  59. package/src/cli/templates/generators/index.ts +339 -0
  60. package/src/cli/templates/generators/types.ts +56 -0
  61. package/src/cli/templates/index.ts +35 -2
  62. package/src/cli/templates/project/api.ts +81 -0
  63. package/src/cli/templates/project/default.ts +140 -0
  64. package/src/cli/templates/project/fullstack.ts +111 -0
  65. package/src/cli/templates/project/index.ts +95 -0
  66. package/src/cli/templates/project/minimal.ts +45 -0
  67. package/src/cli/templates/project/types.ts +94 -0
  68. package/src/cli/templates/project/website.ts +263 -0
  69. package/src/cli/utils/fs.ts +55 -41
  70. package/src/cli/utils/index.ts +3 -2
  71. package/src/cli/utils/strings.ts +47 -33
  72. package/src/cli/utils/version.ts +47 -0
  73. package/src/config/env-validation.ts +100 -0
  74. package/src/config/env.ts +169 -41
  75. package/src/config/index.ts +28 -20
  76. package/src/config/loader.ts +25 -16
  77. package/src/config/merge.ts +21 -10
  78. package/src/config/types.ts +545 -25
  79. package/src/config/validation.ts +215 -7
  80. package/src/container/forward-ref.ts +22 -22
  81. package/src/container/index.ts +34 -12
  82. package/src/context/index.ts +11 -1
  83. package/src/database/index.ts +7 -190
  84. package/src/database/orm/builder.ts +457 -0
  85. package/src/database/orm/casts/index.ts +130 -0
  86. package/src/database/orm/casts/types.ts +25 -0
  87. package/src/database/orm/compiler.ts +304 -0
  88. package/src/database/orm/hooks/index.ts +114 -0
  89. package/src/database/orm/index.ts +61 -0
  90. package/src/database/orm/model-registry.ts +59 -0
  91. package/src/database/orm/model.ts +821 -0
  92. package/src/database/orm/relationships/base.ts +146 -0
  93. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  94. package/src/database/orm/relationships/belongs-to.ts +56 -0
  95. package/src/database/orm/relationships/has-many.ts +45 -0
  96. package/src/database/orm/relationships/has-one.ts +41 -0
  97. package/src/database/orm/relationships/index.ts +11 -0
  98. package/src/database/orm/scopes/index.ts +55 -0
  99. package/src/events/__tests__/event-system.test.ts +235 -0
  100. package/src/events/config.ts +238 -0
  101. package/src/events/example-usage.ts +185 -0
  102. package/src/events/index.ts +278 -0
  103. package/src/events/manager.ts +385 -0
  104. package/src/events/registry.ts +182 -0
  105. package/src/events/types.ts +124 -0
  106. package/src/frontend/api-routes.ts +65 -23
  107. package/src/frontend/bundler.ts +76 -34
  108. package/src/frontend/console-client.ts +2 -2
  109. package/src/frontend/console-stream.ts +94 -38
  110. package/src/frontend/dev-server.ts +94 -46
  111. package/src/frontend/file-router.ts +61 -19
  112. package/src/frontend/frameworks/index.ts +37 -10
  113. package/src/frontend/frameworks/react.ts +10 -8
  114. package/src/frontend/frameworks/solid.ts +11 -9
  115. package/src/frontend/frameworks/svelte.ts +15 -9
  116. package/src/frontend/frameworks/vue.ts +13 -11
  117. package/src/frontend/hmr-client.ts +12 -10
  118. package/src/frontend/hmr.ts +146 -103
  119. package/src/frontend/index.ts +14 -5
  120. package/src/frontend/islands.ts +41 -22
  121. package/src/frontend/isr.ts +59 -37
  122. package/src/frontend/layout.ts +36 -21
  123. package/src/frontend/ssr/react.ts +74 -27
  124. package/src/frontend/ssr/solid.ts +54 -20
  125. package/src/frontend/ssr/svelte.ts +48 -14
  126. package/src/frontend/ssr/vue.ts +50 -18
  127. package/src/frontend/ssr.ts +83 -39
  128. package/src/frontend/types.ts +91 -56
  129. package/src/health/index.ts +21 -9
  130. package/src/i18n/engine.ts +305 -0
  131. package/src/i18n/index.ts +38 -0
  132. package/src/i18n/loader.ts +218 -0
  133. package/src/i18n/middleware.ts +164 -0
  134. package/src/i18n/negotiator.ts +162 -0
  135. package/src/i18n/types.ts +158 -0
  136. package/src/index.ts +179 -27
  137. package/src/jobs/drivers/memory.ts +315 -0
  138. package/src/jobs/drivers/redis.ts +459 -0
  139. package/src/jobs/index.ts +30 -0
  140. package/src/jobs/queue.ts +281 -0
  141. package/src/jobs/types.ts +295 -0
  142. package/src/jobs/worker.ts +380 -0
  143. package/src/logger/index.ts +1 -3
  144. package/src/logger/transports/index.ts +62 -22
  145. package/src/metrics/index.ts +25 -16
  146. package/src/migrations/index.ts +9 -0
  147. package/src/modules/filters.ts +13 -17
  148. package/src/modules/guards.ts +49 -26
  149. package/src/modules/index.ts +409 -298
  150. package/src/modules/interceptors.ts +58 -20
  151. package/src/modules/lazy.ts +11 -19
  152. package/src/modules/lifecycle.ts +15 -7
  153. package/src/modules/metadata.ts +15 -5
  154. package/src/modules/pipes.ts +94 -72
  155. package/src/notification/channels/base.ts +68 -0
  156. package/src/notification/channels/email.ts +105 -0
  157. package/src/notification/channels/push.ts +104 -0
  158. package/src/notification/channels/sms.ts +105 -0
  159. package/src/notification/channels/whatsapp.ts +104 -0
  160. package/src/notification/index.ts +48 -0
  161. package/src/notification/service.ts +354 -0
  162. package/src/notification/types.ts +344 -0
  163. package/src/observability/__tests__/observability.test.ts +483 -0
  164. package/src/observability/breadcrumbs.ts +114 -0
  165. package/src/observability/index.ts +136 -0
  166. package/src/observability/interceptor.ts +85 -0
  167. package/src/observability/service.ts +303 -0
  168. package/src/observability/trace.ts +37 -0
  169. package/src/observability/types.ts +196 -0
  170. package/src/openapi/__tests__/decorators.test.ts +335 -0
  171. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  172. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  173. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  174. package/src/openapi/decorators.ts +328 -0
  175. package/src/openapi/document-builder.ts +274 -0
  176. package/src/openapi/index.ts +112 -0
  177. package/src/openapi/metadata.ts +112 -0
  178. package/src/openapi/route-scanner.ts +289 -0
  179. package/src/openapi/schema-generator.ts +256 -0
  180. package/src/openapi/swagger-module.ts +166 -0
  181. package/src/openapi/types.ts +398 -0
  182. package/src/orm/index.ts +10 -0
  183. package/src/rpc/index.ts +3 -1
  184. package/src/schema/index.ts +9 -0
  185. package/src/security/index.ts +15 -6
  186. package/src/ssg/index.ts +9 -8
  187. package/src/telemetry/index.ts +76 -22
  188. package/src/template/index.ts +7 -0
  189. package/src/templates/engine.ts +224 -0
  190. package/src/templates/index.ts +9 -0
  191. package/src/templates/loader.ts +331 -0
  192. package/src/templates/renderers/markdown.ts +212 -0
  193. package/src/templates/renderers/simple.ts +269 -0
  194. package/src/templates/types.ts +154 -0
  195. package/src/testing/index.ts +100 -27
  196. package/src/types/optional-deps.d.ts +347 -187
  197. package/src/validation/index.ts +92 -2
  198. package/src/validation/schemas.ts +536 -0
  199. package/tests/integration/fullstack.test.ts +4 -4
  200. package/tests/unit/database.test.ts +2 -72
  201. package/tests/unit/env-validation.test.ts +166 -0
  202. package/tests/unit/events.test.ts +910 -0
  203. package/tests/unit/i18n.test.ts +455 -0
  204. package/tests/unit/jobs.test.ts +493 -0
  205. package/tests/unit/notification.test.ts +988 -0
  206. package/tests/unit/observability.test.ts +453 -0
  207. package/tests/unit/orm/builder.test.ts +323 -0
  208. package/tests/unit/orm/casts.test.ts +179 -0
  209. package/tests/unit/orm/compiler.test.ts +220 -0
  210. package/tests/unit/orm/eager-loading.test.ts +285 -0
  211. package/tests/unit/orm/hooks.test.ts +191 -0
  212. package/tests/unit/orm/model.test.ts +373 -0
  213. package/tests/unit/orm/relationships.test.ts +303 -0
  214. package/tests/unit/orm/scopes.test.ts +74 -0
  215. package/tests/unit/templates-simple.test.ts +53 -0
  216. package/tests/unit/templates.test.ts +454 -0
  217. package/tests/unit/validation.test.ts +18 -24
  218. 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
+ }