@zipbul/baker 2.2.0 → 3.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 (87) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/MIGRATION-3.0.md +104 -0
  3. package/README.md +109 -63
  4. package/dist/index.d.ts +7 -6
  5. package/dist/index.js +10 -321
  6. package/dist/src/collect.d.ts +13 -10
  7. package/dist/src/collect.js +26 -0
  8. package/dist/src/configure.d.ts +8 -6
  9. package/dist/src/configure.js +43 -0
  10. package/dist/src/create-rule.js +41 -0
  11. package/dist/src/decorators/field.d.ts +22 -18
  12. package/dist/src/decorators/field.js +268 -0
  13. package/dist/src/decorators/index.d.ts +1 -0
  14. package/dist/src/decorators/index.js +2 -2
  15. package/dist/src/decorators/recipe.d.ts +17 -0
  16. package/dist/src/decorators/recipe.js +23 -0
  17. package/dist/src/errors.d.ts +27 -17
  18. package/dist/src/errors.js +52 -0
  19. package/dist/src/functions/check-call-options.d.ts +8 -0
  20. package/dist/src/functions/check-call-options.js +51 -0
  21. package/dist/src/functions/deserialize.d.ts +13 -6
  22. package/dist/src/functions/deserialize.js +57 -0
  23. package/dist/src/functions/serialize.d.ts +10 -4
  24. package/dist/src/functions/serialize.js +52 -0
  25. package/dist/src/functions/validate.d.ts +13 -10
  26. package/dist/src/functions/validate.js +49 -0
  27. package/dist/src/interfaces.d.ts +1 -1
  28. package/dist/src/interfaces.js +4 -0
  29. package/dist/src/meta-access.d.ts +19 -0
  30. package/dist/src/meta-access.js +75 -0
  31. package/dist/src/registry.js +8 -0
  32. package/dist/src/rule-metadata.d.ts +11 -0
  33. package/dist/src/rule-metadata.js +17 -0
  34. package/dist/src/rule-plan.d.ts +10 -11
  35. package/dist/src/rule-plan.js +117 -0
  36. package/dist/src/rules/array.d.ts +7 -6
  37. package/dist/src/rules/array.js +96 -0
  38. package/dist/src/rules/common.js +77 -0
  39. package/dist/src/rules/date.js +35 -0
  40. package/dist/src/rules/index.d.ts +2 -4
  41. package/dist/src/rules/index.js +8 -21
  42. package/dist/src/rules/locales.d.ts +5 -4
  43. package/dist/src/rules/locales.js +249 -0
  44. package/dist/src/rules/number.js +79 -0
  45. package/dist/src/rules/object.d.ts +1 -1
  46. package/dist/src/rules/object.js +49 -0
  47. package/dist/src/rules/string.d.ts +83 -80
  48. package/dist/src/rules/string.js +1998 -0
  49. package/dist/src/rules/typechecker.js +143 -0
  50. package/dist/src/seal/circular-analyzer.js +63 -0
  51. package/dist/src/seal/codegen-utils.js +18 -0
  52. package/dist/src/seal/deserialize-builder.d.ts +8 -4
  53. package/dist/src/seal/deserialize-builder.js +1546 -0
  54. package/dist/src/seal/expose-validator.d.ts +3 -2
  55. package/dist/src/seal/expose-validator.js +65 -0
  56. package/dist/src/seal/seal-state.d.ts +10 -0
  57. package/dist/src/seal/seal-state.js +18 -0
  58. package/dist/src/seal/seal.d.ts +22 -21
  59. package/dist/src/seal/seal.js +431 -0
  60. package/dist/src/seal/serialize-builder.d.ts +3 -2
  61. package/dist/src/seal/serialize-builder.js +374 -0
  62. package/dist/src/seal/validate-meta.d.ts +13 -0
  63. package/dist/src/seal/validate-meta.js +61 -0
  64. package/dist/src/symbols.d.ts +1 -1
  65. package/dist/src/symbols.js +13 -2
  66. package/dist/src/transformers/collection.transformer.js +25 -0
  67. package/dist/src/transformers/date.transformer.js +18 -0
  68. package/dist/src/transformers/index.js +6 -2
  69. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  70. package/dist/src/transformers/luxon.transformer.js +34 -0
  71. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  72. package/dist/src/transformers/moment.transformer.js +32 -0
  73. package/dist/src/transformers/number.transformer.js +8 -0
  74. package/dist/src/transformers/string.transformer.js +12 -0
  75. package/dist/src/types.d.ts +27 -25
  76. package/dist/src/types.js +1 -0
  77. package/dist/src/utils.d.ts +2 -2
  78. package/dist/src/utils.js +10 -0
  79. package/package.json +80 -67
  80. package/dist/index-03cysbck.js +0 -3
  81. package/dist/index-dcbd798a.js +0 -3
  82. package/dist/index-jp2yjd6g.js +0 -3
  83. package/dist/index-mw7met6r.js +0 -3
  84. package/dist/index-xdn55cz3.js +0 -1
  85. package/dist/src/functions/_run-sealed.d.ts +0 -7
  86. package/dist/src/functions/index.d.ts +0 -3
  87. package/dist/src/seal/index.d.ts +0 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,256 @@
