@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,285 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { DocumentBuilder } from '../document-builder';
3
+
4
+ describe('DocumentBuilder', () => {
5
+ describe('Basic configuration', () => {
6
+ test('should create a basic document with defaults', () => {
7
+ const builder = new DocumentBuilder();
8
+ const doc = builder.build();
9
+
10
+ expect(doc.openapi).toBe('3.1.0');
11
+ expect(doc.info?.title).toBe('API');
12
+ expect(doc.info?.version).toBe('1.0.0');
13
+ expect(doc.paths).toBeDefined();
14
+ });
15
+
16
+ test('should set title', () => {
17
+ const builder = new DocumentBuilder();
18
+ builder.setTitle('My API');
19
+ const doc = builder.build();
20
+
21
+ expect(doc.info?.title).toBe('My API');
22
+ });
23
+
24
+ test('should set description', () => {
25
+ const builder = new DocumentBuilder();
26
+ builder.setDescription('API description');
27
+ const doc = builder.build();
28
+
29
+ expect(doc.info?.description).toBe('API description');
30
+ });
31
+
32
+ test('should set version', () => {
33
+ const builder = new DocumentBuilder();
34
+ builder.setVersion('2.0.0');
35
+ const doc = builder.build();
36
+
37
+ expect(doc.info?.version).toBe('2.0.0');
38
+ });
39
+
40
+ test('should set contact information', () => {
41
+ const builder = new DocumentBuilder();
42
+ builder.setContact('Support', 'https://support.example.com', 'support@example.com');
43
+ const doc = builder.build();
44
+
45
+ expect(doc.info?.contact?.name).toBe('Support');
46
+ expect(doc.info?.contact?.url).toBe('https://support.example.com');
47
+ expect(doc.info?.contact?.email).toBe('support@example.com');
48
+ });
49
+
50
+ test('should set license information', () => {
51
+ const builder = new DocumentBuilder();
52
+ builder.setLicense('MIT', 'https://opensource.org/licenses/MIT');
53
+ const doc = builder.build();
54
+
55
+ expect(doc.info?.license?.name).toBe('MIT');
56
+ expect(doc.info?.license?.url).toBe('https://opensource.org/licenses/MIT');
57
+ });
58
+ });
59
+
60
+ describe('Server configuration', () => {
61
+ test('should add a single server', () => {
62
+ const builder = new DocumentBuilder();
63
+ builder.addServer('https://api.example.com', 'Production API');
64
+ const doc = builder.build();
65
+
66
+ expect(doc.servers).toBeDefined();
67
+ expect(doc.servers?.length).toBe(1);
68
+ expect(doc.servers?.[0].url).toBe('https://api.example.com');
69
+ expect(doc.servers?.[0].description).toBe('Production API');
70
+ });
71
+
72
+ test('should add multiple servers', () => {
73
+ const builder = new DocumentBuilder();
74
+ builder
75
+ .addServer('https://api.example.com', 'Production')
76
+ .addServer('https://staging-api.example.com', 'Staging')
77
+ .addServer('http://localhost:3000', 'Development');
78
+
79
+ const doc = builder.build();
80
+ expect(doc.servers?.length).toBe(3);
81
+ });
82
+
83
+ test('should add server without description', () => {
84
+ const builder = new DocumentBuilder();
85
+ builder.addServer('https://api.example.com');
86
+ const doc = builder.build();
87
+
88
+ expect(doc.servers?.[0].url).toBe('https://api.example.com');
89
+ expect(doc.servers?.[0].description).toBeUndefined();
90
+ });
91
+ });
92
+
93
+ describe('Security schemes', () => {
94
+ test('should add bearer auth', () => {
95
+ const builder = new DocumentBuilder();
96
+ builder.addBearerAuth();
97
+ const doc = builder.build();
98
+
99
+ expect(doc.components?.securitySchemes?.['bearer']).toBeDefined();
100
+ const bearer = doc.components?.securitySchemes?.['bearer'];
101
+ expect(bearer?.type).toBe('http');
102
+ expect(bearer?.scheme).toBe('bearer');
103
+ expect(bearer?.bearerFormat).toBe('JWT');
104
+ });
105
+
106
+ test('should add bearer auth with custom format', () => {
107
+ const builder = new DocumentBuilder();
108
+ builder.addBearerAuth('jwt', { bearerFormat: 'Bearer' });
109
+ const doc = builder.build();
110
+
111
+ const bearer = doc.components?.securitySchemes?.['jwt'];
112
+ expect(bearer?.bearerFormat).toBe('Bearer');
113
+ });
114
+
115
+ test('should add bearer auth with description', () => {
116
+ const builder = new DocumentBuilder();
117
+ builder.addBearerAuth('bearer', { description: 'JWT bearer token' });
118
+ const doc = builder.build();
119
+
120
+ const bearer = doc.components?.securitySchemes?.['bearer'];
121
+ expect(bearer?.description).toBe('JWT bearer token');
122
+ });
123
+
124
+ test('should add basic auth', () => {
125
+ const builder = new DocumentBuilder();
126
+ builder.addBasicAuth();
127
+ const doc = builder.build();
128
+
129
+ expect(doc.components?.securitySchemes?.['basic']).toBeDefined();
130
+ const basic = doc.components?.securitySchemes?.['basic'];
131
+ expect(basic?.type).toBe('http');
132
+ expect(basic?.scheme).toBe('basic');
133
+ });
134
+
135
+ test('should add API key auth', () => {
136
+ const builder = new DocumentBuilder();
137
+ builder.addApiKey({ in: 'header', name: 'X-API-Key' });
138
+ const doc = builder.build();
139
+
140
+ expect(doc.components?.securitySchemes?.['api_key']).toBeDefined();
141
+ const apiKey = doc.components?.securitySchemes?.['api_key'];
142
+ expect(apiKey?.type).toBe('apiKey');
143
+ expect(apiKey?.in).toBe('header');
144
+ expect(apiKey?.name).toBe('X-API-Key');
145
+ });
146
+
147
+ test('should add API key with description', () => {
148
+ const builder = new DocumentBuilder();
149
+ builder.addApiKey(
150
+ { in: 'header', name: 'X-API-Key', description: 'API key for access' },
151
+ 'api_key',
152
+ );
153
+ const doc = builder.build();
154
+
155
+ const apiKey = doc.components?.securitySchemes?.['api_key'];
156
+ expect(apiKey?.description).toBe('API key for access');
157
+ });
158
+
159
+ test('should add OAuth2 security scheme', () => {
160
+ const builder = new DocumentBuilder();
161
+ builder.addOAuth2('oauth2', 'https://example.com/oauth/authorize', 'https://example.com/oauth/token');
162
+ const doc = builder.build();
163
+
164
+ expect(doc.components?.securitySchemes?.['oauth2']).toBeDefined();
165
+ const oauth2 = doc.components?.securitySchemes?.['oauth2'];
166
+ expect(oauth2?.type).toBe('oauth2');
167
+ expect((oauth2?.flows?.authorizationCode as any)?.authorizationUrl).toBe(
168
+ 'https://example.com/oauth/authorize',
169
+ );
170
+ });
171
+
172
+ test('should add OpenID Connect security scheme', () => {
173
+ const builder = new DocumentBuilder();
174
+ builder.addOpenIdConnect('openid', 'https://example.com/.well-known/openid-configuration');
175
+ const doc = builder.build();
176
+
177
+ expect(doc.components?.securitySchemes?.['openid']).toBeDefined();
178
+ const openid = doc.components?.securitySchemes?.['openid'];
179
+ expect(openid?.type).toBe('openIdConnect');
180
+ expect(openid?.openIdConnectUrl).toBe('https://example.com/.well-known/openid-configuration');
181
+ });
182
+
183
+ test('should add multiple security schemes', () => {
184
+ const builder = new DocumentBuilder();
185
+ builder
186
+ .addBearerAuth()
187
+ .addApiKey({ in: 'header', name: 'X-API-Key' })
188
+ .addBasicAuth('basic2');
189
+
190
+ const doc = builder.build();
191
+ const schemes = doc.components?.securitySchemes;
192
+
193
+ expect(Object.keys(schemes ?? {}).length).toBeGreaterThanOrEqual(3);
194
+ });
195
+ });
196
+
197
+ describe('Tags', () => {
198
+ test('should add a single tag', () => {
199
+ const builder = new DocumentBuilder();
200
+ builder.addTag('users', 'User management endpoints');
201
+ const doc = builder.build();
202
+
203
+ expect(doc.tags).toBeDefined();
204
+ expect(doc.tags?.length).toBe(1);
205
+ expect(doc.tags?.[0].name).toBe('users');
206
+ expect(doc.tags?.[0].description).toBe('User management endpoints');
207
+ });
208
+
209
+ test('should add multiple tags', () => {
210
+ const builder = new DocumentBuilder();
211
+ builder
212
+ .addTag('users', 'User management')
213
+ .addTag('posts', 'Post management')
214
+ .addTag('comments', 'Comment management');
215
+
216
+ const doc = builder.build();
217
+ expect(doc.tags?.length).toBe(3);
218
+ });
219
+
220
+ test('should add tag without description', () => {
221
+ const builder = new DocumentBuilder();
222
+ builder.addTag('admin');
223
+ const doc = builder.build();
224
+
225
+ expect(doc.tags?.[0].name).toBe('admin');
226
+ expect(doc.tags?.[0].description).toBeUndefined();
227
+ });
228
+ });
229
+
230
+ describe('Fluent API', () => {
231
+ test('should support method chaining', () => {
232
+ const builder = new DocumentBuilder();
233
+ const doc = builder
234
+ .setTitle('My API')
235
+ .setDescription('An awesome API')
236
+ .setVersion('1.0.0')
237
+ .addServer('https://api.example.com')
238
+ .addBearerAuth()
239
+ .addTag('users')
240
+ .build();
241
+
242
+ expect(doc.info?.title).toBe('My API');
243
+ expect(doc.info?.description).toBe('An awesome API');
244
+ expect(doc.info?.version).toBe('1.0.0');
245
+ expect(doc.servers?.length).toBe(1);
246
+ expect(doc.components?.securitySchemes?.['bearer']).toBeDefined();
247
+ expect(doc.tags?.length).toBe(1);
248
+ });
249
+
250
+ test('should return builder instance for chaining', () => {
251
+ const builder = new DocumentBuilder();
252
+ const result = builder.setTitle('Test');
253
+
254
+ expect(result).toBe(builder);
255
+ });
256
+ });
257
+
258
+ describe('Complete document generation', () => {
259
+ test('should generate a complete OpenAPI document', () => {
260
+ const doc = new DocumentBuilder()
261
+ .setTitle('E-Commerce API')
262
+ .setDescription('RESTful API for e-commerce platform')
263
+ .setVersion('1.0.0')
264
+ .setContact('API Support', 'https://support.example.com', 'api@example.com')
265
+ .setLicense('MIT')
266
+ .addServer('https://api.example.com', 'Production')
267
+ .addServer('http://localhost:3000', 'Development')
268
+ .addBearerAuth()
269
+ .addApiKey({ in: 'header', name: 'X-API-Key' })
270
+ .addTag('products', 'Product management')
271
+ .addTag('orders', 'Order management')
272
+ .build();
273
+
274
+ expect(doc.openapi).toBe('3.1.0');
275
+ expect(doc.info?.title).toBe('E-Commerce API');
276
+ expect(doc.info?.description).toBe('RESTful API for e-commerce platform');
277
+ expect(doc.info?.version).toBe('1.0.0');
278
+ expect(doc.info?.contact).toBeDefined();
279
+ expect(doc.info?.license).toBeDefined();
280
+ expect(doc.servers?.length).toBe(2);
281
+ expect(Object.keys(doc.components?.securitySchemes ?? {}).length).toBe(2);
282
+ expect(doc.tags?.length).toBe(2);
283
+ });
284
+ });
285
+ });
@@ -0,0 +1,334 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { Controller, Get, Post } from '../../modules';
3
+ import { RouteScanner } from '../route-scanner';
4
+ import { SchemaGenerator } from '../schema-generator';
5
+ import {
6
+ ApiOperation,
7
+ ApiResponse,
8
+ ApiParam,
9
+ ApiQuery,
10
+ ApiBody,
11
+ ApiProperty,
12
+ ApiTags,
13
+ } from '../decorators';
14
+ import { setMetadata, setPrototypeMetadata } from '../../modules/metadata';
15
+ import { setApiMetadata, setApiMethodMetadata } from '../metadata';
16
+
17
+ // ============= Helper =============
18
+
19
+ /** Manually inject routes metadata (as @Get/@Post would) */
20
+ function setRoutes(
21
+ proto: object,
22
+ routes: Array<{ method: string; path: string; handler: string }>,
23
+ ) {
24
+ setPrototypeMetadata(proto, 'routes', routes);
25
+ }
26
+
27
+ // ============= Test Fixtures =============
28
+
29
+ class UserDto {
30
+ @ApiProperty({ description: 'User ID' })
31
+ id!: string;
32
+
33
+ @ApiProperty({ description: 'User email' })
34
+ email!: string;
35
+ }
36
+
37
+ // ============= Tests =============
38
+
39
+ describe('RouteScanner', () => {
40
+ describe('Basic route scanning', () => {
41
+ test('should scan empty controller and return empty paths', () => {
42
+ @Controller('/users')
43
+ class UsersController {}
44
+
45
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
46
+ expect(typeof paths).toBe('object');
47
+ expect(Object.keys(paths).length).toBe(0);
48
+ });
49
+
50
+ test('should extract controller base path', () => {
51
+ @Controller('/users')
52
+ class UsersController {
53
+ getAll() {}
54
+ }
55
+
56
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
57
+
58
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
59
+ expect(paths['/users']).toBeDefined();
60
+ });
61
+
62
+ test('should combine controller path with route path', () => {
63
+ @Controller('/api/users')
64
+ class UsersController {
65
+ getById() {}
66
+ }
67
+
68
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '/:id', handler: 'getById' }]);
69
+
70
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
71
+ expect(paths['/api/users/{id}']).toBeDefined();
72
+ });
73
+ });
74
+
75
+ describe('Path parameter conversion', () => {
76
+ test('should convert :param to {param}', () => {
77
+ @Controller('/users')
78
+ class UsersController {
79
+ getById() {}
80
+ }
81
+
82
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '/:id', handler: 'getById' }]);
83
+
84
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
85
+ expect(paths['/users/{id}']).toBeDefined();
86
+ expect(paths['/users/:id']).toBeUndefined();
87
+ });
88
+
89
+ test('should handle multiple path parameters', () => {
90
+ @Controller('/api')
91
+ class ApiController {
92
+ getPost() {}
93
+ }
94
+
95
+ setRoutes(ApiController.prototype, [
96
+ { method: 'GET', path: '/users/:userId/posts/:postId', handler: 'getPost' },
97
+ ]);
98
+
99
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([ApiController]);
100
+ expect(paths['/api/users/{userId}/posts/{postId}']).toBeDefined();
101
+ });
102
+ });
103
+
104
+ describe('HTTP methods', () => {
105
+ test('should register GET operations', () => {
106
+ @Controller('/users')
107
+ class UsersController {
108
+ getAll() {}
109
+ }
110
+
111
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
112
+
113
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
114
+ expect(paths['/users']?.get).toBeDefined();
115
+ });
116
+
117
+ test('should register POST operations', () => {
118
+ @Controller('/users')
119
+ class UsersController {
120
+ create() {}
121
+ }
122
+
123
+ setRoutes(UsersController.prototype, [{ method: 'POST', path: '', handler: 'create' }]);
124
+
125
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
126
+ expect(paths['/users']?.post).toBeDefined();
127
+ });
128
+
129
+ test('should handle multiple HTTP methods on same path', () => {
130
+ @Controller('/users')
131
+ class UsersController {
132
+ getAll() {}
133
+ create() {}
134
+ }
135
+
136
+ setRoutes(UsersController.prototype, [
137
+ { method: 'GET', path: '', handler: 'getAll' },
138
+ { method: 'POST', path: '', handler: 'create' },
139
+ ]);
140
+
141
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
142
+ expect(paths['/users']?.get).toBeDefined();
143
+ expect(paths['/users']?.post).toBeDefined();
144
+ });
145
+ });
146
+
147
+ describe('Operation metadata', () => {
148
+ test('should include operation summary and description', () => {
149
+ @Controller('/users')
150
+ class UsersController {
151
+ @ApiOperation({ summary: 'List users', description: 'Get all users' })
152
+ getAll() {}
153
+ }
154
+
155
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
156
+
157
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
158
+ const op = paths['/users']?.get;
159
+ expect(op?.summary).toBe('List users');
160
+ expect(op?.description).toBe('Get all users');
161
+ });
162
+
163
+ test('should include class-level tags', () => {
164
+ @Controller('/users')
165
+ @ApiTags('users')
166
+ class UsersController {
167
+ getAll() {}
168
+ }
169
+
170
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
171
+
172
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
173
+ const op = paths['/users']?.get;
174
+ expect(op?.tags).toContain('users');
175
+ });
176
+ });
177
+
178
+ describe('Multiple controllers', () => {
179
+ test('should scan multiple controllers', () => {
180
+ @Controller('/users')
181
+ class UsersController {
182
+ getAll() {}
183
+ }
184
+
185
+ @Controller('/posts')
186
+ class PostsController {
187
+ getAll() {}
188
+ }
189
+
190
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
191
+ setRoutes(PostsController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
192
+
193
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([
194
+ UsersController,
195
+ PostsController,
196
+ ]);
197
+ expect(paths['/users']?.get).toBeDefined();
198
+ expect(paths['/posts']?.get).toBeDefined();
199
+ });
200
+
201
+ test('should handle controllers with different paths', () => {
202
+ @Controller('/api')
203
+ class ApiV1Controller {
204
+ info() {}
205
+ }
206
+
207
+ @Controller('/api/v2')
208
+ class ApiV2Controller {
209
+ info() {}
210
+ }
211
+
212
+ setRoutes(ApiV1Controller.prototype, [{ method: 'GET', path: '', handler: 'info' }]);
213
+ setRoutes(ApiV2Controller.prototype, [{ method: 'GET', path: '', handler: 'info' }]);
214
+
215
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([
216
+ ApiV1Controller,
217
+ ApiV2Controller,
218
+ ]);
219
+ expect(paths['/api']?.get).toBeDefined();
220
+ expect(paths['/api/v2']?.get).toBeDefined();
221
+ });
222
+ });
223
+
224
+ describe('Excluded controllers and endpoints', () => {
225
+ test('should skip excluded controllers', () => {
226
+ @Controller('/hidden')
227
+ class HiddenController {
228
+ getAll() {}
229
+ }
230
+
231
+ setRoutes(HiddenController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
232
+ setApiMetadata(HiddenController as any, 'api:exclude', true);
233
+
234
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([HiddenController]);
235
+ expect(Object.keys(paths).length).toBe(0);
236
+ });
237
+ });
238
+
239
+ describe('Default responses', () => {
240
+ test('should add default 200 response when none documented', () => {
241
+ @Controller('/users')
242
+ class UsersController {
243
+ getAll() {}
244
+ }
245
+
246
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
247
+
248
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
249
+ const op = paths['/users']?.get;
250
+ expect(op?.responses['200']).toBeDefined();
251
+ expect(op?.responses['200'].description).toBe('Success');
252
+ });
253
+ });
254
+
255
+ describe('Documented responses', () => {
256
+ test('should include documented response statuses', () => {
257
+ @Controller('/users')
258
+ class UsersController {
259
+ @ApiResponse({ status: 200, description: 'OK' })
260
+ @ApiResponse({ status: 404, description: 'Not found' })
261
+ getUser() {}
262
+ }
263
+
264
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '/:id', handler: 'getUser' }]);
265
+
266
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
267
+ const op = paths['/users/{id}']?.get;
268
+ expect(op?.responses['200']).toBeDefined();
269
+ expect(op?.responses['404']).toBeDefined();
270
+ });
271
+ });
272
+
273
+ describe('Path parameters in operation', () => {
274
+ test('should include @ApiParam in operation parameters', () => {
275
+ @Controller('/users')
276
+ class UsersController {
277
+ @ApiParam({ name: 'id', type: 'string', description: 'User ID' })
278
+ getUser() {}
279
+ }
280
+
281
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '/:id', handler: 'getUser' }]);
282
+
283
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
284
+ const op = paths['/users/{id}']?.get;
285
+ const param = op?.parameters?.find((p) => p.name === 'id');
286
+ expect(param).toBeDefined();
287
+ expect(param?.in).toBe('path');
288
+ });
289
+ });
290
+
291
+ describe('Query parameters in operation', () => {
292
+ test('should include @ApiQuery in operation parameters', () => {
293
+ @Controller('/users')
294
+ class UsersController {
295
+ @ApiQuery({ name: 'page', type: 'number', required: false })
296
+ getAll() {}
297
+ }
298
+
299
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
300
+
301
+ const paths = new RouteScanner(new SchemaGenerator()).scanControllers([UsersController]);
302
+ const op = paths['/users']?.get;
303
+ const param = op?.parameters?.find((p) => p.name === 'page');
304
+ expect(param).toBeDefined();
305
+ expect(param?.in).toBe('query');
306
+ expect(param?.required).toBe(false);
307
+ });
308
+ });
309
+
310
+ describe('Schema generator integration', () => {
311
+ test('should have access to schema generator', () => {
312
+ const generator = new SchemaGenerator();
313
+ const scanner = new RouteScanner(generator);
314
+ expect(scanner.getSchemaGenerator()).toBe(generator);
315
+ });
316
+
317
+ test('should populate schemas for response types', () => {
318
+ @Controller('/users')
319
+ class UsersController {
320
+ @ApiResponse({ status: 200, description: 'Success', type: UserDto })
321
+ getAll() {}
322
+ }
323
+
324
+ setRoutes(UsersController.prototype, [{ method: 'GET', path: '', handler: 'getAll' }]);
325
+
326
+ const generator = new SchemaGenerator();
327
+ const scanner = new RouteScanner(generator);
328
+ scanner.scanControllers([UsersController]);
329
+
330
+ const schemas = generator.getSchemas();
331
+ expect(schemas).toHaveProperty('UserDto');
332
+ });
333
+ });
334
+ });