@buenojs/bueno 0.8.0

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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Exception Filters System
3
+ *
4
+ * Filters catch exceptions thrown during request processing and transform
5
+ * them into appropriate responses. They act as error boundaries in the
6
+ * request pipeline.
7
+ *
8
+ * Execution Order:
9
+ * Incoming Request → Guards → Pipes → Handler → Filters (if error) → Error Response
10
+ */
11
+
12
+ import type { Context } from "../context";
13
+ import type { Token } from "../types";
14
+ import { BuenoError, ValidationError, NotFoundError } from "../types";
15
+
16
+ // ============= Types =============
17
+
18
+ /**
19
+ * Exception filter interface
20
+ * Filters implement this interface to handle specific exception types
21
+ */
22
+ export interface ExceptionFilter<T = Error> {
23
+ catch(exception: T, context: Context): Response | Promise<Response>;
24
+ }
25
+
26
+ /**
27
+ * Filter function type for functional filters
28
+ */
29
+ export type FilterFn<T = Error> = (
30
+ exception: T,
31
+ context: Context,
32
+ ) => Response | Promise<Response>;
33
+
34
+ /**
35
+ * Filter type - can be a token, instance, or function
36
+ */
37
+ export type Filter<T = Error> =
38
+ | Token<ExceptionFilter<T>>
39
+ | ExceptionFilter<T>
40
+ | FilterFn<T>;
41
+
42
+ /**
43
+ * Metadata stored for caught exception types
44
+ */
45
+ interface CatchMetadata {
46
+ exceptionType: new (...args: unknown[]) => Error;
47
+ }
48
+
49
+ // ============= Metadata Storage =============
50
+
51
+ // Type alias for class constructors
52
+ type Constructor = new (...args: unknown[]) => unknown;
53
+
54
+ // Metadata storage for filters
55
+ const filterMetadataStore = new WeakMap<Constructor, Map<string, unknown>>();
56
+ const catchMetadataStore = new WeakMap<Constructor, CatchMetadata>();
57
+
58
+ // Prototype metadata for method decorators
59
+ const filterPrototypeStore = new WeakMap<object, Map<string, unknown>>();
60
+
61
+ function setFilterMetadata(
62
+ target: Constructor,
63
+ key: string,
64
+ value: unknown,
65
+ ): void {
66
+ if (!filterMetadataStore.has(target)) {
67
+ filterMetadataStore.set(target, new Map());
68
+ }
69
+ filterMetadataStore.get(target)?.set(key, value);
70
+ }
71
+
72
+ function getFilterMetadata<T>(
73
+ target: Constructor,
74
+ key: string,
75
+ ): T | undefined {
76
+ return filterMetadataStore.get(target)?.get(key) as T | undefined;
77
+ }
78
+
79
+ function setFilterPrototypeMetadata(
80
+ target: object,
81
+ key: string,
82
+ value: unknown,
83
+ ): void {
84
+ if (!filterPrototypeStore.has(target)) {
85
+ filterPrototypeStore.set(target, new Map());
86
+ }
87
+ filterPrototypeStore.get(target)?.set(key, value);
88
+ }
89
+
90
+ function getFilterPrototypeMetadata<T>(
91
+ target: object,
92
+ key: string,
93
+ ): T | undefined {
94
+ return filterPrototypeStore.get(target)?.get(key) as T | undefined;
95
+ }
96
+
97
+ // ============= Decorators =============
98
+
99
+ /**
100
+ * Decorator to apply filters to a controller class or method
101
+ * Filters are executed in order: global → class → method
102
+ *
103
+ * @param filters - Filters to apply
104
+ * @returns ClassDecorator & MethodDecorator
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * @Controller('users')
109
+ * @UseFilters(CustomExceptionFilter) // Applied to all methods
110
+ * class UsersController {
111
+ * @Get(':id')
112
+ * @UseFilters(NotFoundExceptionFilter) // Additional filter for this method
113
+ * getUser() {}
114
+ * }
115
+ * ```
116
+ */
117
+ export function UseFilters(...filters: Filter[]): MethodDecorator & ClassDecorator {
118
+ const decorator = (
119
+ target: unknown,
120
+ propertyKey?: string | symbol,
121
+ descriptor?: PropertyDescriptor,
122
+ ): PropertyDescriptor | void => {
123
+ if (descriptor && propertyKey !== undefined) {
124
+ // Method decorator
125
+ const targetObj = target as object;
126
+ const existingFilters =
127
+ getFilterPrototypeMetadata<Filter[]>(targetObj, "filters:method") ?? [];
128
+ setFilterPrototypeMetadata(targetObj, "filters:method", [
129
+ ...existingFilters,
130
+ ...filters,
131
+ ]);
132
+ return descriptor;
133
+ } else {
134
+ // Class decorator
135
+ const targetClass = target as Constructor;
136
+ const existingFilters =
137
+ getFilterMetadata<Filter[]>(targetClass, "filters:class") ?? [];
138
+ setFilterMetadata(targetClass, "filters:class", [
139
+ ...existingFilters,
140
+ ...filters,
141
+ ]);
142
+ }
143
+ };
144
+ return decorator as MethodDecorator & ClassDecorator;
145
+ }
146
+
147
+ /**
148
+ * Decorator to mark a filter as catching a specific exception type
149
+ *
150
+ * @param exceptionType - The exception class to catch
151
+ * @returns ClassDecorator
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * @Catch(ValidationError)
156
+ * @Injectable()
157
+ * class CustomValidationFilter implements ExceptionFilter<ValidationError> {
158
+ * catch(exception: ValidationError, context: Context): Response {
159
+ * return context.status(422).json({
160
+ * error: 'Validation Failed',
161
+ * issues: exception.issues
162
+ * });
163
+ * }
164
+ * }
165
+ * ```
166
+ */
167
+ export function Catch<T extends Error>(
168
+ exceptionType: new (...args: never[]) => T,
169
+ ): ClassDecorator {
170
+ const decorator = (target: Constructor): void => {
171
+ catchMetadataStore.set(target, { exceptionType: exceptionType as unknown as new (...args: unknown[]) => Error });
172
+ };
173
+ return decorator as ClassDecorator;
174
+ }
175
+
176
+ // ============= Helper Functions =============
177
+
178
+ /**
179
+ * Get class-level filters from a controller
180
+ */
181
+ export function getClassFilters(target: Constructor): Filter[] | undefined {
182
+ return getFilterMetadata<Filter[]>(target, "filters:class");
183
+ }
184
+
185
+ /**
186
+ * Get method-level filters from a controller method
187
+ */
188
+ export function getMethodFilters(
189
+ target: object,
190
+ propertyKey: string | symbol,
191
+ ): Filter[] | undefined {
192
+ // First check method-specific filters
193
+ const methodFilters = getFilterPrototypeMetadata<Filter[]>(
194
+ target,
195
+ `filters:method:${String(propertyKey)}`,
196
+ );
197
+ if (methodFilters) {
198
+ return methodFilters;
199
+ }
200
+ // Fall back to general method filters
201
+ return getFilterPrototypeMetadata<Filter[]>(target, "filters:method");
202
+ }
203
+
204
+ /**
205
+ * Get the exception type that a filter catches
206
+ */
207
+ export function getCatchType(
208
+ filter: Filter,
209
+ ): (new (...args: unknown[]) => Error) | undefined {
210
+ // If it's a class constructor, check the catch metadata
211
+ if (typeof filter === "function" && filter.prototype !== undefined) {
212
+ const metadata = catchMetadataStore.get(filter as Constructor);
213
+ return metadata?.exceptionType;
214
+ }
215
+ // If it's an instance, check its constructor
216
+ if (typeof filter === "object" && filter !== null && "catch" in filter) {
217
+ const constructor = (filter as object).constructor as Constructor;
218
+ const metadata = catchMetadataStore.get(constructor);
219
+ return metadata?.exceptionType;
220
+ }
221
+ return undefined;
222
+ }
223
+
224
+ /**
225
+ * Check if a filter can handle a specific exception
226
+ */
227
+ export function canHandleException(
228
+ filter: Filter,
229
+ exception: Error,
230
+ ): boolean {
231
+ const catchType = getCatchType(filter);
232
+ if (!catchType) {
233
+ // No specific catch type means it handles all exceptions
234
+ return true;
235
+ }
236
+ return exception instanceof catchType;
237
+ }
238
+
239
+ /**
240
+ * Type guard to check if a value is an ExceptionFilter instance
241
+ */
242
+ export function isExceptionFilter(value: unknown): value is ExceptionFilter {
243
+ return (
244
+ typeof value === "object" &&
245
+ value !== null &&
246
+ "catch" in value &&
247
+ typeof (value as ExceptionFilter).catch === "function"
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Type guard to check if a value is a FilterFn
253
+ */
254
+ export function isFilterFn(value: unknown): value is FilterFn {
255
+ return typeof value === "function" && value.length === 2;
256
+ }
257
+
258
+ /**
259
+ * Execute a filter and return the response
260
+ */
261
+ export async function executeFilter(
262
+ filter: Filter,
263
+ exception: Error,
264
+ context: Context,
265
+ resolveFilter?: (filter: Filter) => ExceptionFilter | null,
266
+ ): Promise<Response> {
267
+ let filterInstance: ExceptionFilter | null = null;
268
+
269
+ if (isExceptionFilter(filter)) {
270
+ filterInstance = filter;
271
+ } else if (isFilterFn(filter)) {
272
+ // Convert filter function to response
273
+ return filter(exception, context);
274
+ } else if (typeof filter === "function") {
275
+ // It's a class constructor - try to resolve from container or instantiate
276
+ if (resolveFilter) {
277
+ filterInstance = resolveFilter(filter);
278
+ }
279
+ if (!filterInstance) {
280
+ // Try to instantiate directly (for simple filters without dependencies)
281
+ try {
282
+ const Constructor = filter as new () => ExceptionFilter;
283
+ filterInstance = new Constructor();
284
+ } catch {
285
+ // Cannot instantiate - will fall through to default handling
286
+ }
287
+ }
288
+ } else if (
289
+ typeof filter === "object" &&
290
+ filter !== null &&
291
+ !isExceptionFilter(filter)
292
+ ) {
293
+ // It's a token - try to resolve
294
+ if (resolveFilter) {
295
+ filterInstance = resolveFilter(filter);
296
+ }
297
+ }
298
+
299
+ if (filterInstance && isExceptionFilter(filterInstance)) {
300
+ return filterInstance.catch(exception, context);
301
+ }
302
+
303
+ // Fallback - should not reach here if a catch-all filter is configured
304
+ return createInternalErrorResponse(exception);
305
+ }
306
+
307
+ /**
308
+ * Options for executing filters
309
+ */
310
+ export interface ExecuteFiltersOptions {
311
+ globalFilters: Filter[];
312
+ classFilters: Filter[];
313
+ methodFilters: Filter[];
314
+ resolveFilter?: (filter: Filter) => ExceptionFilter | null;
315
+ }
316
+
317
+ /**
318
+ * Find and execute the appropriate filter for an exception
319
+ * Filters are checked in order: method → class → global
320
+ * The first filter that can handle the exception is used
321
+ */
322
+ export async function findAndExecuteFilter(
323
+ exception: Error,
324
+ context: Context,
325
+ options: ExecuteFiltersOptions,
326
+ ): Promise<Response> {
327
+ const { globalFilters, classFilters, methodFilters, resolveFilter } = options;
328
+
329
+ // Combine all filters in execution order (method first, then class, then global)
330
+ const allFilters = [
331
+ ...methodFilters,
332
+ ...classFilters,
333
+ ...globalFilters,
334
+ ];
335
+
336
+ // Find the first filter that can handle this exception type
337
+ for (const filter of allFilters) {
338
+ if (canHandleException(filter, exception)) {
339
+ return executeFilter(filter, exception, context, resolveFilter);
340
+ }
341
+ }
342
+
343
+ // No specific filter found - use default error response
344
+ return createDefaultErrorResponse(exception, context);
345
+ }
346
+
347
+ // ============= Built-in Filters =============
348
+
349
+ /**
350
+ * HttpExceptionFilter - Handles BuenoError exceptions
351
+ * Returns appropriate HTTP status codes and error details
352
+ */
353
+ @Catch(BuenoError)
354
+ export class HttpExceptionFilter implements ExceptionFilter<BuenoError> {
355
+ catch(exception: BuenoError, context: Context): Response {
356
+ return context.status(exception.statusCode).json({
357
+ error: exception.name,
358
+ message: exception.message,
359
+ code: exception.code,
360
+ statusCode: exception.statusCode,
361
+ });
362
+ }
363
+ }
364
+
365
+ /**
366
+ * ValidationFilter - Handles ValidationError exceptions
367
+ * Returns validation error details with issues list
368
+ */
369
+ @Catch(ValidationError)
370
+ export class ValidationFilter implements ExceptionFilter<ValidationError> {
371
+ catch(exception: ValidationError, context: Context): Response {
372
+ return context.status(422).json({
373
+ error: "Validation Failed",
374
+ message: exception.message,
375
+ code: "VALIDATION_ERROR",
376
+ issues: exception.issues,
377
+ });
378
+ }
379
+ }
380
+
381
+ /**
382
+ * NotFoundFilter - Handles NotFoundError exceptions
383
+ * Returns 404 response with resource not found message
384
+ */
385
+ @Catch(NotFoundError)
386
+ export class NotFoundFilter implements ExceptionFilter<NotFoundError> {
387
+ catch(exception: NotFoundError, context: Context): Response {
388
+ return context.status(404).json({
389
+ error: "Not Found",
390
+ message: exception.message,
391
+ code: "NOT_FOUND",
392
+ });
393
+ }
394
+ }
395
+
396
+ /**
397
+ * AllExceptionsFilter - Catch-all filter for any Error
398
+ * This filter handles all unhandled exceptions
399
+ */
400
+ export class AllExceptionsFilter implements ExceptionFilter<Error> {
401
+ catch(exception: Error, context: Context): Response {
402
+ // Log the error for debugging
403
+ console.error("Unhandled exception:", exception);
404
+
405
+ // Check if it's a known error type
406
+ if (exception instanceof BuenoError) {
407
+ return context.status(exception.statusCode).json({
408
+ error: exception.name,
409
+ message: exception.message,
410
+ code: exception.code,
411
+ });
412
+ }
413
+
414
+ // Generic error response
415
+ return context.status(500).json({
416
+ error: "Internal Server Error",
417
+ message: exception.message || "An unexpected error occurred",
418
+ });
419
+ }
420
+ }
421
+
422
+ // ============= Helper Response Functions =============
423
+
424
+ /**
425
+ * Create a default error response when no filter matches
426
+ */
427
+ export function createDefaultErrorResponse(
428
+ exception: Error,
429
+ context: Context,
430
+ ): Response {
431
+ // Log the unhandled exception
432
+ console.error("Unhandled exception (no filter matched):", exception);
433
+
434
+ return context.status(500).json({
435
+ error: "Internal Server Error",
436
+ message: exception.message || "An unexpected error occurred",
437
+ });
438
+ }
439
+
440
+ /**
441
+ * Create an internal error response
442
+ */
443
+ export function createInternalErrorResponse(exception: Error): Response {
444
+ console.error("Internal server error:", exception);
445
+
446
+ return new Response(
447
+ JSON.stringify({
448
+ error: "Internal Server Error",
449
+ message: "An unexpected error occurred",
450
+ }),
451
+ {
452
+ status: 500,
453
+ headers: {
454
+ "Content-Type": "application/json",
455
+ },
456
+ },
457
+ );
458
+ }