1
+ # @zipbul/baker
2
+
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 421fd54: 3.0 — error system redesign and API hardening (breaking).
8
+
9
+ **Error channel.** A single `BakerError` class is now thrown for every developer/config/schema
10
+ misuse (it carries `cause`). The validation-result types are renamed for clarity:
11
+
12
+ - `SealError` → `BakerError` (the thrown class)
13
+ - the field-error interface `BakerError` → `BakerIssue`
14
+ - `BakerErrors` → `BakerIssueSet`
15
+ - `isBakerError` → `isBakerIssueSet`
16
+
17
+ The split is now explicit: **throw `BakerError`** for misuse discoverable without input;
18
+ **return `BakerIssueSet`** for external-input validation failures from `deserialize`/`validate`.
19
+
20
+ **API hardening.** `validate(Class, input)` is DTO-only (the ad-hoc `validate(value, ...rules)`
21
+ mode was removed — call a rule directly instead). `configure()` rejects unknown keys and
22
+ post-`seal()` calls, and seal-time options can no longer be passed per-call.
23
+
24
+ See `MIGRATION-3.0.md` for the full upgrade guide.
25
+
26
+ ### Minor Changes
27
+
28
+ - 421fd54: Add the `isHttpToken` rule — validates the RFC 9110 §5.6.2 HTTP `token` production
29
+ (`1*tchar`), used for HTTP method names and header field-names. Usable as a predicate
30
+ (`isHttpToken(value)`) or as `@Field(isHttpToken)`, and exported from `@zipbul/baker/rules`.
31
+
32
+ ## 3.0.0
33
+
34
+ ### DX reform — breaking changes
35
+
36
+ - **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `SealError`.
37
+ - Migration: import `seal` and call `seal()` once before any deserialize/serialize/validate call. For tests, call `seal()` after each `unseal()` / `configure(...)` reconfiguration.
38
+ - **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `SealError`. Move those keys into `configure({...})` before `seal()`.
39
+ - **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `SealError` immediately with the four valid forms listed in the message.
40
+ - **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `TypeError` — previously the key was silently coerced via `[object Object]` and collided.
41
+
42
+ ### API additions
43
+
44
+ - `seal(...classes?)` — explicit AOT seal trigger.
45
+ - `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `SealError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
46
+
47
+ ### Defect fixes
48
+
49
+ - **F-1** `circular-analyzer.walk()` now walks `meta.type.collectionValue` — Set/Map nested DTO cycles are caught at seal time, no more `stack overflow` at runtime.
50
+ - **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `SealError` with a precise message instead of producing invalid generated JS.
51
+ - **F-3** Discriminator default branch now reports `context: { received, validSubTypes: [...] }` so callers can show the user the allowed values.
52
+ - **F-4** Per-call options other than `groups` are rejected with `SealError` instead of being silently dropped.
53
+ - **F-8** FR passport regex now anchors both ends (`/^[A-Z0-9]{9}$/i`).
54
+ - **F-9** `MAGNET_URI_RE` is anchored on the trailing end.
55
+ - **N-3** Circular-detection `WeakSet` is now allocated per call via `Symbol.for('baker:circular-seen')` threaded through `_opts` — concurrent async calls no longer false-circular on shared input objects.
56
+ - **N-4** `extractCode` checks `Object.hasOwn(input, key)` before reading — prototype-chain values no longer leak into DTO results.
57
+ - **N-6** `mergeInheritance` validation dedup now compares by `ruleName`, so a child redeclaring the same rule (e.g. `minLength(5)`) replaces the parent's rule instead of producing duplicate errors.
58
+
59
+ ### Dead code
60
+
61
+ - `src/functions/_run-sealed.ts` removed. The corresponding internal-only tests in `test/e2e/change-coverage.test.ts` were dropped — their coverage is now provided by public-API tests.
62
+
63
+ ## 2.2.0
64
+
65
+ ### Minor Changes
66
+
67
+ - 78d701a: feat: validate-only executor with inline nested code generation
68
+
69
+ - Add `_validate` sealed executor — validates input without Object.create or property assignment
70
+ - validate() now uses dedicated `_validate` executor instead of routing through `_deserialize`
71
+ - Inline nested DTO validation: nested DTO fields are expanded directly into the parent function body, eliminating per-item function call overhead
72
+ - Recursive inline for all nesting patterns: nested objects, arrays of nested, discriminator, collections (Set/Map), transforms, groups
73
+ - Only circular references fall back to function call (physically impossible to inline)
74
+ - 14 refs-based validators converted to inline emit (isISBN, isISIN, isIBAN, isFQDN, etc.)
75
+ - Type gate dead code removal: 11 redundant checks eliminated in gated paths
76
+ - Rule Plan IR `stripSelfComparison` for AST-level optimization inside type gates
77
+ - Shared codegen utilities extracted to `codegen-utils.ts`
78
+ - GEN constants centralized in serialize-builder to prevent typo-related bugs
79
+ - `makeRule`/`makePlannedRule` factory functions for cleaner rule creation
80
+ - Sync/async contract enforcement for declared-sync rules
81
+
82
+ Performance:
83
+
84
+ - validate() nested 3-level: 8.79ns (typebox: 11.56ns) — 1.3x faster than typebox
85
+ - validate() array 1000 items: 2.35µs (typebox: 2.37µs) — equivalent to typebox
86
+ - validate() vs deserialize(): 2-5x faster across all benchmarks
87
+ - Zero memory leaks verified under 10M sustained operations
88
+ - 26.6M ops/sec throughput (validate valid, flat DTO)
89
+
90
+ ## 2.1.0
91
+
92
+ ### Minor Changes
93
+
94
+ - 5696199: feat: Transformer interface, built-in transformers, isULID, isCUID2
95
+
96
+ ### Breaking Changes
97
+
98
+ - `FieldOptions.transform` now accepts `Transformer | Transformer[]` instead of function
99
+ - `FieldTransformParams`, `FieldTransformFn` types removed — use `Transformer` interface
100
+ - `transformDirection` option removed — use passthrough in the unused direction method
101
+ - Serialize direction applies transforms in reverse order (codec stack)
102
+
103
+ ### New Features
104
+
105
+ - `Transformer` interface with separate `deserialize`/`serialize` methods
106
+ - `transform` option accepts arrays — serialize applies in reverse order
107
+ - `type` + `transform` combination support in serialize (nested serialize → transform)
108
+ - 9 built-in core transformers: trim, toLowerCase, toUpperCase, round, unixSeconds, unixMillis, isoString, csv, json
109
+ - 2 optional peer transformers: luxon, moment (async factory, `await import()`)
110
+ - `isULID()` validator
111
+ - `isCUID2()` validator
112
+ - `@zipbul/baker/transformers` subpath export
113
+
114
+ ### Improvements
115
+
116
+ - `when` callback typed as `(obj: Record<string, any>)` instead of `any`
117
+ - Sourcemap removed from build output
118
+ - README rewritten with GEO optimization (FAQ, benchmarks, comparison tables)
119
+ - package.json description and keywords optimized
120
+
121
+ ## 2.0.0
122
+
123
+ ### Major Changes
124
+
125
+ - 5d01955: feat!: v2 API overhaul — isBakerError, validate, performance optimization
126
+
127
+ ### Breaking Changes
128
+
129
+ - `deserialize()` no longer throws on validation failure — returns `T | BakerErrors | Promise<T | BakerErrors>`
130
+ - `serialize()` returns directly for sync DTOs — `Record<string, unknown> | Promise<Record<string, unknown>>`
131
+ - `BakerValidationError` class removed — use `isBakerError()` type guard
132
+ - `toJsonSchema()` removed
133
+ - `@Schema` decorator and `schema` field option removed
134
+ - `JsonSchemaOverride`, `ToJsonSchemaOptions` types removed
135
+ - `BAKER_ERROR` symbol no longer exported (internal only)
136
+ - `README.ko.md` removed
137
+
138
+ ### New Features
139
+
140
+ - `validate(Class, input, options?)` — DTO-level validation without instantiation
141
+ - `validate(input, ...rules)` — ad-hoc single value validation
142
+ - `isBakerError()` — type guard for narrowing validation results
143
+ - Sync DTOs return directly (no Promise wrapper) across all APIs
144
+ - Memory leak detection CI step
145
+
146
+ ### Performance
147
+
148
+ - Valid path: 188ns → 38ns (5x improvement)
149
+ - Invalid path: 6.08µs → 76ns (80x improvement)
150
+
151
+ ### Bug Fixes
152
+
153
+ - WeakSet circular detection false positive on same-object reuse
154
+ - serialize-builder async discriminator array syntax error
155
+ - 13 rules missing constraints in metadata
156
+
157
+ ## 1.1.0
158
+
159
+ ### Minor Changes
160
+
161
+ - b27cdf6: ## New Features
162
+
163
+ - **Sync API optimization** — `deserialize()` and `serialize()` are no longer `async function`. Sync DTOs (no async transforms/rules) skip `Promise` allocation via `Promise.resolve()`. Async DTOs use the executor's native `Promise`. Return type remains `Promise<T>` for backward compatibility.
164
+
165
+ - **Map/Set auto-conversion** — New `type: () => Map` and `type: () => Set` support in `@Field()`:
166
+
167
+ - `Set<T>`: JSON array ↔ `Set`, with optional `setValue: () => DtoClass` for nested DTOs
168
+ - `Map<string, T>`: JSON object ↔ `Map`, with optional `mapValue: () => DtoClass` for nested DTOs
169
+ - JSON Schema: Set → `{ type: 'array', uniqueItems: true }`, Map → `{ type: 'object', additionalProperties }`
170
+
171
+ - **Per-field error messages** — `message` and `context` options on `@Field()` apply to all rules on the field. Supports static strings, dynamic functions with `{ property, value, constraints }`, and arbitrary context values including falsy ones (`0`, `false`, `''`).
172
+
173
+ ## Chores
174
+
175
+ - Translate all Korean comments and documentation to English (82 files)
176
+ - Delete REVIEW.md (all 42 items completed)
177
+ - 1808 tests, 2639 assertions
178
+
179
+ ## 1.0.0
180
+
181
+ ### Major Changes
182
+
183
+ - b7ea675: ## Breaking Changes
184
+
185
+ - **`@Field()` unified decorator** — Replaces 30+ individual decorators with a single `@Field()` that accepts rules as arguments and options as an object.
186
+ - **Auto-seal** — `seal()` removed. DTOs auto-seal on first `deserialize()`/`serialize()` call.
187
+ - **`configure()` replaces `seal()` options** — `configure({ autoConvert, stopAtFirstError, forbidUnknown, ... })`.
188
+ - **`configure()` returns `{ warnings: string[] }`** instead of `void`.
189
+ - **`enableCircularCheck` removed** — Circular detection always runs automatically.
190
+ - **`stripUnknown` renamed to `forbidUnknown`** — `stripUnknown` kept as deprecated alias.
191
+
192
+ ## Bug Fixes
193
+
194
+ - C-1: Fix analyzeAsync discriminator visited Set sharing (infinite recursion risk)
195
+ - C-2: Fix Set/Map stopAtFirstError error path missing element index
196
+ - C-3: Fix discriminator JSON Schema $ref+properties sibling (allOf wrapper)
197
+ - C-5: Throw on isDivisibleBy(0)
198
+ - C-6: Fix isURL accepting ports 65536-99999
199
+ - C-7: Fix isNumber maxDecimalPlaces scientific notation bypass
200
+ - C-8: Implement serialize discriminator with instanceof dispatch
201
+ - C-9: Fix nullable $ref invalid JSON Schema (oneOf wrapper)
202
+ - C-11: Null guard for nested array serialize
203
+ - C-12: Throw on min(NaN)/max(Infinity)
204
+ - C-13~C-17, B-1~B-11: 11 additional safety guards and silent failure fixes
205
+
206
+ ## New Features
207
+
208
+ - Debug mode: `configure({ debug: true })`
209
+ - `onUnmappedRule` callback for `toJsonSchema()`
210
+ - `forbidUnknown` option (renamed from `stripUnknown`)
211
+
212
+ ## Refactoring
213
+
214
+ - Decompose buildRulesCode (250 lines → 5 functions) and Field() (125 lines → 4 helpers)
215
+ - Deduplicate Array/Set/Map each codegen, extract GEN constants, strategy pattern for nullable/optional
216
+ - 1730 tests, 2509 assertions, 99.94% Funcs / 99.83% Lines
217
+
218
+ ## 0.1.2
219
+
220
+ ### Patch Changes
221
+
222
+ - 76657db: fix: pin CI Bun version to 1.3.9 to avoid 1.3.10 bundler regression, optimize isIn/isNotIn with Set, improve npm packaging and test coverage
223
+
224
+ ## 0.1.1
225
+
226
+ ### Patch Changes
227
+
228
+ - 95ce993: Add coverage badge gist configuration
229
+
230
+ ## 0.1.0
231
+
232
+ ### Minor Changes
233
+
234
+ - 214f664: ### Breaking Changes
235
+
236
+ - Remove `src/aot/` module and `@zipbul/baker/aot` subpath export. The zipbul CLI now reads baker decorators directly via AST.
237
+ - `MessageArgs.constraints` type changed from `unknown[]` to `Record<string, unknown>`.
238
+ - Default behavior for fields without `@IsOptional`/`@IsNullable`: `undefined`/`null` input now emits `isDefined` error code instead of falling through to type gate errors (e.g., `isString`).
239
+
240
+ ### Features
241
+
242
+ - **`@Nested(fn, opts?)`** — Single-decorator shorthand for `@ValidateNested()` + `@Type(fn)` with discriminator support.
243
+ - **`@IsNullable()`** — Allow `null` (skip validation), reject `undefined`. Complements `@IsOptional()` for OAS 3.0 `nullable: true` semantics.
244
+ - **`@Schema(schema)`** — Attach JSON Schema Draft 2020-12 metadata at class or property level. Supports object and function forms.
245
+ - **`toJsonSchema(Class, opts?)`** — Generate JSON Schema Draft 2020-12 from DTO decorators. Supports `direction`, `groups`, circular references, discriminator `oneOf`, and `@Schema()` overrides.
246
+ - **`seal({ whitelist: true })`** — Reject undeclared fields with `whitelistViolation` error code.
247
+ - **`@Min(n, { exclusive: true })` / `@Max(n, { exclusive: true })`** — Exclusive minimum/maximum support.
248
+ - **`enableImplicitConversion`** — Automatic type conversion (string/number/boolean/date) based on `requiresType` and `@Type()` hints.
249
+ - **`EmittableRule.constraints`** — All built-in rules now expose their parameters via `constraints` for JSON Schema mapping and `message` callback access.
250
+ - **`requiresType` expansion** — Added `'boolean'` and `'date'` variants. Fixed silent rule loss for non-string/non-number `requiresType` values.
251
+
252
+ ### Internal
253
+
254
+ - Upgrade `@zipbul/result` from `^0.0.3` to `^0.1.4` and adopt `Result<T, E>` / `ResultAsync<T, E>` type aliases.
255
+ - Fix seal placeholder to throw `SealError` instead of bare `Error`.
256
+ - Remove dead branch in deserialize input type guard.
@@ -0,0 +1,104 @@
1
+ # Baker 2.x → 3.x Migration
2
+
3
+ This release replaces the implicit "auto-seal on first call" model with explicit, user-triggered `seal()`. It also tightens per-call options validation and adds strict sync/async variants.
4
+
5
+ ## Required changes
6
+
7
+ ### 1. Call `seal()` once at app startup
8
+
9
+ **Before**
10
+
11
+ ```ts
12
+ // Module load registers DTOs; first deserialize implicitly seals everything.
13
+ const r = await deserialize(UserDto, payload);
14
+ ```
15
+
16
+ **After**
17
+
18
+ ```ts
19
+ import { seal, deserialize } from '@zipbul/baker';
20
+ // Call after every DTO module has been imported (before HTTP server / job runner starts).
21
+ seal();
22
+ const r = await deserialize(UserDto, payload);
23
+ ```
24
+
25
+ `deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed.
26
+
27
+ ### 2. Move per-call options into `configure(...)`
28
+
29
+ Only `groups` survives as a per-call option.
30
+
31
+ **Before**
32
+
33
+ ```ts
34
+ await deserialize(UserDto, payload, { stopAtFirstError: true });
35
+ ```
36
+
37
+ **After**
38
+
39
+ ```ts
40
+ import { configure, seal } from '@zipbul/baker';
41
+ configure({ stopAtFirstError: true });
42
+ seal();
43
+ await deserialize(UserDto, payload);
44
+ ```
45
+
46
+ All other keys (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`) and their legacy `SealOptions` aliases (`enableImplicitConversion`, `exposeDefaultValues`, `whitelist`) now throw `BakerError` when passed per-call.
47
+
48
+ ### 3. `configure()` must run before `seal()`
49
+
50
+ After `seal()`, `configure(...)` throws `BakerError`. Tests that need to reconfigure must call the test-only `unseal()` helper, change config, then `seal()` again.
51
+
52
+ ### 4. `@Field` argument validation is strict
53
+
54
+ Passing a non-rule value (factory not invoked, primitive, plain function without `.emit` / `.ruleName`) throws `BakerError` at decorator-evaluation time with the four valid forms listed.
55
+
56
+ ```ts
57
+ @Field(isNumber) // ✗ factory not invoked → BakerError
58
+ @Field(isNumber()) // ✓
59
+ @Field(isString) // ✓ constant rule
60
+ @Field() // ✓ marker only
61
+ @Field(isString, { optional: true }) // ✓
62
+ @Field({ type: () => NestedDto }) // ✓
63
+ ```
64
+
65
+ ### 5. `Map<K, V>` requires string keys at serialize
66
+
67
+ Serializing a `Map` with non-string keys throws `TypeError`. Previously the key was silently coerced via `String(key)`, producing collisions like `'[object Object]'`.
68
+
69
+ ### 6. New strict sync/async variants
70
+
71
+ Six new entry points enforce the call-direction asymmetry at the type level:
72
+
73
+ | Integrated | Strict sync | Strict async |
74
+ | ------------- | ----------------- | ------------------ |
75
+ | `deserialize` | `deserializeSync` | `deserializeAsync` |
76
+ | `serialize` | `serializeSync` | `serializeAsync` |
77
+ | `validate` | `validateSync` | `validateAsync` |
78
+
79
+ `*Sync` throws `BakerError` if the DTO is async on that direction (e.g. async transform on deserialize side for `deserializeSync`). `*Async` always returns `Promise` (sync DTOs are wrapped via `Promise.resolve`). The integrated `deserialize` / `serialize` / `validate` remain available for ergonomic use.
80
+
81
+ ## Defect fixes (no migration needed)
82
+
83
+ The following bugs in 2.x are silently fixed in 3.x:
84
+
85
+ - **Set/Map nested DTO cycles** no longer cause stack overflow (`circular-analyzer` now walks `collectionValue`).
86
+ - **Set/Map value DTOs marked async** now correctly propagate `_isAsync` / `_isSerializeAsync` to the parent.
87
+ - **Discriminator with empty `subTypes`** throws `BakerError` at seal time instead of producing invalid generated JS.
88
+ - **Concurrent async deserialize on the same input** no longer reports a false `circular` error (per-call `WeakSet` via `Symbol.for('baker:circular-seen')`).
89
+ - **`Object.hasOwn` checks** prevent prototype-chain values from leaking into DTO results.
90
+ - **Discriminator default branch** error now reports `context: { received, validSubTypes: [...] }`.
91
+ - **FR passport regex** now anchors both ends.
92
+ - **MAGNET URI regex** now anchors the trailing end.
93
+ - **Inheritance dedup** now compares by `ruleName` — a child re-declaring the same rule replaces the parent's rule.
94
+ - **`seal(Class)` failure** is now transactional: a failed seal removes the placeholder so retry can succeed.
95
+ - **`collectionValue` thunk** errors are wrapped in `BakerError` with the field name.
96
+
97
+ ## Removed APIs
98
+
99
+ - `_runSealed` (was internal, but some test code depended on it). Use the public functions instead.
100
+
101
+ ## Notes
102
+
103
+ - The decorator-side registry (`globalRegistry`) is still used internally as an index of decorated classes so `seal()` (no args) can seal everything. This is not auto-seal — `seal()` must be called explicitly.
104
+ - `unseal()` is exported by `test/integration/helpers/unseal.ts` for testing only. It is not part of the public API.
package/README.md CHANGED
@@ -6,12 +6,12 @@ The fastest decorator-based DTO validation library for TypeScript. Generates opt
6
6
  bun add @zipbul/baker
