@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.
- package/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
package/src/modules/pipes.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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<
|
|
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 = [
|
|
379
|
-
private readonly falsyValues = [
|
|
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 !==
|
|
426
|
-
throw new Error(
|
|
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
|
|
472
|
+
constructor(private separator = ",") {}
|
|
460
473
|
|
|
461
474
|
transform(value: string, context: PipeContext): string[] {
|
|
462
|
-
if (typeof value !==
|
|
463
|
-
throw new Error(
|
|
475
|
+
if (typeof value !== "string") {
|
|
476
|
+
throw new Error("Value must be a string");
|
|
464
477
|
}
|
|
465
|
-
return value
|
|
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 (
|
|
513
|
-
|
|
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
|
|
584
|
+
case "body":
|
|
566
585
|
return await context.body();
|
|
567
|
-
|
|
568
|
-
case
|
|
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
|
|
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
|
|
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(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
}
|