@zipbul/baker 1.1.0 → 2.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/README.md CHANGED
@@ -1,627 +1,267 @@
1
- <p align="center">
2
- <h1 align="center">@zipbul/baker</h1>
3
- <p align="center">
4
- <strong>Decorator-based validate + transform with inline code generation</strong>
5
- </p>
6
- <p align="center">
7
- Single <code>@Field()</code> decorator &middot; AOT-level performance &middot; zero reflect-metadata
8
- </p>
9
- <p align="center">
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>
11
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/v/@zipbul/baker.svg" alt="npm version"></a>
12
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/dm/@zipbul/baker.svg" alt="npm downloads"></a>
13
- <a href="https://github.com/zipbul/baker/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zipbul/baker.svg" alt="license"></a>
14
- </p>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="./README.ko.md">한국어</a>
19
- </p>
20
-
21
- ---
22
-
23
- ## Why Baker?
24
-
25
- | | class-validator | Zod | TypeBox | **Baker** |
26
- |---|---|---|---|---|
27
- | Schema style | Decorators | Function chaining | JSON Schema builder | **Single `@Field()` decorator** |
28
- | Performance | Runtime interpreter | Runtime interpreter | JIT compile | **`new Function()` inline codegen** |
29
- | Transform built-in | Separate package | `.transform()` | N/A | **Unified** |
30
- | reflect-metadata | Required | N/A | N/A | **Not needed** |
31
- | class-validator migration | — | Full rewrite | Full rewrite | **Near drop-in** |
32
-
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
-
35
- ---
36
-
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
54
-
55
- ---
56
-
57
- ## Installation
1
+ # @zipbul/baker
2
+
3
+ Decorator-based validation + transformation with inline code generation. Zero `reflect-metadata`.
58
4
 
59
5
  ```bash
60
6
  bun add @zipbul/baker
61
7
  ```
62
8
 
63
- > **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
9
+ Requires `"experimentalDecorators": true` in `tsconfig.json`.
64
10
 
65
- ```jsonc
66
- // tsconfig.json
67
- {
68
- "compilerOptions": {
69
- "experimentalDecorators": true
70
- }
71
- }
72
- ```
11
+ ## API
73
12
 
74
- ---
13
+ ### `deserialize<T>(Class, input, options?): T | BakerErrors | Promise<T | BakerErrors>`
75
14
 
76
- ## Quick Start
77
-
78
- ### 1. Define a DTO
15
+ Validates input and creates a class instance. Sync DTOs return directly. Async DTOs (async transform/rules) return Promise. Always safe to `await`.
79
16
 
80
17
  ```typescript
81
- import { Field } from '@zipbul/baker';
82
- import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
18
+ import { deserialize, isBakerError, Field } from '@zipbul/baker';
19
+ import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
83
20
 
84
- class CreateUserDto {
85
- @Field(isString)
86
- name!: string;
21
+ class UserDto {
22
+ @Field(isString, minLength(2)) name!: string;
23
+ @Field(isNumber(), min(0)) age!: number;
24
+ @Field(isString, isEmail()) email!: string;
25
+ }
87
26
 
88
- @Field(isInt, min(0), max(120))
89
- age!: number;
27
+ const result = await deserialize(UserDto, { name: 'Alice', age: 30, email: 'alice@test.com' });
90
28
 
91
- @Field(isEmail())
92
- email!: string;
29
+ if (isBakerError(result)) {
30
+ console.log(result.errors); // { path: string, code: string }[]
31
+ } else {
32
+ console.log(result.name); // 'Alice'
93
33
  }
94
34
  ```
95
35
 
96
- ### 2. Deserialize (auto-seals on first call)
36
+ Never throws on validation failure. Throws `SealError` only for programming errors (no `@Field` decorators, banned field names).
97
37
 
98
- ```typescript
99
- import { deserialize, BakerValidationError } from '@zipbul/baker';
100
-
101
- try {
102
- const user = await deserialize(CreateUserDto, requestBody);
103
- // user is a validated CreateUserDto instance
104
- } catch (e) {
105
- if (e instanceof BakerValidationError) {
106
- console.log(e.errors); // BakerError[]
107
- }
108
- }
109
- ```
38
+ ### `validate(Class, input, options?): true | BakerErrors | Promise<true | BakerErrors>`
110
39
 
111
- ### 3. Serialize
40
+ Same validation as `deserialize` without instance creation.
112
41
 