7
7
  ```
8
8
 
9
- Zero `reflect-metadata`. Sealed codegen. 1,975 tests. 99%+ line coverage.
9
+ Zero `reflect-metadata`. Sealed codegen. 99%+ line coverage.
10
10
 
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { deserialize, isBakerError, Field } from '@zipbul/baker';
14
+ import { deserialize, isBakerIssueSet, Field, seal } from '@zipbul/baker';
15
15
  import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
16
16
 
17
17
  class UserDto {
@@ -20,14 +20,19 @@ class UserDto {
20
20
  @Field(isString, isEmail()) email!: string;
21
21
  }
22
22
 
23
+ // Call once at app startup, after all DTOs are loaded.
24
+ seal();
25
+
23
26
  const result = await deserialize(UserDto, {
24
- name: 'Alice', age: 30, email: 'alice@test.com',
27
+ name: 'Alice',
28
+ age: 30,
29
+ email: 'alice@test.com',
25
30
  });
26
31
 
27
- if (isBakerError(result)) {
32
+ if (isBakerIssueSet(result)) {
28
33
  console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
29
34
  } else {
30
- console.log(result.name); // 'Alice' — typed as UserDto
35
+ console.log(result.name); // 'Alice' — typed as UserDto
31
36
  }
32
37
  ```
