@zipbul/baker 2.0.0 → 2.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.md CHANGED
@@ -1,18 +1,14 @@
1
1
  # @zipbul/baker
2
2
 
3
- Decorator-based validation + transformation with inline code generation. Zero `reflect-metadata`.
3
+ The fastest decorator-based DTO validation library for TypeScript. Generates optimized validation code at class definition time (AOT), delivering **42ns per validation** — up to 163x faster than class-validator, 16x faster than Zod.
4
4
 
5
5
  ```bash
6
6
  bun add @zipbul/baker
7
7
  ```
8
8
 
9
- Requires `"experimentalDecorators": true` in `tsconfig.json`.
9
+ Zero `reflect-metadata`. Zero runtime overhead. 1,890 tests. 99%+ line coverage.
10
10
 
11
- ## API
12
-
13
- ### `deserialize<T>(Class, input, options?): T | BakerErrors | Promise<T | BakerErrors>`
14
-
15
- Validates input and creates a class instance. Sync DTOs return directly. Async DTOs (async transform/rules) return Promise. Always safe to `await`.
11
+ ## Quick Start
16
12
 
17
13
  ```typescript
18
14
  import { deserialize, isBakerError, Field } from '@zipbul/baker';
@@ -24,92 +20,82 @@ class UserDto {
24
20
  @Field(isString, isEmail()) email!: string;
25
21
  }
26
22
 
27
- const result = await deserialize(UserDto, { name: 'Alice', age: 30, email: 'alice@test.com' });
23
+ const result = await deserialize(UserDto, {
24
+ name: 'Alice', age: 30, email: 'alice@test.com',
25
+ });
28
26
 
29
27
  if (isBakerError(result)) {
30
- console.log(result.errors); // { path: string, code: string }[]
28
+ console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
31
29
  } else {
32
- console.log(result.name); // 'Alice'
30
+ console.log(result.name); // 'Alice' — typed as UserDto
33
31
  }
34
32
  ```
35
33
 
36
- Never throws on validation failure. Throws `SealError` only for programming errors (no `@Field` decorators, banned field names).
34
+ ## Why Baker?
37
35
 
38
- ### `validate(Class, input, options?): true | BakerErrors | Promise<true | BakerErrors>`
36
+ Baker generates optimized JavaScript validation functions **once** at class definition time, then executes them on every call — no interpretation, no schema traversal, no runtime compilation cost after the first seal.
39
37
 
40
- Same validation as `deserialize` without instance creation.
38
+ | Feature | baker | class-validator | Zod |
39
+ |---|---|---|---|
40
+ | Valid path (5 fields) | **42ns** | 6,852ns | 675ns |
41
+ | Invalid path (5 fields) | **93ns** | 10,109ns | 7,948ns |
42
+ | Approach | AOT code generation | Runtime interpretation | Schema method chain |
43
+ | Decorators | `@Field` (unified) | 30+ individual | N/A |
44
+ | `reflect-metadata` | Not needed | Required | N/A |
45
+ | Sync DTO return | Direct value | Promise | Direct value |
41
46
 
42
- ```typescript
43
- import { validate, isBakerError } from '@zipbul/baker';
47
+ ## Performance
44
48
 
45
- const result = await validate(UserDto, input);
46
- if (isBakerError(result)) { /* errors */ }
47
- ```
49
+ Benchmarked against 6 libraries on a simple 5-field DTO (valid + invalid input):
48
50
 
49
- ### `validate(input, ...rules): true | BakerErrors | Promise<true | BakerErrors>`
51
+ | Library | Valid | Invalid | vs baker (valid) | vs baker (invalid) |
52
+ |---|---|---|---|---|
53
+ | **baker** | **42ns** | **93ns** | — | — |
54
+ | TypeBox | 123ns | 112ns | 2.9x slower | 1.2x slower |
55
+ | AJV | 142ns | 201ns | 3.4x slower | 2.2x slower |
56
+ | ArkType | 145ns | 8,591ns | 3.4x slower | 92x slower |
57
+ | Valibot | 281ns | 1,070ns | 6.7x slower | 12x slower |
58
+ | Zod | 675ns | 7,948ns | 16x slower | 85x slower |
59
+ | class-validator | 6,852ns | 10,109ns | 163x slower | 109x slower |
50
60
 
51
- Ad-hoc single value validation. No DTO needed.
61
+ ## API
52
62
 
53
- ```typescript
54
- const result = await validate('hello@test.com', isString, isEmail());
55
- // result === true
56
- ```
63
+ ### `deserialize<T>(Class, input, options?)`
57
64
 
58
- ### `serialize<T>(instance, options?): Record<string, unknown> | Promise<Record<string, unknown>>`
65
+ Returns `T | BakerErrors | Promise<T | BakerErrors>`. Sync DTOs return directly — no Promise wrapping. Never throws on validation failure.
59
66
 