113
42
  ```typescript
114
- import { serialize } from '@zipbul/baker';
43
+ import { validate, isBakerError } from '@zipbul/baker';
115
44
 
116
- const plain = await serialize(userInstance);
117
- // plain: Record<string, unknown>
45
+ const result = await validate(UserDto, input);
46
+ if (isBakerError(result)) { /* errors */ }
118
47
  ```
119
48
 
120
- > No `seal()` call needed baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
49
+ ### `validate(input, ...rules): true | BakerErrors | Promise<true | BakerErrors>`
121
50
 
122
- ---
51
+ Ad-hoc single value validation. No DTO needed.
123
52
 
124
- ## The `@Field()` Decorator
53
+ ```typescript
54
+ const result = await validate('hello@test.com', isString, isEmail());
55
+ // result === true
56
+ ```
125
57
 
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.
58
+ ### `serialize<T>(instance, options?): Record<string, unknown> | Promise<Record<string, unknown>>`
127
59
 
128
- ### Signatures
60
+ Converts a class instance to a plain object. No validation. Sync DTOs return directly.
129
61
 
130
62
  ```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'] })
63
+ import { serialize } from '@zipbul/baker';
139
64
 
140
- // No rules (plain field)
141
- @Field()
65
+ const plain = await serialize(userInstance);
142
66
  ```
143
67
 
144
- ### FieldOptions
68
+ ### `isBakerError(value): value is BakerErrors`
69
+
70
+ Type guard. Narrows `deserialize`/`validate` result to error type.
145
71
 
146
72
  ```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
73
+ interface BakerError {
74
+ readonly path: string; // 'name', 'address.city', 'items[0].value'
75
+ readonly code: string; // 'isString', 'minLength', 'invalidInput'
76
+ readonly message?: string; // custom message if set
77
+ readonly context?: unknown; // custom context if set
170
78
  }
171
79
  ```
172
80
 
173
- ### Per-field Error Messages
81
+ ### `configure(config): ConfigureResult`
174
82
 
175
- Use `message` and `context` to customize validation error output:
83
+ Global configuration. Call before first `deserialize`/`serialize`/`validate`.
176
84
 
177
85
  ```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
- ```
86
+ import { configure } from '@zipbul/baker';
187
87
 
188
- The `message` and `context` are applied to all rules on the field. They appear in `BakerError.message` and `BakerError.context` on validation failure.
88
+ configure({
89
+ autoConvert: true, // "123" → 123. Default: false
90
+ allowClassDefaults: true, // use class field initializers for missing keys. Default: false
91
+ stopAtFirstError: true, // return on first validation failure. Default: false
92
+ forbidUnknown: true, // reject undeclared fields. Default: false
93
+ });
94
+ ```
189
95
 
190
- ### `arrayOf()` — Array Element Validation
96
+ ### `createRule(name, validate): EmittableRule`
191
97
 
192
- `arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
98
+ Creates a custom validation rule.
193
99
 
194
100
  ```typescript
195
- import { Field, arrayOf } from '@zipbul/baker';
196
- import { isString, minLength } from '@zipbul/baker/rules';
101
+ import { createRule } from '@zipbul/baker';
197
102
 
198
- class TagsDto {
199
- @Field(arrayOf(isString, minLength(1)))
200
- tags!: string[];
201
- }
103
+ const isEven = createRule('isEven', (v) => typeof v === 'number' && v % 2 === 0);
104
+
105
+ const isUnique = createRule({
106
+ name: 'isUnique',
107
+ validate: async (v) => await db.checkUnique(v),
108
+ constraints: { table: 'users' },
109
+ });
202
110
  ```
203
111
 
204
- You can mix `arrayOf()` with top-level array rules:
112
+ ## `@Field` Decorator
205
113
 
206
114
  ```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
- }
115
+ @Field(...rules)
116
+ @Field(...rules, options)
117
+ @Field(options)
213
118
  ```
214
119
 
215
- ---
120
+ ### Options
216
121
 