33
38
 
@@ -35,14 +40,14 @@ if (isBakerError(result)) {
35
40
 
36
41
  Baker generates optimized JavaScript functions once on first seal, then executes them on every call.
37
42
 
38
- | Feature | baker | class-validator | Zod |
39
- |---|---|---|---|
40
- | Valid path (5 fields) | **fast sealed path** | slower | slower |
41
- | Invalid path (5 fields) | **fast sealed path** | slower | slower |
42
- | Approach | AOT code generation | Runtime interpretation | Schema method chain |
43
- | Decorators | `@Field` (unified) | 30+ individual | N/A |
44
- | `reflect-metadata` | Not needed | Required | N/A |
45
- | Sync DTO return | Direct value | Promise | Direct value |
43
+ | Feature | baker | class-validator | Zod |
44
+ | ----------------------- | -------------------- | ---------------------- | ------------------- |
45
+ | Valid path (5 fields) | **fast sealed path** | slower | slower |
46
+ | Invalid path (5 fields) | **fast sealed path** | slower | slower |
47
+ | Approach | AOT code generation | Runtime interpretation | Schema method chain |
48
+ | Decorators | `@Field` (unified) | 30+ individual | N/A |
49
+ | `reflect-metadata` | Not needed | Required | N/A |
50
+ | Sync DTO return | Direct value | Promise | Direct value |
46
51
 
47
52
  ## Performance
48
53
 
@@ -52,32 +57,52 @@ See [`bench/`](./bench) for the current benchmark suite and exact scenarios.
52
57
 
53
58
  ## API
54
59
 
60
+ ### `seal(...classes?)`
61
+
62
+ **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.
63
+
64
+ `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.
65
+
55
66
  ### `deserialize<T>(Class, input, options?)`
56
67
 
57
- Returns `T | BakerErrors` for sync DTOs, `Promise<T | BakerErrors>` for async DTOs. Never throws on validation failure.
68
+ Returns `T | BakerIssueSet` for sync DTOs, `Promise<T | BakerIssueSet>` for async DTOs. Never throws on validation failure.
69
+
70
+ 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).
71
+
72
+ ### `deserializeSync<T>` / `deserializeAsync<T>`
73
+
74
+ 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`).
58
75
 