60
- Converts a class instance to a plain object. No validation. Sync DTOs return directly.
67
+ ### `serialize<T>(instance, options?)`
61
68
 
62
- ```typescript
63
- import { serialize } from '@zipbul/baker';
69
+ Returns `Record<string, unknown> | Promise<Record<string, unknown>>`. No validation. Sync DTOs return directly.
64
70
 
65
- const plain = await serialize(userInstance);
66
- ```
71
+ ### `validate(Class, input, options?)` / `validate(input, ...rules)`
67
72
 
68
- ### `isBakerError(value): value is BakerErrors`
73
+ DTO-level or ad-hoc single-value validation. Returns `true | BakerErrors`.
69
74
 
70
- Type guard. Narrows `deserialize`/`validate` result to error type.
75
+ ### `isBakerError(value)`
71
76
 
72
- ```typescript
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
78
- }
79
- ```
77
+ Type guard. Narrows result to `BakerErrors` containing `{ path, code, message?, context? }[]`.
80
78
 
81
- ### `configure(config): ConfigureResult`
79
+ ### `configure(config)`
82
80
 
83
- Global configuration. Call before first `deserialize`/`serialize`/`validate`.
81
+ Global configuration. Call before first deserialize/serialize/validate.
84
82
 
85
83
  ```typescript
86
- import { configure } from '@zipbul/baker';
87
-
88
84
  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
85
+ autoConvert: true, // coerce "123" → 123
86
+ allowClassDefaults: true, // use class field initializers for missing keys
87
+ stopAtFirstError: true, // return on first validation failure
88
+ forbidUnknown: true, // reject undeclared fields
93
89
  });
94
90
  ```
95
91
 
96
- ### `createRule(name, validate): EmittableRule`
92
+ ### `createRule(name, validate)`
97
93
 
98
- Creates a custom validation rule.
99
-
100
- ```typescript
101
- import { createRule } from '@zipbul/baker';
94
+ Custom validation rule with optional AOT `emit()` for maximum performance.
102
95
 
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
- });
110
- ```
96
+ ## @Field Decorator
111
97
 
112
- ## `@Field` Decorator
98
+ One decorator for everything — replaces 30+ individual decorators from class-validator.
113
99
 
114
100
  ```typescript
115
101
  @Field(...rules)
@@ -119,43 +105,103 @@ const isUnique = createRule({
119
105
 
120
106
  ### Options
121
107
 
108
+ | Option | Type | Description |
109
+ |---|---|---|
110
+ | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
111
+ | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
112
+ | `optional` | `boolean` | Allow undefined |
113
+ | `nullable` | `boolean` | Allow null |
114
+ | `name` | `string` | Bidirectional key mapping |
115
+ | `deserializeName` | `string` | Input key mapping |
116
+ | `serializeName` | `string` | Output key mapping |
117
+ | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
118
+ | `groups` | `string[]` | Conditional visibility |
119
+ | `when` | `(obj) => boolean` | Conditional validation |
120
+ | `transform` | `Transformer \| Transformer[]` | Value transformer |
121
+ | `message` | `string \| (args) => string` | Error message override |
122
+ | `context` | `unknown` | Error context |
123
+ | `mapValue` | `() => Dto` | Map value DTO |
124
+ | `setValue` | `() => Dto` | Set element DTO |
125
+
126
+ ## Transformers
127
+
128
+ Bidirectional value transformers with separate `deserialize` and `serialize` methods.
129
+
130
+ ```typescript
131
+ import type { Transformer } from '@zipbul/baker';
132
+
133
+ const centsTransformer: Transformer = {
134
+ deserialize: ({ value }) => typeof value === 'number' ? value * 100 : value,
135
+ serialize: ({ value }) => typeof value === 'number' ? value / 100 : value,
136
+ };
137
+ ```
138
+
139
+ ### Built-in Transformers
140
+
122
141
  ```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
