@zipbul/baker 2.2.0 → 3.0.1
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 +263 -0
- package/README.md +132 -69
- package/dist/index.d.ts +7 -6
- package/dist/index.js +10 -321
- package/dist/src/collect.d.ts +13 -10
- package/dist/src/collect.js +26 -0
- package/dist/src/configure.d.ts +8 -6
- package/dist/src/configure.js +43 -0
- package/dist/src/create-rule.js +41 -0
- package/dist/src/decorators/field.d.ts +22 -18
- package/dist/src/decorators/field.js +268 -0
- package/dist/src/decorators/index.d.ts +1 -0
- package/dist/src/decorators/index.js +2 -2
- package/dist/src/decorators/recipe.d.ts +17 -0
- package/dist/src/decorators/recipe.js +23 -0
- package/dist/src/errors.d.ts +27 -17
- package/dist/src/errors.js +52 -0
- package/dist/src/functions/check-call-options.d.ts +8 -0
- package/dist/src/functions/check-call-options.js +51 -0
- package/dist/src/functions/deserialize.d.ts +13 -6
- package/dist/src/functions/deserialize.js +57 -0
- package/dist/src/functions/serialize.d.ts +10 -4
- package/dist/src/functions/serialize.js +52 -0
- package/dist/src/functions/validate.d.ts +13 -10
- package/dist/src/functions/validate.js +49 -0
- package/dist/src/interfaces.d.ts +1 -1
- package/dist/src/interfaces.js +4 -0
- package/dist/src/meta-access.d.ts +19 -0
- package/dist/src/meta-access.js +75 -0
- package/dist/src/registry.js +8 -0
- package/dist/src/rule-metadata.d.ts +11 -0
- package/dist/src/rule-metadata.js +17 -0
- package/dist/src/rule-plan.d.ts +10 -11
- package/dist/src/rule-plan.js +117 -0
- package/dist/src/rules/array.d.ts +7 -6
- package/dist/src/rules/array.js +96 -0
- package/dist/src/rules/common.js +77 -0
- package/dist/src/rules/date.js +35 -0
- package/dist/src/rules/index.d.ts +2 -4
- package/dist/src/rules/index.js +8 -21
- package/dist/src/rules/locales.d.ts +5 -4
- package/dist/src/rules/locales.js +249 -0
- package/dist/src/rules/number.js +79 -0
- package/dist/src/rules/object.d.ts +1 -1
- package/dist/src/rules/object.js +49 -0
- package/dist/src/rules/string.d.ts +83 -80
- package/dist/src/rules/string.js +1998 -0
- package/dist/src/rules/typechecker.js +143 -0
- package/dist/src/seal/circular-analyzer.js +63 -0
- package/dist/src/seal/codegen-utils.js +18 -0
- package/dist/src/seal/deserialize-builder.d.ts +8 -4
- package/dist/src/seal/deserialize-builder.js +1546 -0
- package/dist/src/seal/expose-validator.d.ts +3 -2
- package/dist/src/seal/expose-validator.js +65 -0
- package/dist/src/seal/seal-state.d.ts +10 -0
- package/dist/src/seal/seal-state.js +18 -0
- package/dist/src/seal/seal.d.ts +22 -21
- package/dist/src/seal/seal.js +431 -0
- package/dist/src/seal/serialize-builder.d.ts +3 -2
- package/dist/src/seal/serialize-builder.js +374 -0
- package/dist/src/seal/validate-meta.d.ts +13 -0
- package/dist/src/seal/validate-meta.js +61 -0
- package/dist/src/symbols.d.ts +1 -1
- package/dist/src/symbols.js +13 -2
- package/dist/src/transformers/collection.transformer.js +25 -0
- package/dist/src/transformers/date.transformer.js +18 -0
- package/dist/src/transformers/index.js +6 -2
- package/dist/src/transformers/luxon.transformer.d.ts +4 -2
- package/dist/src/transformers/luxon.transformer.js +34 -0
- package/dist/src/transformers/moment.transformer.d.ts +4 -2
- package/dist/src/transformers/moment.transformer.js +32 -0
- package/dist/src/transformers/number.transformer.js +8 -0
- package/dist/src/transformers/string.transformer.js +12 -0
- package/dist/src/types.d.ts +27 -25
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +2 -2
- package/dist/src/utils.js +10 -0
- package/package.json +80 -68
- package/dist/index-03cysbck.js +0 -3
- package/dist/index-dcbd798a.js +0 -3
- package/dist/index-jp2yjd6g.js +0 -3
- package/dist/index-mw7met6r.js +0 -3
- package/dist/index-xdn55cz3.js +0 -1
- package/dist/src/functions/_run-sealed.d.ts +0 -7
- package/dist/src/functions/index.d.ts +0 -3
- package/dist/src/seal/index.d.ts +0 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# @zipbul/baker
|
|
2
|
+
|
|
3
|
+
## 3.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea40a75: Docs/metadata accuracy: every README example now includes the required `@Recipe` decorator
|
|
8
|
+
(without it `seal()` does not register the class and `deserialize` throws), drop the
|
|
9
|
+
unnecessary `() => Set as any` / `Map as any`, state the Bun-only requirement (Bun ≥ 1.3.13),
|
|
10
|
+
and replace unsubstantiated speed multipliers with honest qualitative claims. Refresh the
|
|
11
|
+
package description and remove the now-redundant MIGRATION-3.0.md (its content lives in the
|
|
12
|
+
CHANGELOG 3.0.0 entry).
|
|
13
|
+
|
|
14
|
+
## 3.0.0
|
|
15
|
+
|
|
16
|
+
### Major Changes
|
|
17
|
+
|
|
18
|
+
- 421fd54: 3.0 — error system redesign and API hardening (breaking).
|
|
19
|
+
|
|
20
|
+
**Error channel.** A single `BakerError` class is now thrown for every developer/config/schema
|
|
21
|
+
misuse (it carries `cause`). The validation-result types are renamed for clarity:
|
|
22
|
+
|
|
23
|
+
- `SealError` → `BakerError` (the thrown class)
|
|
24
|
+
- the field-error interface `BakerError` → `BakerIssue`
|
|
25
|
+
- `BakerErrors` → `BakerIssueSet`
|
|
26
|
+
- `isBakerError` → `isBakerIssueSet`
|
|
27
|
+
|
|
28
|
+
The split is now explicit: **throw `BakerError`** for misuse discoverable without input;
|
|
29
|
+
**return `BakerIssueSet`** for external-input validation failures from `deserialize`/`validate`.
|
|
30
|
+
|
|
31
|
+
**API hardening.** `validate(Class, input)` is DTO-only (the ad-hoc `validate(value, ...rules)`
|
|
32
|
+
mode was removed — call a rule directly instead). `configure()` rejects unknown keys and
|
|
33
|
+
post-`seal()` calls, and seal-time options can no longer be passed per-call.
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- 421fd54: Add the `isHttpToken` rule — validates the RFC 9110 §5.6.2 HTTP `token` production
|
|
38
|
+
(`1*tchar`), used for HTTP method names and header field-names. Usable as a predicate
|
|
39
|
+
(`isHttpToken(value)`) or as `@Field(isHttpToken)`, and exported from `@zipbul/baker/rules`.
|
|
40
|
+
|
|
41
|
+
### DX reform — breaking changes
|
|
42
|
+
|
|
43
|
+
- **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `BakerError`.
|
|
44
|
+
- Migration: import `seal` and call `seal()` once before any deserialize/serialize/validate call. For tests, call `seal()` after each `unseal()` / `configure(...)` reconfiguration.
|
|
45
|
+
- **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `BakerError`. Move those keys into `configure({...})` before `seal()`.
|
|
46
|
+
- **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `BakerError` immediately with the four valid forms listed in the message.
|
|
47
|
+
- **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `BakerError` — previously the key was silently coerced via `[object Object]` and collided.
|
|
48
|
+
|
|
49
|
+
### API additions
|
|
50
|
+
|
|
51
|
+
- `seal(...classes?)` — explicit AOT seal trigger.
|
|
52
|
+
- `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `BakerError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
|
|
53
|
+
|
|
54
|
+
### Defect fixes
|
|
55
|
+
|
|
56
|
+
- **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.
|
|
57
|
+
- **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `BakerError` with a precise message instead of producing invalid generated JS.
|
|
58
|
+
- **F-3** Discriminator default branch now reports `context: { received, validSubTypes: [...] }` so callers can show the user the allowed values.
|
|
59
|
+
- **F-4** Per-call options other than `groups` are rejected with `BakerError` instead of being silently dropped.
|
|
60
|
+
- **F-8** FR passport regex now anchors both ends (`/^[A-Z0-9]{9}$/i`).
|
|
61
|
+
- **F-9** `MAGNET_URI_RE` is anchored on the trailing end.
|
|
62
|
+
- **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.
|
|
63
|
+
- **N-4** `extractCode` checks `Object.hasOwn(input, key)` before reading — prototype-chain values no longer leak into DTO results.
|
|
64
|
+
- **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.
|
|
65
|
+
|
|
66
|
+
### Dead code
|
|
67
|
+
|
|
68
|
+
- `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.
|
|
69
|
+
|
|
70
|
+
## 2.2.0
|
|
71
|
+
|
|
72
|
+
### Minor Changes
|
|
73
|
+
|
|
74
|
+
- 78d701a: feat: validate-only executor with inline nested code generation
|
|
75
|
+
|
|
76
|
+
- Add `_validate` sealed executor — validates input without Object.create or property assignment
|
|
77
|
+
- validate() now uses dedicated `_validate` executor instead of routing through `_deserialize`
|
|
78
|
+
- Inline nested DTO validation: nested DTO fields are expanded directly into the parent function body, eliminating per-item function call overhead
|
|
79
|
+
- Recursive inline for all nesting patterns: nested objects, arrays of nested, discriminator, collections (Set/Map), transforms, groups
|
|
80
|
+
- Only circular references fall back to function call (physically impossible to inline)
|
|
81
|
+
- 14 refs-based validators converted to inline emit (isISBN, isISIN, isIBAN, isFQDN, etc.)
|
|
82
|
+
- Type gate dead code removal: 11 redundant checks eliminated in gated paths
|
|
83
|
+
- Rule Plan IR `stripSelfComparison` for AST-level optimization inside type gates
|
|
84
|
+
- Shared codegen utilities extracted to `codegen-utils.ts`
|
|
85
|
+
- GEN constants centralized in serialize-builder to prevent typo-related bugs
|
|
86
|
+
- `makeRule`/`makePlannedRule` factory functions for cleaner rule creation
|
|
87
|
+
- Sync/async contract enforcement for declared-sync rules
|
|
88
|
+
|
|
89
|
+
Performance:
|
|
90
|
+
|
|
91
|
+
- validate() nested 3-level: 8.79ns (typebox: 11.56ns) — 1.3x faster than typebox
|
|
92
|
+
- validate() array 1000 items: 2.35µs (typebox: 2.37µs) — equivalent to typebox
|
|
93
|
+
- validate() vs deserialize(): 2-5x faster across all benchmarks
|
|
94
|
+
- Zero memory leaks verified under 10M sustained operations
|
|
95
|
+
- 26.6M ops/sec throughput (validate valid, flat DTO)
|
|
96
|
+
|
|
97
|
+
## 2.1.0
|
|
98
|
+
|
|
99
|
+
### Minor Changes
|
|
100
|
+
|
|
101
|
+
- 5696199: feat: Transformer interface, built-in transformers, isULID, isCUID2
|
|
102
|
+
|
|
103
|
+
### Breaking Changes
|
|
104
|
+
|
|
105
|
+
- `FieldOptions.transform` now accepts `Transformer | Transformer[]` instead of function
|
|
106
|
+
- `FieldTransformParams`, `FieldTransformFn` types removed — use `Transformer` interface
|
|
107
|
+
- `transformDirection` option removed — use passthrough in the unused direction method
|
|
108
|
+
- Serialize direction applies transforms in reverse order (codec stack)
|
|
109
|
+
|
|
110
|
+
### New Features
|
|
111
|
+
|
|
112
|
+
- `Transformer` interface with separate `deserialize`/`serialize` methods
|
|
113
|
+
- `transform` option accepts arrays — serialize applies in reverse order
|
|
114
|
+
- `type` + `transform` combination support in serialize (nested serialize → transform)
|
|
115
|
+
- 9 built-in core transformers: trim, toLowerCase, toUpperCase, round, unixSeconds, unixMillis, isoString, csv, json
|
|
116
|
+
- 2 optional peer transformers: luxon, moment (async factory, `await import()`)
|
|
117
|
+
- `isULID()` validator
|
|
118
|
+
- `isCUID2()` validator
|
|
119
|
+
- `@zipbul/baker/transformers` subpath export
|
|
120
|
+
|
|
121
|
+
### Improvements
|
|
122
|
+
|
|
123
|
+
- `when` callback typed as `(obj: Record<string, any>)` instead of `any`
|
|
124
|
+
- Sourcemap removed from build output
|
|
125
|
+
- README rewritten with GEO optimization (FAQ, benchmarks, comparison tables)
|
|
126
|
+
- package.json description and keywords optimized
|
|
127
|
+
|
|
128
|
+
## 2.0.0
|
|
129
|
+
|
|
130
|
+
### Major Changes
|
|
131
|
+
|
|
132
|
+
- 5d01955: feat!: v2 API overhaul — isBakerError, validate, performance optimization
|
|
133
|
+
|
|
134
|
+
### Breaking Changes
|
|
135
|
+
|
|
136
|
+
- `deserialize()` no longer throws on validation failure — returns `T | BakerErrors | Promise<T | BakerErrors>`
|
|
137
|
+
- `serialize()` returns directly for sync DTOs — `Record<string, unknown> | Promise<Record<string, unknown>>`
|
|
138
|
+
- `BakerValidationError` class removed — use `isBakerError()` type guard
|
|
139
|
+
- `toJsonSchema()` removed
|
|
140
|
+
- `@Schema` decorator and `schema` field option removed
|
|
141
|
+
- `JsonSchemaOverride`, `ToJsonSchemaOptions` types removed
|
|
142
|
+
- `BAKER_ERROR` symbol no longer exported (internal only)
|
|
143
|
+
- `README.ko.md` removed
|
|
144
|
+
|
|
145
|
+
### New Features
|
|
146
|
+
|
|
147
|
+
- `validate(Class, input, options?)` — DTO-level validation without instantiation
|
|
148
|
+
- `validate(input, ...rules)` — ad-hoc single value validation
|
|
149
|
+
- `isBakerError()` — type guard for narrowing validation results
|
|
150
|
+
- Sync DTOs return directly (no Promise wrapper) across all APIs
|
|
151
|
+
- Memory leak detection CI step
|
|
152
|
+
|
|
153
|
+
### Performance
|
|
154
|
+
|
|
155
|
+
- Valid path: 188ns → 38ns (5x improvement)
|
|
156
|
+
- Invalid path: 6.08µs → 76ns (80x improvement)
|
|
157
|
+
|
|
158
|
+
### Bug Fixes
|
|
159
|
+
|
|
160
|
+
- WeakSet circular detection false positive on same-object reuse
|
|
161
|
+
- serialize-builder async discriminator array syntax error
|
|
162
|
+
- 13 rules missing constraints in metadata
|
|
163
|
+
|
|
164
|
+
## 1.1.0
|
|
165
|
+
|
|
166
|
+
### Minor Changes
|
|
167
|
+
|
|
168
|
+
- b27cdf6: ## New Features
|
|
169
|
+
|
|
170
|
+
- **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.
|
|
171
|
+
|
|
172
|
+
- **Map/Set auto-conversion** — New `type: () => Map` and `type: () => Set` support in `@Field()`:
|
|
173
|
+
|
|
174
|
+
- `Set<T>`: JSON array ↔ `Set`, with optional `setValue: () => DtoClass` for nested DTOs
|
|
175
|
+
- `Map<string, T>`: JSON object ↔ `Map`, with optional `mapValue: () => DtoClass` for nested DTOs
|
|
176
|
+
- JSON Schema: Set → `{ type: 'array', uniqueItems: true }`, Map → `{ type: 'object', additionalProperties }`
|
|
177
|
+
|
|
178
|
+
- **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`, `''`).
|
|
179
|
+
|
|
180
|
+
## Chores
|
|
181
|
+
|
|
182
|
+
- Translate all Korean comments and documentation to English (82 files)
|
|
183
|
+
- Delete REVIEW.md (all 42 items completed)
|
|
184
|
+
- 1808 tests, 2639 assertions
|
|
185
|
+
|
|
186
|
+
## 1.0.0
|
|
187
|
+
|
|
188
|
+
### Major Changes
|
|
189
|
+
|
|
190
|
+
- b7ea675: ## Breaking Changes
|
|
191
|
+
|
|
192
|
+
- **`@Field()` unified decorator** — Replaces 30+ individual decorators with a single `@Field()` that accepts rules as arguments and options as an object.
|
|
193
|
+
- **Auto-seal** — `seal()` removed. DTOs auto-seal on first `deserialize()`/`serialize()` call.
|
|
194
|
+
- **`configure()` replaces `seal()` options** — `configure({ autoConvert, stopAtFirstError, forbidUnknown, ... })`.
|
|
195
|
+
- **`configure()` returns `{ warnings: string[] }`** instead of `void`.
|
|
196
|
+
- **`enableCircularCheck` removed** — Circular detection always runs automatically.
|
|
197
|
+
- **`stripUnknown` renamed to `forbidUnknown`** — `stripUnknown` kept as deprecated alias.
|
|
198
|
+
|
|
199
|
+
## Bug Fixes
|
|
200
|
+
|
|
201
|
+
- C-1: Fix analyzeAsync discriminator visited Set sharing (infinite recursion risk)
|
|
202
|
+
- C-2: Fix Set/Map stopAtFirstError error path missing element index
|
|
203
|
+
- C-3: Fix discriminator JSON Schema $ref+properties sibling (allOf wrapper)
|
|
204
|
+
- C-5: Throw on isDivisibleBy(0)
|
|
205
|
+
- C-6: Fix isURL accepting ports 65536-99999
|
|
206
|
+
- C-7: Fix isNumber maxDecimalPlaces scientific notation bypass
|
|
207
|
+
- C-8: Implement serialize discriminator with instanceof dispatch
|
|
208
|
+
- C-9: Fix nullable $ref invalid JSON Schema (oneOf wrapper)
|
|
209
|
+
- C-11: Null guard for nested array serialize
|
|
210
|
+
- C-12: Throw on min(NaN)/max(Infinity)
|
|
211
|
+
- C-13~C-17, B-1~B-11: 11 additional safety guards and silent failure fixes
|
|
212
|
+
|
|
213
|
+
## New Features
|
|
214
|
+
|
|
215
|
+
- Debug mode: `configure({ debug: true })`
|
|
216
|
+
- `onUnmappedRule` callback for `toJsonSchema()`
|
|
217
|
+
- `forbidUnknown` option (renamed from `stripUnknown`)
|
|
218
|
+
|
|
219
|
+
## Refactoring
|
|
220
|
+
|
|
221
|
+
- Decompose buildRulesCode (250 lines → 5 functions) and Field() (125 lines → 4 helpers)
|
|
222
|
+
- Deduplicate Array/Set/Map each codegen, extract GEN constants, strategy pattern for nullable/optional
|
|
223
|
+
- 1730 tests, 2509 assertions, 99.94% Funcs / 99.83% Lines
|
|
224
|
+
|
|
225
|
+
## 0.1.2
|
|
226
|
+
|
|
227
|
+
### Patch Changes
|
|
228
|
+
|
|
229
|
+
- 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
|
|
230
|
+
|
|
231
|
+
## 0.1.1
|
|
232
|
+
|
|
233
|
+
### Patch Changes
|
|
234
|
+
|
|
235
|
+
- 95ce993: Add coverage badge gist configuration
|
|
236
|
+
|
|
237
|
+
## 0.1.0
|
|
238
|
+
|
|
239
|
+
### Minor Changes
|
|
240
|
+
|
|
241
|
+
- 214f664: ### Breaking Changes
|
|
242
|
+
|
|
243
|
+
- Remove `src/aot/` module and `@zipbul/baker/aot` subpath export. The zipbul CLI now reads baker decorators directly via AST.
|
|
244
|
+
- `MessageArgs.constraints` type changed from `unknown[]` to `Record<string, unknown>`.
|
|
245
|
+
- 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`).
|
|
246
|
+
|
|
247
|
+
### Features
|
|
248
|
+
|
|
249
|
+
- **`@Nested(fn, opts?)`** — Single-decorator shorthand for `@ValidateNested()` + `@Type(fn)` with discriminator support.
|
|
250
|
+
- **`@IsNullable()`** — Allow `null` (skip validation), reject `undefined`. Complements `@IsOptional()` for OAS 3.0 `nullable: true` semantics.
|
|
251
|
+
- **`@Schema(schema)`** — Attach JSON Schema Draft 2020-12 metadata at class or property level. Supports object and function forms.
|
|
252
|
+
- **`toJsonSchema(Class, opts?)`** — Generate JSON Schema Draft 2020-12 from DTO decorators. Supports `direction`, `groups`, circular references, discriminator `oneOf`, and `@Schema()` overrides.
|
|
253
|
+
- **`seal({ whitelist: true })`** — Reject undeclared fields with `whitelistViolation` error code.
|
|
254
|
+
- **`@Min(n, { exclusive: true })` / `@Max(n, { exclusive: true })`** — Exclusive minimum/maximum support.
|
|
255
|
+
- **`enableImplicitConversion`** — Automatic type conversion (string/number/boolean/date) based on `requiresType` and `@Type()` hints.
|
|
256
|
+
- **`EmittableRule.constraints`** — All built-in rules now expose their parameters via `constraints` for JSON Schema mapping and `message` callback access.
|
|
257
|
+
- **`requiresType` expansion** — Added `'boolean'` and `'date'` variants. Fixed silent rule loss for non-string/non-number `requiresType` values.
|
|
258
|
+
|
|
259
|
+
### Internal
|
|
260
|
+
|
|
261
|
+
- Upgrade `@zipbul/result` from `^0.0.3` to `^0.1.4` and adopt `Result<T, E>` / `ResultAsync<T, E>` type aliases.
|
|
262
|
+
- Fix seal placeholder to throw `SealError` instead of bare `Error`.
|
|
263
|
+
- Remove dead branch in deserialize input type guard.
|
package/README.md
CHANGED
|
@@ -6,28 +6,36 @@ 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.
|
|
9
|
+
Zero `reflect-metadata`. Sealed codegen. 99%+ line coverage.
|
|
10
|
+
|
|
11
|
+
> **Requires Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
|
|
10
12
|
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
15
|
```typescript
|
|
14
|
-
import { deserialize,
|
|
16
|
+
import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
|
|
15
17
|
import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
|
|
16
18
|
|
|
19
|
+
@Recipe
|
|
17
20
|
class UserDto {
|
|
18
21
|
@Field(isString, minLength(2)) name!: string;
|
|
19
22
|
@Field(isNumber(), min(0)) age!: number;
|
|
20
23
|
@Field(isString, isEmail()) email!: string;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
// Call once at app startup, after all DTOs are loaded.
|
|
27
|
+
seal();
|
|
28
|
+
|
|
23
29
|
const result = await deserialize(UserDto, {
|
|
24
|
-
name: 'Alice',
|
|
30
|
+
name: 'Alice',
|
|
31
|
+
age: 30,
|
|
32
|
+
email: 'alice@test.com',
|
|
25
33
|
});
|
|
26
34
|
|
|
27
|
-
if (
|
|
35
|
+
if (isBakerIssueSet(result)) {
|
|
28
36
|
console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
|
|
29
37
|
} else {
|
|
30
|
-
console.log(result.name);
|
|
38
|
+
console.log(result.name); // 'Alice' — typed as UserDto
|
|
31
39
|
}
|
|
32
40
|
```
|
|
33
41
|
|
|
@@ -35,14 +43,14 @@ if (isBakerError(result)) {
|
|
|
35
43
|
|
|
36
44
|
Baker generates optimized JavaScript functions once on first seal, then executes them on every call.
|
|
37
45
|
|
|
38
|
-
| Feature
|
|
39
|
-
|
|
40
|
-
| Valid path (5 fields)
|
|
41
|
-
| Invalid path (5 fields) | **fast sealed path** | slower
|
|
42
|
-
| Approach
|
|
43
|
-
| Decorators
|
|
44
|
-
| `reflect-metadata`
|
|
45
|
-
| Sync DTO return
|
|
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 |
|
|
46
54
|
|
|
47
55
|
## Performance
|
|
48
56
|
|
|
@@ -52,43 +60,67 @@ See [`bench/`](./bench) for the current benchmark suite and exact scenarios.
|
|
|
52
60
|
|
|
53
61
|
## API
|
|
54
62
|
|
|
63
|
+
### `seal(...classes?)`
|
|
64
|
+
|
|
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.
|
|
66
|
+
|
|
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.
|
|
68
|
+
|
|
55
69
|
### `deserialize<T>(Class, input, options?)`
|
|
56
70
|
|
|
57
|
-
Returns `T |
|
|
71
|
+
Returns `T | BakerIssueSet` for sync DTOs, `Promise<T | BakerIssueSet>` for async DTOs. Never throws on validation failure.
|
|
72
|
+
|
|
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).
|
|
74
|
+
|
|
75
|
+
### `deserializeSync<T>` / `deserializeAsync<T>`
|
|
76
|
+
|
|
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`).
|
|
58
78
|
|
|
59
79
|
### `serialize<T>(instance, options?)`
|
|
60
80
|
|
|
61
|
-
Returns `Record<string, unknown>` for sync DTOs, `Promise<Record<string, unknown>>` for async DTOs. No validation.
|
|
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.
|
|
82
|
+
|
|
83
|
+
### `serializeSync<T>` / `serializeAsync<T>`
|
|
84
|
+
|
|
85
|
+
Strict variants. `serializeSync` throws `BakerError` if the DTO is async on the serialize side.
|
|
86
|
+
|
|
87
|
+
### `validate(Class, input, options?)`
|
|
62
88
|
|
|
63
|
-
|
|
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)`).
|
|
64
90
|
|
|
65
|
-
|
|
91
|
+
### `validateSync` / `validateAsync`
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
Strict variants. `validateSync` throws `BakerError` if the DTO is async; `validateAsync` always returns `Promise`.
|
|
68
94
|
|
|
69
|
-
|
|
95
|
+
### `isBakerIssueSet(value)`
|
|
96
|
+
|
|
97
|
+
Type guard. Narrows result to `BakerIssueSet` containing `{ path, code, message?, context? }[]`.
|
|
70
98
|
|
|
71
99
|
### `configure(config)`
|
|
72
100
|
|
|
73
|
-
Global configuration.
|
|
101
|
+
Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`; reconfiguring requires `unseal()` (test-only helper) + `configure(...)` + `seal()` again.
|
|
74
102
|
|
|
75
103
|
```typescript
|
|
76
104
|
configure({
|
|
77
|
-
autoConvert: true,
|
|
105
|
+
autoConvert: true, // coerce "123" → 123
|
|
78
106
|
allowClassDefaults: true, // use class field initializers for missing keys
|
|
79
|
-
stopAtFirstError: true,
|
|
80
|
-
forbidUnknown: true,
|
|
107
|
+
stopAtFirstError: true, // return on first validation failure
|
|
108
|
+
forbidUnknown: true, // reject undeclared fields
|
|
81
109
|
});
|
|
82
110
|
```
|
|
83
111
|
|
|
84
|
-
### `createRule(name, validate)`
|
|
112
|
+
### `createRule(name, validate)` / `createRule(options)`
|
|
85
113
|
|
|
86
|
-
Custom validation rule.
|
|
114
|
+
Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
|
|
118
|
+
```
|
|
87
119
|
|
|
88
120
|
```typescript
|
|
89
121
|
const isEven = createRule({
|
|
90
122
|
name: 'isEven',
|
|
91
|
-
validate:
|
|
123
|
+
validate: v => typeof v === 'number' && v % 2 === 0,
|
|
92
124
|
requiresType: 'number',
|
|
93
125
|
});
|
|
94
126
|
```
|
|
@@ -97,31 +129,37 @@ const isEven = createRule({
|
|
|
97
129
|
|
|
98
130
|
One decorator for everything — replaces 30+ individual decorators from class-validator.
|
|
99
131
|
|
|
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.
|
|
133
|
+
|
|
100
134
|
```typescript
|
|
101
135
|
@Field(...rules)
|
|
102
136
|
@Field(...rules, options)
|
|
103
137
|
@Field(options)
|
|
138
|
+
@Field() // marker-only (no rules)
|
|
104
139
|
```
|
|
105
140
|
|
|
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.
|
|
142
|
+
|
|
106
143
|
### Options
|
|
107
144
|
|
|
108
|
-
| Option
|
|
109
|
-
|
|
110
|
-
| `type`
|
|
111
|
-
| `discriminator`
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
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 |
|
|
125
163
|
|
|
126
164
|
## Transformers
|
|
127
165
|
|
|
@@ -131,8 +169,8 @@ Bidirectional value transformers with separate `deserialize` and `serialize` met
|
|
|
131
169
|
import type { Transformer } from '@zipbul/baker';
|
|
132
170
|
|
|
133
171
|
const centsTransformer: Transformer = {
|
|
134
|
-
deserialize: ({ value }) => typeof value === 'number' ? value * 100 : value,
|
|
135
|
-
serialize: ({ value }) => typeof value === 'number' ? value / 100 : value,
|
|
172
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value * 100 : value),
|
|
173
|
+
serialize: ({ value }) => (typeof value === 'number' ? value / 100 : value),
|
|
136
174
|
};
|
|
137
175
|
```
|
|
138
176
|
|
|
@@ -140,27 +178,34 @@ const centsTransformer: Transformer = {
|
|
|
140
178
|
|
|
141
179
|
```typescript
|
|
142
180
|
import {
|
|
143
|
-
trimTransformer,
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
trimTransformer,
|
|
182
|
+
toLowerCaseTransformer,
|
|
183
|
+
toUpperCaseTransformer,
|
|
184
|
+
roundTransformer,
|
|
185
|
+
unixSecondsTransformer,
|
|
186
|
+
unixMillisTransformer,
|
|
187
|
+
isoStringTransformer,
|
|
188
|
+
csvTransformer,
|
|
189
|
+
jsonTransformer,
|
|
146
190
|
} from '@zipbul/baker/transformers';
|
|
147
191
|
```
|
|
148
192
|
|
|
149
|
-
| Transformer
|
|
150
|
-
|
|
151
|
-
| `trimTransformer`
|
|
152
|
-
| `toLowerCaseTransformer` | lowercase
|
|
153
|
-
| `toUpperCaseTransformer` | uppercase
|
|
154
|
-
| `roundTransformer(n?)`
|
|
155
|
-
| `unixSecondsTransformer` | unix seconds → Date
|
|
156
|
-
| `unixMillisTransformer`
|
|
157
|
-
| `isoStringTransformer`
|
|
158
|
-
| `csvTransformer(sep?)`
|
|
159
|
-
| `jsonTransformer`
|
|
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 → Date | Date → unix seconds |
|
|
200
|
+
| `unixMillisTransformer` | unix ms → Date | Date → unix ms |
|
|
201
|
+
| `isoStringTransformer` | ISO string → Date | Date → ISO string |
|
|
202
|
+
| `csvTransformer(sep?)` | `"a,b"` → `["a","b"]` | `["a","b"]` → `"a,b"` |
|
|
203
|
+
| `jsonTransformer` | JSON string → object | object → JSON string |
|
|
160
204
|
|
|
161
205
|
### Transform Array Order
|
|
162
206
|
|
|
163
207
|
Multiple transformers apply as a codec stack:
|
|
208
|
+
|
|
164
209
|
- **Deserialize**: left to right — `[A, B, C]` applies A, then B, then C
|
|
165
210
|
- **Serialize**: right to left — `[A, B, C]` applies C, then B, then A
|
|
166
211
|
|
|
@@ -178,6 +223,7 @@ email!: string;
|
|
|
178
223
|
import { luxonTransformer } from '@zipbul/baker/transformers';
|
|
179
224
|
const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
|
|
180
225
|
|
|
226
|
+
@Recipe
|
|
181
227
|
class EventDto {
|
|
182
228
|
@Field({ transform: luxon }) startAt!: DateTime;
|
|
183
229
|
}
|
|
@@ -189,9 +235,11 @@ import { momentTransformer } from '@zipbul/baker/transformers';
|
|
|
189
235
|
const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
190
236
|
```
|
|
191
237
|
|
|
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
|
+
|
|
192
240
|
## Rules
|
|
193
241
|
|
|
194
|
-
|
|
242
|
+
105 built-in validation rules.
|
|
195
243
|
|
|
196
244
|
### Type Checkers
|
|
197
245
|
|
|
@@ -209,7 +257,7 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
|
209
257
|
|
|
210
258
|
### Formats
|
|
211
259
|
|
|
212
|
-
`isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`
|
|
260
|
+
`isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
|
|
213
261
|
|
|
214
262
|
### Arrays
|
|
215
263
|
|
|
@@ -230,10 +278,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
|
230
278
|
## Nested DTOs
|
|
231
279
|
|
|
232
280
|
```typescript
|
|
281
|
+
@Recipe
|
|
233
282
|
class AddressDto {
|
|
234
283
|
@Field(isString) city!: string;
|
|
235
284
|
}
|
|
236
285
|
|
|
286
|
+
@Recipe
|
|
237
287
|
class UserDto {
|
|
238
288
|
@Field({ type: () => AddressDto }) address!: AddressDto;
|
|
239
289
|
@Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
|
|
@@ -243,15 +293,19 @@ class UserDto {
|
|
|
243
293
|
## Collections
|
|
244
294
|
|
|
245
295
|
```typescript
|
|
296
|
+
@Recipe
|
|
246
297
|
class UserDto {
|
|
247
|
-
@Field({ type: () => Set
|
|
248
|
-
@Field({ type: () => Map
|
|
298
|
+
@Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
|
|
299
|
+
@Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
|
|
249
300
|
}
|
|
250
301
|
```
|
|
251
302
|
|
|
303
|
+
> 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
|
+
|
|
252
305
|
## Discriminator
|
|
253
306
|
|
|
254
307
|
```typescript
|
|
308
|
+
@Recipe
|
|
255
309
|
class PetOwner {
|
|
256
310
|
@Field({
|
|
257
311
|
type: () => CatDto,
|
|
@@ -262,17 +316,20 @@ class PetOwner {
|
|
|
262
316
|
{ value: DogDto, name: 'dog' },
|
|
263
317
|
],
|
|
264
318
|
},
|
|
265
|
-
})
|
|
319
|
+
})
|
|
320
|
+
pet!: CatDto | DogDto;
|
|
266
321
|
}
|
|
267
322
|
```
|
|
268
323
|
|
|
269
324
|
## Inheritance
|
|
270
325
|
|
|
271
326
|
```typescript
|
|
327
|
+
@Recipe
|
|
272
328
|
class BaseDto {
|
|
273
329
|
@Field(isString) id!: string;
|
|
274
330
|
}
|
|
275
331
|
|
|
332
|
+
@Recipe
|
|
276
333
|
class UserDto extends BaseDto {
|
|
277
334
|
@Field(isString) name!: string;
|
|
278
335
|
// inherits 'id' field with isString rule
|
|
@@ -283,11 +340,11 @@ class UserDto extends BaseDto {
|
|
|
283
340
|
|
|
284
341
|
### When should I use baker instead of class-validator?
|
|
285
342
|
|
|
286
|
-
When performance matters. baker
|
|
343
|
+
When performance matters. baker generates optimized validation/serialization code at seal time instead of interpreting rules on every call, so it is substantially faster than class-validator on both valid and invalid input while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency. Run [`bench/`](./bench) to measure the exact difference on your machine.
|
|
287
344
|
|
|
288
345
|
### How does baker compare to Zod?
|
|
289
346
|
|
|
290
|
-
Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker
|
|
347
|
+
Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design or Node support; choose baker if you need class-based DTOs on Bun with maximum performance.
|
|
291
348
|
|
|
292
349
|
### Does baker support async validation?
|
|
293
350
|
|
|
@@ -299,13 +356,19 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
|
|
|
299
356
|
|
|
300
357
|
### How does the AOT code generation work?
|
|
301
358
|
|
|
302
|
-
|
|
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.
|
|
303
360
|
|
|
304
361
|
## Exports
|
|
305
362
|
|
|
306
363
|
```typescript
|
|
307
|
-
import {
|
|
308
|
-
|
|
364
|
+
import {
|
|
365
|
+
seal,
|
|
366
|
+
deserialize, deserializeSync, deserializeAsync,
|
|
367
|
+
validate, validateSync, validateAsync,
|
|
368
|
+
serialize, serializeSync, serializeAsync,
|
|
369
|
+
configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
|
|
370
|
+
} from '@zipbul/baker';
|
|
371
|
+
import type { Transformer, TransformParams, BakerError, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
|
|
309
372
|
import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
|
|
310
373
|
import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
|
|
311
374
|
```
|