@zipbul/baker 3.4.1 → 5.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +256 -159
  3. package/dist/index.d.ts +3 -3
  4. package/dist/index.js +1 -1
  5. package/dist/src/baker.d.ts +31 -0
  6. package/dist/src/baker.js +1 -0
  7. package/dist/src/configure.d.ts +3 -8
  8. package/dist/src/configure.js +1 -1
  9. package/dist/src/create-rule.d.ts +2 -1
  10. package/dist/src/decorators/field.d.ts +2 -1
  11. package/dist/src/decorators/field.js +1 -1
  12. package/dist/src/decorators/index.d.ts +0 -1
  13. package/dist/src/decorators/index.js +1 -1
  14. package/dist/src/enums.d.ts +51 -0
  15. package/dist/src/enums.js +1 -0
  16. package/dist/src/errors.d.ts +1 -1
  17. package/dist/src/errors.js +1 -1
  18. package/dist/src/functions/check-call-options.d.ts +1 -1
  19. package/dist/src/functions/check-call-options.js +1 -1
  20. package/dist/src/functions/deserialize.d.ts +1 -1
  21. package/dist/src/functions/serialize.d.ts +1 -1
  22. package/dist/src/rule-plan.d.ts +5 -3
  23. package/dist/src/rule-plan.js +1 -1
  24. package/dist/src/rules/array.js +1 -1
  25. package/dist/src/rules/date.js +1 -1
  26. package/dist/src/rules/locales.js +1 -1
  27. package/dist/src/rules/number.js +1 -1
  28. package/dist/src/rules/object.js +1 -1
  29. package/dist/src/rules/string.js +5 -5
  30. package/dist/src/rules/typechecker.js +5 -5
  31. package/dist/src/seal/deserialize-builder.js +230 -230
  32. package/dist/src/seal/enums.d.ts +8 -0
  33. package/dist/src/seal/enums.js +1 -0
  34. package/dist/src/seal/expose-validator.js +1 -1
  35. package/dist/src/seal/seal.d.ts +10 -21
  36. package/dist/src/seal/seal.js +1 -1
  37. package/dist/src/seal/serialize-builder.js +8 -8
  38. package/dist/src/seal/validate-meta.js +1 -1
  39. package/dist/src/symbols.d.ts +1 -1
  40. package/dist/src/types.d.ts +15 -14
  41. package/package.json +1 -1
  42. package/dist/src/decorators/recipe.d.ts +0 -17
  43. package/dist/src/decorators/recipe.js +0 -1
  44. package/dist/src/registry.d.ts +0 -8
  45. package/dist/src/registry.js +0 -1
  46. package/dist/src/seal/seal-state.d.ts +0 -10
  47. package/dist/src/seal/seal-state.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @zipbul/baker
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8ea7162: Remove the global registration API in favor of the `Baker` class. `new Baker(config?)`
8
+ is now the only way to register and seal DTOs: use `@app.Recipe` to register a class and
9
+ `app.seal()` to seal it. The global `@Recipe`, `seal()`, `configure()`, and the `createBaker()`
10
+ factory have been removed — each `Baker` instance owns its own isolated registry and config, so
11
+ multiple apps in one process never mix. `@Field`, the rule/transformer factories, and
12
+ `deserialize`/`validate`/`serialize` are unchanged.
13
+
14
+ Migration: replace `configure(opts)` + global `@Recipe`/`seal()` with
15
+ `const app = new Baker(opts); @app.Recipe class Dto {}; app.seal();`.
16
+
17
+ ## 4.0.0
18
+
19
+ ### Major Changes
20
+
21
+ - 98c9a0a: **Breaking:** rule type hints and field exclusion are now enums instead of string literals.
22
+ `createRule({ requiresType: 'number' })` becomes `requiresType: RequiredType.Number`, and
23
+ `@Field({ exclude: 'serializeOnly' })` becomes `exclude: ExcludeMode.SerializeOnly`. Runtime
24
+ behaviour is unchanged — the enums are string-valued, so generated code and validation results are
25
+ identical; only the public type surface changed. `RequiredType` and `ExcludeMode` are now exported.
26
+
27
+ ### Minor Changes
28
+
29
+ - 98c9a0a: Add `createBaker()` for multi-app isolation. Each scope owns its own registration and config, so
30
+ multiple apps in one process — or a bundler-duplicated copy of baker — no longer fragment `seal()`
31
+ (the previous "`<Class> is not sealed`" failure). Use:
32
+
33
+ ```ts
34
+ const app = createBaker({ autoConvert: true });
35
+ @app.Recipe
36
+ class UserDto {
37
+ @Field(isString) name!: string;
38
+ }
39
+ app.seal();
40
+ deserialize(UserDto, input);
41
+ ```
42
+
43
+ `@Field`, rules, and `deserialize/serialize/validate` stay global. Distinct classes are fully
44
+ isolated (each sealed with its scope's config); a class shared across scopes is reused as one sealed
45
+ form. Single-app code is unchanged — global `@Recipe` / `seal()` / `configure()` still work. Exports
46
+ `createBaker` and the `Baker` type.
47
+
3
48
  ## 3.4.1
4
49
 
5
50
  ### Patch Changes
package/README.md CHANGED
@@ -1,165 +1,195 @@
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
 
15
29
  ```typescript
16
- import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
30
+ import { Baker, Field, deserialize, isBakerIssueSet } from '@zipbul/baker';
17
31
  import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
18
32
 
19
- @Recipe
33
+ const baker = new Baker();
34
+
35
+ @baker.Recipe
20
36
  class UserDto {
21
37
  @Field(isString, minLength(2)) name!: string;
22
38
  @Field(isNumber(), min(0)) age!: number;
23
39
  @Field(isString, isEmail()) email!: string;
24
40
  }
25
41
 
26
- // Call once at app startup, after all DTOs are loaded.
27
- seal();
42
+ // Call once at startup, after this baker's DTOs are defined.
43
+ baker.seal();
28
44
 
29
- const result = await deserialize(UserDto, {
45
+ // All rules here are sync, so deserialize returns the value directly (no await).
46
+ const result = deserialize(UserDto, {
30
47
  name: 'Alice',
31
48
  age: 30,
32
49
  email: 'alice@test.com',
33
50
  });
34
51
 
35
52
  if (isBakerIssueSet(result)) {
36
- console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
53
+ // Reached only for invalid input, e.g. [{ path: 'email', code: 'isEmail' }]
54
+ console.log(result.errors);
37
55
  } else {
38
56
  console.log(result.name); // 'Alice' — typed as UserDto
39
57
  }
40
58
  ```
41
59
 
42
- ## Why Baker?
60
+ `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)).
61
+
62
+ ## Core Concepts
43
63
 
44
- Baker generates optimized JavaScript functions once on first seal, then executes them on every call.
64
+ | Concept | What it does |
65
+ | ------------------- | ------------------------------------------------------------------------------ |
66
+ | `new Baker(config?)` | An isolated registration + seal scope. Multiple bakers never mix. Use `@app.Recipe` and `app.seal()`. |
67
+ | `@app.Recipe` | Marks a class as a DTO of that baker. Only `@Field` properties are part of the contract. |
68
+ | `@Field(...rules)` | Declares a validated field. Global — works with any baker. |
69
+ | `app.seal()` | Compiles that baker's DTOs into executor functions. Call once, at startup. |
70
+ | `deserialize` / `validate` / `serialize` | Global — run the sealed executors stored on the class: parse+validate, validate-only, or emit a plain object. |
45
71
 
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 |
72
+ > Examples below assume a `const baker = new Baker()` in scope and a single `baker.seal()` after the DTOs are defined.
73
+
74
+ ## Why baker?
75
+
76
+ baker generates optimized JavaScript functions once on first seal, then executes them on every call — no per-call rule interpretation.
77
+
78
+ | Feature | baker | class-validator | Zod |
79
+ | ------------------ | -------------------- | ---------------------- | ------------------- |
80
+ | Approach | AOT code generation | Runtime interpretation | Schema method chain |
81
+ | Decorators | `@Field` (unified) | 30+ individual | N/A |
82
+ | `reflect-metadata` | Not needed | Required | N/A |
83
+ | Sync DTO return | Direct value | Promise | Direct value |
54
84
 
55
85
  ## Performance
56
86
 
57
- Benchmarked against multiple libraries on simple, nested, array, and error-collection scenarios. Exact numbers vary by machine and runtime.
87
+ 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
88
 
59
- See [`bench/`](./bench) for the current benchmark suite and exact scenarios.
89
+ ## @Field Decorator
60
90
 
61
- ## API
91
+ One decorator for everything — replaces 30+ individual decorators from class-validator.
62
92
 
63
- ### `seal(...classes?)`
93
+ 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
94
 
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.
95
+ ```typescript
96
+ @Field(...rules)
97
+ @Field(...rules, options)
98
+ @Field(options)
99
+ @Field() // marker-only (no rules)
100
+ ```
66
101
 
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.
102
+ 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
103
 
69
- ### `deserialize<T>(Class, input, options?)`
104
+ ### Options
70
105
 
71
- Returns `T | BakerIssueSet` for sync DTOs, `Promise<T | BakerIssueSet>` for async DTOs. Never throws on validation failure.
106
+ Most fields need only rules. The options below cover nested, conditional, collection, and key-mapping cases reach for them as needed.
107
+
108
+ | Option | Type | Description |
109
+ | --------------------------- | ------------------------------------------------- | ---------------------------------------- |
110
+ | `type` | `() => Dto \| [Dto] \| Set \| Map` | Nested DTO. `[Dto]` for arrays; `Set`/`Map` for collections |
111
+ | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch (requires `type`) |
112
+ | `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
113
+ | `optional` | `boolean` | Allow undefined |
114
+ | `nullable` | `boolean` | Allow null |
115
+ | `name` | `string` | Bidirectional key mapping |
116
+ | `deserializeName` | `string` | Input key mapping |
117
+ | `serializeName` | `string` | Output key mapping |
118
+ | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
119
+ | `groups` | `string[]` | Conditional visibility |
120
+ | `when` | `(obj) => boolean` | Conditional validation |
121
+ | `transform` | `Transformer \| Transformer[]` | Value transformer |
122
+ | `message` | `string \| (args) => string` | Error message override |
123
+ | `context` | `unknown` | Error context |
124
+ | `mapValue` | `() => Dto` | Map value DTO |
125
+ | `setValue` | `() => Dto` | Set element DTO |
126
+
127
+ ### Conditional fields & custom messages
72
128
 
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).
129
+ ```typescript
130
+ @baker.Recipe
131
+ class UserDto {
132
+ @Field(isString) name!: string;
74
133
 
75
- ### `deserializeSync<T>` / `deserializeAsync<T>`
134
+ // Validated & exposed only when a matching group is requested at runtime.
135
+ @Field(isString, { groups: ['admin'] }) ssn!: string;
76
136
 
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`).
137
+ // Rules apply only when the predicate returns true for the input object.
138
+ @Field(isString, isEmail(), { when: obj => obj.contactable === true })
139
+ email!: string;
78
140
 