142
+ import {
143
+ trimTransformer, toLowerCaseTransformer, toUpperCaseTransformer,
144
+ roundTransformer, unixSecondsTransformer, unixMillisTransformer,
145
+ isoStringTransformer, csvTransformer, jsonTransformer,
146
+ } from '@zipbul/baker/transformers';
147
+ ```
148
+
149
+ | Transformer | deserialize | serialize |
150
+ |---|---|---|
151
+ | `trimTransformer` | trim string | trim string |
152
+ | `toLowerCaseTransformer` | lowercase | lowercase |
153
+ | `toUpperCaseTransformer` | uppercase | uppercase |
154
+ | `roundTransformer(n?)` | round to n decimals | round to n decimals |
155
+ | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
156
+ | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
157
+ | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
158
+ | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
159
+ | `jsonTransformer` | JSON string &rarr; object | object &rarr; JSON string |
160
+
161
+ ### Transform Array Order
162
+
163
+ Multiple transformers apply as a codec stack:
164
+ - **Deserialize**: left to right — `[A, B, C]` applies A, then B, then C
165
+ - **Serialize**: right to left — `[A, B, C]` applies C, then B, then A
166
+
167
+ ```typescript
168
+ @Field(isString, { transform: [trimTransformer, toLowerCaseTransformer] })
169
+ email!: string;
170
+ // deserialize " HELLO " → trim → toLowerCase → "hello"
171
+ // serialize "hello" → toLowerCase → trim → "hello"
172
+ ```
173
+
174
+ ### Optional Peer Transformers
175
+
176
+ ```typescript
177
+ // bun add luxon
178
+ import { luxonTransformer } from '@zipbul/baker/transformers';
179
+ const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
180
+
181
+ class EventDto {
182
+ @Field({ transform: luxon }) startAt!: DateTime;
145
183
  }
146
184
  ```
147
185
 
186
+ ```typescript
187
+ // bun add moment
188
+ import { momentTransformer } from '@zipbul/baker/transformers';
189
+ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
190
+ ```
191
+
148
192
  ## Rules
149
193
 
194
+ 104 built-in validation rules.
195
+
150
196
  ### Type Checkers
151
197
 
152
- `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()`.
198
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()` needed.
153
199
 
154
- `isNumber(options?)`, `isEnum(entity)` — factories, need `()`.
200
+ `isNumber(options?)`, `isEnum(entity)` — factories, require `()`.
155
201
 
156
202
  ### Numbers
157
203
 
158
- `min(n)`, `max(n)`, `min(n, { exclusive: true })`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
204
+ `min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
159
205
 
160
206
  ### Strings
161
207
 
@@ -163,7 +209,7 @@ interface FieldOptions {
163
209
 
164
210
  ### Formats
165
211
 
166
- `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber`, `isStrongPassword()`
212
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`
167
213
 
168
214
  ### Arrays
169
215
 