217
- ## Built-in Rules
218
-
219
- All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
122
+ ```typescript
123
+ interface FieldOptions {
124
+ type?: () => DtoClass | [DtoClass]; // nested DTO. [Dto] for arrays
125
+ discriminator?: { // polymorphic dispatch
126
+ property: string;
127
+ subTypes: { value: Function; name: string }[];
128
+ };
129
+ keepDiscriminatorProperty?: boolean; // preserve discriminator in result. Default: false
130
+ rules?: EmittableRule[]; // rules as array (alternative to variadic)
131
+ optional?: boolean; // allow undefined. Default: false
132
+ nullable?: boolean; // allow null. Default: false
133
+ name?: string; // bidirectional key mapping
134
+ deserializeName?: string; // input key mapping
135
+ serializeName?: string; // output key mapping
136
+ exclude?: boolean | 'deserializeOnly' | 'serializeOnly'; // field exclusion
137
+ groups?: string[]; // conditional visibility
138
+ when?: (obj: any) => boolean; // conditional validation
139
+ transform?: (params: FieldTransformParams) => unknown; // value transform
140
+ transformDirection?: 'deserializeOnly' | 'serializeOnly'; // transform direction
141
+ message?: string | ((args) => string); // error message override
142
+ context?: unknown; // error context
143
+ mapValue?: () => DtoClass; // Map value DTO
144
+ setValue?: () => DtoClass; // Set element DTO
145
+ }
146
+ ```
220
147
 
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.
148
+ ## Rules
222
149
 
223
150
  ### Type Checkers
224
151
 
225
- | Rule | Description |
226
- |---|---|
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.
237
-
238
- ### Common
239
-
240
- | Rule | Description |
241
- |---|---|
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.
250
-
251
- ### Number
252
-
253
- | Rule | Description |
254
- |---|---|
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.
262
-
263
- ### String
264
-
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 |
338
-
339
- ### Array
340
-
341
- | Rule | Description |
342
- |---|---|
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 |
152
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()`.
349
153
 
350
- ### Date
154
+ `isNumber(options?)`, `isEnum(entity)` — factories, need `()`.
351
155
 
352
- | Rule | Description |
353
- |---|---|
354
- | `minDate(date)` | Minimum date |
355
- | `maxDate(date)` | Maximum date |
156
+ ### Numbers
356
157
 
357
- ### Object
158
+ `min(n)`, `max(n)`, `min(n, { exclusive: true })`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
358
159
 
359
- | Rule | Description |
360
- |---|---|
361
- | `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
362
- | `isInstance(Class)` | `instanceof` check against given class |
160
+ ### Strings
363
161
 
364
- ### Locale
162
+ `minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
365
163
 
366
- Locale-specific validators that accept a locale string parameter.
164
+ ### Formats
367
165
 
368
- | Rule | Description |
369
- |---|---|
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'`) |
166
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber`, `isStrongPassword()`
374
167
 
375
- ---
168
+ ### Arrays
376
169
 
377
- ## Configuration
170
+ `arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayOf(...rules)`
378
171
 
379
- Call `configure()` **before** the first `deserialize()`/`serialize()`:
172
+ ### Common
380
173
 
381
- ```typescript
382
- import { configure } from '@zipbul/baker';
174
+ `equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
383
175
 
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
- });
391
- ```
176
+ ### Date
392
177
 
393
- `configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
178
+ `minDate(date)`, `maxDate(date)`
394
179
 
395
- ---
180
+ ### Locale
396
181
 
397
- ## Error Handling
182
+ `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
398
183
 
399
- When validation fails, `deserialize()` throws a `BakerValidationError`:
184
+ ## Nested DTOs
400
185
 
401
186
  ```typescript
402
- class BakerValidationError extends Error {
403
- readonly errors: BakerError[];
404
- readonly className: string;
187
+ class AddressDto {
188
+ @Field(isString) city!: string;
405
189
  }
406
190
 
407
- interface BakerError {
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
191
+ class UserDto {
192
+ @Field({ type: () => AddressDto }) address!: AddressDto;
193
+ @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
412
194
  }
413
195
  ```
414
196
 
415
- ---
416
-
417
- ## Nested Objects
418
-
419
- Use `type` option for nested DTO validation:
197
+ ## Collections
420
198
 
421
199
  ```typescript
422
- class AddressDto {
423
- @Field(isString)
424
- city!: string;
425
- }
426
-
427
200
  class UserDto {
428
- @Field({ type: () => AddressDto })
429
- address!: AddressDto;
430
-
431
- // Array of nested DTOs
432
- @Field({ type: () => [AddressDto] })
433
- addresses!: AddressDto[];
201
+ @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
202
+ @Field({ type: () => Map as any, mapValue: () => TagDto }) tagMap!: Map<string, TagDto>;
434
203
  }
435
204
  ```
