@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 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. Generates optimized validation and serialization code on first seal, then reuses the sealed executors on every call.
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. 99%+ line coverage.
9
+ Zero `reflect-metadata`. Sealed codegen.
10
10
 
11
- > **Requires Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
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 all DTOs are loaded.
40
+ // Call once at app startup, after every DTO has been imported.
27
41
  seal();
28
42
 
29
- const result = await deserialize(UserDto, {
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
- console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
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
- ## Why Baker?
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
- Baker generates optimized JavaScript functions once on first seal, then executes them on every call.
69
+ ## Why baker?
45
70
 
46
- | Feature | baker | class-validator | Zod |
47
- | ----------------------- | -------------------- | ---------------------- | ------------------- |
48
- | Valid path (5 fields) | **fast sealed path** | slower | slower |
49
- | Invalid path (5 fields) | **fast sealed path** | slower | slower |
50
- | Approach | AOT code generation | Runtime interpretation | Schema method chain |
51
- | Decorators | `@Field` (unified) | 30+ individual | N/A |
52
- | `reflect-metadata` | Not needed | Required | N/A |
53
- | Sync DTO return | Direct value | Promise | Direct value |
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
- See [`bench/`](./bench) for the current benchmark suite and exact scenarios.
84
+ ## @Field Decorator
60
85
 
61
- ## API
86
+ One decorator for everything — replaces 30+ individual decorators from class-validator.
62
87
 
63
- ### `seal(...classes?)`
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
- **Required.** Call once at app startup, after every DTO module has been imported. With no arguments, seals every class registered via `@Field` so far. With class arguments, seals only those (and any nested DTOs they reach). Idempotent.
90
+ ```typescript
91
+ @Field(...rules)
92
+ @Field(...rules, options)
93
+ @Field(options)
94
+ @Field() // marker-only (no rules)
95
+ ```
66
96
 
67
- `deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed. Tests that need to mutate decorator metadata should call `seal()` after each `configure(...)` reconfiguration.
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
- ### `deserialize<T>(Class, input, options?)`
99
+ ### Options
70
100
 
71
- Returns `T | BakerIssueSet` for sync DTOs, `Promise<T | BakerIssueSet>` for async DTOs. Never throws on validation failure.
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
- If the DTO has any async rule or transformer, `deserialize` returns a `Promise`. Otherwise it returns the value directly. For full type safety pick a strict variant (see below).
124
+ ```typescript
125
+ @Recipe
126
+ class UserDto {
127
+ @Field(isString) name!: string;
74
128
 
75
- ### `deserializeSync<T>` / `deserializeAsync<T>`
129
+ // Validated & exposed only when a matching group is requested at runtime.
130
+ @Field(isString, { groups: ['admin'] }) ssn!: string;
76
131
 
77
- Strict variants. `deserializeSync` throws `BakerError` if the DTO is async on the deserialize side. `deserializeAsync` always returns `Promise` (sync DTOs are wrapped via `Promise.resolve`).
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
- ### `serialize<T>(instance, options?)`
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
- Returns `Record<string, unknown>` for sync DTOs, `Promise<Record<string, unknown>>` for async DTOs. No validation. Async asymmetry: `_isSerializeAsync` is independent of `_isAsync` — a DTO can be async on deserialize but sync on serialize, and vice versa.
141
+ deserialize(UserDto, input); // `ssn` is skipped
142
+ deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
143
+ ```
82
144
 
83
- ### `serializeSync<T>` / `serializeAsync<T>`
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
- Strict variants. `serializeSync` throws `BakerError` if the DTO is async on the serialize side.
147
+ ## Rules
86
148
 
87
- ### `validate(Class, input, options?)`
149
+ 114 built-in validation rules.
88
150
 
89
- Validates `input` against a decorated class's schema. Returns `true | BakerIssueSet` for sync paths, `Promise<true | BakerIssueSet>` for async paths. To validate a single primitive without a DTO, call the rule directly (e.g. `isEmail()(value)`).
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
- ### `validateSync` / `validateAsync`
153
+ ### Type Checkers
92
154
 
93
- Strict variants. `validateSync` throws `BakerError` if the DTO is async; `validateAsync` always returns `Promise`.
155
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` constants, no `()` needed.
94
156
 
95
- ### `isBakerIssueSet(value)`
157
+ `isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
96
158
 
97
- Type guard. Narrows result to `BakerIssueSet` containing `{ path, code, message?, context? }[]`.
159
+ ### Numbers
98
160
 
99
- ### `configure(config)`
161
+ `min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
100
162
 
101
- Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`; reconfiguring requires `unseal()` (test-only helper) + `configure(...)` + `seal()` again.
163
+ ### Strings
102
164
 
103
- ```typescript
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
- ### `createRule(name, validate)` / `createRule(options)`
167
+ ### Formats
113
168
 
114
- Custom validation rule. Two forms a `(name, validate)` shorthand or an options object:
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
- ```typescript
117
- const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
118
- ```
171
+ ### Arrays
119
172
 
120
- ```typescript
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
- ## @Field Decorator
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
- One decorator for everything — replaces 30+ individual decorators from class-validator.
177
+ ### Common
131
178
 
132
- 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.
179
+ `equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
133
180
 
134
- ```typescript
135
- @Field(...rules)
136
- @Field(...rules, options)
137
- @Field(options)
138
- @Field() // marker-only (no rules)
139
- ```
181
+ ### Date
140
182
 
141
- 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.
183
+ `minDate(date)`, `maxDate(date)`
142
184
 
143
- ### Options
185
+ ### Locale
144
186
 
145
- | Option | Type | Description |
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 | serialize |
194
- | ------------------------ | -------------------------- | -------------------------- |
195
- | `trimTransformer` | trim string | trim string |
196
- | `toLowerCaseTransformer` | lowercase | lowercase |
197
- | `toUpperCaseTransformer` | uppercase | uppercase |
198
- | `roundTransformer(n?)` | round to n decimals | round to n decimals |
199
- | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
200
- | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
201
- | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
202
- | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
203
- | `jsonTransformer` | JSON string &rarr; object | object &rarr; 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`**: 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).
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
- `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
267
+ ## Composing DTOs
277
268
 
278
- ## Nested DTOs
269
+ ### Nested DTOs
279
270
 
280
271
  ```typescript
281
272
  @Recipe
@@ -290,7 +281,7 @@ class UserDto {
290
281
  }
291
282
  ```
292
283
 
293
- ## Collections
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
- ## Discriminator
296
+ ### Discriminator
306
297
 
307
298
  ```typescript
308
299
  @Recipe
@@ -321,7 +312,7 @@ class PetOwner {
321
312
  }
322
313
  ```
323
314
 
324
- ## Inheritance
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 validation functions via `new Function()`, and caches them. Subsequent `deserialize`/`serialize`/`validate` calls execute the pre-compiled functions directly. There is no auto-seal — forgetting to call `seal()` raises `BakerError` on first use.
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, validateSync, validateAsync,
368
- serialize, serializeSync, serializeAsync,
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, BakerError, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
372
- import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
373
- import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
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}}}
@@ -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 };
@@ -1 +1 @@
1
- import{BakerError as j}from"./errors.js";import{isSealed as w}from"./seal/seal-state.js";const v=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);let m=Object.freeze({});function configure(h){if(w())throw new j("[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().");if(h===null||typeof h!=="object"||Array.isArray(h))throw new j(`[baker] configure() requires a plain object. Received: ${h===null?"null":Array.isArray(h)?"array":typeof h}.`);for(const q of Object.keys(h))if(!v.has(q))throw new j(`[baker] configure(): unknown key '${q}'. Valid keys: ${[...v].join(", ")}.`);m=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 m}function resetConfigForTesting(){m=Object.freeze({})}export{configure,getGlobalOptions,resetConfigForTesting};
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};