59
76
  ### `serialize<T>(instance, options?)`
60
77
 
61
- Returns `Record<string, unknown>` for sync DTOs, `Promise<Record<string, unknown>>` for async DTOs. No validation.
78
+ 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.
79
+
80
+ ### `serializeSync<T>` / `serializeAsync<T>`
62
81
 
63
- ### `validate(Class, input, options?)` / `validate(input, ...rules)`
82
+ Strict variants. `serializeSync` throws `BakerError` if the DTO is async on the serialize side.
64
83
 
65
- DTO-level or ad-hoc single-value validation. Returns `true | BakerErrors` for sync paths, `Promise<true | BakerErrors>` for async paths.
84
+ ### `validate(Class, input, options?)`
66
85
 
67
- ### `isBakerError(value)`
86
+ 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)`).
68
87
 
69
- Type guard. Narrows result to `BakerErrors` containing `{ path, code, message?, context? }[]`.
88
+ ### `validateSync` / `validateAsync`
89
+
90
+ Strict variants. `validateSync` throws `BakerError` if the DTO is async; `validateAsync` always returns `Promise`.
91
+
92
+ ### `isBakerIssueSet(value)`
93
+
94
+ Type guard. Narrows result to `BakerIssueSet` containing `{ path, code, message?, context? }[]`.
70
95
 
71
96
  ### `configure(config)`
72
97
 
73
- Global configuration. Call before first deserialize/serialize/validate. Calling it after auto-seal throws `SealError`.
98
+ Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`; reconfiguring requires `unseal()` (test-only helper) + `configure(...)` + `seal()` again.
74
99
 