79
- ### `serialize<T>(instance, options?)`
141
+ // Override the default error message for this field's failures.
142
+ @Field(isString, minLength(2), { message: 'Name must be at least 2 characters' })
143
+ displayName!: string;
144
+ }
80
145
 
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.
146
+ deserialize(UserDto, input); // `ssn` is skipped
147
+ deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
148
+ ```
82
149
 
83
- ### `serializeSync<T>` / `serializeAsync<T>`
150
+ 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
151
 
85
- Strict variants. `serializeSync` throws `BakerError` if the DTO is async on the serialize side.
152
+ ## Rules
86
153
 
87
- ### `validate(Class, input, options?)`
154
+ 114 built-in validation rules.
88
155
 
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)`).
156
+ > **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
157
 
91
- ### `validateSync` / `validateAsync`
158
+ ### Type Checkers
92
159
 
93
- Strict variants. `validateSync` throws `BakerError` if the DTO is async; `validateAsync` always returns `Promise`.
160
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` constants, no `()` needed.
94
161
 
95
- ### `isBakerIssueSet(value)`
162
+ `isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
96
163
 
97
- Type guard. Narrows result to `BakerIssueSet` containing `{ path, code, message?, context? }[]`.
164
+ ### Numbers
98
165
 
99
- ### `configure(config)`
166
+ `min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
100
167
 
101
- Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`; reconfiguring requires `unseal()` (test-only helper) + `configure(...)` + `seal()` again.
168
+ ### Strings
102
169
 
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
- ```
170
+ `minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
111
171
 