436
205
 
437
- ### Discriminator (Polymorphism)
206
+ ## Discriminator
438
207
 
439
208
  ```typescript
440
- class DogDto {
441
- @Field(isString) breed!: string;
442
- }
443
- class CatDto {
444
- @Field(isBoolean) indoor!: boolean;
445
- }
446
-
447
- class PetOwnerDto {
209
+ class PetOwner {
448
210
  @Field({
449
- type: () => DogDto,
211
+ type: () => CatDto,
450
212
  discriminator: {
451
- property: 'type',
213
+ property: 'kind',
452
214
  subTypes: [
453
- { value: DogDto, name: 'dog' },
454
215
  { value: CatDto, name: 'cat' },
216
+ { value: DogDto, name: 'dog' },
455
217
  ],
456
218
  },
457
- })
458
- pet!: DogDto | CatDto;
219
+ }) pet!: CatDto | DogDto;
459
220
  }
460
221
  ```
461
222
 
462
- Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
463
-
464
- ### Map / Set Collections
465
-
466
- Baker auto-converts between `Map`/`Set` and JSON-compatible types:
467
-
468
- ```typescript
469
- // Set<primitive>: JSON array ↔ Set
470
- @Field({ type: () => Set })
471
- tags!: Set<string>;
472
-
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>;
484
- ```
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
-
488
- ---
489
-
490
223
  ## Inheritance
491
224
 
492
- Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
493
-
494
225
  ```typescript
495
226
  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
- }
505
- ```
506
-
507
- ---
508
-
509
- ## Transform
510
-
511
- The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
512
-
513
- ```typescript
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;
523
-
524
- @Field(isString, {
525
- transform: async ({ value }) => {
526
- return await someAsyncOperation(value);
527
- },
528
- transformDirection: 'deserializeOnly',
529
- })
530
- data!: string;
531
- }
532
- ```
533
-
534
- ---
535
-
536
- ## Custom Rules
537
-
538
- ```typescript
539
- import { createRule } from '@zipbul/baker';
540
-
541
- const isPositiveInt = createRule({
542
- name: 'isPositiveInt',
543
- validate: (value) => Number.isInteger(value) && (value as number) > 0,
544
- });
545
-
546
- class Dto {
547
- @Field(isPositiveInt)
548
- count!: number;
227
+ @Field(isString) id!: string;
549
228
  }
550
- ```
551
-
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.
559
-
560
- ```typescript
561
- import { collectClassSchema } from '@zipbul/baker/src/collect';
562
229
 
563
- class CreateUserDto {
230
+ class UserDto extends BaseDto {
564
231
  @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
- })
581
- name!: string;
232
+ // inherits 'id' field with isString rule
582
233
  }
583
234
  ```
584
235
 
585
- ---
586
-
587
- ## JSON Schema
588
-
589
- Generate JSON Schema Draft 2020-12 from your DTOs:
236
+ ## Exports
590
237
 
591
238
  ```typescript
592
- import { toJsonSchema } from '@zipbul/baker';
239
+ // Functions
240
+ import { deserialize, validate, serialize, configure, createRule } from '@zipbul/baker';
593
241
 
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
- ```
242
+ // Decorators
243
+ import { Field, arrayOf } from '@zipbul/baker';
600
244
 
601
- ---
245
+ // Error handling
246
+ import { isBakerError, SealError } from '@zipbul/baker';
602
247
 
603
- ## How It Works
248
+ // Types
249
+ import type {
250
+ BakerError, BakerErrors, FieldOptions, FieldTransformParams,
251
+ ArrayOfMarker, EmittableRule, BakerConfig, ConfigureResult, RuntimeOptions,
252
+ } from '@zipbul/baker';
604
253
 
254
+ // Rules (subpath)
255
+ import { isString, isNumber, ... } from '@zipbul/baker/rules';
605
256
  ```
606
- Decorators (@Field) auto-seal (first call) deserialize() / serialize()
607
- metadata -> new Function() codegen -> execute generated code
608
- ```
609
-
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
257
 
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. |
258
+ ## What Baker Does Not Do
622
259
 
623
- ---
260
+ - JSON Schema / OpenAPI generation
261
+ - GraphQL schema generation
262
+ - Runtime type inference from schemas
263
+ - `reflect-metadata` dependency
624
264
 
625
265
  ## License
626
266
 
627
- [MIT](./LICENSE)
267
+ MIT