@buenojs/bueno 0.8.4 → 0.8.6

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 (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -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/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -0,0 +1,275 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { SchemaGenerator } from '../schema-generator';
3
+ import { ApiProperty, ApiPropertyOptional } from '../decorators';
4
+
5
+ // ============= Test Fixtures =============
6
+
7
+ class SimpleDto {
8
+ @ApiProperty({ description: 'User name' })
9
+ name!: string;
10
+
11
+ @ApiPropertyOptional({ description: 'User email' })
12
+ email?: string;
13
+ }
14
+
15
+ class UserDto {
16
+ @ApiProperty({ description: 'User ID' })
17
+ id!: string;
18
+
19
+ @ApiProperty({ description: 'Email address', format: 'email' })
20
+ email!: string;
21
+
22
+ @ApiProperty({ description: 'Full name', minLength: 2, maxLength: 100 })
23
+ name!: string;
24
+
25
+ @ApiPropertyOptional({ description: 'Age', minimum: 0, maximum: 150 })
26
+ age?: number;
27
+ }
28
+
29
+ class CreateUserDto {
30
+ @ApiProperty({ description: 'Email address', example: 'user@example.com' })
31
+ email!: string;
32
+
33
+ @ApiProperty({ description: 'Password', minLength: 8 })
34
+ password!: string;
35
+
36
+ @ApiPropertyOptional({ description: 'Full name' })
37
+ name?: string;
38
+ }
39
+
40
+ // ============= Tests =============
41
+
42
+ describe('SchemaGenerator', () => {
43
+ describe('Primitive type generation', () => {
44
+ test('should generate string schema from String type', () => {
45
+ const generator = new SchemaGenerator();
46
+ const schema = generator.generateSchema(String);
47
+ expect(schema.type).toBe('string');
48
+ });
49
+
50
+ test('should generate number schema from Number type', () => {
51
+ const generator = new SchemaGenerator();
52
+ const schema = generator.generateSchema(Number);
53
+ expect(schema.type).toBe('number');
54
+ });
55
+
56
+ test('should generate boolean schema from Boolean type', () => {
57
+ const generator = new SchemaGenerator();
58
+ const schema = generator.generateSchema(Boolean);
59
+ expect(schema.type).toBe('boolean');
60
+ });
61
+
62
+ test('should generate schema from string type names', () => {
63
+ const generator = new SchemaGenerator();
64
+
65
+ expect(generator.generateSchema('string').type).toBe('string');
66
+ expect(generator.generateSchema('number').type).toBe('number');
67
+ expect(generator.generateSchema('integer').type).toBe('integer');
68
+ expect(generator.generateSchema('boolean').type).toBe('boolean');
69
+ });
70
+
71
+ test('should generate email format for email type', () => {
72
+ const generator = new SchemaGenerator();
73
+ const schema = generator.generateSchema('email');
74
+ expect(schema.type).toBe('string');
75
+ expect(schema.format).toBe('email');
76
+ });
77
+
78
+ test('should generate date-time format for datetime type', () => {
79
+ const generator = new SchemaGenerator();
80
+ const schema = generator.generateSchema('datetime');
81
+ expect(schema.type).toBe('string');
82
+ expect(schema.format).toBe('date-time');
83
+ });
84
+
85
+ test('should generate uuid format for uuid type', () => {
86
+ const generator = new SchemaGenerator();
87
+ const schema = generator.generateSchema('uuid');
88
+ expect(schema.type).toBe('string');
89
+ expect(schema.format).toBe('uuid');
90
+ });
91
+
92
+ test('should generate uri format for url type', () => {
93
+ const generator = new SchemaGenerator();
94
+ const schema = generator.generateSchema('url');
95
+ expect(schema.type).toBe('string');
96
+ expect(schema.format).toBe('uri');
97
+ });
98
+ });
99
+
100
+ describe('Object/DTO schema generation', () => {
101
+ test('should generate schema for simple DTO', () => {
102
+ const generator = new SchemaGenerator();
103
+ const schema = generator.generateSchema(SimpleDto);
104
+
105
+ expect(schema.$ref).toBeDefined();
106
+ expect(schema.$ref).toContain('SimpleDto');
107
+ });
108
+
109
+ test('should cache and return reference for same DTO', () => {
110
+ const generator = new SchemaGenerator();
111
+ const schema1 = generator.generateSchema(SimpleDto);
112
+ const schema2 = generator.generateSchema(SimpleDto);
113
+
114
+ expect(schema1).toEqual(schema2);
115
+ expect(schema1.$ref).toBe(schema2.$ref);
116
+ });
117
+
118
+ test('should generate schemas map with DTO properties', () => {
119
+ const generator = new SchemaGenerator();
120
+ generator.generateSchema(UserDto);
121
+
122
+ const schemas = generator.getSchemas();
123
+ expect(schemas).toHaveProperty('UserDto');
124
+
125
+ const userSchema = schemas['UserDto'];
126
+ expect(userSchema.type).toBe('object');
127
+ expect(userSchema.properties).toBeDefined();
128
+ expect(userSchema.properties?.['name']).toBeDefined();
129
+ expect(userSchema.properties?.['email']).toBeDefined();
130
+ expect(userSchema.properties?.['age']).toBeDefined();
131
+ });
132
+
133
+ test('should respect required fields', () => {
134
+ const generator = new SchemaGenerator();
135
+ generator.generateSchema(UserDto);
136
+
137
+ const schemas = generator.getSchemas();
138
+ const userSchema = schemas['UserDto'];
139
+
140
+ expect(userSchema.required).toContain('name');
141
+ expect(userSchema.required).toContain('email');
142
+ expect(userSchema.required).toContain('id');
143
+
144
+ // Optional fields should not be in required
145
+ if (userSchema.required) {
146
+ expect(userSchema.required.includes('age')).toBe(false);
147
+ }
148
+ });
149
+
150
+ test('should include property descriptions', () => {
151
+ const generator = new SchemaGenerator();
152
+ generator.generateSchema(UserDto);
153
+
154
+ const schemas = generator.getSchemas();
155
+ const userSchema = schemas['UserDto'];
156
+
157
+ expect((userSchema.properties?.['name'] as any)?.description).toBe('Full name');
158
+ expect((userSchema.properties?.['email'] as any)?.description).toBe('Email address');
159
+ });
160
+
161
+ test('should include format information', () => {
162
+ const generator = new SchemaGenerator();
163
+ generator.generateSchema(UserDto);
164
+
165
+ const schemas = generator.getSchemas();
166
+ const userSchema = schemas['UserDto'];
167
+
168
+ expect((userSchema.properties?.['email'] as any)?.format).toBe('email');
169
+ });
170
+
171
+ test('should include validation constraints', () => {
172
+ const generator = new SchemaGenerator();
173
+ generator.generateSchema(UserDto);
174
+
175
+ const schemas = generator.getSchemas();
176
+ const userSchema = schemas['UserDto'];
177
+
178
+ const nameProperty = userSchema.properties?.['name'] as any;
179
+ expect(nameProperty?.minLength).toBe(2);
180
+ expect(nameProperty?.maxLength).toBe(100);
181
+
182
+ const ageProperty = userSchema.properties?.['age'] as any;
183
+ expect(ageProperty?.minimum).toBe(0);
184
+ expect(ageProperty?.maximum).toBe(150);
185
+ });
186
+
187
+ test('should include examples and defaults', () => {
188
+ const generator = new SchemaGenerator();
189
+ generator.generateSchema(CreateUserDto);
190
+
191
+ const schemas = generator.getSchemas();
192
+ const createSchema = schemas['CreateUserDto'];
193
+
194
+ const emailProperty = createSchema.properties?.['email'] as any;
195
+ expect(emailProperty?.example).toBe('user@example.com');
196
+ });
197
+ });
198
+
199
+ describe('Multiple DTO schema generation', () => {
200
+ test('should generate schemas for multiple DTOs', () => {
201
+ const generator = new SchemaGenerator();
202
+ generator.generateSchema(UserDto);
203
+ generator.generateSchema(CreateUserDto);
204
+
205
+ const schemas = generator.getSchemas();
206
+ expect(schemas).toHaveProperty('UserDto');
207
+ expect(schemas).toHaveProperty('CreateUserDto');
208
+ });
209
+
210
+ test('should handle DTOs with same property names differently', () => {
211
+ const generator = new SchemaGenerator();
212
+ const userRef = generator.generateSchema(UserDto);
213
+ const createRef = generator.generateSchema(CreateUserDto);
214
+
215
+ expect(userRef.$ref).not.toBe(createRef.$ref);
216
+ });
217
+ });
218
+
219
+ describe('Schema clearing', () => {
220
+ test('should clear all cached schemas', () => {
221
+ const generator = new SchemaGenerator();
222
+ generator.generateSchema(UserDto);
223
+ generator.generateSchema(CreateUserDto);
224
+
225
+ let schemas = generator.getSchemas();
226
+ expect(Object.keys(schemas).length).toBeGreaterThan(0);
227
+
228
+ generator.clear();
229
+ schemas = generator.getSchemas();
230
+ expect(Object.keys(schemas).length).toBe(0);
231
+ });
232
+
233
+ test('should regenerate schemas after clearing', () => {
234
+ const generator = new SchemaGenerator();
235
+ generator.generateSchema(UserDto);
236
+ generator.clear();
237
+
238
+ const ref1 = generator.generateSchema(UserDto);
239
+ const ref2 = generator.generateSchema(UserDto);
240
+
241
+ expect(ref1).toEqual(ref2);
242
+ });
243
+ });
244
+
245
+ describe('Array type handling', () => {
246
+ test('should handle Array type', () => {
247
+ const generator = new SchemaGenerator();
248
+ const schema = generator.generateSchema(Array);
249
+ expect(schema.type).toBe('array');
250
+ });
251
+ });
252
+
253
+ describe('Type name generation', () => {
254
+ test('should use class name for DTO', () => {
255
+ const generator = new SchemaGenerator();
256
+ generator.generateSchema(UserDto);
257
+
258
+ const schemas = generator.getSchemas();
259
+ expect(schemas).toHaveProperty('UserDto');
260
+ });
261
+
262
+ test('should generate unique names for multiple generations', () => {
263
+ const generator = new SchemaGenerator();
264
+
265
+ // Create anonymous classes
266
+ const AnonClass1 = class {};
267
+ const AnonClass2 = class {};
268
+
269
+ const schema1 = generator.generateSchema(AnonClass1);
270
+ const schema2 = generator.generateSchema(AnonClass2);
271
+
272
+ expect(schema1.$ref).not.toBe(schema2.$ref);
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,328 @@
1
+ /**
2
+ * OpenAPI Decorators
3
+ *
4
+ * Decorators for documenting controllers, methods, parameters, and DTOs with OpenAPI metadata.
5
+ */
6
+
7
+ import type {
8
+ ApiBodyOptions,
9
+ ApiHeaderOptions,
10
+ ApiKeySecurityOptions,
11
+ ApiOperationOptions,
12
+ ApiParamOptions,
13
+ ApiPropertyOptions,
14
+ ApiQueryOptions,
15
+ ApiResponseOptions,
16
+ Constructor,
17
+ OpenAPISecurity,
18
+ SecuritySchemeOptions,
19
+ } from './types';
20
+ import {
21
+ getApiMetadata,
22
+ getApiMethodMetadata,
23
+ getApiPropertyMetadata,
24
+ setApiMetadata,
25
+ setApiMethodMetadata,
26
+ setApiPropertyMetadata,
27
+ } from './metadata';
28
+
29
+ // ============= Class-Level Decorators =============
30
+
31
+ /**
32
+ * Mark one or more tags that apply to all operations in this controller
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * @Controller('/users')
37
+ * @ApiTags('users', 'accounts')
38
+ * class UserController { ... }
39
+ * ```
40
+ */
41
+ export function ApiTags(...tags: string[]): ClassDecorator {
42
+ return <TFunction extends Function>(target: TFunction): TFunction => {
43
+ const existingTags = getApiMetadata<string[]>(target as unknown as Constructor, 'api:tags') ?? [];
44
+ const combined = [...new Set([...existingTags, ...tags])];
45
+ setApiMetadata(target as unknown as Constructor, 'api:tags', combined);
46
+ return target;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Mark this controller with Bearer token authentication
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * @Controller('/api')
56
+ * @ApiBearerAuth()
57
+ * class ApiController { ... }
58
+ * ```
59
+ */
60
+ export function ApiBearerAuth(name = 'bearer', options?: SecuritySchemeOptions): ClassDecorator | MethodDecorator {
61
+ return function (target: unknown, propertyKey?: string | symbol) {
62
+ const security: OpenAPISecurity[] = [{ [name]: [] }];
63
+ const targetObj = propertyKey
64
+ ? (target as object) // Method decorator
65
+ : (target as unknown as Constructor); // Class decorator
66
+
67
+ const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
68
+ const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
69
+ const existingSecurity = store<OpenAPISecurity[]>(
70
+ targetObj,
71
+ 'api:security',
72
+ ) ?? [];
73
+
74
+ setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
75
+ return propertyKey ? descriptor : target;
76
+ } as any;
77
+ }
78
+
79
+ /**
80
+ * Mark this controller with Basic authentication
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * @Controller('/api')
85
+ * @ApiBasicAuth()
86
+ * class ApiController { ... }
87
+ * ```
88
+ */
89
+ export function ApiBasicAuth(name = 'basic'): ClassDecorator | MethodDecorator {
90
+ return function (target: unknown, propertyKey?: string | symbol) {
91
+ const security: OpenAPISecurity[] = [{ [name]: [] }];
92
+ const targetObj = propertyKey
93
+ ? (target as object) // Method decorator
94
+ : (target as unknown as Constructor); // Class decorator
95
+
96
+ const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
97
+ const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
98
+ const existingSecurity = store<OpenAPISecurity[]>(
99
+ targetObj,
100
+ 'api:security',
101
+ ) ?? [];
102
+
103
+ setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
104
+ return propertyKey ? descriptor : target;
105
+ } as any;
106
+ }
107
+
108
+ /**
109
+ * Mark this controller with API Key authentication
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * @Controller('/api')
114
+ * @ApiApiKey({ in: 'header', name: 'X-API-Key' })
115
+ * class ApiController { ... }
116
+ * ```
117
+ */
118
+ export function ApiApiKey(options: ApiKeySecurityOptions, name = 'api_key'): ClassDecorator | MethodDecorator {
119
+ return function (target: unknown, propertyKey?: string | symbol) {
120
+ const security: OpenAPISecurity[] = [{ [name]: [] }];
121
+ const targetObj = propertyKey
122
+ ? (target as object) // Method decorator
123
+ : (target as unknown as Constructor); // Class decorator
124
+
125
+ const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
126
+ const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
127
+ const existingSecurity = store<OpenAPISecurity[]>(
128
+ targetObj,
129
+ 'api:security',
130
+ ) ?? [];
131
+
132
+ // Store security scheme metadata for document builder
133
+ setSt(targetObj, `api:security:scheme:${name}`, options);
134
+ setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
135
+ return propertyKey ? descriptor : target;
136
+ } as any;
137
+ }
138
+
139
+ /**
140
+ * Exclude this entire controller from OpenAPI documentation
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * @Controller('/internal')
145
+ * @ApiExcludeController()
146
+ * class InternalController { ... }
147
+ * ```
148
+ */
149
+ export function ApiExcludeController(): ClassDecorator {
150
+ return <TFunction extends Function>(target: TFunction): TFunction => {
151
+ setApiMetadata(target as unknown as Constructor, 'api:exclude', true);
152
+ return target;
153
+ };
154
+ }
155
+
156
+ // ============= Method-Level Decorators =============
157
+
158
+ /**
159
+ * Document the operation (HTTP method) with summary, description, and other metadata
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * @Get('/:id')
164
+ * @ApiOperation({ summary: 'Get user by ID', description: 'Retrieve a single user' })
165
+ * getUser(@Param('id') id: string) { ... }
166
+ * ```
167
+ */
168
+ export function ApiOperation(options: ApiOperationOptions): MethodDecorator {
169
+ return (target: unknown, propertyKey: string | symbol) => {
170
+ setApiMethodMetadata(target as object, `api:operation:${String(propertyKey)}`, options);
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Document an HTTP response from this operation
176
+ * Can be used multiple times for different status codes
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * @ApiResponse({ status: 200, description: 'Success', type: UserDto })
181
+ * @ApiResponse({ status: 404, description: 'Not found' })
182
+ * getUser() { ... }
183
+ * ```
184
+ */
185
+ export function ApiResponse(options: ApiResponseOptions): MethodDecorator {
186
+ return (target: unknown, propertyKey: string | symbol) => {
187
+ const key = `api:responses:${String(propertyKey)}`;
188
+ const existing = getApiMethodMetadata<ApiResponseOptions[]>(target as object, key) ?? [];
189
+ existing.push(options);
190
+ setApiMethodMetadata(target as object, key, existing);
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Document a path parameter
196
+ * Can be used multiple times for different parameters
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * @Get('/:userId/posts/:postId')
201
+ * @ApiParam({ name: 'userId', type: 'string', description: 'User ID' })
202
+ * @ApiParam({ name: 'postId', type: 'string', description: 'Post ID' })
203
+ * getPost(@Param('userId') userId: string, @Param('postId') postId: string) { ... }
204
+ * ```
205
+ */
206
+ export function ApiParam(options: ApiParamOptions): MethodDecorator {
207
+ return (target: unknown, propertyKey: string | symbol) => {
208
+ const key = `api:params:${String(propertyKey)}`;
209
+ const existing = getApiMethodMetadata<ApiParamOptions[]>(target as object, key) ?? [];
210
+ existing.push(options);
211
+ setApiMethodMetadata(target as object, key, existing);
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Document a query parameter
217
+ * Can be used multiple times for different parameters
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * @Get()
222
+ * @ApiQuery({ name: 'page', type: 'number', description: 'Page number', required: false })
223
+ * @ApiQuery({ name: 'limit', type: 'number', description: 'Items per page', required: false })
224
+ * getUsers(@Query('page') page?: number, @Query('limit') limit?: number) { ... }
225
+ * ```
226
+ */
227
+ export function ApiQuery(options: ApiQueryOptions): MethodDecorator {
228
+ return (target: unknown, propertyKey: string | symbol) => {
229
+ const key = `api:query:${String(propertyKey)}`;
230
+ const existing = getApiMethodMetadata<ApiQueryOptions[]>(target as object, key) ?? [];
231
+ existing.push(options);
232
+ setApiMethodMetadata(target as object, key, existing);
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Document an HTTP header
238
+ * Can be used multiple times for different headers
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * @Post()
243
+ * @ApiHeader({ name: 'X-Request-ID', description: 'Request ID', required: true })
244
+ * create() { ... }
245
+ * ```
246
+ */
247
+ export function ApiHeader(options: ApiHeaderOptions): MethodDecorator {
248
+ return (target: unknown, propertyKey: string | symbol) => {
249
+ const key = `api:headers:${String(propertyKey)}`;
250
+ const existing = getApiMethodMetadata<ApiHeaderOptions[]>(target as object, key) ?? [];
251
+ existing.push(options);
252
+ setApiMethodMetadata(target as object, key, existing);
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Document the request body
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * @Post()
262
+ * @ApiBody({ type: CreateUserDto, description: 'User data to create' })
263
+ * create(@Body() dto: CreateUserDto) { ... }
264
+ * ```
265
+ */
266
+ export function ApiBody(options: ApiBodyOptions): MethodDecorator {
267
+ return (target: unknown, propertyKey: string | symbol) => {
268
+ setApiMethodMetadata(target as object, `api:body:${String(propertyKey)}`, options);
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Exclude this endpoint from OpenAPI documentation
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * @Get()
278
+ * @ApiExcludeEndpoint()
279
+ * internalOnly() { ... }
280
+ * ```
281
+ */
282
+ export function ApiExcludeEndpoint(): MethodDecorator {
283
+ return (target: unknown, propertyKey: string | symbol) => {
284
+ setApiMethodMetadata(target as object, `api:exclude:${String(propertyKey)}`, true);
285
+ };
286
+ }
287
+
288
+ // ============= Property-Level Decorators (for DTOs) =============
289
+
290
+ /**
291
+ * Document a DTO property with type, description, validation rules, etc.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * class CreateUserDto {
296
+ * @ApiProperty({ description: 'Email address', example: 'user@example.com' })
297
+ * email: string;
298
+ *
299
+ * @ApiProperty({ minLength: 2, maxLength: 50, description: 'Full name' })
300
+ * name: string;
301
+ * }
302
+ * ```
303
+ */
304
+ export function ApiProperty(options?: ApiPropertyOptions): PropertyDecorator {
305
+ return (target: object, propertyKey: string | symbol) => {
306
+ const opts: ApiPropertyOptions = { ...options, required: options?.required !== false };
307
+ setApiPropertyMetadata(target, propertyKey, opts);
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Document an optional DTO property
313
+ * Equivalent to ApiProperty with required: false
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * class UserFilterDto {
318
+ * @ApiPropertyOptional({ description: 'Filter by name', example: 'John' })
319
+ * name?: string;
320
+ * }
321
+ * ```
322
+ */
323
+ export function ApiPropertyOptional(options?: Omit<ApiPropertyOptions, 'required'>): PropertyDecorator {
324
+ return (target: object, propertyKey: string | symbol) => {
325
+ const opts: ApiPropertyOptions = { ...options, required: false };
326
+ setApiPropertyMetadata(target, propertyKey, opts);
327
+ };
328
+ }