112
- ### `createRule(name, validate)` / `createRule(options)`
172
+ ### Formats
113
173
 
114
- Custom validation rule. Two forms a `(name, validate)` shorthand or an options object:
174
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
115
175
 
116
- ```typescript
117
- const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
118
- ```
176
+ ### Arrays
119
177
 
120
- ```typescript
121
- const isEven = createRule({
122
- name: 'isEven',
123
- validate: v => typeof v === 'number' && v % 2 === 0,
124
- requiresType: 'number',
125
- });
126
- ```
178
+ `arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayNotContains(values)`
127
179
 
128
- ## @Field Decorator
180
+ > `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
181
 
130
- One decorator for everything — replaces 30+ individual decorators from class-validator.
182
+ ### Common
131
183
 
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.
184
+ `equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
133
185
 
134
- ```typescript
135
- @Field(...rules)
136
- @Field(...rules, options)
137
- @Field(options)
138
- @Field() // marker-only (no rules)
139
- ```
186
+ ### Date
140
187
 
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.
188
+ `minDate(date)`, `maxDate(date)`
142
189
 
143
- ### Options
190
+ ### Locale
144
191
 
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 |
192
+ `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
163
193
 
164
194
  ## Transformers
165
195
 
@@ -190,17 +220,17 @@ import {
190
220
  } from '@zipbul/baker/transformers';
191
221
  ```
