@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
@@ -13,10 +13,14 @@
13
13
  * - Provide default values
14
14
  */
15
15
 
16
- import type { Context } from "../context";
17
16
  import type { Token } from "../container";
17
+ import type { Context } from "../context";
18
18
  import type { StandardSchema } from "../types";
19
- import { validate, isStandardSchema, type ValidationResult } from "../validation";
19
+ import {
20
+ type ValidationResult,
21
+ isStandardSchema,
22
+ validate,
23
+ } from "../validation";
20
24
 
21
25
  // ============= Types =============
22
26
 
@@ -74,7 +78,7 @@ export interface PipeTransform<T = unknown, R = unknown> {
74
78
  */
75
79
  export type PipeFn<T = unknown, R = unknown> = (
76
80
  value: T,
77
- context: PipeContext
81
+ context: PipeContext,
78
82
  ) => R | Promise<R>;
79
83
 
80
84
  /**
@@ -94,7 +98,10 @@ export type Pipe<T = unknown, R = unknown> =
94
98
  type Constructor = new (...args: unknown[]) => unknown;
95
99
 
96
100
  // WeakMap for storing pipes metadata on method prototypes
97
- const pipesMethodMetadata = new WeakMap<object, Map<string | symbol, ParameterPipeMetadata[]>>();
101
+ const pipesMethodMetadata = new WeakMap<
102
+ object,
103
+ Map<string | symbol, ParameterPipeMetadata[]>
104
+ >();
98
105
 
99
106
  /**
100
107
  * Metadata for a parameter with pipes
@@ -103,7 +110,7 @@ export interface ParameterPipeMetadata {
103
110
  /** Parameter index */
104
111
  index: number;
105
112
  /** Parameter decorator type */
106
- decorator: 'body' | 'query' | 'param' | 'custom';
113
+ decorator: "body" | "query" | "param" | "custom";
107
114
  /** Key for query/param decorators */
108
115
  key?: string;
109
116
  /** Schema for validation */
@@ -118,7 +125,7 @@ export interface ParameterPipeMetadata {
118
125
  function setMethodPipes(
119
126
  target: object,
120
127
  propertyKey: string | symbol,
121
- metadata: ParameterPipeMetadata[]
128
+ metadata: ParameterPipeMetadata[],
122
129
  ): void {
123
130
  if (!pipesMethodMetadata.has(target)) {
124
131
  pipesMethodMetadata.set(target, new Map());
@@ -131,7 +138,7 @@ function setMethodPipes(
131
138
  */
132
139
  export function getMethodPipes(
133
140
  target: object,
134
- propertyKey: string | symbol
141
+ propertyKey: string | symbol,
135
142
  ): ParameterPipeMetadata[] | undefined {
136
143
  return pipesMethodMetadata.get(target)?.get(propertyKey);
137
144
  }
@@ -155,7 +162,7 @@ export function UsePipes(...pipes: Pipe[]): ParameterDecorator {
155
162
  return (
156
163
  target: unknown,
157
164
  propertyKey: string | symbol | undefined,
158
- parameterIndex: number
165
+ parameterIndex: number,
159
166
  ) => {
160
167
  if (propertyKey === undefined) {
161
168
  throw new Error("UsePipes can only be used on method parameters");
@@ -163,19 +170,19 @@ export function UsePipes(...pipes: Pipe[]): ParameterDecorator {
163
170
 
164
171
  const targetObj = target as object;
165
172
  const existing = getMethodPipes(targetObj, propertyKey) ?? [];
166
-
173
+
167
174
  // Find existing metadata for this parameter or create new
168
- const existingParam = existing.find(p => p.index === parameterIndex);
175
+ const existingParam = existing.find((p) => p.index === parameterIndex);
169
176
  if (existingParam) {
170
177
  existingParam.pipes.push(...pipes);
171
178
  } else {
172
179
  existing.push({
173
180
  index: parameterIndex,
174
- decorator: 'custom',
175
- pipes: [...pipes]
181
+ decorator: "custom",
182
+ pipes: [...pipes],
176
183
  });
177
184
  }
178
-
185
+
179
186
  setMethodPipes(targetObj, propertyKey, existing);
180
187
  };
181
188
  }
@@ -198,7 +205,7 @@ export function Body(schema?: StandardSchema): ParameterDecorator {
198
205
  return (
199
206
  target: unknown,
200
207
  propertyKey: string | symbol | undefined,
201
- parameterIndex: number
208
+ parameterIndex: number,
202
209
  ) => {
203
210
  if (propertyKey === undefined) {
204
211
  throw new Error("Body can only be used on method parameters");
@@ -206,14 +213,14 @@ export function Body(schema?: StandardSchema): ParameterDecorator {
206
213
 
207
214
  const targetObj = target as object;
208
215
  const existing = getMethodPipes(targetObj, propertyKey) ?? [];
209
-
216
+
210
217
  existing.push({
211
218
  index: parameterIndex,
212
- decorator: 'body',
219
+ decorator: "body",
213
220
  schema,
214
- pipes: schema ? [new ValidationPipe(schema)] : []
221
+ pipes: schema ? [new ValidationPipe(schema)] : [],
215
222
  });
216
-
223
+
217
224
  setMethodPipes(targetObj, propertyKey, existing);
218
225
  };
219
226
  }
@@ -234,11 +241,14 @@ export function Body(schema?: StandardSchema): ParameterDecorator {
234
241
  * search(@Query('limit', limitSchema) limit: number) {}
235
242
  * ```
236
243
  */
237
- export function Query(key?: string, schema?: StandardSchema): ParameterDecorator {
244
+ export function Query(
245
+ key?: string,
246
+ schema?: StandardSchema,
247
+ ): ParameterDecorator {
238
248
  return (
239
249
  target: unknown,
240
250
  propertyKey: string | symbol | undefined,
241
- parameterIndex: number
251
+ parameterIndex: number,
242
252
  ) => {
243
253
  if (propertyKey === undefined) {
244
254
  throw new Error("Query can only be used on method parameters");
@@ -246,15 +256,15 @@ export function Query(key?: string, schema?: StandardSchema): ParameterDecorator
246
256
 
247
257
  const targetObj = target as object;
248
258
  const existing = getMethodPipes(targetObj, propertyKey) ?? [];
249
-
259
+
250
260
  existing.push({
251
261
  index: parameterIndex,
252
- decorator: 'query',
262
+ decorator: "query",
253
263
  key,
254
264
  schema,
255
- pipes: schema ? [new ValidationPipe(schema)] : []
265
+ pipes: schema ? [new ValidationPipe(schema)] : [],
256
266
  });
257
-
267
+
258
268
  setMethodPipes(targetObj, propertyKey, existing);
259
269
  };
260
270
  }
@@ -276,7 +286,7 @@ export function Param(key?: string, ...pipes: Pipe[]): ParameterDecorator {
276
286
  return (
277
287
  target: unknown,
278
288
  propertyKey: string | symbol | undefined,
279
- parameterIndex: number
289
+ parameterIndex: number,
280
290
  ) => {
281
291
  if (propertyKey === undefined) {
282
292
  throw new Error("Param can only be used on method parameters");
@@ -284,14 +294,14 @@ export function Param(key?: string, ...pipes: Pipe[]): ParameterDecorator {
284
294
 
285
295
  const targetObj = target as object;
286
296
  const existing = getMethodPipes(targetObj, propertyKey) ?? [];
287
-
297
+
288
298
  existing.push({
289
299
  index: parameterIndex,
290
- decorator: 'param',
300
+ decorator: "param",
291
301
  key,
292
- pipes: [...pipes]
302
+ pipes: [...pipes],
293
303
  });
294
-
304
+
295
305
  setMethodPipes(targetObj, propertyKey, existing);
296
306
  };
297
307
  }
@@ -313,13 +323,16 @@ export class ValidationPipe<T = unknown> implements PipeTransform<unknown, T> {
313
323
 
314
324
  async transform(value: unknown, context: PipeContext): Promise<T> {
315
325
  const result: ValidationResult<T> = await validate(this.schema, value);
316
-
326
+
317
327
  if (result.success) {
318
328
  return result.data;
319
329
  }
320
-
330
+
321
331
  // Validation failed
322
- const failedResult = result as Extract<ValidationResult<T>, { success: false }>;
332
+ const failedResult = result as Extract<
333
+ ValidationResult<T>,
334
+ { success: false }
335
+ >;
323
336
  const error = new Error("Validation failed");
324
337
  (error as Error & { issues: unknown[] }).issues = [...failedResult.issues];
325
338
  throw error;
@@ -336,12 +349,12 @@ export class ValidationPipe<T = unknown> implements PipeTransform<unknown, T> {
336
349
  */
337
350
  export class ParseIntPipe implements PipeTransform<string, number> {
338
351
  transform(value: string, context: PipeContext): number {
339
- const parsed = parseInt(value, 10);
340
-
352
+ const parsed = Number.parseInt(value, 10);
353
+
341
354
  if (isNaN(parsed)) {
342
355
  throw new Error(`Validation failed: "${value}" is not a valid integer`);
343
356
  }
344
-
357
+
345
358
  return parsed;
346
359
  }
347
360
  }
@@ -356,12 +369,12 @@ export class ParseIntPipe implements PipeTransform<string, number> {
356
369
  */
357
370
  export class ParseFloatPipe implements PipeTransform<string, number> {
358
371
  transform(value: string, context: PipeContext): number {
359
- const parsed = parseFloat(value);
360
-
372
+ const parsed = Number.parseFloat(value);
373
+
361
374
  if (isNaN(parsed)) {
362
375
  throw new Error(`Validation failed: "${value}" is not a valid number`);
363
376
  }
364
-
377
+
365
378
  return parsed;
366
379
  }
367
380
  }
@@ -375,20 +388,20 @@ export class ParseFloatPipe implements PipeTransform<string, number> {
375
388
  * ```
376
389
  */
377
390
  export class ParseBoolPipe implements PipeTransform<string, boolean> {
378
- private readonly truthyValues = ['true', '1', 'yes', 'on'];
379
- private readonly falsyValues = ['false', '0', 'no', 'off'];
391
+ private readonly truthyValues = ["true", "1", "yes", "on"];
392
+ private readonly falsyValues = ["false", "0", "no", "off"];
380
393
 
381
394
  transform(value: string, context: PipeContext): boolean {
382
395
  const lower = value.toLowerCase();
383
-
396
+
384
397
  if (this.truthyValues.includes(lower)) {
385
398
  return true;
386
399
  }
387
-
400
+
388
401
  if (this.falsyValues.includes(lower)) {
389
402
  return false;
390
403
  }
391
-
404
+
392
405
  throw new Error(`Validation failed: "${value}" is not a valid boolean`);
393
406
  }
394
407
  }
@@ -422,8 +435,8 @@ export class DefaultValuePipe<T> implements PipeTransform<unknown, T> {
422
435
  */
423
436
  export class TrimPipe implements PipeTransform<string, string> {
424
437
  transform(value: string, context: PipeContext): string {
425
- if (typeof value !== 'string') {
426
- throw new Error('Value must be a string');
438
+ if (typeof value !== "string") {
439
+ throw new Error("Value must be a string");
427
440
  }
428
441
  return value.trim();
429
442
  }
@@ -456,13 +469,16 @@ export class ParseJsonPipe<T = unknown> implements PipeTransform<string, T> {
456
469
  * ```
457
470
  */
458
471
  export class ParseArrayPipe implements PipeTransform<string, string[]> {
459
- constructor(private separator: string = ',') {}
472
+ constructor(private separator = ",") {}
460
473
 
461
474
  transform(value: string, context: PipeContext): string[] {
462
- if (typeof value !== 'string') {
463
- throw new Error('Value must be a string');
475
+ if (typeof value !== "string") {
476
+ throw new Error("Value must be a string");
464
477
  }
465
- return value.split(this.separator).map(s => s.trim()).filter(s => s.length > 0);
478
+ return value
479
+ .split(this.separator)
480
+ .map((s) => s.trim())
481
+ .filter((s) => s.length > 0);
466
482
  }
467
483
  }
468
484
 
@@ -492,7 +508,7 @@ export interface PipeExecutorOptions {
492
508
  export async function executePipes<T = unknown>(
493
509
  value: unknown,
494
510
  context: PipeContext,
495
- options: PipeExecutorOptions
511
+ options: PipeExecutorOptions,
496
512
  ): Promise<T> {
497
513
  const { globalPipes = [], parameterPipes = [], resolvePipe } = options;
498
514
 
@@ -509,8 +525,11 @@ export async function executePipes<T = unknown>(
509
525
  if (typeof pipe === "function") {
510
526
  // Check if it's a pipe function or a class constructor
511
527
  const funcPipe = pipe as { prototype?: unknown; transform?: unknown };
512
- if (funcPipe.prototype && typeof funcPipe.prototype === "object" &&
513
- "transform" in (funcPipe.prototype as object)) {
528
+ if (
529
+ funcPipe.prototype &&
530
+ typeof funcPipe.prototype === "object" &&
531
+ "transform" in (funcPipe.prototype as object)
532
+ ) {
514
533
  // It's a class constructor - try to resolve from container or create instance
515
534
  pipeInstance = resolvePipe ? resolvePipe(pipe) : null;
516
535
  if (!pipeInstance) {
@@ -559,25 +578,25 @@ export async function executePipes<T = unknown>(
559
578
  */
560
579
  export async function extractParameterValue(
561
580
  context: Context,
562
- metadata: ParameterPipeMetadata
581
+ metadata: ParameterPipeMetadata,
563
582
  ): Promise<unknown> {
564
583
  switch (metadata.decorator) {
565
- case 'body':
584
+ case "body":
566
585
  return await context.body();
567
-
568
- case 'query':
586
+
587
+ case "query":
569
588
  if (metadata.key) {
570
589
  return context.query[metadata.key];
571
590
  }
572
591
  return context.query;
573
-
574
- case 'param':
592
+
593
+ case "param":
575
594
  if (metadata.key) {
576
595
  return context.params[metadata.key];
577
596
  }
578
597
  return context.params;
579
-
580
- case 'custom':
598
+
599
+ case "custom":
581
600
  default:
582
601
  return undefined;
583
602
  }
@@ -590,18 +609,21 @@ export async function extractParameterValue(
590
609
  */
591
610
  export function createBadRequestResponse(error: Error): Response {
592
611
  const issues = (error as Error & { issues?: unknown[] }).issues;
593
-
594
- return new Response(JSON.stringify({
595
- statusCode: 400,
596
- error: "Bad Request",
597
- message: error.message,
598
- ...(issues && { issues })
599
- }), {
600
- status: 400,
601
- headers: {
602
- "Content-Type": "application/json",
612
+
613
+ return new Response(
614
+ JSON.stringify({
615
+ statusCode: 400,
616
+ error: "Bad Request",
617
+ message: error.message,
618
+ ...(issues && { issues }),
619
+ }),
620
+ {
621
+ status: 400,
622
+ headers: {
623
+ "Content-Type": "application/json",
624
+ },
603
625
  },
604
- });
626
+ );
605
627
  }
606
628
 
607
629
  // ============= Type Guards =============
@@ -623,4 +645,4 @@ export function isPipeTransform(value: unknown): value is PipeTransform {
623
645
  */
624
646
  export function isPipeFn(value: unknown): value is PipeFn {
625
647
  return typeof value === "function" && !isPipeTransform(value);
626
- }
648
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Base Channel Service
3
+ *
4
+ * Abstract base class for all channel implementations
5
+ */
6
+
7
+ import type {
8
+ ChannelHealth,
9
+ ChannelService,
10
+ NotificationMessage,
11
+ } from "../types";
12
+
13
+ // ============= Abstract Base Channel Service =============
14
+
15
+ export abstract class BaseChannelService<
16
+ T extends NotificationMessage = NotificationMessage,
17
+ > implements ChannelService<T>
18
+ {
19
+ abstract readonly name: string;
20
+ readonly configSchema?: unknown;
21
+
22
+ /**
23
+ * Validate message structure
24
+ * Should throw if message is invalid
25
+ */
26
+ abstract validate(message: unknown): asserts message is T;
27
+
28
+ /**
29
+ * Send a notification message
30
+ */
31
+ abstract send(message: T): Promise<string | undefined>;
32
+
33
+ /**
34
+ * Get health status
35
+ */
36
+ async getHealth(): Promise<ChannelHealth> {
37
+ return {
38
+ status: "healthy",
39
+ message: "Channel is operational",
40
+ checkedAt: new Date(),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Validate that value matches expected type
46
+ */
47
+ protected validateField(
48
+ value: unknown,
49
+ expectedType: string,
50
+ fieldName: string,
51
+ ): void {
52
+ const actualType = typeof value;
53
+ if (actualType !== expectedType) {
54
+ throw new Error(
55
+ `Invalid ${this.name} message: ${fieldName} must be ${expectedType}, got ${actualType}`,
56
+ );
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Validate that required field exists
62
+ */
63
+ protected validateRequired(value: unknown, fieldName: string): void {
64
+ if (value === undefined || value === null) {
65
+ throw new Error(`Invalid ${this.name} message: ${fieldName} is required`);
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Email Channel Service
3
+ *
4
+ * Sends notifications via email through various drivers
5
+ * (SMTP, Sendgrid, Brevo, Resend)
6
+ */
7
+
8
+ import type { EmailChannelConfig, EmailMessage } from "../types";
9
+ import { BaseChannelService } from "./base";
10
+
11
+ // ============= Email Channel Service =============
12
+
13
+ export class EmailChannelService extends BaseChannelService<EmailMessage> {
14
+ readonly name = "email";
15
+ private config: EmailChannelConfig;
16
+ private sentCount = 0;
17
+ private failureCount = 0;
18
+
19
+ constructor(config: EmailChannelConfig) {
20
+ super();
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Validate email message structure
26
+ */
27
+ validate(message: unknown): asserts message is EmailMessage {
28
+ if (typeof message !== "object" || message === null) {
29
+ throw new Error("Invalid email message: must be an object");
30
+ }
31
+
32
+ const msg = message as Record<string, unknown>;
33
+
34
+ // Check required fields
35
+ if (msg.channel !== "email") {
36
+ throw new Error('Invalid email message: channel must be "email"');
37
+ }
38
+
39
+ if (typeof msg.recipient !== "string" || !msg.recipient.includes("@")) {
40
+ throw new Error("Invalid email message: recipient must be a valid email");
41
+ }
42
+
43
+ if (typeof msg.subject !== "string" || msg.subject.length === 0) {
44
+ throw new Error("Invalid email message: subject is required");
45
+ }
46
+
47
+ // Check that at least one of html or text is present
48
+ if (!msg.html && !msg.text) {
49
+ throw new Error(
50
+ "Invalid email message: either html or text must be provided",
51
+ );
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Send email message
57
+ */
58
+ async send(message: EmailMessage): Promise<string | undefined> {
59
+ try {
60
+ if (this.config.dryRun) {
61
+ console.log(`[EmailChannel] Would send email to: ${message.recipient}`);
62
+ console.log(` Subject: ${message.subject}`);
63
+ this.sentCount++;
64
+ return this._generateMessageId();
65
+ }
66
+
67
+ // In a real implementation, delegate to appropriate driver
68
+ // For now, simulate sending
69
+ const messageId = this._generateMessageId();
70
+ console.log(
71
+ `[EmailChannel] Email sent: ${messageId} to ${message.recipient}`,
72
+ );
73
+ this.sentCount++;
74
+
75
+ return messageId;
76
+ } catch (error) {
77
+ this.failureCount++;
78
+ throw new Error(
79
+ `Failed to send email: ${error instanceof Error ? error.message : String(error)}`,
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get email channel metrics
86
+ */
87
+ getMetrics() {
88
+ const total = this.sentCount + this.failureCount;
89
+ return {
90
+ sent: this.sentCount,
91
+ failed: this.failureCount,
92
+ total,
93
+ successRate: total > 0 ? this.sentCount / total : 0,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Generate a message ID
99
+ */
100
+ private _generateMessageId(): string {
101
+ const timestamp = Date.now();
102
+ const random = Math.random().toString(36).substring(2, 15);
103
+ return `${timestamp}.${random}@bueno.local`;
104
+ }
105
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Push Notification Channel Service
3
+ *
4
+ * Sends notifications via push through various drivers
5
+ * (Firebase, APNS, custom)
6
+ */
7
+
8
+ import type { PushChannelConfig, PushNotificationMessage } from "../types";
9
+ import { BaseChannelService } from "./base";
10
+
11
+ // ============= Push Notification Channel Service =============
12
+
13
+ export class PushNotificationChannelService extends BaseChannelService<PushNotificationMessage> {
14
+ readonly name = "push";
15
+ private config: PushChannelConfig;
16
+ private sentCount = 0;
17
+ private failureCount = 0;
18
+
19
+ constructor(config: PushChannelConfig) {
20
+ super();
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Validate push notification message structure
26
+ */
27
+ validate(message: unknown): asserts message is PushNotificationMessage {
28
+ if (typeof message !== "object" || message === null) {
29
+ throw new Error("Invalid push message: must be an object");
30
+ }
31
+
32
+ const msg = message as Record<string, unknown>;
33
+
34
+ if (msg.channel !== "push") {
35
+ throw new Error('Invalid push message: channel must be "push"');
36
+ }
37
+
38
+ if (typeof msg.recipient !== "string" || msg.recipient.length === 0) {
39
+ throw new Error(
40
+ "Invalid push message: recipient (device token) is required",
41
+ );
42
+ }
43
+
44
+ if (typeof msg.title !== "string" || msg.title.length === 0) {
45
+ throw new Error("Invalid push message: title is required");
46
+ }
47
+
48
+ if (typeof msg.body !== "string" || msg.body.length === 0) {
49
+ throw new Error("Invalid push message: body is required");
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Send push notification message
55
+ */
56
+ async send(message: PushNotificationMessage): Promise<string | undefined> {
57
+ try {
58
+ if (this.config.dryRun) {
59
+ console.log(`[PushChannel] Would send push to: ${message.recipient}`);
60
+ console.log(` Title: ${message.title}`);
61
+ console.log(` Body: ${message.body}`);
62
+ this.sentCount++;
63
+ return this._generateMessageId();
64
+ }
65
+
66
+ // In a real implementation, delegate to appropriate driver
67
+ // For now, simulate sending
68
+ const messageId = this._generateMessageId();
69
+ console.log(
70
+ `[PushChannel] Push sent: ${messageId} to ${message.recipient}`,
71
+ );
72
+ this.sentCount++;
73
+
74
+ return messageId;
75
+ } catch (error) {
76
+ this.failureCount++;
77
+ throw new Error(
78
+ `Failed to send push: ${error instanceof Error ? error.message : String(error)}`,
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get push channel metrics
85
+ */
86
+ getMetrics() {
87
+ const total = this.sentCount + this.failureCount;
88
+ return {
89
+ sent: this.sentCount,
90
+ failed: this.failureCount,
91
+ total,
92
+ successRate: total > 0 ? this.sentCount / total : 0,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Generate a message ID
98
+ */
99
+ private _generateMessageId(): string {
100
+ const timestamp = Date.now();
101
+ const random = Math.random().toString(36).substring(2, 15);
102
+ return `push_${timestamp}_${random}`;
103
+ }
104
+ }