@bakery-framework/core 1.1.0 → 1.2.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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Request-body validation for route modules.
3
+ *
4
+ * `defineRoute<{ title: string }>` declares a body shape and enforces nothing —
5
+ * the scaffolder's own template says so twice: *"declares the contract — it
6
+ * does not validate it. The body is still client input."* Every route was left
7
+ * to hand-check or not, and most did not.
8
+ *
9
+ * **No schema library is bundled, and none is depended on.** `@bakery-framework/
10
+ * core` has zero runtime dependencies and that is worth keeping, so validation
11
+ * accepts two shapes it can consume without knowing who produced them:
12
+ *
13
+ * - **Standard Schema** (`~standard`) — the shared interface zod, valibot and
14
+ * arktype all implement. Bring your own library; Bakery never imports it.
15
+ * - **A plain function** that returns the parsed value or throws.
16
+ *
17
+ * The second exists because the first is overkill for `body => { if (!body.id)
18
+ * throw new Error('id required'); return body }`, and a framework that forces a
19
+ * dependency for that has made the common case worse.
20
+ */
21
+
22
+ /** The subset of Standard Schema v1 that validation needs. */
23
+ export interface StandardSchemaLike<T> {
24
+ '~standard': {
25
+ version: 1
26
+ vendor: string
27
+ validate(value: unknown): StandardResult<T> | Promise<StandardResult<T>>
28
+ }
29
+ }
30
+
31
+ type StandardResult<T> =
32
+ | { value: T; issues?: undefined }
33
+ | { issues: readonly StandardIssue[] }
34
+
35
+ interface StandardIssue {
36
+ message: string
37
+ path?: readonly (PropertyKey | { key: PropertyKey })[]
38
+ }
39
+
40
+ /** A function validator: return the parsed value, or throw to reject. */
41
+ export type FunctionValidator<T> = (value: unknown) => T | Promise<T>
42
+
43
+ export type Validator<T> = StandardSchemaLike<T> | FunctionValidator<T>
44
+
45
+ /** One human-readable problem, with the field path when the schema gave one. */
46
+ export interface ValidationIssue {
47
+ path: string
48
+ message: string
49
+ }
50
+
51
+ export type ValidationResult<T> =
52
+ | { ok: true; value: T }
53
+ | { ok: false; issues: ValidationIssue[] }
54
+
55
+ function isStandardSchema<T>(v: Validator<T>): v is StandardSchemaLike<T> {
56
+ return typeof v === 'object' && v !== null && '~standard' in v
57
+ }
58
+
59
+ /**
60
+ * Render a Standard Schema path as dotted notation.
61
+ *
62
+ * Segments may be plain keys or `{ key }` objects — the spec allows both, and a
63
+ * library that uses the object form would otherwise render as `[object Object]`
64
+ * in the very message meant to tell someone which field is wrong.
65
+ */
66
+ function renderPath(path: StandardIssue['path']): string {
67
+ if (!path?.length) return ''
68
+ return path
69
+ .map(seg => (typeof seg === 'object' && seg !== null ? seg.key : seg))
70
+ .join('.')
71
+ }
72
+
73
+ /**
74
+ * Run a validator, never throwing.
75
+ *
76
+ * A function validator that throws is a *rejection*, not a crash: that is the
77
+ * whole idiom for the plain-function form. Its message becomes the issue, so
78
+ * `throw new Error('id must be a number')` reaches the client as written.
79
+ */
80
+ export async function validate<T>(
81
+ validator: Validator<T>,
82
+ value: unknown,
83
+ ): Promise<ValidationResult<T>> {
84
+ if (isStandardSchema(validator)) {
85
+ const result = await validator['~standard'].validate(value)
86
+ if (result.issues) {
87
+ return {
88
+ ok: false,
89
+ issues: result.issues.map(issue => ({
90
+ path: renderPath(issue.path),
91
+ message: issue.message,
92
+ })),
93
+ }
94
+ }
95
+ return { ok: true, value: result.value }
96
+ }
97
+
98
+ try {
99
+ return { ok: true, value: await validator(value) }
100
+ } catch (error: any) {
101
+ return {
102
+ ok: false,
103
+ issues: [{ path: '', message: error?.message ?? 'Invalid request body' }],
104
+ }
105
+ }
106
+ }