@zipbul/baker 3.4.1 → 4.0.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.
- package/CHANGELOG.md +31 -0
- package/README.md +236 -148
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/src/baker.d.ts +26 -0
- package/dist/src/baker.js +1 -0
- package/dist/src/configure.d.ts +7 -1
- package/dist/src/configure.js +1 -1
- package/dist/src/create-rule.d.ts +2 -1
- package/dist/src/decorators/field.d.ts +2 -1
- package/dist/src/decorators/field.js +1 -1
- package/dist/src/enums.d.ts +51 -0
- package/dist/src/enums.js +1 -0
- package/dist/src/rule-plan.d.ts +5 -3
- package/dist/src/rule-plan.js +1 -1
- package/dist/src/rules/array.js +1 -1
- package/dist/src/rules/date.js +1 -1
- package/dist/src/rules/locales.js +1 -1
- package/dist/src/rules/number.js +1 -1
- package/dist/src/rules/object.js +1 -1
- package/dist/src/rules/string.js +5 -5
- package/dist/src/rules/typechecker.js +5 -5
- package/dist/src/seal/deserialize-builder.js +230 -230
- package/dist/src/seal/enums.d.ts +8 -0
- package/dist/src/seal/enums.js +1 -0
- package/dist/src/seal/expose-validator.js +1 -1
- package/dist/src/seal/seal.d.ts +15 -1
- package/dist/src/seal/seal.js +1 -1
- package/dist/src/seal/serialize-builder.js +8 -8
- package/dist/src/seal/validate-meta.js +1 -1
- package/dist/src/types.d.ts +11 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 98c9a0a: **Breaking:** rule type hints and field exclusion are now enums instead of string literals.
|
|
8
|
+
`createRule({ requiresType: 'number' })` becomes `requiresType: RequiredType.Number`, and
|
|
9
|
+
`@Field({ exclude: 'serializeOnly' })` becomes `exclude: ExcludeMode.SerializeOnly`. Runtime
|
|
10
|
+
behaviour is unchanged — the enums are string-valued, so generated code and validation results are
|
|
11
|
+
identical; only the public type surface changed. `RequiredType` and `ExcludeMode` are now exported.
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 98c9a0a: Add `createBaker()` for multi-app isolation. Each scope owns its own registration and config, so
|
|
16
|
+
multiple apps in one process — or a bundler-duplicated copy of baker — no longer fragment `seal()`
|
|
17
|
+
(the previous "`<Class> is not sealed`" failure). Use:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const app = createBaker({ autoConvert: true });
|
|
21
|
+
@app.Recipe
|
|
22
|
+
class UserDto {
|
|
23
|
+
@Field(isString) name!: string;
|
|
24
|
+
}
|
|
25
|
+
app.seal();
|
|
26
|
+
deserialize(UserDto, input);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`@Field`, rules, and `deserialize/serialize/validate` stay global. Distinct classes are fully
|
|
30
|
+
isolated (each sealed with its scope's config); a class shared across scopes is reused as one sealed
|
|
31
|
+
form. Single-app code is unchanged — global `@Recipe` / `seal()` / `configure()` still work. Exports
|
|
32
|
+
`createBaker` and the `Baker` type.
|
|
33
|
+
|
|
3
34
|
## 3.4.1
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
-
The fastest decorator-based DTO validation library for TypeScript.
|
|
3
|
+
The fastest decorator-based DTO validation library for TypeScript. baker generates optimized validation and serialization code once at `seal()` time, then reuses the sealed executors on every call.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
bun add @zipbul/baker
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Zero `reflect-metadata`. Sealed codegen.
|
|
9
|
+
Zero `reflect-metadata`. Sealed codegen.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
|
|
14
|
+
- **ESM only.** baker ships no CommonJS build.
|
|
15
|
+
- **TypeScript ≥ 5.2** with native (TC39, Stage 3) decorators. Bun runs TypeScript directly, so your DTOs need no separate build step.
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
// tsconfig.json
|
|
19
|
+
{
|
|
20
|
+
"compilerOptions": {
|
|
21
|
+
"target": "ESNext", // must include Symbol.metadata (ES2022+/ESNext)
|
|
22
|
+
"experimentalDecorators": false // use native TC39 decorators — this is the default; do NOT enable it
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
12
26
|
|
|
13
27
|
## Quick Start
|
|
14
28
|
|
|
@@ -23,143 +37,154 @@ class UserDto {
|
|
|
23
37
|
@Field(isString, isEmail()) email!: string;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
// Call once at app startup, after
|
|
40
|
+
// Call once at app startup, after every DTO has been imported.
|
|
27
41
|
seal();
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
// All rules here are sync, so deserialize returns the value directly (no await).
|
|
44
|
+
const result = deserialize(UserDto, {
|
|
30
45
|
name: 'Alice',
|
|
31
46
|
age: 30,
|
|
32
47
|
email: 'alice@test.com',
|
|
33
48
|
});
|
|
34
49
|
|
|
35
50
|
if (isBakerIssueSet(result)) {
|
|
36
|
-
|
|
51
|
+
// Reached only for invalid input, e.g. [{ path: 'email', code: 'isEmail' }]
|
|
52
|
+
console.log(result.errors);
|
|
37
53
|
} else {
|
|
38
54
|
console.log(result.name); // 'Alice' — typed as UserDto
|
|
39
55
|
}
|
|
40
56
|
```
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
`deserialize` returns either your typed instance or a `BakerIssueSet`; narrow between them with `isBakerIssueSet`. If any rule or transformer on the DTO is async, `deserialize` returns a `Promise` instead — `await` it (see [Runtime API](#runtime-api)).
|
|
59
|
+
|
|
60
|
+
## Core Concepts
|
|
61
|
+
|
|
62
|
+
| Concept | What it does |
|
|
63
|
+
| ------------------- | ------------------------------------------------------------------------------ |
|
|
64
|
+
| `@Recipe` | Marks a class as a DTO and registers it with baker. |
|
|
65
|
+
| `@Field(...rules)` | Declares a validated field. Only `@Field` properties are part of the contract. |
|
|
66
|
+
| `seal()` | Compiles every registered DTO into executor functions. Call once, at startup. |
|
|
67
|
+
| `deserialize` / `validate` / `serialize` | Run the sealed executors: parse+validate, validate-only, or emit a plain object. |
|
|
43
68
|
|
|
44
|
-
|
|
69
|
+
## Why baker?
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
| Approach
|
|
51
|
-
| Decorators
|
|
52
|
-
| `reflect-metadata`
|
|
53
|
-
| Sync DTO return
|
|
71
|
+
baker generates optimized JavaScript functions once on first seal, then executes them on every call — no per-call rule interpretation.
|
|
72
|
+
|
|
73
|
+
| Feature | baker | class-validator | Zod |
|
|
74
|
+
| ------------------ | -------------------- | ---------------------- | ------------------- |
|
|
75
|
+
| Approach | AOT code generation | Runtime interpretation | Schema method chain |
|
|
76
|
+
| Decorators | `@Field` (unified) | 30+ individual | N/A |
|
|
77
|
+
| `reflect-metadata` | Not needed | Required | N/A |
|
|
78
|
+
| Sync DTO return | Direct value | Promise | Direct value |
|
|
54
79
|
|
|
55
80
|
## Performance
|
|
56
81
|
|
|
57
|
-
Benchmarked against multiple libraries on simple, nested, array, and error-collection scenarios. Exact numbers vary by machine and runtime.
|
|
82
|
+
Benchmarked against multiple libraries on simple, nested, array, and error-collection scenarios. Exact numbers vary by machine and runtime — see [`bench/`](./bench) for the suite and to measure on your machine.
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
## @Field Decorator
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
One decorator for everything — replaces 30+ individual decorators from class-validator.
|
|
62
87
|
|
|
63
|
-
|
|
88
|
+
Only fields decorated with `@Field` participate in validation, deserialization, and serialization. Undecorated fields are silently absent from results — they are not part of the DTO contract.
|
|
64
89
|
|
|
65
|
-
|
|
90
|
+
```typescript
|
|
91
|
+
@Field(...rules)
|
|
92
|
+
@Field(...rules, options)
|
|
93
|
+
@Field(options)
|
|
94
|
+
@Field() // marker-only (no rules)
|
|
95
|
+
```
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
Each rule must be an emittable rule object created via `createRule()` or one of the built-in rule factories. Passing a raw function (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) throws `BakerError` at decorator-evaluation time.
|
|
68
98
|
|
|
69
|
-
###
|
|
99
|
+
### Options
|
|
70
100
|
|
|
71
|
-
|
|
101
|
+
Most fields need only rules. The options below cover nested, conditional, collection, and key-mapping cases — reach for them as needed.
|
|
102
|
+
|
|
103
|
+
| Option | Type | Description |
|
|
104
|
+
| --------------------------- | ------------------------------------------------- | ---------------------------------------- |
|
|
105
|
+
| `type` | `() => Dto \| [Dto] \| Set \| Map` | Nested DTO. `[Dto]` for arrays; `Set`/`Map` for collections |
|
|
106
|
+
| `discriminator` | `{ property, subTypes }` | Polymorphic dispatch (requires `type`) |
|
|
107
|
+
| `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
|
|
108
|
+
| `optional` | `boolean` | Allow undefined |
|
|
109
|
+
| `nullable` | `boolean` | Allow null |
|
|
110
|
+
| `name` | `string` | Bidirectional key mapping |
|
|
111
|
+
| `deserializeName` | `string` | Input key mapping |
|
|
112
|
+
| `serializeName` | `string` | Output key mapping |
|
|
113
|
+
| `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
|
|
114
|
+
| `groups` | `string[]` | Conditional visibility |
|
|
115
|
+
| `when` | `(obj) => boolean` | Conditional validation |
|
|
116
|
+
| `transform` | `Transformer \| Transformer[]` | Value transformer |
|
|
117
|
+
| `message` | `string \| (args) => string` | Error message override |
|
|
118
|
+
| `context` | `unknown` | Error context |
|
|
119
|
+
| `mapValue` | `() => Dto` | Map value DTO |
|
|
120
|
+
| `setValue` | `() => Dto` | Set element DTO |
|
|
121
|
+
|
|
122
|
+
### Conditional fields & custom messages
|
|
72
123
|
|
|
73
|
-
|
|
124
|
+
```typescript
|
|
125
|
+
@Recipe
|
|
126
|
+
class UserDto {
|
|
127
|
+
@Field(isString) name!: string;
|
|
74
128
|
|
|
75
|
-
|
|
129
|
+
// Validated & exposed only when a matching group is requested at runtime.
|
|
130
|
+
@Field(isString, { groups: ['admin'] }) ssn!: string;
|
|
76
131
|
|
|
77
|
-
|
|
132
|
+
// Rules apply only when the predicate returns true for the input object.
|
|
133
|
+
@Field(isString, isEmail(), { when: obj => obj.contactable === true })
|
|
134
|
+
email!: string;
|
|
78
135
|
|
|
79
|
-
|
|
136
|
+
// Override the default error message for this field's failures.
|
|
137
|
+
@Field(isString, minLength(2), { message: 'Name must be at least 2 characters' })
|
|
138
|
+
displayName!: string;
|
|
139
|
+
}
|
|
80
140
|
|
|
81
|
-
|
|
141
|
+
deserialize(UserDto, input); // `ssn` is skipped
|
|
142
|
+
deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
|
|
143
|
+
```
|
|
82
144
|
|
|
83
|
-
|
|
145
|
+
A field with no `groups` is always included; a field tagged with `groups` participates only when a matching group is passed via [runtime options](#runtime-options). See [`RuntimeOptions`](#runtime-options) for the call-site shape.
|
|
84
146
|
|
|
85
|
-
|
|
147
|
+
## Rules
|
|
86
148
|
|
|
87
|
-
|
|
149
|
+
114 built-in validation rules.
|
|
88
150
|
|
|
89
|
-
|
|
151
|
+
> **Constants vs factories:** rules listed without `()` are pre-built constants — use them bare (`@Field(isString)`). Rules shown with `()` are factories you must call (`@Field(isNumber())`). Passing a factory without calling it throws `BakerError`.
|
|
90
152
|
|
|
91
|
-
###
|
|
153
|
+
### Type Checkers
|
|
92
154
|
|
|
93
|
-
|
|
155
|
+
`isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()` needed.
|
|
94
156
|
|
|
95
|
-
|
|
157
|
+
`isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
|
|
96
158
|
|
|
97
|
-
|
|
159
|
+
### Numbers
|
|
98
160
|
|
|
99
|
-
|
|
161
|
+
`min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
|
|
100
162
|
|
|
101
|
-
|
|
163
|
+
### Strings
|
|
102
164
|
|
|
103
|
-
|
|
104
|
-
configure({
|
|
105
|
-
autoConvert: true, // coerce "123" → 123
|
|
106
|
-
allowClassDefaults: true, // use class field initializers for missing keys
|
|
107
|
-
stopAtFirstError: true, // return on first validation failure
|
|
108
|
-
forbidUnknown: true, // reject undeclared fields
|
|
109
|
-
});
|
|
110
|
-
```
|
|
165
|
+
`minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
|
|
111
166
|
|
|
112
|
-
###
|
|
167
|
+
### Formats
|
|
113
168
|
|
|
114
|
-
|
|
169
|
+
`isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
|
|
115
170
|
|
|
116
|
-
|
|
117
|
-
const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
|
|
118
|
-
```
|
|
171
|
+
### Arrays
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
const isEven = createRule({
|
|
122
|
-
name: 'isEven',
|
|
123
|
-
validate: v => typeof v === 'number' && v % 2 === 0,
|
|
124
|
-
requiresType: 'number',
|
|
125
|
-
});
|
|
126
|
-
```
|
|
173
|
+
`arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayNotContains(values)`
|
|
127
174
|
|
|
128
|
-
|
|
175
|
+
> `arrayOf(...rules)` validates each element of an array against the given rules. It is imported from the main entry (`@zipbul/baker`), not `@zipbul/baker/rules`.
|
|
129
176
|
|
|
130
|
-
|
|
177
|
+
### Common
|
|
131
178
|
|
|
132
|
-
|
|
179
|
+
`equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
|
|
133
180
|
|
|
134
|
-
|
|
135
|
-
@Field(...rules)
|
|
136
|
-
@Field(...rules, options)
|
|
137
|
-
@Field(options)
|
|
138
|
-
@Field() // marker-only (no rules)
|
|
139
|
-
```
|
|
181
|
+
### Date
|
|
140
182
|
|
|
141
|
-
|
|
183
|
+
`minDate(date)`, `maxDate(date)`
|
|
142
184
|
|
|
143
|
-
###
|
|
185
|
+
### Locale
|
|
144
186
|
|
|
145
|
-
|
|
146
|
-
| ----------------- | ------------------------------------------------- | ------------------------------ |
|
|
147
|
-
| `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
|
|
148
|
-
| `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
|
|
149
|
-
| `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
|
|
150
|
-
| `optional` | `boolean` | Allow undefined |
|
|
151
|
-
| `nullable` | `boolean` | Allow null |
|
|
152
|
-
| `name` | `string` | Bidirectional key mapping |
|
|
153
|
-
| `deserializeName` | `string` | Input key mapping |
|
|
154
|
-
| `serializeName` | `string` | Output key mapping |
|
|
155
|
-
| `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
|
|
156
|
-
| `groups` | `string[]` | Conditional visibility |
|
|
157
|
-
| `when` | `(obj) => boolean` | Conditional validation |
|
|
158
|
-
| `transform` | `Transformer \| Transformer[]` | Value transformer |
|
|
159
|
-
| `message` | `string \| (args) => string` | Error message override |
|
|
160
|
-
| `context` | `unknown` | Error context |
|
|
161
|
-
| `mapValue` | `() => Dto` | Map value DTO |
|
|
162
|
-
| `setValue` | `() => Dto` | Set element DTO |
|
|
187
|
+
`isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
|
|
163
188
|
|
|
164
189
|
## Transformers
|
|
165
190
|
|
|
@@ -190,17 +215,17 @@ import {
|
|
|
190
215
|
} from '@zipbul/baker/transformers';
|
|
191
216
|
```
|
|
192
217
|
|
|
193
|
-
| Transformer | deserialize
|
|
194
|
-
| ------------------------ |
|
|
195
|
-
| `trimTransformer` | trim string
|
|
196
|
-
| `toLowerCaseTransformer` | lowercase
|
|
197
|
-
| `toUpperCaseTransformer` | uppercase
|
|
198
|
-
| `roundTransformer(n?)` | round to n decimals
|
|
199
|
-
| `unixSecondsTransformer` | unix seconds
|
|
200
|
-
| `unixMillisTransformer` | unix ms
|
|
201
|
-
| `isoStringTransformer` | ISO string
|
|
202
|
-
| `csvTransformer(sep?)` | `"a,b"`
|
|
203
|
-
| `jsonTransformer` | JSON string
|
|
218
|
+
| Transformer | deserialize | serialize |
|
|
219
|
+
| ------------------------ | ----------------------- | ----------------------- |
|
|
220
|
+
| `trimTransformer` | trim string | trim string |
|
|
221
|
+
| `toLowerCaseTransformer` | lowercase | lowercase |
|
|
222
|
+
| `toUpperCaseTransformer` | uppercase | uppercase |
|
|
223
|
+
| `roundTransformer(n?)` | round to n decimals | round to n decimals |
|
|
224
|
+
| `unixSecondsTransformer` | unix seconds → Date | Date → unix seconds |
|
|
225
|
+
| `unixMillisTransformer` | unix ms → Date | Date → unix ms |
|
|
226
|
+
| `isoStringTransformer` | ISO string → Date | Date → ISO string |
|
|
227
|
+
| `csvTransformer(sep?)` | `"a,b"` → `["a","b"]` | `["a","b"]` → `"a,b"` |
|
|
228
|
+
| `jsonTransformer` | JSON string → object | object → JSON string |
|
|
204
229
|
|
|
205
230
|
### Transform Array Order
|
|
206
231
|
|
|
@@ -218,6 +243,8 @@ email!: string;
|
|
|
218
243
|
|
|
219
244
|
### Optional Peer Transformers
|
|
220
245
|
|
|
246
|
+
`luxonTransformer` and `momentTransformer` require their respective libraries as optional peer dependencies — install whichever you use.
|
|
247
|
+
|
|
221
248
|
```typescript
|
|
222
249
|
// bun add luxon
|
|
223
250
|
import { luxonTransformer } from '@zipbul/baker/transformers';
|
|
@@ -235,47 +262,11 @@ import { momentTransformer } from '@zipbul/baker/transformers';
|
|
|
235
262
|
const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
236
263
|
```
|
|
237
264
|
|
|
238
|
-
> **Note on `format
|
|
239
|
-
|
|
240
|
-
## Rules
|
|
241
|
-
|
|
242
|
-
105 built-in validation rules.
|
|
243
|
-
|
|
244
|
-
### Type Checkers
|
|
245
|
-
|
|
246
|
-
`isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()` needed.
|
|
247
|
-
|
|
248
|
-
`isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
|
|
249
|
-
|
|
250
|
-
### Numbers
|
|
251
|
-
|
|
252
|
-
`min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
|
|
253
|
-
|
|
254
|
-
### Strings
|
|
255
|
-
|
|
256
|
-
`minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
|
|
257
|
-
|
|
258
|
-
### Formats
|
|
259
|
-
|
|
260
|
-
`isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
|
|
261
|
-
|
|
262
|
-
### Arrays
|
|
263
|
-
|
|
264
|
-
`arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayOf(...rules)`
|
|
265
|
-
|
|
266
|
-
### Common
|
|
267
|
-
|
|
268
|
-
`equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
|
|
269
|
-
|
|
270
|
-
### Date
|
|
271
|
-
|
|
272
|
-
`minDate(date)`, `maxDate(date)`
|
|
273
|
-
|
|
274
|
-
### Locale
|
|
265
|
+
> **Note on `format`:** The `format` option in `luxonTransformer` / `momentTransformer` controls the **serialize-side output only**. On deserialize, both transformers parse the input with the library's default parser (ISO-first for Luxon, lenient parser for Moment). Using a lossy format like `'YYYY-MM-DD'` makes the transformer one-way — `serialize → deserialize` will not recover the original time of day. If you need a lossless roundtrip, omit `format` (defaults to ISO 8601).
|
|
275
266
|
|
|
276
|
-
|
|
267
|
+
## Composing DTOs
|
|
277
268
|
|
|
278
|
-
|
|
269
|
+
### Nested DTOs
|
|
279
270
|
|
|
280
271
|
```typescript
|
|
281
272
|
@Recipe
|
|
@@ -290,7 +281,7 @@ class UserDto {
|
|
|
290
281
|
}
|
|
291
282
|
```
|
|
292
283
|
|
|
293
|
-
|
|
284
|
+
### Collections
|
|
294
285
|
|
|
295
286
|
```typescript
|
|
296
287
|
@Recipe
|
|
@@ -302,7 +293,7 @@ class UserDto {
|
|
|
302
293
|
|
|
303
294
|
> Deserialize input shape: a `Set` field accepts a JSON **array**, a `Map` field accepts a plain **object** keyed by string. Serialize emits the same shapes.
|
|
304
295
|
|
|
305
|
-
|
|
296
|
+
### Discriminator
|
|
306
297
|
|
|
307
298
|
```typescript
|
|
308
299
|
@Recipe
|
|
@@ -321,7 +312,7 @@ class PetOwner {
|
|
|
321
312
|
}
|
|
322
313
|
```
|
|
323
314
|
|
|
324
|
-
|
|
315
|
+
### Inheritance
|
|
325
316
|
|
|
326
317
|
```typescript
|
|
327
318
|
@Recipe
|
|
@@ -336,6 +327,99 @@ class UserDto extends BaseDto {
|
|
|
336
327
|
}
|
|
337
328
|
```
|
|
338
329
|
|
|
330
|
+
## Runtime API
|
|
331
|
+
|
|
332
|
+
### `seal()`
|
|
333
|
+
|
|
334
|
+
**Required.** Call once at app startup, after every DTO module has been imported. Takes no arguments — seals every class registered via `@Recipe` so far, plus any nested DTOs they reach. Idempotent: a second call is a no-op.
|
|
335
|
+
|
|
336
|
+
`deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed.
|
|
337
|
+
|
|
338
|
+
### `deserialize` / `serialize` / `validate`
|
|
339
|
+
|
|
340
|
+
Three entry points share the same sync/async shape. If the DTO has any async rule or transformer on the relevant side, the call returns a `Promise`; otherwise it returns the value directly.
|
|
341
|
+
|
|
342
|
+
| Function | Signature | Returns (sync) | Notes |
|
|
343
|
+
| ----------- | ---------------------------------- | ----------------------- | -------------------------------------- |
|
|
344
|
+
| `deserialize` | `(Class, input, options?)` | `T \| BakerIssueSet` | Parse + validate. Never throws on validation failure. |
|
|
345
|
+
| `validate` | `(Class, input, options?)` | `true \| BakerIssueSet` | Validate only. |
|
|
346
|
+
| `serialize` | `(instance, options?)` | `Record<string, unknown>` | Emit a plain object. No validation. |
|
|
347
|
+
|
|
348
|
+
Async returns are wrapped: `Promise<T \| BakerIssueSet>`, `Promise<true \| BakerIssueSet>`, and `Promise<Record<string, unknown>>` respectively. The deserialize and serialize sides are independent — a DTO can be async on deserialize but sync on serialize, and vice versa.
|
|
349
|
+
|
|
350
|
+
To validate a single primitive without a DTO, call the rule directly: `isEmail()(value)`.
|
|
351
|
+
|
|
352
|
+
#### Strict variants
|
|
353
|
+
|
|
354
|
+
Each function has `*Sync` and `*Async` variants for unambiguous types:
|
|
355
|
+
|
|
356
|
+
- `deserializeSync` / `serializeSync` / `validateSync` — throw `BakerError` if the DTO is async on that side.
|
|
357
|
+
- `deserializeAsync` / `serializeAsync` / `validateAsync` — always return a `Promise` (sync DTOs are wrapped via `Promise.resolve`).
|
|
358
|
+
|
|
359
|
+
### Runtime options
|
|
360
|
+
|
|
361
|
+
`deserialize`, `serialize`, and `validate` accept an optional trailing `options` argument:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
interface RuntimeOptions {
|
|
365
|
+
groups?: string[]; // per-request group selection — see @Field `groups`
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Groups are passed at call time (not on `@Field`) because the active set typically varies per request.
|
|
370
|
+
|
|
371
|
+
### `configure(config)`
|
|
372
|
+
|
|
373
|
+
Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
configure({
|
|
377
|
+
autoConvert: true, // coerce "123" → 123
|
|
378
|
+
allowClassDefaults: true, // use class field initializers for missing keys
|
|
379
|
+
stopAtFirstError: true, // return on first validation failure
|
|
380
|
+
forbidUnknown: true, // reject undeclared fields
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `createRule(name, validate)` / `createRule(options)`
|
|
385
|
+
|
|
386
|
+
Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
const isEven = createRule({
|
|
394
|
+
name: 'isEven',
|
|
395
|
+
validate: v => typeof v === 'number' && v % 2 === 0,
|
|
396
|
+
requiresType: 'number',
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `isBakerIssueSet(value)`
|
|
401
|
+
|
|
402
|
+
Type guard. Narrows a result to `BakerIssueSet`, whose `errors` array holds `{ path, code, message?, context? }` issues.
|
|
403
|
+
|
|
404
|
+
## Error Handling
|
|
405
|
+
|
|
406
|
+
baker separates two failure modes:
|
|
407
|
+
|
|
408
|
+
- **`BakerError` (thrown)** — a programming mistake: using a DTO before `seal()`, passing a raw rule function, calling `configure()` after seal, or calling a strict `*Sync` variant on an async DTO. Fix the code; don't catch it in request handlers.
|
|
409
|
+
- **`BakerIssueSet` (returned)** — a validation failure. `deserialize` and `validate` return it instead of throwing. Guard with `isBakerIssueSet` and read `.errors`.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const result = deserialize(UserDto, input);
|
|
413
|
+
|
|
414
|
+
if (isBakerIssueSet(result)) {
|
|
415
|
+
for (const issue of result.errors) {
|
|
416
|
+
console.log(`${issue.path}: ${issue.code}`); // e.g. "email: isEmail"
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// result is a typed UserDto
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
339
423
|
## FAQ
|
|
340
424
|
|
|
341
425
|
### When should I use baker instead of class-validator?
|
|
@@ -356,7 +440,9 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
|
|
|
356
440
|
|
|
357
441
|
### How does the AOT code generation work?
|
|
358
442
|
|
|
359
|
-
Calling `seal()` once at app startup walks every registered DTO, analyzes field metadata, generates optimized JavaScript
|
|
443
|
+
Calling `seal()` once at app startup walks every registered DTO, analyzes field metadata, generates optimized JavaScript executor functions, and caches them. Subsequent `deserialize` / `serialize` / `validate` calls run the pre-compiled functions directly. There is no auto-seal — forgetting to call `seal()` raises `BakerError` on first use.
|
|
444
|
+
|
|
445
|
+
> baker builds its executors with `new Function()`. Under a strict Content-Security-Policy this requires `'unsafe-eval'`; baker will not run in environments that forbid runtime code generation.
|
|
360
446
|
|
|
361
447
|
## Exports
|
|
362
448
|
|
|
@@ -364,15 +450,17 @@ Calling `seal()` once at app startup walks every registered DTO, analyzes field
|
|
|
364
450
|
import {
|
|
365
451
|
seal,
|
|
366
452
|
deserialize, deserializeSync, deserializeAsync,
|
|
367
|
-
validate,
|
|
368
|
-
serialize,
|
|
369
|
-
configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
|
|
453
|
+
validate, validateSync, validateAsync,
|
|
454
|
+
serialize, serializeSync, serializeAsync,
|
|
455
|
+
configure, createRule, Field, Recipe, arrayOf, isBakerIssueSet, BakerError,
|
|
370
456
|
} from '@zipbul/baker';
|
|
371
|
-
import type { Transformer, TransformParams,
|
|
372
|
-
import { isString, isEmail, isULID, isCUID2
|
|
373
|
-
import { trimTransformer, jsonTransformer
|
|
457
|
+
import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
|
|
458
|
+
import { isString, isEmail, isULID, isCUID2 /* …114 rules */ } from '@zipbul/baker/rules';
|
|
459
|
+
import { trimTransformer, jsonTransformer /* …and more */ } from '@zipbul/baker/transformers';
|
|
374
460
|
```
|
|
375
461
|
|
|
462
|
+
Decorators are also available from the `@zipbul/baker/decorators` subpath.
|
|
463
|
+
|
|
376
464
|
## License
|
|
377
465
|
|
|
378
466
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export { createRule } from './src/create-rule';
|
|
|
6
6
|
export { seal } from './src/seal/seal';
|
|
7
7
|
export { Field, arrayOf, Recipe } from './src/decorators/index';
|
|
8
8
|
export type { FieldOptions, ArrayOfMarker } from './src/decorators/index';
|
|
9
|
+
export { createBaker } from './src/baker';
|
|
10
|
+
export type { Baker } from './src/baker';
|
|
11
|
+
export { ExcludeMode, RequiredType } from './src/enums';
|
|
9
12
|
export type { BakerIssue, BakerIssueSet } from './src/errors';
|
|
10
13
|
export { isBakerIssueSet, BakerError } from './src/errors';
|
|
11
14
|
export type { EmittableRule, Transformer, TransformParams } from './src/types';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{deserialize,deserializeSync,deserializeAsync}from"./src/functions/deserialize.js";export{validate,validateSync,validateAsync}from"./src/functions/validate.js";export{serialize,serializeSync,serializeAsync}from"./src/functions/serialize.js";export{configure}from"./src/configure.js";export{createRule}from"./src/create-rule.js";export{seal}from"./src/seal/seal.js";export{Field,arrayOf,Recipe}from"./src/decorators/index.js";export{isBakerIssueSet,BakerError}from"./src/errors.js";
|
|
1
|
+
export{deserialize,deserializeSync,deserializeAsync}from"./src/functions/deserialize.js";export{validate,validateSync,validateAsync}from"./src/functions/validate.js";export{serialize,serializeSync,serializeAsync}from"./src/functions/serialize.js";export{configure}from"./src/configure.js";export{createRule}from"./src/create-rule.js";export{seal}from"./src/seal/seal.js";export{Field,arrayOf,Recipe}from"./src/decorators/index.js";export{createBaker}from"./src/baker.js";export{ExcludeMode,RequiredType}from"./src/enums.js";export{isBakerIssueSet,BakerError}from"./src/errors.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BakerConfig } from './configure';
|
|
2
|
+
/**
|
|
3
|
+
* A baker scope — an isolated registration + seal boundary. Each `createBaker()` owns its own
|
|
4
|
+
* registry and config; classes sealed through it are attributed to it, so separate scopes never
|
|
5
|
+
* mix. `@Field`, rules, transformers, and `deserialize/serialize/validate` stay global (they read
|
|
6
|
+
* the metadata/executor stored on the class), so only the class-collecting `Recipe` and `seal` are
|
|
7
|
+
* scoped.
|
|
8
|
+
*/
|
|
9
|
+
export interface Baker {
|
|
10
|
+
/** Class decorator — registers the class as a root of THIS scope. Use as `@app.Recipe`. */
|
|
11
|
+
readonly Recipe: (value: Function, context: ClassDecoratorContext) => void;
|
|
12
|
+
/** Seal every root registered to this scope (and its nested DTOs) with this scope's config. */
|
|
13
|
+
readonly seal: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an isolated baker scope. Use for libraries and multi-app processes where each app must
|
|
17
|
+
* not mix with another. Single-app code can keep using the global `@Recipe` / `seal()` / `configure()`.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const app = createBaker({ autoConvert: true });
|
|
21
|
+
* @app.Recipe class UserDto { @Field(isString) name!: string }
|
|
22
|
+
* app.seal();
|
|
23
|
+
* deserialize(UserDto, input); // global — reads UserDto's sealed executor
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createBaker(config?: BakerConfig): Baker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{normalizeConfig as H}from"./configure.js";import{sealRegistry as I}from"./seal/seal.js";export function createBaker(j){const q=new Set,E=j===void 0?Object.freeze({}):H(j);let A=!1;return{Recipe(G){q.add(G)},seal(){if(A)return;I(q,E);A=!0}}}
|
package/dist/src/configure.d.ts
CHANGED
|
@@ -16,9 +16,15 @@ interface BakerConfig {
|
|
|
16
16
|
* If not called, defaults are applied.
|
|
17
17
|
*/
|
|
18
18
|
declare function configure(config: BakerConfig): void;
|
|
19
|
+
/**
|
|
20
|
+
* Validate a BakerConfig and map it to the internal SealOptions. Shared by `configure()`
|
|
21
|
+
* (default instance) and `createBaker()` (per-scope instances). Does NOT check seal state —
|
|
22
|
+
* that gate is specific to the global `configure()`.
|
|
23
|
+
*/
|
|
24
|
+
declare function normalizeConfig(config: BakerConfig): SealOptions;
|
|
19
25
|
/** @internal — used by seal. Returns the frozen global options; the only way to change them is configure(). */
|
|
20
26
|
declare function getGlobalOptions(): SealOptions;
|
|
21
27
|
/** @internal — reset to defaults on unseal */
|
|
22
28
|
declare function resetConfigForTesting(): void;
|
|
23
|
-
export { configure, getGlobalOptions, resetConfigForTesting };
|
|
29
|
+
export { configure, getGlobalOptions, resetConfigForTesting, normalizeConfig };
|
|
24
30
|
export type { BakerConfig };
|
package/dist/src/configure.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BakerError as j}from"./errors.js";import{isSealed as
|
|
1
|
+
import{BakerError as j}from"./errors.js";import{isSealed as x}from"./seal/seal-state.js";const w=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);let q=Object.freeze({});function configure(h){if(x())throw new j("[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().");q=normalizeConfig(h)}function normalizeConfig(h){if(h===null||typeof h!=="object"||Array.isArray(h))throw new j(`[baker] config requires a plain object. Received: ${h===null?"null":Array.isArray(h)?"array":typeof h}.`);for(const v of Object.keys(h))if(!w.has(v))throw new j(`[baker] unknown key '${v}'. Valid keys: ${[...w].join(", ")}.`);return Object.freeze({enableImplicitConversion:h.autoConvert??!1,exposeDefaultValues:h.allowClassDefaults??!1,stopAtFirstError:h.stopAtFirstError??!1,whitelist:h.forbidUnknown??!1,debug:h.debug??!1})}function getGlobalOptions(){return q}function resetConfigForTesting(){q=Object.freeze({})}export{configure,getGlobalOptions,resetConfigForTesting,normalizeConfig};
|