192
222
 
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 |
223
+ | Transformer | deserialize | serialize |
224
+ | ------------------------ | ----------------------- | ----------------------- |
225
+ | `trimTransformer` | trim string | trim string |
226
+ | `toLowerCaseTransformer` | lowercase | lowercase |
227
+ | `toUpperCaseTransformer` | uppercase | uppercase |
228
+ | `roundTransformer(n?)` | round to n decimals | round to n decimals |
229
+ | `unixSecondsTransformer` | unix seconds Date | Date unix seconds |
230
+ | `unixMillisTransformer` | unix ms Date | Date unix ms |
231
+ | `isoStringTransformer` | ISO string Date | Date ISO string |
232
+ | `csvTransformer(sep?)` | `"a,b"` `["a","b"]` | `["a","b"]` `"a,b"` |
233
+ | `jsonTransformer` | JSON string object | object JSON string |
204
234
 
205
235
  ### Transform Array Order
206
236
 
@@ -218,12 +248,14 @@ email!: string;
218
248
 
219
249
  ### Optional Peer Transformers
220
250
 
251
+ `luxonTransformer` and `momentTransformer` require their respective libraries as optional peer dependencies — install whichever you use.
252
+
221
253
  ```typescript
222
254
  // bun add luxon
223
255
  import { luxonTransformer } from '@zipbul/baker/transformers';
224
256
  const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
225
257
 
226
- @Recipe
258
+ @baker.Recipe
227
259
  class EventDto {
228
260
  @Field({ transform: luxon }) startAt!: DateTime;
229
261
  }
@@ -235,65 +267,29 @@ import { momentTransformer } from '@zipbul/baker/transformers';
235
267
  const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
236
268
  ```
237
269
 
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
270
+ > **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).
255
271
 
256
- `minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
257
-
258
- ### Formats
272
+ ## Composing DTOs
259
273
 
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
275
-
276
- `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
277
-
278
- ## Nested DTOs
274
+ ### Nested DTOs
279
275
 
280
276
  ```typescript
281
- @Recipe
277
+ @baker.Recipe
282
278
  class AddressDto {
283
279
  @Field(isString) city!: string;
284
280
  }
285
281
 
286
- @Recipe
282
+ @baker.Recipe
287
283
  class UserDto {
288
284
  @Field({ type: () => AddressDto }) address!: AddressDto;
289
285
  @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
290
286
  }
291
287
  ```
292
288
 
293
- ## Collections
289
+ ### Collections
294
290
 