@@ -199,7 +245,7 @@ class UserDto {
199
245
  ```typescript
200
246
  class UserDto {
201
247
  @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
202
- @Field({ type: () => Map as any, mapValue: () => TagDto }) tagMap!: Map<string, TagDto>;
248
+ @Field({ type: () => Map as any, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
203
249
  }
204
250
  ```
205
251
 
@@ -233,34 +279,36 @@ class UserDto extends BaseDto {
233
279
  }
234
280
  ```
235
281
 
236
- ## Exports
282
+ ## FAQ
237
283
 
238
- ```typescript
239
- // Functions
240
- import { deserialize, validate, serialize, configure, createRule } from '@zipbul/baker';
284
+ ### When should I use baker instead of class-validator?
241
285
 
242
- // Decorators
243
- import { Field, arrayOf } from '@zipbul/baker';
286
+ When performance matters. baker is 163x faster on valid input and 109x faster on invalid input, while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency.
244
287
 
245
- // Error handling
246
- import { isBakerError, SealError } from '@zipbul/baker';
288
+ ### How does baker compare to Zod?
247
289
 
248
- // Types
249
- import type {
250
- BakerError, BakerErrors, FieldOptions, FieldTransformParams,
251
- ArrayOfMarker, EmittableRule, BakerConfig, ConfigureResult, RuntimeOptions,
252
- } from '@zipbul/baker';
290
+ Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker is 16x faster on valid input because it generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design; choose baker if you need class-based DTOs with maximum performance.
253
291
 
254
- // Rules (subpath)
255
- import { isString, isNumber, ... } from '@zipbul/baker/rules';
256
- ```
292
+ ### Does baker support async validation?
293
+
294
+ Yes. If any rule or transformer is async, baker automatically detects it at seal time and generates an async executor. Sync DTOs return values directly without Promise wrapping.
295
+
296
+ ### Can I use baker with NestJS?
257
297
 
258
- ## What Baker Does Not Do
298
+ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()` in a custom validation pipe.
259
299
 
260
- - JSON Schema / OpenAPI generation
261
- - GraphQL schema generation
262
- - Runtime type inference from schemas
263
- - `reflect-metadata` dependency
300
+ ### How does the AOT code generation work?
301
+
302
+ On the first call to `deserialize`/`serialize`/`validate`, baker seals all registered DTOs: it analyzes field metadata, generates optimized JavaScript validation functions via `new Function()`, and caches them. Subsequent calls execute the pre-compiled functions directly.
303
+
304
+ ## Exports
305
+
306
+ ```typescript
307
+ import { deserialize, validate, serialize, configure, createRule, Field, arrayOf, isBakerError, SealError } from '@zipbul/baker';
308
+ import type { Transformer, TransformParams, BakerError, BakerErrors, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
309
+ import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
310
+ import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
311
+ ```
264
312
 
265
313
  ## License
266
314
 
@@ -0,0 +1,3 @@
1
+ // @bun
2
+ var f=Symbol.for("baker:raw"),g=Symbol.for("baker:sealed");
3
+ export{f as d,g as e};
@@ -0,0 +1,3 @@
1
+ // @bun
2
+ import{d as v}from"./index-fnv35wrf.js";var z=new Set;function B(h,w){if(!Object.prototype.hasOwnProperty.call(h,v))h[v]=Object.create(null),z.add(h);let G=h[v];return G[w]??={validation:[],transform:[],expose:[],exclude:null,type:null,flags:{}}}var H=Symbol.for("baker:arrayOf");function T(...h){let w={rules:h};return w[H]=!0,w}function J(h){return typeof h==="object"&&h!==null&&h[H]===!0}var U=new Set(["type","discriminator","keepDiscriminatorProperty","rules","optional","nullable","name","deserializeName","serializeName","exclude","groups","when","transform","message","context","mapValue","setValue"]);function C(h){if(typeof h==="function")return!1;if(typeof h!=="object"||h===null)return!1;if(J(h))return!1;let w=Object.keys(h);if(w.length===0)return!0;return w.some((G)=>U.has(G))}function X(h){if(h.length===0)return{rules:[],options:{}};if(h.length===1&&C(h[0])){let G=h[0];return{rules:G.rules??[],options:G}}let w=h[h.length-1];if(C(w)){let G=w,j=h.slice(0,-1);if(G.rules)j=[...j,...G.rules];return{rules:j,options:G}}return{rules:h,options:{}}}function Z(h,w,G){for(let j of w)if(J(j))for(let q of j.rules){let b={rule:q,each:!0,groups:G.groups};if(G.message!==void 0)b.message=G.message;if(G.context!==void 0)b.context=G.context;h.validation.push(b)}else{let q={rule:j,groups:G.groups};if(G.message!==void 0)q.message=G.message;if(G.context!==void 0)q.context=G.context;h.validation.push(q)}}function $(h,w){if(w.name)h.expose.push({name:w.name,groups:w.groups});else if(w.deserializeName||w.serializeName){if(w.deserializeName)h.expose.push({name:w.deserializeName,deserializeOnly:!0,groups:w.groups});if(w.serializeName)h.expose.push({name:w.serializeName,serializeOnly:!0,groups:w.groups})}else if(w.groups)h.expose.push({groups:w.groups});else h.expose.push({})}function K(h,w){if(!w.transform)return;let G=Array.isArray(w.transform)?w.transform:[w.transform];for(let j of G)h.transform.push({fn:j.deserialize,options:{deserializeOnly:!0}},{fn:j.serialize,options:{serializeOnly:!0}})}function E(...h){return(w,G)=>{let j=w.constructor,b=B(j,G),{rules:Q,options:S}=X(h);if(Z(b,Q,S),S.optional)b.flags.isOptional=!0;if(S.nullable)b.flags.isNullable=!0;if(S.when)b.flags.validateIf=S.when;if(S.type)b.type={fn:S.type,discriminator:S.discriminator,keepDiscriminatorProperty:S.keepDiscriminatorProperty,collectionValue:S.mapValue??S.setValue};if($(b,S),S.exclude){if(S.exclude===!0)b.exclude={};else if(S.exclude==="deserializeOnly")b.exclude={deserializeOnly:!0};else if(S.exclude==="serializeOnly")b.exclude={serializeOnly:!0}}K(b,S)}}
3
+ export{z as a,T as b,E as c};
@@ -0,0 +1,3 @@
1
+ // @bun
2
+ var a=import.meta.require;
3
+ export{a as f};
@@ -1,4 +1 @@
1
1
  // @bun
2
-
3
- //# debugId=029AE51BD364B21F64756E2164756E21
4
- //# sourceMappingURL=index-xdn55cz3.js.map
package/dist/index.d.ts CHANGED
@@ -4,9 +4,9 @@ export { serialize } from './src/functions/serialize';
4
4
  export { configure } from './src/configure';
5
5
  export { createRule } from './src/create-rule';
6
6
  export { Field, arrayOf } from './src/decorators/index';
7
- export type { FieldOptions, FieldTransformParams, ArrayOfMarker } from './src/decorators/index';
7
+ export type { FieldOptions, ArrayOfMarker } from './src/decorators/index';
8
8
  export type { BakerError, BakerErrors } from './src/errors';
9
9
  export { isBakerError, SealError } from './src/errors';
10
- export type { EmittableRule } from './src/types';
10
+ export type { EmittableRule, Transformer, TransformParams } from './src/types';
11
11
  export type { BakerConfig, ConfigureResult } from './src/configure';
12
12
  export type { RuntimeOptions } from './src/interfaces';