@zipbul/baker 4.0.0 → 5.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/CHANGELOG.md +31 -0
- package/README.md +53 -47
- package/dist/index.d.ts +2 -8
- package/dist/index.js +1 -1
- package/dist/src/baker.d.ts +34 -18
- package/dist/src/baker.js +1 -1
- package/dist/src/configure.d.ts +2 -13
- package/dist/src/configure.js +1 -1
- package/dist/src/decorators/index.d.ts +0 -1
- package/dist/src/decorators/index.js +1 -1
- 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 +5 -17
- package/dist/src/functions/deserialize.js +1 -1
- package/dist/src/functions/serialize.d.ts +8 -13
- package/dist/src/functions/serialize.js +1 -1
- package/dist/src/functions/validate.d.ts +5 -16
- package/dist/src/functions/validate.js +1 -1
- package/dist/src/meta-access.d.ts +1 -8
- package/dist/src/meta-access.js +1 -1
- package/dist/src/seal/deserialize-builder.d.ts +4 -4
- package/dist/src/seal/deserialize-builder.js +243 -243
- package/dist/src/seal/seal.d.ts +13 -39
- package/dist/src/seal/seal.js +1 -1
- package/dist/src/seal/serialize-builder.d.ts +2 -2
- package/dist/src/seal/serialize-builder.js +48 -48
- package/dist/src/symbols.d.ts +2 -4
- package/dist/src/symbols.js +1 -1
- package/dist/src/types.d.ts +4 -4
- 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,36 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
+
## 5.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2d61542: Baker-scoped runtime: `deserialize`/`validate`/`serialize` (plus the `*Sync`/`*Async`
|
|
8
|
+
variants) are now methods on a `Baker` instance — `app.deserialize(Dto, input)`. Each
|
|
9
|
+
baker compiles its own executor per class into its own map, so the **same class sealed by
|
|
10
|
+
two bakers with different configs behaves per each baker's config** — apps in one process
|
|
11
|
+
never mix. An undecorated subclass resolves to its nearest sealed ancestor within that
|
|
12
|
+
baker. Same-config bakers transparently share one compiled executor via a `(class, config)`
|
|
13
|
+
cache (compile once, no behavior change).
|
|
14
|
+
|
|
15
|
+
**BREAKING CHANGE:** the global `deserialize`/`validate`/`serialize` functions (and their
|
|
16
|
+
`*Sync`/`*Async` variants) are removed from the package entry, along with the published
|
|
17
|
+
`SEALED` symbol (`RAW` remains on `@zipbul/baker/symbols`). Migrate `deserialize(Dto, input)`
|
|
18
|
+
to `app.deserialize(Dto, input)` on the `Baker` instance that sealed the class.
|
|
19
|
+
|
|
20
|
+
## 5.0.0
|
|
21
|
+
|
|
22
|
+
### Major Changes
|
|
23
|
+
|
|
24
|
+
- 8ea7162: Remove the global registration API in favor of the `Baker` class. `new Baker(config?)`
|
|
25
|
+
is now the only way to register and seal DTOs: use `@app.Recipe` to register a class and
|
|
26
|
+
`app.seal()` to seal it. The global `@Recipe`, `seal()`, `configure()`, and the `createBaker()`
|
|
27
|
+
factory have been removed — each `Baker` instance owns its own isolated registry and config, so
|
|
28
|
+
multiple apps in one process never mix. `@Field`, the rule/transformer factories, and
|
|
29
|
+
`deserialize`/`validate`/`serialize` are unchanged.
|
|
30
|
+
|
|
31
|
+
Migration: replace `configure(opts)` + global `@Recipe`/`seal()` with
|
|
32
|
+
`const app = new Baker(opts); @app.Recipe class Dto {}; app.seal();`.
|
|
33
|
+
|
|
3
34
|
## 4.0.0
|
|
4
35
|
|
|
5
36
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
-
The fastest decorator-based DTO validation library for TypeScript. baker generates optimized validation and serialization code once at
|
|
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
|
|
@@ -27,21 +27,23 @@ Zero `reflect-metadata`. Sealed codegen.
|
|
|
27
27
|
## Quick Start
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
|
-
import {
|
|
30
|
+
import { Baker, Field, isBakerIssueSet } from '@zipbul/baker';
|
|
31
31
|
import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const baker = new Baker();
|
|
34
|
+
|
|
35
|
+
@baker.Recipe
|
|
34
36
|
class UserDto {
|
|
35
37
|
@Field(isString, minLength(2)) name!: string;
|
|
36
38
|
@Field(isNumber(), min(0)) age!: number;
|
|
37
39
|
@Field(isString, isEmail()) email!: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
// Call once at
|
|
41
|
-
seal();
|
|
42
|
+
// Call once at startup, after this baker's DTOs are defined.
|
|
43
|
+
baker.seal();
|
|
42
44
|
|
|
43
45
|
// All rules here are sync, so deserialize returns the value directly (no await).
|
|
44
|
-
const result = deserialize(UserDto, {
|
|
46
|
+
const result = baker.deserialize(UserDto, {
|
|
45
47
|
name: 'Alice',
|
|
46
48
|
age: 30,
|
|
47
49
|
email: 'alice@test.com',
|
|
@@ -61,10 +63,13 @@ if (isBakerIssueSet(result)) {
|
|
|
61
63
|
|
|
62
64
|
| Concept | What it does |
|
|
63
65
|
| ------------------- | ------------------------------------------------------------------------------ |
|
|
64
|
-
|
|
|
65
|
-
| `@
|
|
66
|
-
|
|
|
67
|
-
| `
|
|
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
|
+
| `app.deserialize` / `app.validate` / `app.serialize` | Run that baker's compiled executors: parse+validate, validate-only, or emit a plain object. |
|
|
71
|
+
|
|
72
|
+
> Examples below assume a `const baker = new Baker()` in scope and a single `baker.seal()` after the DTOs are defined.
|
|
68
73
|
|
|
69
74
|
## Why baker?
|
|
70
75
|
|
|
@@ -122,7 +127,7 @@ Most fields need only rules. The options below cover nested, conditional, collec
|
|
|
122
127
|
### Conditional fields & custom messages
|
|
123
128
|
|
|
124
129
|
```typescript
|
|
125
|
-
@Recipe
|
|
130
|
+
@baker.Recipe
|
|
126
131
|
class UserDto {
|
|
127
132
|
@Field(isString) name!: string;
|
|
128
133
|
|
|
@@ -138,8 +143,8 @@ class UserDto {
|
|
|
138
143
|
displayName!: string;
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
deserialize(UserDto, input); // `ssn` is skipped
|
|
142
|
-
deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
|
|
146
|
+
baker.deserialize(UserDto, input); // `ssn` is skipped
|
|
147
|
+
baker.deserialize(UserDto, input, { groups: ['admin'] }); // `ssn` is included
|
|
143
148
|
```
|
|
144
149
|
|
|
145
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.
|
|
@@ -250,7 +255,7 @@ email!: string;
|
|
|
250
255
|
import { luxonTransformer } from '@zipbul/baker/transformers';
|
|
251
256
|
const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
|
|
252
257
|
|
|
253
|
-
@Recipe
|
|
258
|
+
@baker.Recipe
|
|
254
259
|
class EventDto {
|
|
255
260
|
@Field({ transform: luxon }) startAt!: DateTime;
|
|
256
261
|
}
|
|
@@ -269,12 +274,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
|
269
274
|
### Nested DTOs
|
|
270
275
|
|
|
271
276
|
```typescript
|
|
272
|
-
@Recipe
|
|
277
|
+
@baker.Recipe
|
|
273
278
|
class AddressDto {
|
|
274
279
|
@Field(isString) city!: string;
|
|
275
280
|
}
|
|
276
281
|
|
|
277
|
-
@Recipe
|
|
282
|
+
@baker.Recipe
|
|
278
283
|
class UserDto {
|
|
279
284
|
@Field({ type: () => AddressDto }) address!: AddressDto;
|
|
280
285
|
@Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
|
|
@@ -284,7 +289,7 @@ class UserDto {
|
|
|
284
289
|
### Collections
|
|
285
290
|
|
|
286
291
|
```typescript
|
|
287
|
-
@Recipe
|
|
292
|
+
@baker.Recipe
|
|
288
293
|
class UserDto {
|
|
289
294
|
@Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
|
|
290
295
|
@Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
|
|
@@ -296,7 +301,7 @@ class UserDto {
|
|
|
296
301
|
### Discriminator
|
|
297
302
|
|
|
298
303
|
```typescript
|
|
299
|
-
@Recipe
|
|
304
|
+
@baker.Recipe
|
|
300
305
|
class PetOwner {
|
|
301
306
|
@Field({
|
|
302
307
|
type: () => CatDto,
|
|
@@ -315,12 +320,12 @@ class PetOwner {
|
|
|
315
320
|
### Inheritance
|
|
316
321
|
|
|
317
322
|
```typescript
|
|
318
|
-
@Recipe
|
|
323
|
+
@baker.Recipe
|
|
319
324
|
class BaseDto {
|
|
320
325
|
@Field(isString) id!: string;
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
@Recipe
|
|
328
|
+
@baker.Recipe
|
|
324
329
|
class UserDto extends BaseDto {
|
|
325
330
|
@Field(isString) name!: string;
|
|
326
331
|
// inherits 'id' field with isString rule
|
|
@@ -329,11 +334,26 @@ class UserDto extends BaseDto {
|
|
|
329
334
|
|
|
330
335
|
## Runtime API
|
|
331
336
|
|
|
332
|
-
### `
|
|
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
|
+
```
|
|
333
353
|
|
|
334
|
-
|
|
354
|
+
`app.deserialize` / `app.serialize` / `app.validate` run that baker's compiled executors and throw `BakerError` if the class was not sealed by this baker.
|
|
335
355
|
|
|
336
|
-
|
|
356
|
+
**Isolation:** each baker compiles its own executor per class into its own map, so the **same class sealed by two bakers behaves per each baker's config** — apps never mix. (An undecorated subclass resolves to its nearest sealed ancestor within that baker.)
|
|
337
357
|
|
|
338
358
|
### `deserialize` / `serialize` / `validate`
|
|
339
359
|
|
|
@@ -368,19 +388,6 @@ interface RuntimeOptions {
|
|
|
368
388
|
|
|
369
389
|
Groups are passed at call time (not on `@Field`) because the active set typically varies per request.
|
|
370
390
|
|
|
371
|
-
### `configure(config)`
|
|
372
|
-
|
|
373
|
-
Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`.
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
configure({
|
|
377
|
-
autoConvert: true, // coerce "123" → 123
|
|
378
|
-
allowClassDefaults: true, // use class field initializers for missing keys
|
|
379
|
-
stopAtFirstError: true, // return on first validation failure
|
|
380
|
-
forbidUnknown: true, // reject undeclared fields
|
|
381
|
-
});
|
|
382
|
-
```
|
|
383
|
-
|
|
384
391
|
### `createRule(name, validate)` / `createRule(options)`
|
|
385
392
|
|
|
386
393
|
Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
|
|
@@ -390,10 +397,12 @@ const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as strin
|
|
|
390
397
|
```
|
|
391
398
|
|
|
392
399
|
```typescript
|
|
400
|
+
import { RequiredType } from '@zipbul/baker';
|
|
401
|
+
|
|
393
402
|
const isEven = createRule({
|
|
394
403
|
name: 'isEven',
|
|
395
404
|
validate: v => typeof v === 'number' && v % 2 === 0,
|
|
396
|
-
requiresType:
|
|
405
|
+
requiresType: RequiredType.Number,
|
|
397
406
|
});
|
|
398
407
|
```
|
|
399
408
|
|
|
@@ -405,11 +414,11 @@ Type guard. Narrows a result to `BakerIssueSet`, whose `errors` array holds `{ p
|
|
|
405
414
|
|
|
406
415
|
baker separates two failure modes:
|
|
407
416
|
|
|
408
|
-
- **`BakerError` (thrown)** — a programming mistake: using a DTO before `seal()`, passing a raw rule function,
|
|
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.
|
|
409
418
|
- **`BakerIssueSet` (returned)** — a validation failure. `deserialize` and `validate` return it instead of throwing. Guard with `isBakerIssueSet` and read `.errors`.
|
|
410
419
|
|
|
411
420
|
```typescript
|
|
412
|
-
const result = deserialize(UserDto, input);
|
|
421
|
+
const result = baker.deserialize(UserDto, input);
|
|
413
422
|
|
|
414
423
|
if (isBakerIssueSet(result)) {
|
|
415
424
|
for (const issue of result.errors) {
|
|
@@ -436,11 +445,11 @@ Yes. If any rule or transformer is async, baker automatically detects it at seal
|
|
|
436
445
|
|
|
437
446
|
### Can I use baker with NestJS?
|
|
438
447
|
|
|
439
|
-
Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()` in a custom validation pipe.
|
|
448
|
+
Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `app.deserialize()` (your `Baker` instance) in a custom validation pipe.
|
|
440
449
|
|
|
441
450
|
### How does the AOT code generation work?
|
|
442
451
|
|
|
443
|
-
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 in that baker's map. Subsequent `app.deserialize` / `app.serialize` / `app.validate` calls run the pre-compiled functions directly. There is no auto-seal — using a DTO before `app.seal()` raises `BakerError`.
|
|
444
453
|
|
|
445
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.
|
|
446
455
|
|
|
@@ -448,13 +457,10 @@ Calling `seal()` once at app startup walks every registered DTO, analyzes field
|
|
|
448
457
|
|
|
449
458
|
```typescript
|
|
450
459
|
import {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
validate, validateSync, validateAsync,
|
|
454
|
-
serialize, serializeSync, serializeAsync,
|
|
455
|
-
configure, createRule, Field, Recipe, arrayOf, isBakerIssueSet, BakerError,
|
|
460
|
+
Baker, // .deserialize / .validate / .serialize (+ *Sync / *Async) live on the instance
|
|
461
|
+
createRule, Field, arrayOf, isBakerIssueSet, BakerError, RequiredType, ExcludeMode,
|
|
456
462
|
} from '@zipbul/baker';
|
|
457
|
-
import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
|
|
463
|
+
import type { Transformer, TransformParams, BakerIssue, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions, BakerConfig } from '@zipbul/baker';
|
|
458
464
|
import { isString, isEmail, isULID, isCUID2 /* …114 rules */ } from '@zipbul/baker/rules';
|
|
459
465
|
import { trimTransformer, jsonTransformer /* …and more */ } from '@zipbul/baker/transformers';
|
|
460
466
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
export { deserialize, deserializeSync, deserializeAsync } from './src/functions/deserialize';
|
|
2
|
-
export { validate, validateSync, validateAsync } from './src/functions/validate';
|
|
3
|
-
export { serialize, serializeSync, serializeAsync } from './src/functions/serialize';
|
|
4
|
-
export { configure } from './src/configure';
|
|
5
1
|
export { createRule } from './src/create-rule';
|
|
6
|
-
export {
|
|
7
|
-
export { Field, arrayOf, Recipe } from './src/decorators/index';
|
|
2
|
+
export { Field, arrayOf } from './src/decorators/index';
|
|
8
3
|
export type { FieldOptions, ArrayOfMarker } from './src/decorators/index';
|
|
9
|
-
export {
|
|
10
|
-
export type { Baker } from './src/baker';
|
|
4
|
+
export { Baker } from './src/baker';
|
|
11
5
|
export { ExcludeMode, RequiredType } from './src/enums';
|
|
12
6
|
export type { BakerIssue, BakerIssueSet } from './src/errors';
|
|
13
7
|
export { isBakerIssueSet, BakerError } from './src/errors';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{
|
|
1
|
+
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";
|
package/dist/src/baker.d.ts
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
1
|
import type { BakerConfig } from './configure';
|
|
2
|
+
import type { BakerIssueSet } from './errors';
|
|
3
|
+
import type { RuntimeOptions } from './interfaces';
|
|
2
4
|
/**
|
|
3
|
-
* A baker
|
|
4
|
-
* registry
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
9
|
-
export interface Baker {
|
|
10
|
-
/** Class decorator — registers the class as a root of THIS scope. Use as `@app.Recipe`. */
|
|
11
|
-
readonly Recipe: (value: Function, context: ClassDecoratorContext) => void;
|
|
12
|
-
/** Seal every root registered to this scope (and its nested DTOs) with this scope's config. */
|
|
13
|
-
readonly seal: () => void;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Create an isolated baker scope. Use for libraries and multi-app processes where each app must
|
|
17
|
-
* not mix with another. Single-app code can keep using the global `@Recipe` / `seal()` / `configure()`.
|
|
5
|
+
* A baker — an isolated registration + seal + runtime boundary. Each `new Baker()` owns its own
|
|
6
|
+
* registry, config, and compiled executors, so multiple bakers in one process (or a bundler-duplicated
|
|
7
|
+
* copy of the library) never fragment each other. `@Field` and the rule factories stay global (they
|
|
8
|
+
* write class-intrinsic schema); registration (`Recipe`), sealing (`seal`), and running
|
|
9
|
+
* (`deserialize`/`serialize`/`validate`) all belong to the instance.
|
|
18
10
|
*
|
|
19
11
|
* ```ts
|
|
20
|
-
* const app =
|
|
12
|
+
* const app = new Baker({ autoConvert: true });
|
|
21
13
|
* @app.Recipe class UserDto { @Field(isString) name!: string }
|
|
22
14
|
* app.seal();
|
|
23
|
-
* deserialize(UserDto, input);
|
|
15
|
+
* app.deserialize(UserDto, input);
|
|
24
16
|
* ```
|
|
17
|
+
*
|
|
18
|
+
* Isolation boundary is class identity, scoped per baker: the SAME class sealed by two bakers with
|
|
19
|
+
* different configs behaves per each baker's config (each compiles its own executor into its own map).
|
|
20
|
+
*
|
|
21
|
+
* `Recipe` and `seal` are arrow-field properties, not prototype methods, by design: `@app.Recipe`
|
|
22
|
+
* is applied as a detached value (the runtime calls the decorator with no `this` receiver), so it
|
|
23
|
+
* must be bound to the instance; arrow fields also keep `const { Recipe, seal } = new Baker()`
|
|
24
|
+
* working.
|
|
25
25
|
*/
|
|
26
|
-
export declare
|
|
26
|
+
export declare class Baker {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(config?: BakerConfig);
|
|
29
|
+
/** Class decorator — registers the class as a root of this baker. Use as `@app.Recipe`. */
|
|
30
|
+
readonly Recipe: (value: Function, _context: ClassDecoratorContext) => void;
|
|
31
|
+
/** Seal every root registered to this baker (and its nested DTOs) with this baker's config. */
|
|
32
|
+
readonly seal: () => void;
|
|
33
|
+
deserialize: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => T | BakerIssueSet | Promise<T | BakerIssueSet>;
|
|
34
|
+
deserializeSync: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => T | BakerIssueSet;
|
|
35
|
+
deserializeAsync: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => Promise<T | BakerIssueSet>;
|
|
36
|
+
validate: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => true | BakerIssueSet | Promise<true | BakerIssueSet>;
|
|
37
|
+
validateSync: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => true | BakerIssueSet;
|
|
38
|
+
validateAsync: <T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions) => Promise<true | BakerIssueSet>;
|
|
39
|
+
serialize: <T>(instance: T, options?: RuntimeOptions) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
40
|
+
serializeSync: <T>(instance: T, options?: RuntimeOptions) => Record<string, unknown>;
|
|
41
|
+
serializeAsync: <T>(instance: T, options?: RuntimeOptions) => Promise<Record<string, unknown>>;
|
|
42
|
+
}
|
package/dist/src/baker.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export class Baker{#G=new Set;#F=new Map;#H;#I=!1;constructor(j){this.#H=j===void 0?Object.freeze({}):K(j)}Recipe=(j,F)=>{this.#G.add(j)};seal=()=>{if(this.#I)return;_(this.#G,this.#H,this.#F);this.#I=!0};#j(j){let F=j;while(F){const H=this.#F.get(F);if(H){if(F!==j)this.#F.set(j,H);return H}const J=Object.getPrototypeOf(F);F=typeof J==="function"?J:null}const G=j.name||"<anonymous class>";throw new L(`${G} is not sealed by this baker`)}deserialize=(j,F,G)=>M(this.#j(j),F,G);deserializeSync=(j,F,G)=>N(this.#j(j),j.name,F,G);deserializeAsync=(j,F,G)=>P(this.#j(j),F,G);validate=(j,F,G)=>X(this.#j(j),F,G);validateSync=(j,F,G)=>Y(this.#j(j),j.name,F,G);validateAsync=(j,F,G)=>Z(this.#j(j),F,G);serialize=(j,F)=>Q(this.#j(I(j,"serialize")),j,F);serializeSync=(j,F)=>{const G=I(j,"serializeSync");return U(this.#j(G),G.name,j,F)};serializeAsync=(j,F)=>W(this.#j(I(j,"serializeAsync")),j,F)}import{normalizeConfig as K}from"./configure.js";import{BakerError as L}from"./errors.js";import{runDeserialize as M,runDeserializeSync as N,runDeserializeAsync as P}from"./functions/deserialize.js";import{resolveSerializeClass as I,runSerialize as Q,runSerializeSync as U,runSerializeAsync as W}from"./functions/serialize.js";import{runValidate as X,runValidateSync as Y,runValidateAsync as Z}from"./functions/validate.js";import{sealRegistry as _}from"./seal/seal.js";
|
package/dist/src/configure.d.ts
CHANGED
|
@@ -12,19 +12,8 @@ interface BakerConfig {
|
|
|
12
12
|
debug?: boolean;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* If not called, defaults are applied.
|
|
17
|
-
*/
|
|
18
|
-
declare function configure(config: BakerConfig): void;
|
|
19
|
-
/**
|
|
20
|
-
* Validate a BakerConfig and map it to the internal SealOptions. Shared by `configure()`
|
|
21
|
-
* (default instance) and `createBaker()` (per-scope instances). Does NOT check seal state —
|
|
22
|
-
* that gate is specific to the global `configure()`.
|
|
15
|
+
* Validate a BakerConfig and map it to the internal SealOptions. Used by `new Baker(config)`.
|
|
23
16
|
*/
|
|
24
17
|
declare function normalizeConfig(config: BakerConfig): SealOptions;
|
|
25
|
-
|
|
26
|
-
declare function getGlobalOptions(): SealOptions;
|
|
27
|
-
/** @internal — reset to defaults on unseal */
|
|
28
|
-
declare function resetConfigForTesting(): void;
|
|
29
|
-
export { configure, getGlobalOptions, resetConfigForTesting, normalizeConfig };
|
|
18
|
+
export { normalizeConfig };
|
|
30
19
|
export type { BakerConfig };
|
package/dist/src/configure.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BakerError as
|
|
1
|
+
import{BakerError as u}from"./errors.js";const v=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);function normalizeConfig(j){if(j===null||typeof j!=="object"||Array.isArray(j))throw new u(`[baker] config requires a plain object. Received: ${j===null?"null":Array.isArray(j)?"array":typeof j}.`);for(const q of Object.keys(j))if(!v.has(q))throw new u(`[baker] unknown key '${q}'. Valid keys: ${[...v].join(", ")}.`);return Object.freeze({enableImplicitConversion:j.autoConvert??!1,exposeDefaultValues:j.allowClassDefaults??!1,stopAtFirstError:j.stopAtFirstError??!1,whitelist:j.forbidUnknown??!1,debug:j.debug??!1})}export{normalizeConfig};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{Field,arrayOf}from"./field.js";
|
|
1
|
+
export{Field,arrayOf}from"./field.js";
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function toBakerIssueSet(errors: BakerIssue[]): BakerIssueSet;
|
|
|
47
47
|
*
|
|
48
48
|
* Thrown when, e.g.:
|
|
49
49
|
* - deserialize()/serialize()/validate() is called on an unsealed class
|
|
50
|
-
* -
|
|
50
|
+
* - new Baker() receives a config object with an unknown key or a non-plain-object
|
|
51
51
|
* - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
|
|
52
52
|
* - per-call options contain unsupported keys
|
|
53
53
|
* - @Field receives a non-rule value, or a rule/transformer factory is misused
|
package/dist/src/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BAKER_ERROR=Symbol.for("baker:error");export function isBakerIssueSet(
|
|
1
|
+
export const BAKER_ERROR=Symbol.for("baker:error");export function isBakerIssueSet(q){return q!=null&&typeof q==="object"&&!Array.isArray(q)&&q[BAKER_ERROR]===!0}export function toBakerIssueSet(q){return{[BAKER_ERROR]:!0,errors:q}}export class BakerError extends Error{constructor(q,C){super(q,C);this.name="BakerError"}}
|
|
@@ -2,7 +2,7 @@ import type { RuntimeOptions } from '../interfaces';
|
|
|
2
2
|
/**
|
|
3
3
|
* @internal — validate per-call options object at public-API entry.
|
|
4
4
|
* `groups` is the only valid per-call key; everything else is rejected:
|
|
5
|
-
* - seal-time keys (BakerConfig / SealOptions) → "move to
|
|
5
|
+
* - seal-time keys (BakerConfig / SealOptions) → "move to new Baker({...})"
|
|
6
6
|
* - any other key → "unknown call option"
|
|
7
7
|
*/
|
|
8
8
|
export declare function checkCallOptions(opts: unknown): RuntimeOptions | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BakerError as v}from"../errors.js";const x=new Set(["groups"]);const F=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug","enableImplicitConversion","exposeDefaultValues","whitelist"]);export function checkCallOptions(d){if(d===void 0||d===null)return;if(typeof d!=="object"||Array.isArray(d))throw new v(`Call options must be a plain object. Received: ${Array.isArray(d)?"array":typeof d}.`);const D=Object.getPrototypeOf(d);if(D!==null&&D!==Object.prototype){const q=d.constructor?.name??"unknown";throw new v(`Call options must be a plain object literal. Received instance of ${q}.`)}for(const q of Object.keys(d)){if(x.has(q))continue;if(F.has(q))throw new v(`Option '${q}' is a seal-time setting and cannot be passed per-call. Move it to
|
|
1
|
+
import{BakerError as v}from"../errors.js";const x=new Set(["groups"]);const F=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug","enableImplicitConversion","exposeDefaultValues","whitelist"]);export function checkCallOptions(d){if(d===void 0||d===null)return;if(typeof d!=="object"||Array.isArray(d))throw new v(`Call options must be a plain object. Received: ${Array.isArray(d)?"array":typeof d}.`);const D=Object.getPrototypeOf(d);if(D!==null&&D!==Object.prototype){const q=d.constructor?.name??"unknown";throw new v(`Call options must be a plain object literal. Received instance of ${q}.`)}for(const q of Object.keys(d)){if(x.has(q))continue;if(F.has(q))throw new v(`Option '${q}' is a seal-time setting and cannot be passed per-call. Move it to new Baker({ ${q}: ... }) at app startup. Per-call options: ${[...x].join(", ")}.`);throw new v(`Unknown per-call option '${q}'. Valid per-call options: ${[...x].join(", ")}. Seal-time options go to new Baker({...}).`)}return d}
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import type { RuntimeOptions } from '../interfaces';
|
|
2
|
+
import type { SealedExecutors } from '../types';
|
|
2
3
|
import { type BakerIssueSet } from '../errors';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* - Success: T
|
|
8
|
-
* - Validation failure: BakerIssueSet (use isBakerIssueSet() to narrow)
|
|
9
|
-
*/
|
|
10
|
-
export declare function deserialize<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): T | BakerIssueSet | Promise<T | BakerIssueSet>;
|
|
11
|
-
/**
|
|
12
|
-
* Sync-asserted deserialize. Throws `BakerError` if Class has any async rule/transform
|
|
13
|
-
* on the deserialize side.
|
|
14
|
-
*/
|
|
15
|
-
export declare function deserializeSync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): T | BakerIssueSet;
|
|
16
|
-
/**
|
|
17
|
-
* Async-asserted deserialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
18
|
-
*/
|
|
19
|
-
export declare function deserializeAsync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): Promise<T | BakerIssueSet>;
|
|
4
|
+
declare function runDeserialize<T>(sealed: SealedExecutors<unknown>, input: unknown, options?: RuntimeOptions): T | BakerIssueSet | Promise<T | BakerIssueSet>;
|
|
5
|
+
declare function runDeserializeSync<T>(sealed: SealedExecutors<unknown>, className: string, input: unknown, options?: RuntimeOptions): T | BakerIssueSet;
|
|
6
|
+
declare function runDeserializeAsync<T>(sealed: SealedExecutors<unknown>, input: unknown, options?: RuntimeOptions): Promise<T | BakerIssueSet>;
|
|
7
|
+
export { runDeserialize, runDeserializeSync, runDeserializeAsync };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isErr as
|
|
1
|
+
import{isErr as x}from"@zipbul/result";import{toBakerIssueSet as F,BakerError as H}from"../errors.js";import{checkCallOptions as G}from"./check-call-options.js";function runDeserialize(g,q,w){const v=G(w);if(g.isAsync)return g.deserialize(q,v).then((f)=>{if(x(f))return F(f.data);return f});const j=g.deserialize(q,v);if(x(j))return F(j.data);return j}function runDeserializeSync(g,q,w,v){const j=G(v);if(g.isAsync)throw new H(`deserializeSync(${q}): DTO has async rules/transforms. Use deserializeAsync() instead.`);const f=g.deserialize(w,j);if(x(f))return F(f.data);return f}function runDeserializeAsync(g,q,w){const v=G(w);if(g.isAsync)return g.deserialize(q,v).then((f)=>{if(x(f))return F(f.data);return f});const j=g.deserialize(q,v);if(x(j))return Promise.resolve(F(j.data));return Promise.resolve(j)}export{runDeserialize,runDeserializeSync,runDeserializeAsync};
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { RuntimeOptions } from '../interfaces';
|
|
2
|
+
import type { SealedExecutors } from '../types';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Sync DTOs return directly; async DTOs return Promise
|
|
6
|
-
* - No validation — always returns Record<string, unknown>
|
|
4
|
+
* Forgery check shared by the Baker serialize methods. Returns the validated constructor; resolution
|
|
5
|
+
* to a sealed executor is done by the caller from its baker map.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
/**
|
|
14
|
-
* Async-asserted serialize. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
15
|
-
*/
|
|
16
|
-
export declare function serializeAsync<T>(instance: T, options?: RuntimeOptions): Promise<Record<string, unknown>>;
|
|
7
|
+
declare function resolveSerializeClass(instance: unknown, fnName: string): Function;
|
|
8
|
+
declare function runSerialize<T>(sealed: SealedExecutors<unknown>, instance: T, options?: RuntimeOptions): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
9
|
+
declare function runSerializeSync<T>(sealed: SealedExecutors<unknown>, className: string, instance: T, options?: RuntimeOptions): Record<string, unknown>;
|
|
10
|
+
declare function runSerializeAsync<T>(sealed: SealedExecutors<unknown>, instance: T, options?: RuntimeOptions): Promise<Record<string, unknown>>;
|
|
11
|
+
export { resolveSerializeClass, runSerialize, runSerializeSync, runSerializeAsync };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BakerError as
|
|
1
|
+
import{BakerError as w}from"../errors.js";import{checkCallOptions as x}from"./check-call-options.js";function resolveSerializeClass(b,g){if(b==null||typeof b!=="object")throw new w(`${g}: expected a class instance, got ${b===null?"null":typeof b}`);const j=b.constructor;if(typeof j!=="function")throw new w(`${g}: instance has no constructor`);if(j===Object||!(b instanceof j))throw new w(`${g}: received a plain object. Pass an instance of a DTO class decorated with @Field.`);return j}function runSerialize(b,g,j){const q=x(j);return b.isSerializeAsync?b.serialize(g,q):b.serialize(g,q)}function runSerializeSync(b,g,j,q){const D=x(q);if(b.isSerializeAsync)throw new w(`serializeSync(${g}): DTO has async serialize transforms. Use serializeAsync() instead.`);return b.serialize(j,D)}function runSerializeAsync(b,g,j){const q=x(j);return b.isSerializeAsync?b.serialize(g,q):Promise.resolve(b.serialize(g,q))}export{resolveSerializeClass,runSerialize,runSerializeSync,runSerializeAsync};
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import type { BakerIssueSet } from '../errors';
|
|
2
2
|
import type { RuntimeOptions } from '../interfaces';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
declare function validate<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): true | BakerIssueSet | Promise<true | BakerIssueSet>;
|
|
9
|
-
/**
|
|
10
|
-
* Sync-asserted validate. Throws `BakerError` if Class has any async rule/transform
|
|
11
|
-
* on the deserialize/validate side. Use when caller code assumes sync return.
|
|
12
|
-
*/
|
|
13
|
-
declare function validateSync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): true | BakerIssueSet;
|
|
14
|
-
/**
|
|
15
|
-
* Async-asserted validate. Always returns Promise (sync DTOs are wrapped via Promise.resolve).
|
|
16
|
-
*/
|
|
17
|
-
declare function validateAsync<T>(Class: new (...args: never[]) => T, input: unknown, options?: RuntimeOptions): Promise<true | BakerIssueSet>;
|
|
18
|
-
export { validate, validateSync, validateAsync };
|
|
3
|
+
import type { SealedExecutors } from '../types';
|
|
4
|
+
declare function runValidate(sealed: SealedExecutors<unknown>, input: unknown, options?: RuntimeOptions): true | BakerIssueSet | Promise<true | BakerIssueSet>;
|
|
5
|
+
declare function runValidateSync(sealed: SealedExecutors<unknown>, className: string, input: unknown, options?: RuntimeOptions): true | BakerIssueSet;
|
|
6
|
+
declare function runValidateAsync(sealed: SealedExecutors<unknown>, input: unknown, options?: RuntimeOptions): Promise<true | BakerIssueSet>;
|
|
7
|
+
export { runValidate, runValidateSync, runValidateAsync };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toBakerIssueSet as
|
|
1
|
+
import{toBakerIssueSet as w,BakerError as z}from"../errors.js";import{checkCallOptions as x}from"./check-call-options.js";function runValidate(b,g,v){const j=x(v);if(b.isAsync)return b.validate(g,j).then((f)=>f===null?!0:w(f));const q=b.validate(g,j);return q===null?!0:w(q)}function runValidateSync(b,g,v,j){const q=x(j);if(b.isAsync)throw new z(`validateSync(${g}): DTO has async rules/transforms. Use validateAsync() instead.`);const f=b.validate(v,q);return f===null?!0:w(f)}function runValidateAsync(b,g,v){const j=x(v);if(b.isAsync)return b.validate(g,j).then((f)=>f===null?!0:w(f));const q=b.validate(g,j);return Promise.resolve(q===null?!0:w(q))}export{runValidate,runValidateSync,runValidateAsync};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type { RawClassMeta
|
|
2
|
-
export declare function getSealed(cls: Function): SealedExecutors<unknown> | undefined;
|
|
3
|
-
/** Same as getSealed but throws if the class is not sealed — for callers that establish the invariant elsewhere. */
|
|
4
|
-
export declare function requireSealed(cls: Function): SealedExecutors<unknown>;
|
|
5
|
-
export declare function setSealed(cls: Function, exec: SealedExecutors<unknown>): void;
|
|
6
|
-
export declare function hasSealedOwn(cls: Function): boolean;
|
|
7
|
-
export declare function deleteSealed(cls: Function): void;
|
|
1
|
+
import type { RawClassMeta } from './types';
|
|
8
2
|
export declare function deleteRaw(cls: Function): void;
|
|
9
3
|
export declare function getRaw(cls: Function): RawClassMeta | undefined;
|
|
10
4
|
/** Same as getRaw but throws if the class has no @Field decorators — for callers that establish the invariant elsewhere. */
|
|
@@ -16,4 +10,3 @@ export declare function setRaw(cls: Function, raw: RawClassMeta): void;
|
|
|
16
10
|
* the dual own-check keeps mergeInheritance from double-counting inherited fields.
|
|
17
11
|
*/
|
|
18
12
|
export declare function hasRawOwn(cls: Function): boolean;
|
|
19
|
-
export declare function freezeRaw(cls: Function): void;
|
package/dist/src/meta-access.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RAW as
|
|
1
|
+
import{RAW as x}from"./symbols.js";function z(j){return j[Symbol.metadata]??void 0}function B(j){if(!Object.hasOwn(j,Symbol.metadata))Object.defineProperty(j,Symbol.metadata,{value:{},writable:!0,configurable:!0,enumerable:!1});return j[Symbol.metadata]}export function deleteRaw(j){if(Object.hasOwn(j,Symbol.metadata))delete j[Symbol.metadata][x]}export function getRaw(j){return z(j)?.[x]}export function requireRaw(j){const q=getRaw(j);if(q===void 0)throw Error(`${j.name||"<anonymous>"}: class has no @Field decorators`);return q}export function setRaw(j,q){B(j)[x]=q}export function hasRawOwn(j){if(!Object.hasOwn(j,Symbol.metadata))return!1;const q=j[Symbol.metadata];return q!=null&&Object.hasOwn(q,x)}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Result, ResultAsync } from '@zipbul/result';
|
|
2
2
|
import type { SealOptions, RuntimeOptions } from '../interfaces';
|
|
3
|
-
import type { RawClassMeta } from '../types';
|
|
3
|
+
import type { RawClassMeta, SealedExecutors } from '../types';
|
|
4
4
|
import { type BakerIssue } from '../errors';
|
|
5
5
|
type DeserializeExecutor<T> = (input: unknown, opts?: RuntimeOptions) => Result<T, BakerIssue[]> | ResultAsync<T, BakerIssue[]>;
|
|
6
6
|
type ValidateExecutor = (input: unknown, opts?: RuntimeOptions) => BakerIssue[] | null | Promise<BakerIssue[] | null>;
|
|
7
|
-
declare function buildDeserializeCode<T>(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean): DeserializeExecutor<T>;
|
|
8
|
-
declare function buildDeserializeCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean, validateOnly: true): ValidateExecutor;
|
|
9
|
-
declare function buildValidateCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean): ValidateExecutor;
|
|
7
|
+
declare function buildDeserializeCode<T>(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean, resolve: (cls: Function) => SealedExecutors<unknown> | undefined): DeserializeExecutor<T>;
|
|
8
|
+
declare function buildDeserializeCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean, resolve: (cls: Function) => SealedExecutors<unknown> | undefined, validateOnly: true): ValidateExecutor;
|
|
9
|
+
declare function buildValidateCode(Class: Function, merged: RawClassMeta, options: SealOptions | undefined, needsCircularCheck: boolean, isAsync: boolean, resolve: (cls: Function) => SealedExecutors<unknown> | undefined): ValidateExecutor;
|
|
10
10
|
export { buildDeserializeCode, buildValidateCode };
|