75
100
  ```typescript
76
101
  configure({
77
- autoConvert: true, // coerce "123" → 123
102
+ autoConvert: true, // coerce "123" → 123
78
103
  allowClassDefaults: true, // use class field initializers for missing keys
79
- stopAtFirstError: true, // return on first validation failure
80
- forbidUnknown: true, // reject undeclared fields
104
+ stopAtFirstError: true, // return on first validation failure
105
+ forbidUnknown: true, // reject undeclared fields
81
106
  });
82
107
  ```
83
108
 
@@ -88,7 +113,7 @@ Custom validation rule.
88
113
  ```typescript
89
114
  const isEven = createRule({
90
115
  name: 'isEven',
91
- validate: (v) => typeof v === 'number' && v % 2 === 0,
116
+ validate: v => typeof v === 'number' && v % 2 === 0,
92
117
  requiresType: 'number',
93
118
  });
94
119
  ```
@@ -97,31 +122,36 @@ const isEven = createRule({
97
122
 
98
123
  One decorator for everything — replaces 30+ individual decorators from class-validator.
99
124
 
125
+ 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.
126
+
100
127
  ```typescript
101
128
  @Field(...rules)
102
129
  @Field(...rules, options)
103
130
  @Field(options)
131
+ @Field() // marker-only (no rules)
104
132
  ```
105
133
 
134
+ 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.
135
+
106
136
  ### Options
107
137
 
108
- | Option | Type | Description |
109
- |---|---|---|
110
- | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
111
- | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
112
- | `optional` | `boolean` | Allow undefined |
113
- | `nullable` | `boolean` | Allow null |
114
- | `name` | `string` | Bidirectional key mapping |
115
- | `deserializeName` | `string` | Input key mapping |
116
- | `serializeName` | `string` | Output key mapping |
117
- | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
118
- | `groups` | `string[]` | Conditional visibility |
119
- | `when` | `(obj) => boolean` | Conditional validation |
120
- | `transform` | `Transformer \| Transformer[]` | Value transformer |
121
- | `message` | `string \| (args) => string` | Error message override |
122
- | `context` | `unknown` | Error context |
123
- | `mapValue` | `() => Dto` | Map value DTO |
124
- | `setValue` | `() => Dto` | Set element DTO |
138
+ | Option | Type | Description |
139
+ | ----------------- | ------------------------------------------------- | ------------------------------ |
140
+ | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
141
+ | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
142
+ | `optional` | `boolean` | Allow undefined |
143
+ | `nullable` | `boolean` | Allow null |
144
+ | `name` | `string` | Bidirectional key mapping |
145
+ | `deserializeName` | `string` | Input key mapping |
146
+ | `serializeName` | `string` | Output key mapping |
147
+ | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
148
+ | `groups` | `string[]` | Conditional visibility |
149
+ | `when` | `(obj) => boolean` | Conditional validation |
150
+ | `transform` | `Transformer \| Transformer[]` | Value transformer |
151
+ | `message` | `string \| (args) => string` | Error message override |
152
+ | `context` | `unknown` | Error context |
153
+ | `mapValue` | `() => Dto` | Map value DTO |
154
+ | `setValue` | `() => Dto` | Set element DTO |
125
155
 
126
156
  ## Transformers
127
157
 
@@ -131,8 +161,8 @@ Bidirectional value transformers with separate `deserialize` and `serialize` met
131
161
  import type { Transformer } from '@zipbul/baker';
132
162
 
133
163
  const centsTransformer: Transformer = {
134
- deserialize: ({ value }) => typeof value === 'number' ? value * 100 : value,
135
- serialize: ({ value }) => typeof value === 'number' ? value / 100 : value,
164
+ deserialize: ({ value }) => (typeof value === 'number' ? value * 100 : value),
165
+ serialize: ({ value }) => (typeof value === 'number' ? value / 100 : value),
136
166
  };
137
167
  ```
138
168
 
@@ -140,27 +170,34 @@ const centsTransformer: Transformer = {
140
170
 
141
171
  ```typescript
142
172
  import {
143
- trimTransformer, toLowerCaseTransformer, toUpperCaseTransformer,
144
- roundTransformer, unixSecondsTransformer, unixMillisTransformer,
145
- isoStringTransformer, csvTransformer, jsonTransformer,
173
+ trimTransformer,
174
+ toLowerCaseTransformer,
175
+ toUpperCaseTransformer,
176
+ roundTransformer,
177
+ unixSecondsTransformer,
178
+ unixMillisTransformer,
179
+ isoStringTransformer,
180
+ csvTransformer,
181
+ jsonTransformer,
146
182
  } from '@zipbul/baker/transformers';
147
183
  ```
148
184
 
149
- | Transformer | deserialize | serialize |
150
- |---|---|---|
151
- | `trimTransformer` | trim string | trim string |
152
- | `toLowerCaseTransformer` | lowercase | lowercase |
153
- | `toUpperCaseTransformer` | uppercase | uppercase |
154
- | `roundTransformer(n?)` | round to n decimals | round to n decimals |
155
- | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
156
- | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
157
- | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
158
- | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
159
- | `jsonTransformer` | JSON string &rarr; object | object &rarr; JSON string |
185
+ | Transformer | deserialize | serialize |
186
+ | ------------------------ | -------------------------- | -------------------------- |
187
+ | `trimTransformer` | trim string | trim string |
188
+ | `toLowerCaseTransformer` | lowercase | lowercase |
189
+ | `toUpperCaseTransformer` | uppercase | uppercase |
190
+ | `roundTransformer(n?)` | round to n decimals | round to n decimals |
191
+ | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
192
+ | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
193
+ | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
194
+ | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
195
+ | `jsonTransformer` | JSON string &rarr; object | object &rarr; JSON string |
160
196
 
161
197
  ### Transform Array Order
162
198
 
163
199
  Multiple transformers apply as a codec stack:
200
+
164
201
  - **Deserialize**: left to right — `[A, B, C]` applies A, then B, then C
165
202
  - **Serialize**: right to left — `[A, B, C]` applies C, then B, then A
166
203
 
@@ -189,9 +226,11 @@ import { momentTransformer } from '@zipbul/baker/transformers';
189
226
  const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
190
227
  ```
191
228
 
229
+ > **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).
230
+
192
231
  ## Rules
193
232
 
194
- 104 built-in validation rules.
233
+ 105 built-in validation rules.
195
234
 
196
235
  ### Type Checkers
197
236
 
@@ -209,7 +248,7 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
209
248
 
210
249
  ### Formats
211
250
 
212
- `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`
251
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
213
252
 
214
253
  ### Arrays
215
254
 
@@ -262,7 +301,8 @@ class PetOwner {
262
301
  { value: DogDto, name: 'dog' },
263
302
  ],
264
303
  },
265
- }) pet!: CatDto | DogDto;
304
+ })
305
+ pet!: CatDto | DogDto;
266
306
  }
267
307
  ```
268
308
 
@@ -299,13 +339,19 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
299
339
 
300
340
  ### How does the AOT code generation work?
301
341
 
302
- On the first call to `deserialize`/`serialize`/`validate`, baker seals all registered DTOs: it analyzes field metadata, generates optimized JavaScript validation functions via `new Function()`, and caches them. Subsequent calls execute the pre-compiled functions directly.
342
+ 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.
303
343
 
304
344
  ## Exports
305
345
 
306
346
  ```typescript
307
- import { deserialize, validate, serialize, configure, createRule, Field, arrayOf, isBakerError, SealError } from '@zipbul/baker';
308
- import type { Transformer, TransformParams, BakerError, BakerErrors, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
347
+ import {
348
+ seal,
349
+ deserialize, deserializeSync, deserializeAsync,
350
+ validate, validateSync, validateAsync,
351
+ serialize, serializeSync, serializeAsync,
352
+ configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
353
+ } from '@zipbul/baker';
354
+ import type { Transformer, TransformParams, BakerError, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
309
355
  import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
310
356
  import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
311
357
  ```