@zipbul/baker 0.1.2 → 1.1.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/README.ko.md +368 -280
- package/README.md +407 -280
- package/dist/index-70ggmxsa.js +6 -0
- package/dist/index-gcptd79v.js +6 -0
- package/dist/index-xdn55cz3.js +4 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +211 -155
- package/dist/src/collect.d.ts +3 -3
- package/dist/src/configure.d.ts +33 -0
- package/dist/src/create-rule.d.ts +10 -19
- package/dist/src/decorators/field.d.ts +86 -0
- package/dist/src/decorators/index.d.ts +2 -14
- package/dist/src/decorators/index.js +2 -2
- package/dist/src/errors.d.ts +17 -17
- package/dist/src/functions/deserialize.d.ts +6 -4
- package/dist/src/functions/serialize.d.ts +5 -4
- package/dist/src/functions/to-json-schema.d.ts +11 -5
- package/dist/src/interfaces.d.ts +9 -30
- package/dist/src/registry.d.ts +4 -12
- package/dist/src/rules/index.d.ts +2 -0
- package/dist/src/rules/index.js +11 -2
- package/dist/src/rules/object.d.ts +1 -1
- package/dist/src/seal/circular-analyzer.d.ts +5 -9
- package/dist/src/seal/expose-validator.d.ts +6 -6
- package/dist/src/seal/index.d.ts +1 -1
- package/dist/src/seal/seal.d.ts +30 -15
- package/dist/src/seal/serialize-builder.d.ts +2 -2
- package/dist/src/symbols.d.ts +5 -5
- package/dist/src/symbols.js +2 -2
- package/dist/src/types.d.ts +38 -32
- package/dist/src/utils.d.ts +2 -0
- package/package.json +1 -1
- package/dist/index-3gcf6hkv.js +0 -5
- package/dist/index-mx6gnk4h.js +0 -6
- package/dist/index-wy5sh2nx.js +0 -15
- package/dist/src/decorators/array.d.ts +0 -13
- package/dist/src/decorators/common.d.ts +0 -39
- package/dist/src/decorators/date.d.ts +0 -5
- package/dist/src/decorators/locales.d.ts +0 -9
- package/dist/src/decorators/nested.d.ts +0 -17
- package/dist/src/decorators/number.d.ts +0 -15
- package/dist/src/decorators/object.d.ts +0 -9
- package/dist/src/decorators/schema.d.ts +0 -13
- package/dist/src/decorators/string.d.ts +0 -72
- package/dist/src/decorators/transform.d.ts +0 -68
- package/dist/src/decorators/typechecker.d.ts +0 -18
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<strong>Decorator-based validate + transform with inline code generation</strong>
|
|
5
5
|
</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
|
|
7
|
+
Single <code>@Field()</code> decorator · AOT-level performance · zero reflect-metadata
|
|
8
8
|
</p>
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://github.com/zipbul/baker/actions"><img src="https://github.com/zipbul/baker/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
@@ -20,42 +20,47 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Why Baker?
|
|
24
24
|
|
|
25
25
|
| | class-validator | Zod | TypeBox | **Baker** |
|
|
26
26
|
|---|---|---|---|---|
|
|
27
|
-
| Schema style | Decorators | Function chaining | JSON Schema builder | **
|
|
27
|
+
| Schema style | Decorators | Function chaining | JSON Schema builder | **Single `@Field()` decorator** |
|
|
28
28
|
| Performance | Runtime interpreter | Runtime interpreter | JIT compile | **`new Function()` inline codegen** |
|
|
29
|
-
| Transform built-in | Separate package | `.transform()` |
|
|
29
|
+
| Transform built-in | Separate package | `.transform()` | N/A | **Unified** |
|
|
30
30
|
| reflect-metadata | Required | N/A | N/A | **Not needed** |
|
|
31
31
|
| class-validator migration | — | Full rewrite | Full rewrite | **Near drop-in** |
|
|
32
32
|
|
|
33
|
-
Baker gives you
|
|
33
|
+
Baker gives you a **single `@Field()` decorator** that combines validation, transformation, exposure control, and type hints. At first use, it auto-seals all DTOs by generating optimized functions via `new Function()` — delivering **AOT-equivalent performance without a compiler plugin**.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Single decorator** — `@Field()` replaces 30+ individual decorators
|
|
40
|
+
- **80+ built-in rules** — `isString`, `min()`, `isEmail()` and more, composed as arguments
|
|
41
|
+
- **Inline code generation** — auto-seal compiles validators at first `deserialize()`/`serialize()` call
|
|
42
|
+
- **Unified validate + transform** — `deserialize()` and `serialize()` in one call (sync DTOs skip async overhead)
|
|
43
|
+
- **Zero reflect-metadata** — no `reflect-metadata` import needed
|
|
44
|
+
- **Circular reference detection** — automatic static analysis at seal time
|
|
45
|
+
- **Group-based validation** — apply different rules per request with `groups`
|
|
46
|
+
- **Custom rules** — `createRule()` for user-defined validators with codegen support
|
|
47
|
+
- **JSON Schema output** — `toJsonSchema()` generates JSON Schema Draft 2020-12 from your DTOs
|
|
48
|
+
- **Polymorphic discriminator** — `@Field({ discriminator })` for union types
|
|
49
|
+
- **Whitelist mode** — reject undeclared fields with `configure({ forbidUnknown: true })`
|
|
50
|
+
- **Class inheritance** — child DTOs inherit parent `@Field()` decorators automatically
|
|
51
|
+
- **Async transforms** — transform functions can be async
|
|
52
|
+
- **Map/Set support** — auto-convert between `Map`/`Set` and JSON-compatible types
|
|
53
|
+
- **Per-field error messages** — `message` and `context` options on `@Field()` for custom errors
|
|
49
54
|
|
|
50
55
|
---
|
|
51
56
|
|
|
52
|
-
##
|
|
57
|
+
## Installation
|
|
53
58
|
|
|
54
59
|
```bash
|
|
55
60
|
bun add @zipbul/baker
|
|
56
61
|
```
|
|
57
62
|
|
|
58
|
-
> **Requirements:** Bun
|
|
63
|
+
> **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
|
|
59
64
|
|
|
60
65
|
```jsonc
|
|
61
66
|
// tsconfig.json
|
|
@@ -68,37 +73,27 @@ bun add @zipbul/baker
|
|
|
68
73
|
|
|
69
74
|
---
|
|
70
75
|
|
|
71
|
-
##
|
|
76
|
+
## Quick Start
|
|
72
77
|
|
|
73
78
|
### 1. Define a DTO
|
|
74
79
|
|
|
75
80
|
```typescript
|
|
76
|
-
import {
|
|
81
|
+
import { Field } from '@zipbul/baker';
|
|
82
|
+
import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
|
|
77
83
|
|
|
78
84
|
class CreateUserDto {
|
|
79
|
-
@
|
|
85
|
+
@Field(isString)
|
|
80
86
|
name!: string;
|
|
81
87
|
|
|
82
|
-
@
|
|
83
|
-
@Min(0)
|
|
84
|
-
@Max(120)
|
|
88
|
+
@Field(isInt, min(0), max(120))
|
|
85
89
|
age!: number;
|
|
86
90
|
|
|
87
|
-
@
|
|
91
|
+
@Field(isEmail())
|
|
88
92
|
email!: string;
|
|
89
93
|
}
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
### 2.
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
import { seal } from '@zipbul/baker';
|
|
96
|
-
|
|
97
|
-
// Compiles all registered DTOs into optimized validators
|
|
98
|
-
seal();
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 3. Deserialize per request
|
|
96
|
+
### 2. Deserialize (auto-seals on first call)
|
|
102
97
|
|
|
103
98
|
```typescript
|
|
104
99
|
import { deserialize, BakerValidationError } from '@zipbul/baker';
|
|
@@ -113,7 +108,7 @@ try {
|
|
|
113
108
|
}
|
|
114
109
|
```
|
|
115
110
|
|
|
116
|
-
###
|
|
111
|
+
### 3. Serialize
|
|
117
112
|
|
|
118
113
|
```typescript
|
|
119
114
|
import { serialize } from '@zipbul/baker';
|
|
@@ -122,180 +117,284 @@ const plain = await serialize(userInstance);
|
|
|
122
117
|
// plain: Record<string, unknown>
|
|
123
118
|
```
|
|
124
119
|
|
|
120
|
+
> No `seal()` call needed — baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
|
|
121
|
+
|
|
125
122
|
---
|
|
126
123
|
|
|
127
|
-
##
|
|
124
|
+
## The `@Field()` Decorator
|
|
125
|
+
|
|
126
|
+
`@Field()` is the single decorator that replaces all individual decorators. It accepts validation rules as positional arguments and an options object for advanced features.
|
|
127
|
+
|
|
128
|
+
### Signatures
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// Rules only
|
|
132
|
+
@Field(isString, minLength(3), maxLength(100))
|
|
133
|
+
|
|
134
|
+
// Options only
|
|
135
|
+
@Field({ optional: true, nullable: true })
|
|
136
|
+
|
|
137
|
+
// Rules + options
|
|
138
|
+
@Field(isString, { name: 'user_name', groups: ['create'] })
|
|
139
|
+
|
|
140
|
+
// No rules (plain field)
|
|
141
|
+
@Field()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### FieldOptions
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface FieldOptions {
|
|
148
|
+
type?: () => Constructor | [Constructor]; // Nested DTO type (thunk for circular refs)
|
|
149
|
+
discriminator?: { // Polymorphic union
|
|
150
|
+
property: string;
|
|
151
|
+
subTypes: { value: Function; name: string }[];
|
|
152
|
+
};
|
|
153
|
+
keepDiscriminatorProperty?: boolean; // Keep discriminator key in output
|
|
154
|
+
rules?: (EmittableRule | ArrayOfMarker)[]; // Validation rules (alternative to positional args)
|
|
155
|
+
optional?: boolean; // Allow undefined
|
|
156
|
+
nullable?: boolean; // Allow null
|
|
157
|
+
name?: string; // JSON key mapping (bidirectional)
|
|
158
|
+
deserializeName?: string; // Deserialize-only key mapping
|
|
159
|
+
serializeName?: string; // Serialize-only key mapping
|
|
160
|
+
exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
|
|
161
|
+
groups?: string[]; // Visibility + conditional validation
|
|
162
|
+
when?: (obj: any) => boolean; // Conditional validation
|
|
163
|
+
schema?: JsonSchemaOverride; // JSON Schema metadata
|
|
164
|
+
transform?: (params: FieldTransformParams) => unknown;
|
|
165
|
+
transformDirection?: 'deserializeOnly' | 'serializeOnly';
|
|
166
|
+
message?: string | ((args: MessageArgs) => string); // Error message for all rules
|
|
167
|
+
context?: unknown; // Error context for all rules
|
|
168
|
+
mapValue?: () => Constructor; // Map value DTO type
|
|
169
|
+
setValue?: () => Constructor; // Set element DTO type
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Per-field Error Messages
|
|
174
|
+
|
|
175
|
+
Use `message` and `context` to customize validation error output:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
@Field(isString, minLength(3), { message: 'Name is invalid' })
|
|
179
|
+
name!: string;
|
|
180
|
+
|
|
181
|
+
@Field(isEmail(), {
|
|
182
|
+
message: ({ property, value }) => `${property} got bad value: ${value}`,
|
|
183
|
+
context: { severity: 'error' },
|
|
184
|
+
})
|
|
185
|
+
email!: string;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The `message` and `context` are applied to all rules on the field. They appear in `BakerError.message` and `BakerError.context` on validation failure.
|
|
189
|
+
|
|
190
|
+
### `arrayOf()` — Array Element Validation
|
|
191
|
+
|
|
192
|
+
`arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { Field, arrayOf } from '@zipbul/baker';
|
|
196
|
+
import { isString, minLength } from '@zipbul/baker/rules';
|
|
197
|
+
|
|
198
|
+
class TagsDto {
|
|
199
|
+
@Field(arrayOf(isString, minLength(1)))
|
|
200
|
+
tags!: string[];
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
You can mix `arrayOf()` with top-level array rules:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
|
|
208
|
+
|
|
209
|
+
class ScoresDto {
|
|
210
|
+
@Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
|
|
211
|
+
scores!: number[];
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Built-in Rules
|
|
218
|
+
|
|
219
|
+
All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
|
|
220
|
+
|
|
221
|
+
> **Constants vs factory functions:** Some rules are pre-built constants (used without `()`) while others are factory functions that accept parameters (used with `()`). The tables below mark constants with a dagger symbol.
|
|
128
222
|
|
|
129
223
|
### Type Checkers
|
|
130
224
|
|
|
131
|
-
|
|
|
225
|
+
| Rule | Description |
|
|
132
226
|
|---|---|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
227
|
+
| `isString` | `typeof === 'string'` |
|
|
228
|
+
| `isNumber(opts?)` | `typeof === 'number'` with NaN/Infinity/maxDecimalPlaces checks |
|
|
229
|
+
| `isInt` | Integer check |
|
|
230
|
+
| `isBoolean` | `typeof === 'boolean'` |
|
|
231
|
+
| `isDate` | `instanceof Date && !isNaN` |
|
|
232
|
+
| `isEnum(enumObj)` | Enum value check |
|
|
233
|
+
| `isArray` | `Array.isArray()` |
|
|
234
|
+
| `isObject` | `typeof === 'object'`, excludes null/Array |
|
|
235
|
+
|
|
236
|
+
> `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` are constants (no parentheses needed). `isNumber(opts?)` and `isEnum(enumObj)` are factory functions.
|
|
141
237
|
|
|
142
238
|
### Common
|
|
143
239
|
|
|
144
|
-
|
|
|
240
|
+
| Rule | Description |
|
|
145
241
|
|---|---|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
| `@IsNotIn(values)` | Value is not in the given array |
|
|
155
|
-
| `@ValidateNested()` | Validate nested DTO |
|
|
156
|
-
| `@ValidateIf(fn)` | Conditional validation |
|
|
242
|
+
| `equals(val)` | Strict equality (`===`) |
|
|
243
|
+
| `notEquals(val)` | Strict inequality (`!==`) |
|
|
244
|
+
| `isEmpty` | `undefined`, `null`, or `''` |
|
|
245
|
+
| `isNotEmpty` | Not `undefined`, `null`, or `''` |
|
|
246
|
+
| `isIn(arr)` | Value is in the given array |
|
|
247
|
+
| `isNotIn(arr)` | Value is not in the given array |
|
|
248
|
+
|
|
249
|
+
> `isEmpty` and `isNotEmpty` are constants. The rest are factory functions.
|
|
157
250
|
|
|
158
251
|
### Number
|
|
159
252
|
|
|
160
|
-
|
|
|
253
|
+
| Rule | Description |
|
|
161
254
|
|---|---|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
255
|
+
| `min(n, opts?)` | `value >= n` (supports `{ exclusive: true }`) |
|
|
256
|
+
| `max(n, opts?)` | `value <= n` (supports `{ exclusive: true }`) |
|
|
257
|
+
| `isPositive` | `value > 0` |
|
|
258
|
+
| `isNegative` | `value < 0` |
|
|
259
|
+
| `isDivisibleBy(n)` | `value % n === 0` |
|
|
260
|
+
|
|
261
|
+
> `isPositive` and `isNegative` are constants (no parentheses). `min()`, `max()`, and `isDivisibleBy()` are factory functions.
|
|
168
262
|
|
|
169
263
|
### String
|
|
170
264
|
|
|
171
|
-
|
|
172
|
-
|
|
265
|
+
All string rules require the value to be a `string` type.
|
|
266
|
+
|
|
267
|
+
| Rule | Kind | Description |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `minLength(n)` | factory | Minimum length |
|
|
270
|
+
| `maxLength(n)` | factory | Maximum length |
|
|
271
|
+
| `length(min, max)` | factory | Length range |
|
|
272
|
+
| `contains(seed)` | factory | Contains substring |
|
|
273
|
+
| `notContains(seed)` | factory | Does not contain substring |
|
|
274
|
+
| `matches(pattern, modifiers?)` | factory | Regex match |
|
|
275
|
+
| `isLowercase` | constant | All lowercase |
|
|
276
|
+
| `isUppercase` | constant | All uppercase |
|
|
277
|
+
| `isAscii` | constant | ASCII only |
|
|
278
|
+
| `isAlpha` | constant | Alphabetic only (en-US) |
|
|
279
|
+
| `isAlphanumeric` | constant | Alphanumeric only (en-US) |
|
|
280
|
+
| `isBooleanString` | constant | `'true'`, `'false'`, `'1'`, or `'0'` |
|
|
281
|
+
| `isNumberString(opts?)` | factory | Numeric string |
|
|
282
|
+
| `isDecimal(opts?)` | factory | Decimal string |
|
|
283
|
+
| `isFullWidth` | constant | Full-width characters |
|
|
284
|
+
| `isHalfWidth` | constant | Half-width characters |
|
|
285
|
+
| `isVariableWidth` | constant | Mix of full-width and half-width |
|
|
286
|
+
| `isMultibyte` | constant | Multibyte characters |
|
|
287
|
+
| `isSurrogatePair` | constant | Surrogate pair characters |
|
|
288
|
+
| `isHexadecimal` | constant | Hexadecimal string |
|
|
289
|
+
| `isOctal` | constant | Octal string |
|
|
290
|
+
| `isEmail(opts?)` | factory | Email format |
|
|
291
|
+
| `isURL(opts?)` | factory | URL format (port range validated) |
|
|
292
|
+
| `isUUID(version?)` | factory | UUID v1-v5 |
|
|
293
|
+
| `isIP(version?)` | factory | IPv4 / IPv6 |
|
|
294
|
+
| `isHexColor` | constant | Hex color (`#fff`, `#ffffff`) |
|
|
295
|
+
| `isRgbColor(includePercent?)` | factory | RGB color string |
|
|
296
|
+
| `isHSL` | constant | HSL color string |
|
|
297
|
+
| `isMACAddress(opts?)` | factory | MAC address |
|
|
298
|
+
| `isISBN(version?)` | factory | ISBN-10 / ISBN-13 |
|
|
299
|
+
| `isISIN` | constant | ISIN (International Securities Identification Number) |
|
|
300
|
+
| `isISO8601(opts?)` | factory | ISO 8601 date string |
|
|
301
|
+
| `isISRC` | constant | ISRC (International Standard Recording Code) |
|
|
302
|
+
| `isISSN(opts?)` | factory | ISSN (International Standard Serial Number) |
|
|
303
|
+
| `isJWT` | constant | JSON Web Token |
|
|
304
|
+
| `isLatLong(opts?)` | factory | Latitude/longitude string |
|
|
305
|
+
| `isLocale` | constant | Locale string (e.g. `en_US`) |
|
|
306
|
+
| `isDataURI` | constant | Data URI |
|
|
307
|
+
| `isFQDN(opts?)` | factory | Fully qualified domain name |
|
|
308
|
+
| `isPort` | constant | Port number string (0-65535) |
|
|
309
|
+
| `isEAN` | constant | EAN (European Article Number) |
|
|
310
|
+
| `isISO31661Alpha2` | constant | ISO 3166-1 alpha-2 country code |
|
|
311
|
+
| `isISO31661Alpha3` | constant | ISO 3166-1 alpha-3 country code |
|
|
312
|
+
| `isBIC` | constant | BIC (Bank Identification Code) / SWIFT code |
|
|
313
|
+
| `isFirebasePushId` | constant | Firebase Push ID |
|
|
314
|
+
| `isSemVer` | constant | Semantic version string |
|
|
315
|
+
| `isMongoId` | constant | MongoDB ObjectId (24-char hex) |
|
|
316
|
+
| `isJSON` | constant | Parseable JSON string |
|
|
317
|
+
| `isBase32(opts?)` | factory | Base32 encoded |
|
|
318
|
+
| `isBase58` | constant | Base58 encoded |
|
|
319
|
+
| `isBase64(opts?)` | factory | Base64 encoded |
|
|
320
|
+
| `isDateString(opts?)` | factory | Date string (configurable strict mode) |
|
|
321
|
+
| `isMimeType` | constant | MIME type string |
|
|
322
|
+
| `isCurrency(opts?)` | factory | Currency string |
|
|
323
|
+
| `isMagnetURI` | constant | Magnet URI |
|
|
324
|
+
| `isCreditCard` | constant | Credit card number (Luhn) |
|
|
325
|
+
| `isIBAN(opts?)` | factory | IBAN |
|
|
326
|
+
| `isByteLength(min, max?)` | factory | Byte length range |
|
|
327
|
+
| `isHash(algorithm)` | factory | Hash string (md4, md5, sha1, sha256, sha384, sha512, etc.) |
|
|
328
|
+
| `isRFC3339` | constant | RFC 3339 date-time string |
|
|
329
|
+
| `isMilitaryTime` | constant | Military time (HH:MM) |
|
|
330
|
+
| `isLatitude` | constant | Latitude string |
|
|
331
|
+
| `isLongitude` | constant | Longitude string |
|
|
332
|
+
| `isEthereumAddress` | constant | Ethereum address |
|
|
333
|
+
| `isBtcAddress` | constant | Bitcoin address |
|
|
334
|
+
| `isISO4217CurrencyCode` | constant | ISO 4217 currency code |
|
|
335
|
+
| `isPhoneNumber` | constant | E.164 international phone number |
|
|
336
|
+
| `isStrongPassword(opts?)` | factory | Strong password (configurable min length, uppercase, lowercase, numbers, symbols) |
|
|
337
|
+
| `isTaxId(locale)` | factory | Tax ID for given locale |
|
|
173
338
|
|
|
174
|
-
|
|
339
|
+
### Array
|
|
340
|
+
|
|
341
|
+
| Rule | Description |
|
|
175
342
|
|---|---|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
| `@IsAlpha()` | Alphabetic only |
|
|
183
|
-
| `@IsAlphanumeric()` | Alphanumeric only |
|
|
184
|
-
| `@IsNumeric()` | Numeric string |
|
|
185
|
-
| `@IsEmail(opts?)` | Email format |
|
|
186
|
-
| `@IsURL(opts?)` | URL format |
|
|
187
|
-
| `@IsUUID(version?)` | UUID v1–v5 |
|
|
188
|
-
| `@IsIP(version?)` | IPv4 / IPv6 |
|
|
189
|
-
| `@IsMACAddress()` | MAC address |
|
|
190
|
-
| `@IsISBN(version?)` | ISBN-10 / ISBN-13 |
|
|
191
|
-
| `@IsISIN()` | ISIN |
|
|
192
|
-
| `@IsIBAN()` | IBAN |
|
|
193
|
-
| `@IsJSON()` | Parseable JSON string |
|
|
194
|
-
| `@IsBase64()` | Base64 encoded |
|
|
195
|
-
| `@IsBase32()` | Base32 encoded |
|
|
196
|
-
| `@IsBase58()` | Base58 encoded |
|
|
197
|
-
| `@IsHexColor()` | Hex color code |
|
|
198
|
-
| `@IsHSL()` | HSL color |
|
|
199
|
-
| `@IsRgbColor()` | RGB color |
|
|
200
|
-
| `@IsHexadecimal()` | Hex string |
|
|
201
|
-
| `@IsBIC()` | BIC/SWIFT code |
|
|
202
|
-
| `@IsISRC()` | ISRC code |
|
|
203
|
-
| `@IsEAN()` | EAN barcode |
|
|
204
|
-
| `@IsMimeType()` | MIME type |
|
|
205
|
-
| `@IsMagnetURI()` | Magnet URI |
|
|
206
|
-
| `@IsCreditCard()` | Credit card number |
|
|
207
|
-
| `@IsHash(algorithm)` | Hash (`md5 \| sha1 \| sha256 \| sha512` etc.) |
|
|
208
|
-
| `@IsRFC3339()` | RFC 3339 date |
|
|
209
|
-
| `@IsMilitaryTime()` | 24h format (`HH:MM`) |
|
|
210
|
-
| `@IsLatitude()` | Latitude (-90 ~ 90) |
|
|
211
|
-
| `@IsLongitude()` | Longitude (-180 ~ 180) |
|
|
212
|
-
| `@IsEthereumAddress()` | Ethereum address |
|
|
213
|
-
| `@IsBtcAddress()` | Bitcoin address (P2PKH/P2SH/bech32) |
|
|
214
|
-
| `@IsISO4217CurrencyCode()` | ISO 4217 currency code |
|
|
215
|
-
| `@IsPhoneNumber()` | E.164 international phone number |
|
|
216
|
-
| `@IsStrongPassword(opts?)` | Strong password |
|
|
217
|
-
| `@IsSemVer()` | Semantic version |
|
|
218
|
-
| `@IsISO8601()` | ISO 8601 date string |
|
|
219
|
-
| `@IsMongoId()` | MongoDB ObjectId |
|
|
220
|
-
| `@IsTaxId(locale)` | Tax ID by locale |
|
|
221
|
-
|
|
222
|
-
</details>
|
|
343
|
+
| `arrayContains(values)` | Contains all given elements |
|
|
344
|
+
| `arrayNotContains(values)` | Contains none of the given elements |
|
|
345
|
+
| `arrayMinSize(n)` | Minimum array length |
|
|
346
|
+
| `arrayMaxSize(n)` | Maximum array length |
|
|
347
|
+
| `arrayUnique()` | No duplicates |
|
|
348
|
+
| `arrayNotEmpty()` | Not empty |
|
|
223
349
|
|
|
224
350
|
### Date
|
|
225
351
|
|
|
226
|
-
|
|
|
352
|
+
| Rule | Description |
|
|
227
353
|
|---|---|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
354
|
+
| `minDate(date)` | Minimum date |
|
|
355
|
+
| `maxDate(date)` | Maximum date |
|
|
230
356
|
|
|
231
|
-
###
|
|
357
|
+
### Object
|
|
232
358
|
|
|
233
|
-
|
|
|
359
|
+
| Rule | Description |
|
|
234
360
|
|---|---|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
| `@ArrayMinSize(n)` | Minimum array length |
|
|
238
|
-
| `@ArrayMaxSize(n)` | Maximum array length |
|
|
239
|
-
| `@ArrayUnique()` | No duplicates |
|
|
240
|
-
| `@ArrayNotEmpty()` | Not empty |
|
|
361
|
+
| `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
|
|
362
|
+
| `isInstance(Class)` | `instanceof` check against given class |
|
|
241
363
|
|
|
242
|
-
### Locale
|
|
243
|
-
|
|
244
|
-
| Decorator | Description |
|
|
245
|
-
|---|---|
|
|
246
|
-
| `@IsMobilePhone(locale)` | Mobile phone by locale |
|
|
247
|
-
| `@IsPostalCode(locale)` | Postal code by locale |
|
|
248
|
-
| `@IsIdentityCard(locale)` | Identity card by locale |
|
|
249
|
-
| `@IsPassportNumber(locale)` | Passport number by locale |
|
|
364
|
+
### Locale
|
|
250
365
|
|
|
251
|
-
|
|
366
|
+
Locale-specific validators that accept a locale string parameter.
|
|
252
367
|
|
|
253
|
-
|
|
|
368
|
+
| Rule | Description |
|
|
254
369
|
|---|---|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
| `@Exclude(opts?)` | Exclude property from serialization |
|
|
260
|
-
| `@Schema(schema)` | Attach JSON Schema metadata (class or property level) |
|
|
370
|
+
| `isMobilePhone(locale)` | Mobile phone number for the given locale (e.g. `'ko-KR'`, `'en-US'`, `'ja-JP'`) |
|
|
371
|
+
| `isPostalCode(locale)` | Postal code for the given locale/country code (e.g. `'US'`, `'KR'`, `'GB'`) |
|
|
372
|
+
| `isIdentityCard(locale)` | National identity card number for the given locale (e.g. `'KR'`, `'US'`, `'CN'`) |
|
|
373
|
+
| `isPassportNumber(locale)` | Passport number for the given locale (e.g. `'US'`, `'KR'`, `'GB'`) |
|
|
261
374
|
|
|
262
375
|
---
|
|
263
376
|
|
|
264
|
-
##
|
|
377
|
+
## Configuration
|
|
265
378
|
|
|
266
|
-
|
|
379
|
+
Call `configure()` **before** the first `deserialize()`/`serialize()`:
|
|
267
380
|
|
|
268
381
|
```typescript
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
382
|
+
import { configure } from '@zipbul/baker';
|
|
383
|
+
|
|
384
|
+
configure({
|
|
385
|
+
autoConvert: false, // Implicit type conversion ("123" -> 123)
|
|
386
|
+
allowClassDefaults: false, // Use class default values for missing keys
|
|
387
|
+
stopAtFirstError: false, // Stop at first error or collect all
|
|
388
|
+
forbidUnknown: false, // Reject undeclared fields
|
|
389
|
+
debug: false, // Emit field exclusion comments in generated code
|
|
390
|
+
});
|
|
279
391
|
```
|
|
280
392
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
class UserDto {
|
|
285
|
-
@IsString({ message: 'Name must be a string' })
|
|
286
|
-
name!: string;
|
|
287
|
-
|
|
288
|
-
@IsInt({
|
|
289
|
-
message: ({ property }) => `${property} must be an integer`,
|
|
290
|
-
context: { httpStatus: 400 },
|
|
291
|
-
})
|
|
292
|
-
age!: number;
|
|
293
|
-
}
|
|
294
|
-
```
|
|
393
|
+
`configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
|
|
295
394
|
|
|
296
395
|
---
|
|
297
396
|
|
|
298
|
-
##
|
|
397
|
+
## Error Handling
|
|
299
398
|
|
|
300
399
|
When validation fails, `deserialize()` throws a `BakerValidationError`:
|
|
301
400
|
|
|
@@ -304,86 +403,50 @@ class BakerValidationError extends Error {
|
|
|
304
403
|
readonly errors: BakerError[];
|
|
305
404
|
readonly className: string;
|
|
306
405
|
}
|
|
307
|
-
```
|
|
308
406
|
|
|
309
|
-
Each error follows the `BakerError` interface:
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
407
|
interface BakerError {
|
|
313
|
-
readonly path: string; //
|
|
314
|
-
readonly code: string; //
|
|
315
|
-
readonly message?: string; // Custom message
|
|
316
|
-
readonly context?: unknown; // Custom context
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## 📋 Array Validation
|
|
323
|
-
|
|
324
|
-
Use `each: true` to apply rules to each element of an Array, Set, or Map:
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
class TagsDto {
|
|
328
|
-
@IsString({ each: true })
|
|
329
|
-
tags!: string[];
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
## 🏷️ Group-based Validation
|
|
336
|
-
|
|
337
|
-
Apply different rules depending on the use case:
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
340
|
-
class UserDto {
|
|
341
|
-
@IsString({ groups: ['create'] })
|
|
342
|
-
name!: string;
|
|
343
|
-
|
|
344
|
-
@IsEmail({ groups: ['create', 'update'] })
|
|
345
|
-
email!: string;
|
|
408
|
+
readonly path: string; // 'user.address.city'
|
|
409
|
+
readonly code: string; // 'isString', 'min', 'isEmail'
|
|
410
|
+
readonly message?: string; // Custom message
|
|
411
|
+
readonly context?: unknown; // Custom context
|
|
346
412
|
}
|
|
347
|
-
|
|
348
|
-
// Only validate rules in the 'create' group
|
|
349
|
-
const user = await deserialize(UserDto, body, { groups: ['create'] });
|
|
350
413
|
```
|
|
351
414
|
|
|
352
415
|
---
|
|
353
416
|
|
|
354
|
-
##
|
|
417
|
+
## Nested Objects
|
|
355
418
|
|
|
356
|
-
Use
|
|
419
|
+
Use `type` option for nested DTO validation:
|
|
357
420
|
|
|
358
421
|
```typescript
|
|
359
|
-
import { Nested, IsString } from '@zipbul/baker';
|
|
360
|
-
|
|
361
422
|
class AddressDto {
|
|
362
|
-
@
|
|
423
|
+
@Field(isString)
|
|
363
424
|
city!: string;
|
|
364
425
|
}
|
|
365
426
|
|
|
366
427
|
class UserDto {
|
|
367
|
-
@
|
|
428
|
+
@Field({ type: () => AddressDto })
|
|
368
429
|
address!: AddressDto;
|
|
430
|
+
|
|
431
|
+
// Array of nested DTOs
|
|
432
|
+
@Field({ type: () => [AddressDto] })
|
|
433
|
+
addresses!: AddressDto[];
|
|
369
434
|
}
|
|
370
435
|
```
|
|
371
436
|
|
|
372
|
-
|
|
437
|
+
### Discriminator (Polymorphism)
|
|
373
438
|
|
|
374
439
|
```typescript
|
|
375
|
-
class
|
|
376
|
-
@
|
|
377
|
-
label!: string;
|
|
440
|
+
class DogDto {
|
|
441
|
+
@Field(isString) breed!: string;
|
|
378
442
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
@Nested(() => ItemDto, { each: true })
|
|
382
|
-
items!: ItemDto[];
|
|
443
|
+
class CatDto {
|
|
444
|
+
@Field(isBoolean) indoor!: boolean;
|
|
383
445
|
}
|
|
384
446
|
|
|
385
447
|
class PetOwnerDto {
|
|
386
|
-
@
|
|
448
|
+
@Field({
|
|
449
|
+
type: () => DogDto,
|
|
387
450
|
discriminator: {
|
|
388
451
|
property: 'type',
|
|
389
452
|
subTypes: [
|
|
@@ -396,105 +459,169 @@ class PetOwnerDto {
|
|
|
396
459
|
}
|
|
397
460
|
```
|
|
398
461
|
|
|
399
|
-
|
|
462
|
+
Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
|
|
400
463
|
|
|
401
|
-
|
|
464
|
+
### Map / Set Collections
|
|
402
465
|
|
|
403
|
-
|
|
466
|
+
Baker auto-converts between `Map`/`Set` and JSON-compatible types:
|
|
404
467
|
|
|
405
468
|
```typescript
|
|
406
|
-
|
|
469
|
+
// Set<primitive>: JSON array ↔ Set
|
|
470
|
+
@Field({ type: () => Set })
|
|
471
|
+
tags!: Set<string>;
|
|
407
472
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
})
|
|
473
|
+
// Set<DTO>: JSON array of objects ↔ Set of DTO instances
|
|
474
|
+
@Field({ type: () => Set, setValue: () => TagDto })
|
|
475
|
+
tags!: Set<TagDto>;
|
|
476
|
+
|
|
477
|
+
// Map<string, primitive>: JSON object ↔ Map
|
|
478
|
+
@Field({ type: () => Map })
|
|
479
|
+
config!: Map<string, unknown>;
|
|
480
|
+
|
|
481
|
+
// Map<string, DTO>: JSON object ↔ Map of DTO instances
|
|
482
|
+
@Field({ type: () => Map, mapValue: () => PriceDto })
|
|
483
|
+
prices!: Map<string, PriceDto>;
|
|
414
484
|
```
|
|
415
485
|
|
|
486
|
+
Map keys are always strings (JSON constraint). JSON Schema maps `Set` to `{ type: 'array', uniqueItems: true }` and `Map` to `{ type: 'object', additionalProperties: ... }`.
|
|
487
|
+
|
|
416
488
|
---
|
|
417
489
|
|
|
418
|
-
##
|
|
490
|
+
## Inheritance
|
|
491
|
+
|
|
492
|
+
Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
|
|
419
493
|
|
|
420
494
|
```typescript
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
495
|
+
class BaseDto {
|
|
496
|
+
@Field(isString)
|
|
497
|
+
name!: string;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
class ExtendedDto extends BaseDto {
|
|
501
|
+
@Field(isInt, min(0))
|
|
502
|
+
age!: number;
|
|
503
|
+
// `name` is inherited from BaseDto
|
|
504
|
+
}
|
|
429
505
|
```
|
|
430
506
|
|
|
431
507
|
---
|
|
432
508
|
|
|
433
|
-
##
|
|
509
|
+
## Transform
|
|
434
510
|
|
|
435
|
-
|
|
511
|
+
The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
|
|
436
512
|
|
|
437
513
|
```typescript
|
|
438
|
-
|
|
514
|
+
class UserDto {
|
|
515
|
+
@Field(isString, {
|
|
516
|
+
transform: ({ value, direction }) => {
|
|
517
|
+
return direction === 'deserialize'
|
|
518
|
+
? (value as string).trim().toLowerCase()
|
|
519
|
+
: value;
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
email!: string;
|
|
439
523
|
|
|
440
|
-
|
|
441
|
-
|
|
524
|
+
@Field(isString, {
|
|
525
|
+
transform: async ({ value }) => {
|
|
526
|
+
return await someAsyncOperation(value);
|
|
527
|
+
},
|
|
528
|
+
transformDirection: 'deserializeOnly',
|
|
529
|
+
})
|
|
530
|
+
data!: string;
|
|
531
|
+
}
|
|
442
532
|
```
|
|
443
533
|
|
|
444
|
-
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Custom Rules
|
|
445
537
|
|
|
446
538
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
539
|
+
import { createRule } from '@zipbul/baker';
|
|
540
|
+
|
|
541
|
+
const isPositiveInt = createRule({
|
|
542
|
+
name: 'isPositiveInt',
|
|
543
|
+
validate: (value) => Number.isInteger(value) && (value as number) > 0,
|
|
450
544
|
});
|
|
545
|
+
|
|
546
|
+
class Dto {
|
|
547
|
+
@Field(isPositiveInt)
|
|
548
|
+
count!: number;
|
|
549
|
+
}
|
|
451
550
|
```
|
|
452
551
|
|
|
453
|
-
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Class-level JSON Schema Metadata
|
|
555
|
+
|
|
556
|
+
Use `collectClassSchema()` to attach class-level JSON Schema metadata (title, description, etc.) to a DTO. This metadata is merged into the output of `toJsonSchema()`.
|
|
557
|
+
|
|
558
|
+
> `collectClassSchema` is a low-level API exported from `src/collect.ts`. It is not available as a subpath export and must be imported directly.
|
|
454
559
|
|
|
455
560
|
```typescript
|
|
456
|
-
|
|
561
|
+
import { collectClassSchema } from '@zipbul/baker/src/collect';
|
|
562
|
+
|
|
457
563
|
class CreateUserDto {
|
|
458
|
-
@
|
|
459
|
-
@
|
|
564
|
+
@Field(isString) name!: string;
|
|
565
|
+
@Field(isEmail()) email!: string;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
collectClassSchema(CreateUserDto, {
|
|
569
|
+
title: 'CreateUserRequest',
|
|
570
|
+
description: 'Payload for creating a new user',
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
For property-level schema overrides, use the `schema` option in `@Field()`:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
class Dto {
|
|
578
|
+
@Field(isString, minLength(1), {
|
|
579
|
+
schema: { description: 'User display name', minLength: 5 },
|
|
580
|
+
})
|
|
460
581
|
name!: string;
|
|
461
582
|
}
|
|
462
583
|
```
|
|
463
584
|
|
|
464
585
|
---
|
|
465
586
|
|
|
466
|
-
##
|
|
587
|
+
## JSON Schema
|
|
467
588
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
589
|
+
Generate JSON Schema Draft 2020-12 from your DTOs:
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { toJsonSchema } from '@zipbul/baker';
|
|
593
|
+
|
|
594
|
+
const schema = toJsonSchema(CreateUserDto, {
|
|
595
|
+
direction: 'deserialize', // 'deserialize' | 'serialize'
|
|
596
|
+
groups: ['create'], // Filter by group
|
|
597
|
+
onUnmappedRule: (name) => { /* custom rules without schema mapping */ },
|
|
598
|
+
});
|
|
599
|
+
```
|
|
474
600
|
|
|
475
601
|
---
|
|
476
602
|
|
|
477
|
-
##
|
|
603
|
+
## How It Works
|
|
478
604
|
|
|
479
605
|
```
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
│ (metadata) │ │ at startup │ │ (inline codegen) │
|
|
483
|
-
└─────────────┘ └──────────────┘ └──────────┬──────────┘
|
|
484
|
-
│
|
|
485
|
-
┌──────────▼──────────┐
|
|
486
|
-
│ deserialize() / │
|
|
487
|
-
│ serialize() │
|
|
488
|
-
│ (execute generated) │
|
|
489
|
-
└─────────────────────┘
|
|
606
|
+
Decorators (@Field) auto-seal (first call) deserialize() / serialize()
|
|
607
|
+
metadata -> new Function() codegen -> execute generated code
|
|
490
608
|
```
|
|
491
609
|
|
|
492
|
-
1.
|
|
493
|
-
2.
|
|
494
|
-
3.
|
|
610
|
+
1. `@Field()` attaches validation metadata to class properties at definition time
|
|
611
|
+
2. First `deserialize()`/`serialize()` call triggers **auto-seal** — reads all metadata, analyzes circular references, generates optimized JavaScript functions via `new Function()`
|
|
612
|
+
3. Subsequent calls execute the generated function directly — no interpretation loops
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Subpath Exports
|
|
617
|
+
|
|
618
|
+
| Import path | Purpose |
|
|
619
|
+
|---|---|
|
|
620
|
+
| `@zipbul/baker` | Main API: `deserialize`, `serialize`, `configure`, `toJsonSchema`, `Field`, `arrayOf`, `createRule` |
|
|
621
|
+
| `@zipbul/baker/rules` | Rule functions and constants: `isString`, `min()`, `isEmail()`, `arrayOf()`, etc. |
|
|
495
622
|
|
|
496
623
|
---
|
|
497
624
|
|
|
498
|
-
##
|
|
625
|
+
## License
|
|
499
626
|
|
|
500
|
-
[MIT](./LICENSE)
|
|
627
|
+
[MIT](./LICENSE)
|