@zipbul/baker 3.0.0 → 3.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 +29 -11
- package/README.md +24 -7
- package/dist/src/rules/index.d.ts +1 -1
- package/dist/src/rules/index.js +1 -1
- package/dist/src/rules/string.d.ts +3 -1
- package/dist/src/rules/string.js +38 -1
- package/package.json +2 -3
- package/MIGRATION-3.0.md +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @zipbul/baker
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c40834b: Add `isOrigin` and `isCorsOrigin` string rules for RFC 6454 §6.2 serialized-origin
|
|
8
|
+
validation. `isOrigin` accepts only the canonical WHATWG URL `.origin` form (rejecting
|
|
9
|
+
trailing slash, path/query/fragment, uppercase scheme/host, explicit default ports,
|
|
10
|
+
userinfo, and raw IDN — punycode required) plus the opaque `'null'` literal. `isCorsOrigin`
|
|
11
|
+
is the CORS superset that additionally accepts the `'*'` wildcard. Both work standalone
|
|
12
|
+
(`isOrigin('https://a.com')`) and as `@Field` rules.
|
|
13
|
+
|
|
14
|
+
## 3.0.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- ea40a75: Docs/metadata accuracy: every README example now includes the required `@Recipe` decorator
|
|
19
|
+
(without it `seal()` does not register the class and `deserialize` throws), drop the
|
|
20
|
+
unnecessary `() => Set as any` / `Map as any`, state the Bun-only requirement (Bun ≥ 1.3.13),
|
|
21
|
+
and replace unsubstantiated speed multipliers with honest qualitative claims. Refresh the
|
|
22
|
+
package description and remove the now-redundant MIGRATION-3.0.md (its content lives in the
|
|
23
|
+
CHANGELOG 3.0.0 entry).
|
|
24
|
+
|
|
3
25
|
## 3.0.0
|
|
4
26
|
|
|
5
27
|
### Major Changes
|
|
@@ -21,35 +43,31 @@
|
|
|
21
43
|
mode was removed — call a rule directly instead). `configure()` rejects unknown keys and
|
|
22
44
|
post-`seal()` calls, and seal-time options can no longer be passed per-call.
|
|
23
45
|
|
|
24
|
-
See `MIGRATION-3.0.md` for the full upgrade guide.
|
|
25
|
-
|
|
26
46
|
### Minor Changes
|
|
27
47
|
|
|
28
48
|
- 421fd54: Add the `isHttpToken` rule — validates the RFC 9110 §5.6.2 HTTP `token` production
|
|
29
49
|
(`1*tchar`), used for HTTP method names and header field-names. Usable as a predicate
|
|
30
50
|
(`isHttpToken(value)`) or as `@Field(isHttpToken)`, and exported from `@zipbul/baker/rules`.
|
|
31
51
|
|
|
32
|
-
## 3.0.0
|
|
33
|
-
|
|
34
52
|
### DX reform — breaking changes
|
|
35
53
|
|
|
36
|
-
- **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `
|
|
54
|
+
- **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `BakerError`.
|
|
37
55
|
- Migration: import `seal` and call `seal()` once before any deserialize/serialize/validate call. For tests, call `seal()` after each `unseal()` / `configure(...)` reconfiguration.
|
|
38
|
-
- **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `
|
|
39
|
-
- **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `
|
|
40
|
-
- **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `
|
|
56
|
+
- **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `BakerError`. Move those keys into `configure({...})` before `seal()`.
|
|
57
|
+
- **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `BakerError` immediately with the four valid forms listed in the message.
|
|
58
|
+
- **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `BakerError` — previously the key was silently coerced via `[object Object]` and collided.
|
|
41
59
|
|
|
42
60
|
### API additions
|
|
43
61
|
|
|
44
62
|
- `seal(...classes?)` — explicit AOT seal trigger.
|
|
45
|
-
- `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `
|
|
63
|
+
- `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `BakerError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
|
|
46
64
|
|
|
47
65
|
### Defect fixes
|
|
48
66
|
|
|
49
67
|
- **F-1** `circular-analyzer.walk()` now walks `meta.type.collectionValue` — Set/Map nested DTO cycles are caught at seal time, no more `stack overflow` at runtime.
|
|
50
|
-
- **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `
|
|
68
|
+
- **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `BakerError` with a precise message instead of producing invalid generated JS.
|
|
51
69
|
- **F-3** Discriminator default branch now reports `context: { received, validSubTypes: [...] }` so callers can show the user the allowed values.
|
|
52
|
-
- **F-4** Per-call options other than `groups` are rejected with `
|
|
70
|
+
- **F-4** Per-call options other than `groups` are rejected with `BakerError` instead of being silently dropped.
|
|
53
71
|
- **F-8** FR passport regex now anchors both ends (`/^[A-Z0-9]{9}$/i`).
|
|
54
72
|
- **F-9** `MAGNET_URI_RE` is anchored on the trailing end.
|
|
55
73
|
- **N-3** Circular-detection `WeakSet` is now allocated per call via `Symbol.for('baker:circular-seen')` threaded through `_opts` — concurrent async calls no longer false-circular on shared input objects.
|
package/README.md
CHANGED
|
@@ -8,12 +8,15 @@ bun add @zipbul/baker
|
|
|
8
8
|
|
|
9
9
|
Zero `reflect-metadata`. Sealed codegen. 99%+ line coverage.
|
|
10
10
|
|
|
11
|
+
> **Requires Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
|
|
12
|
+
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
15
|
```typescript
|
|
14
|
-
import { deserialize, isBakerIssueSet, Field, seal } from '@zipbul/baker';
|
|
16
|
+
import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
|
|
15
17
|
import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
|
|
16
18
|
|
|
19
|
+
@Recipe
|
|
17
20
|
class UserDto {
|
|
18
21
|
@Field(isString, minLength(2)) name!: string;
|
|
19
22
|
@Field(isNumber(), min(0)) age!: number;
|
|
@@ -106,9 +109,13 @@ configure({
|
|
|
106
109
|
});
|
|
107
110
|
```
|
|
108
111
|
|
|
109
|
-
### `createRule(name, validate)`
|
|
112
|
+
### `createRule(name, validate)` / `createRule(options)`
|
|
110
113
|
|
|
111
|
-
Custom validation rule.
|
|
114
|
+
Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
|
|
118
|
+
```
|
|
112
119
|
|
|
113
120
|
```typescript
|
|
114
121
|
const isEven = createRule({
|
|
@@ -139,6 +146,7 @@ Each rule must be an emittable rule object created via `createRule()` or one of
|
|
|
139
146
|
| ----------------- | ------------------------------------------------- | ------------------------------ |
|
|
140
147
|
| `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
|
|
141
148
|
| `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
|
|
149
|
+
| `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
|
|
142
150
|
| `optional` | `boolean` | Allow undefined |
|
|
143
151
|
| `nullable` | `boolean` | Allow null |
|
|
144
152
|
| `name` | `string` | Bidirectional key mapping |
|
|
@@ -215,6 +223,7 @@ email!: string;
|
|
|
215
223
|
import { luxonTransformer } from '@zipbul/baker/transformers';
|
|
216
224
|
const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
|
|
217
225
|
|
|
226
|
+
@Recipe
|
|
218
227
|
class EventDto {
|
|
219
228
|
@Field({ transform: luxon }) startAt!: DateTime;
|
|
220
229
|
}
|
|
@@ -269,10 +278,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
|
|
|
269
278
|
## Nested DTOs
|
|
270
279
|
|
|
271
280
|
```typescript
|
|
281
|
+
@Recipe
|
|
272
282
|
class AddressDto {
|
|
273
283
|
@Field(isString) city!: string;
|
|
274
284
|
}
|
|
275
285
|
|
|
286
|
+
@Recipe
|
|
276
287
|
class UserDto {
|
|
277
288
|
@Field({ type: () => AddressDto }) address!: AddressDto;
|
|
278
289
|
@Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
|
|
@@ -282,15 +293,19 @@ class UserDto {
|
|
|
282
293
|
## Collections
|
|
283
294
|
|
|
284
295
|
```typescript
|
|
296
|
+
@Recipe
|
|
285
297
|
class UserDto {
|
|
286
|
-
@Field({ type: () => Set
|
|
287
|
-
@Field({ type: () => Map
|
|
298
|
+
@Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
|
|
299
|
+
@Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
|
|
288
300
|
}
|
|
289
301
|
```
|
|
290
302
|
|
|
303
|
+
> Deserialize input shape: a `Set` field accepts a JSON **array**, a `Map` field accepts a plain **object** keyed by string. Serialize emits the same shapes.
|
|
304
|
+
|
|
291
305
|
## Discriminator
|
|
292
306
|
|
|
293
307
|
```typescript
|
|
308
|
+
@Recipe
|
|
294
309
|
class PetOwner {
|
|
295
310
|
@Field({
|
|
296
311
|
type: () => CatDto,
|
|
@@ -309,10 +324,12 @@ class PetOwner {
|
|
|
309
324
|
## Inheritance
|
|
310
325
|
|
|
311
326
|
```typescript
|
|
327
|
+
@Recipe
|
|
312
328
|
class BaseDto {
|
|
313
329
|
@Field(isString) id!: string;
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
@Recipe
|
|
316
333
|
class UserDto extends BaseDto {
|
|
317
334
|
@Field(isString) name!: string;
|
|
318
335
|
// inherits 'id' field with isString rule
|
|
@@ -323,11 +340,11 @@ class UserDto extends BaseDto {
|
|
|
323
340
|
|
|
324
341
|
### When should I use baker instead of class-validator?
|
|
325
342
|
|
|
326
|
-
When performance matters. baker
|
|
343
|
+
When performance matters. baker generates optimized validation/serialization code at seal time instead of interpreting rules on every call, so it is substantially faster than class-validator on both valid and invalid input while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency. Run [`bench/`](./bench) to measure the exact difference on your machine.
|
|
327
344
|
|
|
328
345
|
### How does baker compare to Zod?
|
|
329
346
|
|
|
330
|
-
Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker
|
|
347
|
+
Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design or Node support; choose baker if you need class-based DTOs on Bun with maximum performance.
|
|
331
348
|
|
|
332
349
|
### Does baker support async validation?
|
|
333
350
|
|
|
@@ -3,7 +3,7 @@ export type { IsNumberOptions } from './typechecker';
|
|
|
3
3
|
export { min, max, isPositive, isNegative, isDivisibleBy } from './number';
|
|
4
4
|
export { minDate, maxDate } from './date';
|
|
5
5
|
export { equals, notEquals, isEmpty, isNotEmpty, isIn, isNotIn } from './common';
|
|
6
|
-
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, } from './string';
|
|
6
|
+
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isOrigin, isCorsOrigin, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, } from './string';
|
|
7
7
|
export type { IsURLOptions, IsBase64Options, IsMACAddressOptions, IsIBANOptions, IsISSNOptions, IsFQDNOptions, IsISO8601Options, IsNumberStringOptions, IsStrongPasswordOptions, } from './string';
|
|
8
8
|
export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUnique, arrayNotEmpty } from './array';
|
|
9
9
|
export { isNotEmptyObject, isInstance } from './object';
|
package/dist/src/rules/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { isString, isNumber, isBoolean, isDate, isEnum, isInt, isArray, isObject
|
|
|
2
2
|
export { min, max, isPositive, isNegative, isDivisibleBy } from './number.js';
|
|
3
3
|
export { minDate, maxDate } from './date.js';
|
|
4
4
|
export { equals, notEquals, isEmpty, isNotEmpty, isIn, isNotIn } from './common.js';
|
|
5
|
-
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, } from './string.js';
|
|
5
|
+
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isOrigin, isCorsOrigin, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, } from './string.js';
|
|
6
6
|
export { arrayContains, arrayNotContains, arrayMinSize, arrayMaxSize, arrayUnique, arrayNotEmpty } from './array.js';
|
|
7
7
|
export { isNotEmptyObject, isInstance } from './object.js';
|
|
8
8
|
export { isMobilePhone, isPostalCode, isIdentityCard, isPassportNumber } from './locales.js';
|
|
@@ -11,6 +11,8 @@ declare const isAscii: EmittableRule;
|
|
|
11
11
|
declare const isAlpha: EmittableRule;
|
|
12
12
|
declare const isAlphanumeric: EmittableRule;
|
|
13
13
|
declare const isHttpToken: EmittableRule;
|
|
14
|
+
declare const isOrigin: import("../types").InternalRule;
|
|
15
|
+
declare const isCorsOrigin: import("../types").InternalRule;
|
|
14
16
|
declare const isBooleanString: import("../types").InternalRule;
|
|
15
17
|
interface IsNumberStringOptions {
|
|
16
18
|
no_symbols?: boolean;
|
|
@@ -104,5 +106,5 @@ declare function isStrongPassword(options?: IsStrongPasswordOptions): EmittableR
|
|
|
104
106
|
declare function isTaxId(locale: string): EmittableRule;
|
|
105
107
|
declare function isULID(): EmittableRule;
|
|
106
108
|
declare function isCUID2(): EmittableRule;
|
|
107
|
-
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, };
|
|
109
|
+
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isOrigin, isCorsOrigin, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, };
|
|
108
110
|
export type { IsNumberStringOptions, IsURLOptions, IsMACAddressOptions, IsISO8601Options, IsISSNOptions, IsFQDNOptions, IsBase64Options, IsIBANOptions, IsStrongPasswordOptions, };
|
package/dist/src/rules/string.js
CHANGED
|
@@ -129,6 +129,43 @@ const isHttpToken = makeStringRule('isHttpToken', v => HTTP_TOKEN_RE.test(v), (v
|
|
|
129
129
|
const i = ctx.addRegex(HTTP_TOKEN_RE);
|
|
130
130
|
return `if (!re[${i}].test(${varName})) ${ctx.fail('isHttpToken')};`;
|
|
131
131
|
});
|
|
132
|
+
// RFC 6454 §6.2 serialized origin — a string equal to WHATWG URL `.origin`.
|
|
133
|
+
// The opaque-origin literal 'null' is matched explicitly because `new URL('null')` throws.
|
|
134
|
+
// '*' (CORS wildcard) is rejected here; use isCorsOrigin for the CORS superset.
|
|
135
|
+
const isOriginValue = (value) => {
|
|
136
|
+
if (value === 'null') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return new URL(value).origin === value;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const isOrigin = makeRule({
|
|
147
|
+
name: 'isOrigin',
|
|
148
|
+
requiresType: 'string',
|
|
149
|
+
constraints: { format: 'origin' },
|
|
150
|
+
validate: value => typeof value === 'string' && isOriginValue(value),
|
|
151
|
+
emit: (varName, ctx) => {
|
|
152
|
+
const i = ctx.addRef(isOriginValue);
|
|
153
|
+
return `if (!(refs[${i}](${varName}))) ${ctx.fail('isOrigin')};`;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// CORS superset of isOrigin: additionally accepts the '*' wildcard literal
|
|
157
|
+
// (Access-Control-Allow-Origin). Keep '*' out of the general isOrigin.
|
|
158
|
+
const isCorsOriginValue = (value) => value === '*' || isOriginValue(value);
|
|
159
|
+
const isCorsOrigin = makeRule({
|
|
160
|
+
name: 'isCorsOrigin',
|
|
161
|
+
requiresType: 'string',
|
|
162
|
+
constraints: { format: 'origin', allowWildcard: true },
|
|
163
|
+
validate: value => typeof value === 'string' && isCorsOriginValue(value),
|
|
164
|
+
emit: (varName, ctx) => {
|
|
165
|
+
const i = ctx.addRef(isCorsOriginValue);
|
|
166
|
+
return `if (!(refs[${i}](${varName}))) ${ctx.fail('isCorsOrigin')};`;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
132
169
|
// BooleanString: 'true' | 'false' | '1' | '0'
|
|
133
170
|
const isBooleanString = makeRule({
|
|
134
171
|
name: 'isBooleanString',
|
|
@@ -1995,4 +2032,4 @@ function isCUID2() {
|
|
|
1995
2032
|
return `if (!re[${i}].test(${varName})) ${ctx.fail('isCUID2')};`;
|
|
1996
2033
|
}, 'string', { format: 'cuid2' });
|
|
1997
2034
|
}
|
|
1998
|
-
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, };
|
|
2035
|
+
export { minLength, maxLength, length, contains, notContains, matches, isLowercase, isUppercase, isAscii, isAlpha, isAlphanumeric, isHttpToken, isOrigin, isCorsOrigin, isBooleanString, isNumberString, isDecimal, isFullWidth, isHalfWidth, isVariableWidth, isMultibyte, isSurrogatePair, isHexadecimal, isOctal, isEmail, isURL, isUUID, isIP, isHexColor, isRgbColor, isHSL, isMACAddress, isISBN, isISIN, isISO8601, isISRC, isISSN, isJWT, isLatLong, isLocale, isDataURI, isFQDN, isPort, isEAN, isISO31661Alpha2, isISO31661Alpha3, isBIC, isFirebasePushId, isSemVer, isMongoId, isJSON, isBase32, isBase58, isBase64, isDateString, isMimeType, isCurrency, isMagnetURI, isCreditCard, isIBAN, isByteLength, isHash, isRFC3339, isMilitaryTime, isLatitude, isLongitude, isEthereumAddress, isBtcAddress, isISO4217CurrencyCode, isPhoneNumber, isStrongPassword, isTaxId, isULID, isCUID2, };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zipbul/baker",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Bun-only AOT decorator-based DTO validation & serialization. class-validator DX, sealed code generation, zero reflect-metadata.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aot",
|
|
7
7
|
"bun",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"dist/**/*.d.ts",
|
|
38
38
|
"README.md",
|
|
39
39
|
"CHANGELOG.md",
|
|
40
|
-
"MIGRATION-3.0.md",
|
|
41
40
|
"LICENSE"
|
|
42
41
|
],
|
|
43
42
|
"type": "module",
|
package/MIGRATION-3.0.md
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Baker 2.x → 3.x Migration
|
|
2
|
-
|
|
3
|
-
This release replaces the implicit "auto-seal on first call" model with explicit, user-triggered `seal()`. It also tightens per-call options validation and adds strict sync/async variants.
|
|
4
|
-
|
|
5
|
-
## Required changes
|
|
6
|
-
|
|
7
|
-
### 1. Call `seal()` once at app startup
|
|
8
|
-
|
|
9
|
-
**Before**
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
// Module load registers DTOs; first deserialize implicitly seals everything.
|
|
13
|
-
const r = await deserialize(UserDto, payload);
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
**After**
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { seal, deserialize } from '@zipbul/baker';
|
|
20
|
-
// Call after every DTO module has been imported (before HTTP server / job runner starts).
|
|
21
|
-
seal();
|
|
22
|
-
const r = await deserialize(UserDto, payload);
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`deserialize` / `serialize` / `validate` throw `BakerError` if the DTO is not sealed.
|
|
26
|
-
|
|
27
|
-
### 2. Move per-call options into `configure(...)`
|
|
28
|
-
|
|
29
|
-
Only `groups` survives as a per-call option.
|
|
30
|
-
|
|
31
|
-
**Before**
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
await deserialize(UserDto, payload, { stopAtFirstError: true });
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**After**
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { configure, seal } from '@zipbul/baker';
|
|
41
|
-
configure({ stopAtFirstError: true });
|
|
42
|
-
seal();
|
|
43
|
-
await deserialize(UserDto, payload);
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
All other keys (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`) and their legacy `SealOptions` aliases (`enableImplicitConversion`, `exposeDefaultValues`, `whitelist`) now throw `BakerError` when passed per-call.
|
|
47
|
-
|
|
48
|
-
### 3. `configure()` must run before `seal()`
|
|
49
|
-
|
|
50
|
-
After `seal()`, `configure(...)` throws `BakerError`. Tests that need to reconfigure must call the test-only `unseal()` helper, change config, then `seal()` again.
|
|
51
|
-
|
|
52
|
-
### 4. `@Field` argument validation is strict
|
|
53
|
-
|
|
54
|
-
Passing a non-rule value (factory not invoked, primitive, plain function without `.emit` / `.ruleName`) throws `BakerError` at decorator-evaluation time with the four valid forms listed.
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
@Field(isNumber) // ✗ factory not invoked → BakerError
|
|
58
|
-
@Field(isNumber()) // ✓
|
|
59
|
-
@Field(isString) // ✓ constant rule
|
|
60
|
-
@Field() // ✓ marker only
|
|
61
|
-
@Field(isString, { optional: true }) // ✓
|
|
62
|
-
@Field({ type: () => NestedDto }) // ✓
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 5. `Map<K, V>` requires string keys at serialize
|
|
66
|
-
|
|
67
|
-
Serializing a `Map` with non-string keys throws `TypeError`. Previously the key was silently coerced via `String(key)`, producing collisions like `'[object Object]'`.
|
|
68
|
-
|
|
69
|
-
### 6. New strict sync/async variants
|
|
70
|
-
|
|
71
|
-
Six new entry points enforce the call-direction asymmetry at the type level:
|
|
72
|
-
|
|
73
|
-
| Integrated | Strict sync | Strict async |
|
|
74
|
-
| ------------- | ----------------- | ------------------ |
|
|
75
|
-
| `deserialize` | `deserializeSync` | `deserializeAsync` |
|
|
76
|
-
| `serialize` | `serializeSync` | `serializeAsync` |
|
|
77
|
-
| `validate` | `validateSync` | `validateAsync` |
|
|
78
|
-
|
|
79
|
-
`*Sync` throws `BakerError` if the DTO is async on that direction (e.g. async transform on deserialize side for `deserializeSync`). `*Async` always returns `Promise` (sync DTOs are wrapped via `Promise.resolve`). The integrated `deserialize` / `serialize` / `validate` remain available for ergonomic use.
|
|
80
|
-
|
|
81
|
-
## Defect fixes (no migration needed)
|
|
82
|
-
|
|
83
|
-
The following bugs in 2.x are silently fixed in 3.x:
|
|
84
|
-
|
|
85
|
-
- **Set/Map nested DTO cycles** no longer cause stack overflow (`circular-analyzer` now walks `collectionValue`).
|
|
86
|
-
- **Set/Map value DTOs marked async** now correctly propagate `_isAsync` / `_isSerializeAsync` to the parent.
|
|
87
|
-
- **Discriminator with empty `subTypes`** throws `BakerError` at seal time instead of producing invalid generated JS.
|
|
88
|
-
- **Concurrent async deserialize on the same input** no longer reports a false `circular` error (per-call `WeakSet` via `Symbol.for('baker:circular-seen')`).
|
|
89
|
-
- **`Object.hasOwn` checks** prevent prototype-chain values from leaking into DTO results.
|
|
90
|
-
- **Discriminator default branch** error now reports `context: { received, validSubTypes: [...] }`.
|
|
91
|
-
- **FR passport regex** now anchors both ends.
|
|
92
|
-
- **MAGNET URI regex** now anchors the trailing end.
|
|
93
|
-
- **Inheritance dedup** now compares by `ruleName` — a child re-declaring the same rule replaces the parent's rule.
|
|
94
|
-
- **`seal(Class)` failure** is now transactional: a failed seal removes the placeholder so retry can succeed.
|
|
95
|
-
- **`collectionValue` thunk** errors are wrapped in `BakerError` with the field name.
|
|
96
|
-
|
|
97
|
-
## Removed APIs
|
|
98
|
-
|
|
99
|
-
- `_runSealed` (was internal, but some test code depended on it). Use the public functions instead.
|
|
100
|
-
|
|
101
|
-
## Notes
|
|
102
|
-
|
|
103
|
-
- The decorator-side registry (`globalRegistry`) is still used internally as an index of decorated classes so `seal()` (no args) can seal everything. This is not auto-seal — `seal()` must be called explicitly.
|
|
104
|
-
- `unseal()` is exported by `test/integration/helpers/unseal.ts` for testing only. It is not part of the public API.
|