@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.
- package/CHANGELOG.md +45 -0
- package/README.md +256 -159
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/src/baker.d.ts +31 -0
- package/dist/src/baker.js +1 -0
- package/dist/src/configure.d.ts +3 -8
- package/dist/src/configure.js +1 -1
- package/dist/src/create-rule.d.ts +2 -1
- package/dist/src/decorators/field.d.ts +2 -1
- package/dist/src/decorators/field.js +1 -1
- package/dist/src/decorators/index.d.ts +0 -1
- package/dist/src/decorators/index.js +1 -1
- package/dist/src/enums.d.ts +51 -0
- package/dist/src/enums.js +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +1 -1
- package/dist/src/functions/check-call-options.d.ts +1 -1
- package/dist/src/functions/check-call-options.js +1 -1
- package/dist/src/functions/deserialize.d.ts +1 -1
- package/dist/src/functions/serialize.d.ts +1 -1
- package/dist/src/rule-plan.d.ts +5 -3
- package/dist/src/rule-plan.js +1 -1
- package/dist/src/rules/array.js +1 -1
- package/dist/src/rules/date.js +1 -1
- package/dist/src/rules/locales.js +1 -1
- package/dist/src/rules/number.js +1 -1
- package/dist/src/rules/object.js +1 -1
- package/dist/src/rules/string.js +5 -5
- package/dist/src/rules/typechecker.js +5 -5
- package/dist/src/seal/deserialize-builder.js +230 -230
- package/dist/src/seal/enums.d.ts +8 -0
- package/dist/src/seal/enums.js +1 -0
- package/dist/src/seal/expose-validator.js +1 -1
- package/dist/src/seal/seal.d.ts +10 -21
- package/dist/src/seal/seal.js +1 -1
- package/dist/src/seal/serialize-builder.js +8 -8
- package/dist/src/seal/validate-meta.js +1 -1
- package/dist/src/symbols.d.ts +1 -1
- package/dist/src/types.d.ts +15 -14
- package/package.json +1 -1
- package/dist/src/decorators/recipe.d.ts +0 -17
- package/dist/src/decorators/recipe.js +0 -1
- package/dist/src/registry.d.ts +0 -8
- package/dist/src/registry.js +0 -1
- package/dist/src/seal/seal-state.d.ts +0 -10
- 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.
|
|
3
|
+
The fastest decorator-based DTO validation library for TypeScript. baker generates optimized validation and serialization code once at seal time, then reuses the sealed executors on every call.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
bun add @zipbul/baker
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Zero `reflect-metadata`. Sealed codegen.
|
|
9
|
+
Zero `reflect-metadata`. Sealed codegen.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
|
|
14
|
+
- **ESM only.** baker ships no CommonJS build.
|
|
15
|
+
- **TypeScript ≥ 5.2** with native (TC39, Stage 3) decorators. Bun runs TypeScript directly, so your DTOs need no separate build step.
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
// tsconfig.json
|
|
19
|
+
{
|
|
20
|
+
"compilerOptions": {
|
|
21
|
+
"target": "ESNext", // must include Symbol.metadata (ES2022+/ESNext)
|
|
22
|
+
"experimentalDecorators": false // use native TC39 decorators — this is the default; do NOT enable it
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
12
26
|
|
|
13
27
|
## Quick Start
|
|
14
28
|
|
|
15
29
|
```typescript
|
|
16
|
-
import {
|
|
30
|
+
import { Baker, Field, deserialize, isBakerIssueSet } from '@zipbul/baker';
|
|
17
31
|
import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
|
|
18
32
|
|
|
19
|
-
|
|
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
|
|
27
|
-
seal();
|
|
42
|
+
// Call once at startup, after this baker's DTOs are defined.
|
|
43
|
+
baker.seal();
|
|
28
44
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
|
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
|
-
|
|
89
|
+
## @Field Decorator
|
|
60
90
|
|
|
61
|
-
|
|
91
|
+
One decorator for everything — replaces 30+ individual decorators from class-validator.
|
|
62
92
|
|
|
63
|
-
|
|
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
|
-
|
|
95
|
+
```typescript
|
|
96
|
+
@Field(...rules)
|
|
97
|
+
@Field(...rules, options)
|
|
98
|
+
@Field(options)
|
|
99
|
+
@Field() // marker-only (no rules)
|
|
100
|
+
```
|
|
66
101
|
|
|
67
|
-
|
|
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
|
-
###
|
|
104
|
+
### Options
|
|
70
105
|
|
|
71
|
-
|
|
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
|
-
|
|
129
|
+
```typescript
|
|
130
|
+
@baker.Recipe
|
|
131
|
+
class UserDto {
|
|
132
|
+
@Field(isString) name!: string;
|
|
74
133
|
|
|
75
|
-
|
|
134
|
+
// Validated & exposed only when a matching group is requested at runtime.
|
|
135
|
+
@Field(isString, { groups: ['admin'] }) ssn!: string;
|
|
76
136
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
+
deserialize(UserDto, input); // `ssn` is skipped
|
|
147
|
+
deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
|
|
148
|
+
```
|
|
82
149
|
|
|
83
|
-
|
|
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
|
-
|
|
152
|
+
## Rules
|
|
86
153
|
|
|
87
|
-
|
|
154
|
+
114 built-in validation rules.
|
|
88
155
|
|
|
89
|
-
|
|
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
|
-
###
|
|
158
|
+
### Type Checkers
|
|
92
159
|
|
|
93
|
-
|
|
160
|
+
`isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()` needed.
|
|
94
161
|
|
|
95
|
-
|
|
162
|
+
`isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
|
|
96
163
|
|
|
97
|
-
|
|
164
|
+
### Numbers
|
|
98
165
|
|
|
99
|
-
|
|
166
|
+
`min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
|
|
100
167
|
|
|
101
|
-
|
|
168
|
+
### Strings
|
|
102
169
|
|
|
103
|
-
|
|
104
|
-
configure({
|
|
105
|
-
autoConvert: true, // coerce "123" → 123
|
|
106
|
-
allowClassDefaults: true, // use class field initializers for missing keys
|
|
107
|
-
stopAtFirstError: true, // return on first validation failure
|
|
108
|
-
forbidUnknown: true, // reject undeclared fields
|
|
109
|
-
});
|
|
110
|
-
```
|
|
170
|
+
`minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
|
|
111
171
|
|
|
112
|
-
###
|
|
172
|
+
### Formats
|
|
113
173
|
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
|
|
118
|
-
```
|
|
176
|
+
### Arrays
|
|
119
177
|
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
+
### Common
|
|
131
183
|
|
|
132
|
-
|
|
184
|
+
`equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
|
|
133
185
|
|
|
134
|
-
|
|
135
|
-
@Field(...rules)
|
|
136
|
-
@Field(...rules, options)
|
|
137
|
-
@Field(options)
|
|
138
|
-
@Field() // marker-only (no rules)
|
|
139
|
-
```
|
|
186
|
+
### Date
|
|
140
187
|
|
|
141
|
-
|
|
188
|
+
`minDate(date)`, `maxDate(date)`
|
|
142
189
|
|
|
143
|
-
###
|
|
190
|
+
### Locale
|
|
144
191
|
|
|
145
|
-
|
|
146
|
-
| ----------------- | ------------------------------------------------- | ------------------------------ |
|
|
147
|
-
| `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
|
|
148
|
-
| `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
|
|
149
|
-
| `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
|
|
150
|
-
| `optional` | `boolean` | Allow undefined |
|
|
151
|
-
| `nullable` | `boolean` | Allow null |
|
|
152
|
-
| `name` | `string` | Bidirectional key mapping |
|
|
153
|
-
| `deserializeName` | `string` | Input key mapping |
|
|
154
|
-
| `serializeName` | `string` | Output key mapping |
|
|
155
|
-
| `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
|
|
156
|
-
| `groups` | `string[]` | Conditional visibility |
|
|
157
|
-
| `when` | `(obj) => boolean` | Conditional validation |
|
|
158
|
-
| `transform` | `Transformer \| Transformer[]` | Value transformer |
|
|
159
|
-
| `message` | `string \| (args) => string` | Error message override |
|
|
160
|
-
| `context` | `unknown` | Error context |
|
|
161
|
-
| `mapValue` | `() => Dto` | Map value DTO |
|
|
162
|
-
| `setValue` | `() => Dto` | Set element DTO |
|
|
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
|
|
194
|
-
| ------------------------ |
|
|
195
|
-
| `trimTransformer` | trim string
|
|
196
|
-
| `toLowerCaseTransformer` | lowercase
|
|
197
|
-
| `toUpperCaseTransformer` | uppercase
|
|
198
|
-
| `roundTransformer(n?)` | round to n decimals
|
|
199
|
-
| `unixSecondsTransformer` | unix seconds
|
|
200
|
-
| `unixMillisTransformer` | unix ms
|
|
201
|
-
| `isoStringTransformer` | ISO string
|
|
202
|
-
| `csvTransformer(sep?)` | `"a,b"`
|
|
203
|
-
| `jsonTransformer` | JSON string
|
|
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
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
### Formats
|
|
272
|
+
## Composing DTOs
|
|
259
273
|
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
460
|
+
Baker,
|
|
366
461
|
deserialize, deserializeSync, deserializeAsync,
|
|
367
|
-
validate,
|
|
368
|
-
serialize,
|
|
369
|
-
|
|
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,
|
|
372
|
-
import { isString, isEmail, isULID, isCUID2
|
|
373
|
-
import { trimTransformer, jsonTransformer
|
|
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 {
|
|
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{
|
|
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";
|