295
291
  ```typescript
296
- @Recipe
292
+ @baker.Recipe
297
293
  class UserDto {
298
294
  @Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
299
295
  @Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
@@ -302,10 +298,10 @@ class UserDto {
302
298
 
303
299
  > 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
300
 
305
- ## Discriminator
301
+ ### Discriminator
306
302
 
307
303
  ```typescript
308
- @Recipe
304
+ @baker.Recipe
309
305
  class PetOwner {
310
306
  @Field({
311
307
  type: () => CatDto,
@@ -321,21 +317,118 @@ class PetOwner {
321
317
  }
322
318
  ```
323
319
 
324
- ## Inheritance
320
+ ### Inheritance
325
321
 
326
322
  ```typescript
327
- @Recipe
323
+ @baker.Recipe
328
324
  class BaseDto {
329
325
  @Field(isString) id!: string;
330
326
  }
331
327
 
332
- @Recipe
328
+ @baker.Recipe
333
329
  class UserDto extends BaseDto {
334
330
  @Field(isString) name!: string;
335
331
  // inherits 'id' field with isString rule
336
332
  }
337
333
  ```
338
334
 
335
+ ## Runtime API
336
+
337
+ ### `new Baker(config?)`
338
+
339
+ A `Baker` is an isolated registration + seal scope. Construct one per app/library; multiple bakers in one process never mix.
340
+
341
+ - `@app.Recipe` — class decorator; registers the class as one of this baker's DTOs.
342
+ - `app.seal()` — **required.** Compiles the baker's DTOs (and any nested DTOs they reach) into executor functions. Call once at startup, after the baker's DTOs are defined. Idempotent.
343
+ - Config is passed to the constructor:
344
+
345
+ ```typescript
346
+ const app = new Baker({
347
+ autoConvert: true, // coerce "123" → 123
348
+ allowClassDefaults: true, // use class field initializers for missing keys
349
+ stopAtFirstError: true, // return on first validation failure
350
+ forbidUnknown: true, // reject undeclared fields
351
+ });
352
+ ```
353
+
354
+ `deserialize` / `serialize` / `validate` are global — they read the sealed executor off the class — and throw `BakerError` if the class is not sealed.
355
+
356
+ **Isolation:** distinct classes are fully isolated (each sealed with its baker's config). A class shared across bakers is sealed once (first seal wins) — use separate classes if you need different config.
357
+
358
+ ### `deserialize` / `serialize` / `validate`
359
+
360
+ 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.
361
+
362
+ | Function | Signature | Returns (sync) | Notes |
363
+ | ----------- | ---------------------------------- | ----------------------- | -------------------------------------- |
364
+ | `deserialize` | `(Class, input, options?)` | `T \| BakerIssueSet` | Parse + validate. Never throws on validation failure. |
365
+ | `validate` | `(Class, input, options?)` | `true \| BakerIssueSet` | Validate only. |
366
+ | `serialize` | `(instance, options?)` | `Record<string, unknown>` | Emit a plain object. No validation. |
367
+
368
+ 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.
369
+
370
+ To validate a single primitive without a DTO, call the rule directly: `isEmail()(value)`.
371
+
372
+ #### Strict variants
373
+
374
+ Each function has `*Sync` and `*Async` variants for unambiguous types:
375
+
376
+ - `deserializeSync` / `serializeSync` / `validateSync` — throw `BakerError` if the DTO is async on that side.
377
+ - `deserializeAsync` / `serializeAsync` / `validateAsync` — always return a `Promise` (sync DTOs are wrapped via `Promise.resolve`).
378
+
379
+ ### Runtime options
380
+
381
+ `deserialize`, `serialize`, and `validate` accept an optional trailing `options` argument:
382
+
383
+ ```typescript
384
+ interface RuntimeOptions {
385
+ groups?: string[]; // per-request group selection — see @Field `groups`
386
+ }
387
+ ```
388
+
389
+ Groups are passed at call time (not on `@Field`) because the active set typically varies per request.
390
+
391
+ ### `createRule(name, validate)` / `createRule(options)`
392
+
393
+ Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
394
+
395
+ ```typescript
396
+ const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
397
+ ```
398
+
399
+ ```typescript
400
+ import { RequiredType } from '@zipbul/baker';
401
+
402
+ const isEven = createRule({
403
+ name: 'isEven',
404
+ validate: v => typeof v === 'number' && v % 2 === 0,
405
+ requiresType: RequiredType.Number,
406
+ });
407
+ ```
408
+
409
+ ### `isBakerIssueSet(value)`
410
+
411
+ Type guard. Narrows a result to `BakerIssueSet`, whose `errors` array holds `{ path, code, message?, context? }` issues.
412
+
413
+ ## Error Handling
414
+
415
+ baker separates two failure modes:
416
+
417
+ - **`BakerError` (thrown)** — a programming mistake: using a DTO before `app.seal()`, passing a raw rule function, an unknown config key, or calling a strict `*Sync` variant on an async DTO. Fix the code; don't catch it in request handlers.
418
+ - **`BakerIssueSet` (returned)** — a validation failure. `deserialize` and `validate` return it instead of throwing. Guard with `isBakerIssueSet` and read `.errors`.
419
+
420
+ ```typescript
421
+ const result = deserialize(UserDto, input);
422
+
423
+ if (isBakerIssueSet(result)) {
424
+ for (const issue of result.errors) {
425
+ console.log(`${issue.path}: ${issue.code}`); // e.g. "email: isEmail"
426
+ }
427
+ } else {
428
+ // result is a typed UserDto
429
+ }
430
+ ```
431
+
339
432
  ## FAQ
340
433
 
341
434
  ### When should I use baker instead of class-validator?
@@ -356,23 +449,27 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
356
449
 
357
450
  ### How does the AOT code generation work?
358
451
 
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.
452
+ Calling `app.seal()` once at startup walks the baker's DTOs (and their nested DTOs), analyzes field metadata, generates optimized JavaScript executor functions, and stores them on each class. Subsequent `deserialize` / `serialize` / `validate` calls run the pre-compiled functions directly. There is no auto-seal — using a DTO before `app.seal()` raises `BakerError`.
453
+
454
+ > 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
455
 
361
456
  ## Exports
362
457
 
363
458
  ```typescript
364
459
  import {
365
- seal,
460
+ Baker,
366
461
  deserialize, deserializeSync, deserializeAsync,
367
- validate, validateSync, validateAsync,
368
- serialize, serializeSync, serializeAsync,
369
- configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
462
+ validate, validateSync, validateAsync,
463
+ serialize, serializeSync, serializeAsync,
464
+ createRule, Field, arrayOf, isBakerIssueSet, BakerError, RequiredType, ExcludeMode,
370
465
  } 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';
466
+ import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions, BakerConfig } from '@zipbul/baker';
467
+ import { isString, isEmail, isULID, isCUID2 /* …114 rules */ } from '@zipbul/baker/rules';
468
+ import { trimTransformer, jsonTransformer /* …and more */ } from '@zipbul/baker/transformers';
374
469
  ```
375
470
 
471
+ Decorators are also available from the `@zipbul/baker/decorators` subpath.
472
+
376
473
  ## License
377
474
 
378
475
  MIT
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export { deserialize, deserializeSync, deserializeAsync } from './src/functions/deserialize';
2
2
  export { validate, validateSync, validateAsync } from './src/functions/validate';
3
3
  export { serialize, serializeSync, serializeAsync } from './src/functions/serialize';
4
- export { configure } from './src/configure';
5
4
  export { createRule } from './src/create-rule';
6
- export { seal } from './src/seal/seal';
7
- export { Field, arrayOf, Recipe } from './src/decorators/index';
5
+ export { Field, arrayOf } from './src/decorators/index';
8
6
  export type { FieldOptions, ArrayOfMarker } from './src/decorators/index';
7
+ export { Baker } from './src/baker';
8
+ export { ExcludeMode, RequiredType } from './src/enums';
9
9
  export type { BakerIssue, BakerIssueSet } from './src/errors';
10
10
  export { isBakerIssueSet, BakerError } from './src/errors';
11
11
  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{createRule}from"./src/create-rule.js";export{Field,arrayOf}from"./src/decorators/index.js";export{Baker}from"./src/baker.js";export{ExcludeMode,RequiredType}from"./src/enums.js";export{isBakerIssueSet,BakerError}from"./src/errors.js";