@zipbul/baker 3.4.0 → 4.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +236 -148
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +1 -10
  5. package/dist/src/baker.d.ts +26 -0
  6. package/dist/src/baker.js +1 -0
  7. package/dist/src/collect.js +1 -26
  8. package/dist/src/configure.d.ts +7 -1
  9. package/dist/src/configure.js +1 -43
  10. package/dist/src/create-rule.d.ts +2 -1
  11. package/dist/src/create-rule.js +1 -41
  12. package/dist/src/decorators/field.d.ts +2 -1
  13. package/dist/src/decorators/field.js +1 -277
  14. package/dist/src/decorators/index.js +1 -2
  15. package/dist/src/decorators/recipe.js +1 -23
  16. package/dist/src/enums.d.ts +51 -0
  17. package/dist/src/enums.js +1 -0
  18. package/dist/src/errors.js +1 -52
  19. package/dist/src/functions/check-call-options.js +1 -51
  20. package/dist/src/functions/deserialize.js +1 -57
  21. package/dist/src/functions/serialize.js +1 -52
  22. package/dist/src/functions/validate.js +1 -49
  23. package/dist/src/interfaces.js +0 -4
  24. package/dist/src/meta-access.js +1 -75
  25. package/dist/src/registry.js +1 -8
  26. package/dist/src/rule-metadata.js +1 -17
  27. package/dist/src/rule-plan.d.ts +5 -3
  28. package/dist/src/rule-plan.js +1 -117
  29. package/dist/src/rules/array.js +1 -96
  30. package/dist/src/rules/binary.js +3 -51
  31. package/dist/src/rules/combinators.js +1 -111
  32. package/dist/src/rules/common.js +1 -77
  33. package/dist/src/rules/date.js +1 -35
  34. package/dist/src/rules/index.js +1 -10
  35. package/dist/src/rules/locales.js +1 -249
  36. package/dist/src/rules/number.js +1 -79
  37. package/dist/src/rules/object.js +1 -49
  38. package/dist/src/rules/string.js +10 -2033
  39. package/dist/src/rules/typechecker.js +5 -171
  40. package/dist/src/seal/circular-analyzer.js +1 -63
  41. package/dist/src/seal/codegen-utils.js +1 -18
  42. package/dist/src/seal/deserialize-builder.js +265 -1564
  43. package/dist/src/seal/enums.d.ts +8 -0
  44. package/dist/src/seal/enums.js +1 -0
  45. package/dist/src/seal/expose-validator.js +1 -65
  46. package/dist/src/seal/seal-state.js +1 -18
  47. package/dist/src/seal/seal.d.ts +15 -1
  48. package/dist/src/seal/seal.js +1 -431
  49. package/dist/src/seal/serialize-builder.js +66 -370
  50. package/dist/src/seal/validate-meta.js +1 -61
  51. package/dist/src/symbols.js +1 -13
  52. package/dist/src/transformers/collection.transformer.js +1 -25
  53. package/dist/src/transformers/date.transformer.js +1 -18
  54. package/dist/src/transformers/index.js +1 -6
  55. package/dist/src/transformers/luxon.transformer.js +1 -34
  56. package/dist/src/transformers/moment.transformer.js +1 -32
  57. package/dist/src/transformers/number.transformer.js +1 -8
  58. package/dist/src/transformers/string.transformer.js +1 -12
  59. package/dist/src/types.d.ts +11 -10
  60. package/dist/src/types.js +0 -1
  61. package/dist/src/utils.js +1 -10
  62. package/package.json +2 -2
@@ -1,26 +1 @@
1
- import { RAW } from './symbols.js';
2
- // ─────────────────────────────────────────────────────────────────────────────
3
- // ensureMeta — Internal utility (§3.1)
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- /**
6
- * Returns the RawPropertyMeta for the given propertyKey on the class's decorator metadata.
7
- * Creates the RAW slot and the per-key default meta if absent.
8
- *
9
- * The own-RAW check is required: a subclass's metadata inherits the parent's RAW via the
10
- * metadata prototype chain, so a bare assignment would pollute the parent. Creating a fresh
11
- * own RAW (null prototype) keeps child fields isolated.
12
- */
13
- export function ensureMeta(metadata, key) {
14
- if (!Object.hasOwn(metadata, RAW)) {
15
- metadata[RAW] = Object.create(null);
16
- }
17
- const raw = metadata[RAW];
18
- return (raw[key] ??= {
19
- validation: [],
20
- transform: [],
21
- expose: [],
22
- exclude: null,
23
- type: null,
24
- flags: {},
25
- });
26
- }
1
+ import{RAW as x}from"./symbols.js";export function ensureMeta(j,z){if(!Object.hasOwn(j,x))j[x]=Object.create(null);const B=j[x];return B[z]??={validation:[],transform:[],expose:[],exclude:null,type:null,flags:{}}}
@@ -16,9 +16,15 @@ interface BakerConfig {
16
16
  * If not called, defaults are applied.
17
17
  */
18
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()`.
23
+ */
24
+ declare function normalizeConfig(config: BakerConfig): SealOptions;
19
25
  /** @internal — used by seal. Returns the frozen global options; the only way to change them is configure(). */
20
26
  declare function getGlobalOptions(): SealOptions;
21
27
  /** @internal — reset to defaults on unseal */
22
28
  declare function resetConfigForTesting(): void;
23
- export { configure, getGlobalOptions, resetConfigForTesting };
29
+ export { configure, getGlobalOptions, resetConfigForTesting, normalizeConfig };
24
30
  export type { BakerConfig };
@@ -1,43 +1 @@
1
- import { BakerError } from './errors.js';
2
- import { isSealed } from './seal/seal-state.js';
3
- const BAKER_CONFIG_KEYS = new Set([
4
- 'autoConvert',
5
- 'allowClassDefaults',
6
- 'stopAtFirstError',
7
- 'forbidUnknown',
8
- 'debug',
9
- ]);
10
- let globalOptionsState = Object.freeze({});
11
- /**
12
- * Baker global configuration. Call before `seal()`.
13
- * If not called, defaults are applied.
14
- */
15
- function configure(config) {
16
- if (isSealed()) {
17
- throw new BakerError('[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().');
18
- }
19
- if (config === null || typeof config !== 'object' || Array.isArray(config)) {
20
- throw new BakerError(`[baker] configure() requires a plain object. Received: ${config === null ? 'null' : Array.isArray(config) ? 'array' : typeof config}.`);
21
- }
22
- for (const key of Object.keys(config)) {
23
- if (!BAKER_CONFIG_KEYS.has(key)) {
24
- throw new BakerError(`[baker] configure(): unknown key '${key}'. ` + `Valid keys: ${[...BAKER_CONFIG_KEYS].join(', ')}.`);
25
- }
26
- }
27
- globalOptionsState = Object.freeze({
28
- enableImplicitConversion: config.autoConvert ?? false,
29
- exposeDefaultValues: config.allowClassDefaults ?? false,
30
- stopAtFirstError: config.stopAtFirstError ?? false,
31
- whitelist: config.forbidUnknown ?? false,
32
- debug: config.debug ?? false,
33
- });
34
- }
35
- /** @internal — used by seal. Returns the frozen global options; the only way to change them is configure(). */
36
- function getGlobalOptions() {
37
- return globalOptionsState;
38
- }
39
- /** @internal — reset to defaults on unseal */
40
- function resetConfigForTesting() {
41
- globalOptionsState = Object.freeze({});
42
- }
43
- export { configure, getGlobalOptions, resetConfigForTesting };
1
+ import{BakerError as j}from"./errors.js";import{isSealed as x}from"./seal/seal-state.js";const w=new Set(["autoConvert","allowClassDefaults","stopAtFirstError","forbidUnknown","debug"]);let q=Object.freeze({});function configure(h){if(x())throw new j("[baker] configure() called after seal(). Already-sealed classes are not affected. Call configure() before seal().");q=normalizeConfig(h)}function normalizeConfig(h){if(h===null||typeof h!=="object"||Array.isArray(h))throw new j(`[baker] config requires a plain object. Received: ${h===null?"null":Array.isArray(h)?"array":typeof h}.`);for(const v of Object.keys(h))if(!w.has(v))throw new j(`[baker] unknown key '${v}'. Valid keys: ${[...w].join(", ")}.`);return Object.freeze({enableImplicitConversion:h.autoConvert??!1,exposeDefaultValues:h.allowClassDefaults??!1,stopAtFirstError:h.stopAtFirstError??!1,whitelist:h.forbidUnknown??!1,debug:h.debug??!1})}function getGlobalOptions(){return q}function resetConfigForTesting(){q=Object.freeze({})}export{configure,getGlobalOptions,resetConfigForTesting,normalizeConfig};
@@ -1,3 +1,4 @@
1
+ import type { RequiredType } from './enums';
1
2
  import type { EmittableRule } from './types';
2
3
  export interface CreateRuleOptions {
3
4
  /** Rule name. Used as the error code. */
@@ -7,7 +8,7 @@ export interface CreateRuleOptions {
7
8
  /** Rule parameters */
8
9
  constraints?: Record<string, unknown>;
9
10
  /** Type assumed by this rule — used for type gate optimization */
10
- requiresType?: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object';
11
+ requiresType?: RequiredType;
11
12
  }
12
13
  /**
13
14
  * Creates a user-defined validation rule.
@@ -1,41 +1 @@
1
- import { BakerError } from './errors.js';
2
- import { defineRuleMetadata } from './rule-metadata.js';
3
- import { isAsyncFunction, isPromiseLike } from './utils.js';
4
- export function createRule(nameOrOptions, validateFn) {
5
- const name = typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions.name;
6
- const validate = typeof nameOrOptions === 'string' ? validateFn : nameOrOptions.validate;
7
- // The overloads require `validate`; guard the untyped-JS path instead of asserting with `!`,
8
- // so misuse fails clearly at creation rather than as a confusing TypeError at validation time.
9
- if (typeof validate !== 'function') {
10
- throw new BakerError(`createRule(${name}): a validate function is required.`);
11
- }
12
- const constraints = typeof nameOrOptions === 'object' ? nameOrOptions.constraints : undefined;
13
- const requiresType = typeof nameOrOptions === 'object' ? nameOrOptions.requiresType : undefined;
14
- const isAsyncFn = isAsyncFunction(validate);
15
- // Validation function wrapper — enforces that sync rules stay sync.
16
- const fn = function (value) {
17
- const result = validate(value);
18
- if (!isAsyncFn && isPromiseLike(result)) {
19
- throw new BakerError(`createRule(${name}): sync rule returned Promise. Declare the validator with async if it is asynchronous.`);
20
- }
21
- return result;
22
- };
23
- // .emit() — generates function call code via the refs array
24
- fn.emit = function (varName, ctx) {
25
- const i = ctx.addRef(fn);
26
- return `if(!(${isAsyncFn ? 'await ' : ''}refs[${i}](${varName}))) ${ctx.fail(name)};`;
27
- };
28
- const meta = {
29
- emit: fn.emit,
30
- ruleName: name,
31
- isAsync: isAsyncFn,
32
- };
33
- if (constraints !== undefined) {
34
- meta.constraints = constraints;
35
- }
36
- if (requiresType !== undefined) {
37
- meta.requiresType = requiresType;
38
- }
39
- defineRuleMetadata(fn, meta);
40
- return fn;
41
- }
1
+ import{BakerError as K}from"./errors.js";import{defineRuleMetadata as U}from"./rule-metadata.js";import{isAsyncFunction as V,isPromiseLike as W}from"./utils.js";export function createRule(b,Q){const z=typeof b==="string"?b:b.name,C=typeof b==="string"?Q:b.validate;if(typeof C!=="function")throw new K(`createRule(${z}): a validate function is required.`);const I=typeof b==="object"?b.constraints:void 0,J=typeof b==="object"?b.requiresType:void 0,D=V(C),j=function(H){const w=C(H);if(!D&&W(w))throw new K(`createRule(${z}): sync rule returned Promise. Declare the validator with async if it is asynchronous.`);return w};j.emit=function(H,w){const S=w.addRef(j);return`if(!(${D?"await ":""}refs[${S}](${H}))) ${w.fail(z)};`};const G={emit:j.emit,ruleName:z,isAsync:D};if(I!==void 0)G.constraints=I;if(J!==void 0)G.requiresType=J;U(j,G);return j}
@@ -1,4 +1,5 @@
1
1
  import type { ClassCtor, EmittableRule, Transformer } from '../types';
2
+ import { ExcludeMode } from '../enums';
2
3
  interface ArrayOfMarker {
3
4
  readonly [key: symbol]: true;
4
5
  readonly rules: EmittableRule[];
@@ -39,7 +40,7 @@ interface FieldOptions {
39
40
  /** Serialize direction key mapping (cannot be used with name) */
40
41
  serializeName?: string;
41
42
  /** Field exclusion — true: bidirectional, 'deserializeOnly': deserialization only, 'serializeOnly': serialization only */
42
- exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
43
+ exclude?: boolean | ExcludeMode;
43
44
  /** Groups — field visibility control + conditional validation rule application */
44
45
  groups?: string[];
45
46
  /** Conditional validation — skip all field validation when false */
@@ -1,277 +1 @@
1
- import { ensureMeta } from '../collect.js';
2
- import { BakerError } from '../errors.js';
3
- import { isAsyncFunction, isPromiseLike } from '../utils.js';
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // arrayOf — Array element validation marker (replaces each: true)
6
- // ─────────────────────────────────────────────────────────────────────────────
7
- const ARRAY_OF = Symbol.for('baker:arrayOf');
8
- /**
9
- * Apply rules to each element of an array.
10
- *
11
- * @example
12
- * ```ts
13
- * \@Field(arrayOf(isString(), minLength(1)))
14
- * tags!: string[];
15
- * ```
16
- */
17
- function arrayOf(...rules) {
18
- const marker = { rules, [ARRAY_OF]: true };
19
- return marker;
20
- }
21
- function isArrayOfMarker(arg) {
22
- return typeof arg === 'object' && arg !== null && arg[ARRAY_OF] === true;
23
- }
24
- // ─────────────────────────────────────────────────────────────────────────────
25
- // FieldOptions detection — distinguish from EmittableRule/ArrayOfMarker
26
- // ─────────────────────────────────────────────────────────────────────────────
27
- const FIELD_OPTION_KEYS = new Set([
28
- 'type',
29
- 'discriminator',
30
- 'keepDiscriminatorProperty',
31
- 'rules',
32
- 'optional',
33
- 'nullable',
34
- 'name',
35
- 'deserializeName',
36
- 'serializeName',
37
- 'exclude',
38
- 'groups',
39
- 'when',
40
- 'transform',
41
- 'message',
42
- 'context',
43
- 'mapValue',
44
- 'setValue',
45
- ]);
46
- function isFieldOptions(arg) {
47
- if (typeof arg === 'function') {
48
- return false;
49
- }
50
- if (typeof arg !== 'object' || arg === null) {
51
- return false;
52
- }
53
- if (isArrayOfMarker(arg)) {
54
- return false;
55
- }
56
- // Treat as FieldOptions if at least one known key exists
57
- const keys = Object.keys(arg);
58
- if (keys.length === 0) {
59
- return true;
60
- } // @Field({})
61
- return keys.some(k => FIELD_OPTION_KEYS.has(k));
62
- }
63
- /** W5: assert that a value is a valid baker rule (has `.emit` fn + `.ruleName` string). */
64
- function assertRule(value, fieldKey, slot) {
65
- const loc = slot ? `${fieldKey} ${slot}` : fieldKey;
66
- const validForms = ` Valid @Field forms: @Field(), @Field(rule, ...), @Field(options), @Field(rule, ..., options).`;
67
- if (typeof value === 'function') {
68
- const fn = value;
69
- if (typeof fn.emit !== 'function' || typeof fn.ruleName !== 'string') {
70
- const hint = fn.name
71
- ? ` Did you forget to call '${fn.name}()'? Factories must be invoked (e.g., '${fn.name}()'). Rule constants are passed directly (e.g., 'isString' without parentheses).`
72
- : ` Use createRule() or import a rule from @zipbul/baker/rules.`;
73
- throw new BakerError(`@Field on ${loc}: argument is not a baker rule.${hint}${validForms}`);
74
- }
75
- return;
76
- }
77
- throw new BakerError(`@Field on ${loc}: expected a baker rule (function with .emit and .ruleName), got ${value === null ? 'null' : typeof value}. Use createRule() or import a rule from @zipbul/baker/rules.${validForms}`);
78
- }
79
- /** Normalize 4 overload signatures into `{ rules, options }` */
80
- function parseFieldArgs(args) {
81
- if (args.length === 0) {
82
- // Form 1: @Field()
83
- return { rules: [], options: {} };
84
- }
85
- if (args.length === 1 && isFieldOptions(args[0])) {
86
- // Form 3: @Field({ type: () => Dto })
87
- const options = args[0];
88
- return { rules: options.rules ?? [], options };
89
- }
90
- // Form 2 or 4
91
- const lastArg = args[args.length - 1];
92
- if (isFieldOptions(lastArg)) {
93
- // Form 4: @Field(isString(), { optional: true })
94
- const options = lastArg;
95
- let rules = args.slice(0, -1);
96
- if (options.rules) {
97
- rules = [...rules, ...options.rules];
98
- }
99
- return { rules, options };
100
- }
101
- // Form 2: @Field(isString(), email())
102
- return { rules: args, options: {} };
103
- }
104
- /** Register validation rules + handle arrayOf */
105
- function applyValidation(meta, rules, options) {
106
- for (const rule of rules) {
107
- if (isArrayOfMarker(rule)) {
108
- for (const innerRule of rule.rules) {
109
- const rd = { rule: innerRule, each: true };
110
- if (options.groups !== undefined) {
111
- rd.groups = options.groups;
112
- }
113
- if (options.message !== undefined) {
114
- rd.message = options.message;
115
- }
116
- if (options.context !== undefined) {
117
- rd.context = options.context;
118
- }
119
- meta.validation.push(rd);
120
- }
121
- }
122
- else {
123
- const rd = { rule: rule };
124
- if (options.groups !== undefined) {
125
- rd.groups = options.groups;
126
- }
127
- if (options.message !== undefined) {
128
- rd.message = options.message;
129
- }
130
- if (options.context !== undefined) {
131
- rd.context = options.context;
132
- }
133
- meta.validation.push(rd);
134
- }
135
- }
136
- }
137
- /** Handle expose 5-branch logic */
138
- function applyExpose(meta, options) {
139
- if (options.name) {
140
- const ed = { name: options.name };
141
- if (options.groups !== undefined) {
142
- ed.groups = options.groups;
143
- }
144
- meta.expose.push(ed);
145
- }
146
- else if (options.deserializeName || options.serializeName) {
147
- if (options.deserializeName) {
148
- const ed = { name: options.deserializeName, deserializeOnly: true };
149
- if (options.groups !== undefined) {
150
- ed.groups = options.groups;
151
- }
152
- meta.expose.push(ed);
153
- }
154
- if (options.serializeName) {
155
- const ed = { name: options.serializeName, serializeOnly: true };
156
- if (options.groups !== undefined) {
157
- ed.groups = options.groups;
158
- }
159
- meta.expose.push(ed);
160
- }
161
- }
162
- else if (options.groups) {
163
- meta.expose.push({ groups: options.groups });
164
- }
165
- else {
166
- meta.expose.push({});
167
- }
168
- }
169
- /** Register Transformer — split into direction-specific TransformDefs */
170
- function wrapTransform(propertyKey, direction, fn) {
171
- const isAsync = isAsyncFunction(fn);
172
- const wrapped = (params => {
173
- const result = fn(params);
174
- if (!isAsync && isPromiseLike(result)) {
175
- throw new BakerError(`@Field(${propertyKey}) ${direction} transform returned Promise. Declare the transform with async if it is asynchronous.`);
176
- }
177
- return result;
178
- });
179
- return { fn: wrapped, isAsync };
180
- }
181
- /** Register Transformer — split into direction-specific TransformDefs */
182
- function applyTransform(meta, propertyKey, options) {
183
- if (!options.transform) {
184
- return;
185
- }
186
- const transformers = Array.isArray(options.transform) ? options.transform : [options.transform];
187
- for (const t of transformers) {
188
- const deserialize = wrapTransform(propertyKey, 'deserialize', t.deserialize);
189
- const serialize = wrapTransform(propertyKey, 'serialize', t.serialize);
190
- meta.transform.push({ fn: deserialize.fn, isAsync: deserialize.isAsync, options: { deserializeOnly: true } }, { fn: serialize.fn, isAsync: serialize.isAsync, options: { serializeOnly: true } });
191
- }
192
- }
193
- function Field(...args) {
194
- return (_value, context) => {
195
- if (context.static) {
196
- throw new BakerError(`@Field cannot decorate static fields.`);
197
- }
198
- if (context.private) {
199
- throw new BakerError(`@Field cannot decorate private fields.`);
200
- }
201
- if (typeof context.name === 'symbol') {
202
- throw new BakerError(`@Field: symbol property keys are not supported. Use a string property name.`);
203
- }
204
- const propertyKey = context.name;
205
- const meta = ensureMeta(context.metadata, propertyKey);
206
- const { rules, options } = parseFieldArgs(args);
207
- // `name` is bidirectional; `deserializeName`/`serializeName` are per-direction. Combining them
208
- // is contradictory — reject it instead of silently dropping the per-direction names. Truthiness
209
- // matches applyExpose: an empty-string name is treated as "no name" consistently throughout.
210
- if (options.name && (options.deserializeName || options.serializeName)) {
211
- throw new BakerError(`@Field on ${propertyKey}: 'name' cannot be combined with 'deserializeName'/'serializeName'. Use one or the other.`);
212
- }
213
- // W5: validate each rule shape — `.emit` function + `.ruleName` string required.
214
- // Catches D2/D4: `@Field(isString())` (boolean), `@Field(isNumber)` (factory unstamped), `@Field(() => true)`.
215
- for (let i = 0; i < rules.length; i++) {
216
- const r = rules[i];
217
- if (isArrayOfMarker(r)) {
218
- for (let j = 0; j < r.rules.length; j++) {
219
- assertRule(r.rules[j], propertyKey, `arrayOf[${j}]`);
220
- }
221
- }
222
- else {
223
- assertRule(r, propertyKey);
224
- }
225
- }
226
- applyValidation(meta, rules, options);
227
- // Field-level message/context — stored regardless of rules so non-rule failures
228
- // (type gate, required-missing, conversion, structural gates) and type-only fields
229
- // can carry them, not just rule-body failures.
230
- if (options.context !== undefined) {
231
- meta.context = options.context;
232
- }
233
- if (options.message !== undefined) {
234
- meta.message = options.message;
235
- }
236
- // ── flags ──
237
- if (options.optional) {
238
- meta.flags.isOptional = true;
239
- }
240
- if (options.nullable) {
241
- meta.flags.isNullable = true;
242
- }
243
- if (options.when) {
244
- meta.flags.validateIf = options.when;
245
- }
246
- // ── type (nested DTO + discriminator + collection) ──
247
- if (options.type) {
248
- const td = { fn: options.type };
249
- if (options.discriminator !== undefined) {
250
- td.discriminator = options.discriminator;
251
- }
252
- if (options.keepDiscriminatorProperty !== undefined) {
253
- td.keepDiscriminatorProperty = options.keepDiscriminatorProperty;
254
- }
255
- const cv = options.mapValue ?? options.setValue;
256
- if (cv !== undefined) {
257
- td.collectionValue = cv;
258
- }
259
- meta.type = td;
260
- }
261
- applyExpose(meta, options);
262
- // ── exclude ──
263
- if (options.exclude) {
264
- if (options.exclude === true) {
265
- meta.exclude = {};
266
- }
267
- else if (options.exclude === 'deserializeOnly') {
268
- meta.exclude = { deserializeOnly: true };
269
- }
270
- else if (options.exclude === 'serializeOnly') {
271
- meta.exclude = { serializeOnly: true };
272
- }
273
- }
274
- applyTransform(meta, propertyKey, options);
275
- };
276
- }
277
- export { arrayOf, Field };
1
+ import{ensureMeta as L}from"../collect.js";import{Direction as h,ExcludeMode as N}from"../enums.js";import{BakerError as X}from"../errors.js";import{isAsyncFunction as P,isPromiseLike as Y}from"../utils.js";const I=Symbol.for("baker:arrayOf");function arrayOf(...q){return{rules:q,[I]:!0}}function $(q){return typeof q==="object"&&q!==null&&q[I]===!0}const j=new Set(["type","discriminator","keepDiscriminatorProperty","rules","optional","nullable","name","deserializeName","serializeName","exclude","groups","when","transform","message","context","mapValue","setValue"]);function S(q){if(typeof q==="function")return!1;if(typeof q!=="object"||q===null)return!1;if($(q))return!1;const C=Object.keys(q);if(C.length===0)return!0;return C.some((b)=>j.has(b))}function V(q,C,b){const J=b?`${C} ${b}`:C;if(typeof q==="function"){const Q=q;if(typeof Q.emit!=="function"||typeof Q.ruleName!=="string"){const G=Q.name?` Did you forget to call '${Q.name}()'? Factories must be invoked (e.g., '${Q.name}()'). Rule constants are passed directly (e.g., 'isString' without parentheses).`:" Use createRule() or import a rule from @zipbul/baker/rules.";throw new X(`@Field on ${J}: argument is not a baker rule.${G} Valid @Field forms: @Field(), @Field(rule, ...), @Field(options), @Field(rule, ..., options).`)}return}throw new X(`@Field on ${J}: expected a baker rule (function with .emit and .ruleName), got ${q===null?"null":typeof q}. Use createRule() or import a rule from @zipbul/baker/rules. Valid @Field forms: @Field(), @Field(rule, ...), @Field(options), @Field(rule, ..., options).`)}function z(q){if(q.length===0)return{rules:[],options:{}};if(q.length===1&&S(q[0])){const b=q[0];return{rules:b.rules??[],options:b}}const C=q[q.length-1];if(S(C)){const b=C;let J=q.slice(0,-1);if(b.rules)J=[...J,...b.rules];return{rules:J,options:b}}return{rules:q,options:{}}}function D(q,C,b){for(const J of C)if($(J))for(const H of J.rules){const Q={rule:H,each:!0};if(b.groups!==void 0)Q.groups=b.groups;if(b.message!==void 0)Q.message=b.message;if(b.context!==void 0)Q.context=b.context;q.validation.push(Q)}else{const H={rule:J};if(b.groups!==void 0)H.groups=b.groups;if(b.message!==void 0)H.message=b.message;if(b.context!==void 0)H.context=b.context;q.validation.push(H)}}function _(q,C){if(C.name){const b={name:C.name};if(C.groups!==void 0)b.groups=C.groups;q.expose.push(b)}else if(C.deserializeName||C.serializeName){if(C.deserializeName){const b={name:C.deserializeName,deserializeOnly:!0};if(C.groups!==void 0)b.groups=C.groups;q.expose.push(b)}if(C.serializeName){const b={name:C.serializeName,serializeOnly:!0};if(C.groups!==void 0)b.groups=C.groups;q.expose.push(b)}}else if(C.groups)q.expose.push({groups:C.groups});else q.expose.push({})}function w(q,C,b){const J=P(b);return{fn:(Q)=>{const G=b(Q);if(!J&&Y(G))throw new X(`@Field(${q}) ${C} transform returned Promise. Declare the transform with async if it is asynchronous.`);return G},isAsync:J}}function T(q,C,b){if(!b.transform)return;const J=Array.isArray(b.transform)?b.transform:[b.transform];for(const H of J){const Q=w(C,h.Deserialize,H.deserialize),G=w(C,h.Serialize,H.serialize);q.transform.push({fn:Q.fn,isAsync:Q.isAsync,options:{deserializeOnly:!0}},{fn:G.fn,isAsync:G.isAsync,options:{serializeOnly:!0}})}}function Field(...q){return(C,b)=>{if(b.static)throw new X("@Field cannot decorate static fields.");if(b.private)throw new X("@Field cannot decorate private fields.");if(typeof b.name==="symbol")throw new X("@Field: symbol property keys are not supported. Use a string property name.");const J=b.name,H=L(b.metadata,J),{rules:Q,options:G}=z(q);if(G.name&&(G.deserializeName||G.serializeName))throw new X(`@Field on ${J}: 'name' cannot be combined with 'deserializeName'/'serializeName'. Use one or the other.`);for(let U=0;U<Q.length;U++){const W=Q[U];if($(W))for(let Z=0;Z<W.rules.length;Z++)V(W.rules[Z],J,`arrayOf[${Z}]`);else V(W,J)}D(H,Q,G);if(G.context!==void 0)H.context=G.context;if(G.message!==void 0)H.message=G.message;if(G.optional)H.flags.isOptional=!0;if(G.nullable)H.flags.isNullable=!0;if(G.when)H.flags.validateIf=G.when;if(G.type){const U={fn:G.type};if(G.discriminator!==void 0)U.discriminator=G.discriminator;if(G.keepDiscriminatorProperty!==void 0)U.keepDiscriminatorProperty=G.keepDiscriminatorProperty;const W=G.mapValue??G.setValue;if(W!==void 0)U.collectionValue=W;H.type=U}_(H,G);if(G.exclude){if(G.exclude===!0)H.exclude={};else if(G.exclude===N.DeserializeOnly)H.exclude={deserializeOnly:!0};else if(G.exclude===N.SerializeOnly)H.exclude={serializeOnly:!0}}T(H,J,G)}}export{arrayOf,Field};
@@ -1,2 +1 @@
1
- export { Field, arrayOf } from './field.js';
2
- export { Recipe } from './recipe.js';
1
+ export{Field,arrayOf}from"./field.js";export{Recipe}from"./recipe.js";
@@ -1,23 +1 @@
1
- import { globalRegistry } from '../registry.js';
2
- // ─────────────────────────────────────────────────────────────────────────────
3
- // @Recipe — class decorator that registers a DTO for argless seal()
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- /**
6
- * Marks a class as a baker DTO so `seal()` (called with no arguments) discovers and seals it.
7
- *
8
- * Modern (TC39) field decorators receive no class reference, so `@Field` alone cannot register
9
- * the owning class. `@Recipe` runs after the field decorators and registers the class itself.
10
- *
11
- * @example
12
- * ```ts
13
- * \@Recipe
14
- * class UserDto {
15
- * \@Field(isString()) name!: string;
16
- * }
17
- * seal();
18
- * ```
19
- */
20
- function Recipe(value, _context) {
21
- globalRegistry.add(value);
22
- }
23
- export { Recipe };
1
+ import{globalRegistry as q}from"../registry.js";function Recipe(j,z){q.add(j)}export{Recipe};
@@ -0,0 +1,51 @@
1
+ /** Type a rule assumes for its value — drives the builder's type gate, gate dedup, and autoConvert target. */
2
+ export declare enum RequiredType {
3
+ String = "string",
4
+ Number = "number",
5
+ Boolean = "boolean",
6
+ Date = "date",
7
+ Array = "array",
8
+ Object = "object"
9
+ }
10
+ /** Direction of a (de)serialization pass. */
11
+ export declare enum Direction {
12
+ Deserialize = "deserialize",
13
+ Serialize = "serialize"
14
+ }
15
+ /** Collection container type for a nested field. */
16
+ export declare enum CollectionType {
17
+ Map = "Map",
18
+ Set = "Set"
19
+ }
20
+ /** Cached accessor a RulePlan reuses across checks. */
21
+ export declare enum CacheKey {
22
+ Length = "length",
23
+ Time = "time"
24
+ }
25
+ /** Discriminant for a RulePlanExpr node. */
26
+ export declare enum RulePlanExprKind {
27
+ Value = "value",
28
+ Member = "member",
29
+ Call0 = "call0",
30
+ Literal = "literal"
31
+ }
32
+ /** Discriminant for a RulePlanCheck node. */
33
+ export declare enum RulePlanCheckKind {
34
+ Compare = "compare",
35
+ And = "and",
36
+ Or = "or"
37
+ }
38
+ /** Comparison operator emitted into generated check code. */
39
+ export declare enum RuleOp {
40
+ Lt = "<",
41
+ Lte = "<=",
42
+ Gt = ">",
43
+ Gte = ">=",
44
+ Eq = "===",
45
+ Neq = "!=="
46
+ }
47
+ /** Direction in which a field is excluded. */
48
+ export declare enum ExcludeMode {
49
+ DeserializeOnly = "deserializeOnly",
50
+ SerializeOnly = "serializeOnly"
51
+ }
@@ -0,0 +1 @@
1
+ export var RequiredType;((b)=>{b.String="string";b.Number="number";b.Boolean="boolean";b.Date="date";b.Array="array";b.Object="object"})(RequiredType||={});export var Direction;((g)=>{g.Deserialize="deserialize";g.Serialize="serialize"})(Direction||={});export var CollectionType;((g)=>{g.Map="Map";g.Set="Set"})(CollectionType||={});export var CacheKey;((g)=>{g.Length="length";g.Time="time"})(CacheKey||={});export var RulePlanExprKind;((j)=>{j.Value="value";j.Member="member";j.Call0="call0";j.Literal="literal"})(RulePlanExprKind||={});export var RulePlanCheckKind;((m)=>{m.Compare="compare";m.And="and";m.Or="or"})(RulePlanCheckKind||={});export var RuleOp;((b)=>{b.Lt="<";b.Lte="<=";b.Gt=">";b.Gte=">=";b.Eq="===";b.Neq="!=="})(RuleOp||={});export var ExcludeMode;((g)=>{g.DeserializeOnly="deserializeOnly";g.SerializeOnly="serializeOnly"})(ExcludeMode||={});
@@ -1,52 +1 @@
1
- // ─────────────────────────────────────────────────────────────────────────────
2
- // BakerIssue — Individual field error (§12.2)
3
- // ─────────────────────────────────────────────────────────────────────────────
4
- // ─────────────────────────────────────────────────────────────────────────────
5
- // BakerIssueSet — Validation failure return (§12.2)
6
- // ─────────────────────────────────────────────────────────────────────────────
7
- /** Symbol tag for isBakerIssueSet() type guard — collision-proof discriminator */
8
- export const BAKER_ERROR = Symbol.for('baker:error');
9
- /**
10
- * Type guard — narrows deserialize()/validate() result to BakerIssueSet.
11
- *
12
- * @example
13
- * const result = await deserialize(UserDto, input);
14
- * if (isBakerIssueSet(result)) {
15
- * result.errors // readonly BakerIssue[]
16
- * } else {
17
- * result // UserDto
18
- * }
19
- */
20
- export function isBakerIssueSet(value) {
21
- return (value != null &&
22
- typeof value === 'object' &&
23
- !Array.isArray(value) &&
24
- value[BAKER_ERROR] === true);
25
- }
26
- /** @internal — create BakerIssueSet object */
27
- export function toBakerIssueSet(errors) {
28
- return { [BAKER_ERROR]: true, errors };
29
- }
30
- // ─────────────────────────────────────────────────────────────────────────────
31
- // BakerError — the single throw channel (§12.2)
32
- // ─────────────────────────────────────────────────────────────────────────────
33
- /**
34
- * The single error thrown by baker for any developer/config/schema misuse — i.e. anything
35
- * discoverable without external input. End-user input-data failures are NOT thrown; they are
36
- * returned as a {@link BakerIssueSet}.
37
- *
38
- * Thrown when, e.g.:
39
- * - deserialize()/serialize()/validate() is called on an unsealed class
40
- * - configure() is called after seal(), or with an unknown key
41
- * - seal-time metadata invariants fail (discriminator, Map keys, banned names, …)
42
- * - per-call options contain unsupported keys
43
- * - @Field receives a non-rule value, or a rule/transformer factory is misused
44
- * - a user @Type/collectionValue thunk throws (wrapped, with the original error as `cause`)
45
- * - an optional peer dependency (luxon/moment) is missing
46
- */
47
- export class BakerError extends Error {
48
- constructor(message, options) {
49
- super(message, options);
50
- this.name = 'BakerError';
51
- }
52
- }
1
+ export const BAKER_ERROR=Symbol.for("baker:error");export function isBakerIssueSet(j){return j!=null&&typeof j==="object"&&!Array.isArray(j)&&j[BAKER_ERROR]===!0}export function toBakerIssueSet(j){return{[BAKER_ERROR]:!0,errors:j}}export class BakerError extends Error{constructor(j,q){super(j,q);this.name="BakerError"}}
@@ -1,51 +1 @@
1
- import { BakerError } from '../errors.js';
2
- const CALL_OPTION_KEYS = new Set(['groups']);
3
- const SEAL_TIME_KEYS = new Set([
4
- // BakerConfig (public, configure-time)
5
- 'autoConvert',
6
- 'allowClassDefaults',
7
- 'stopAtFirstError',
8
- 'forbidUnknown',
9
- 'debug',
10
- // SealOptions (internal, legacy aliases — same set covered by public names)
11
- 'enableImplicitConversion',
12
- 'exposeDefaultValues',
13
- 'whitelist',
14
- ]);
15
- /**
16
- * @internal — validate per-call options object at public-API entry.
17
- * `groups` is the only valid per-call key; everything else is rejected:
18
- * - seal-time keys (BakerConfig / SealOptions) → "move to configure({...})"
19
- * - any other key → "unknown call option"
20
- */
21
- export function checkCallOptions(opts) {
22
- if (opts === undefined || opts === null) {
23
- return undefined;
24
- }
25
- if (typeof opts !== 'object' || Array.isArray(opts)) {
26
- throw new BakerError(`Call options must be a plain object. Received: ${Array.isArray(opts) ? 'array' : typeof opts}.`);
27
- }
28
- // Strict same-realm plain-object check.
29
- // Accept: `{}` (proto === Object.prototype) and `Object.create(null)` (proto === null).
30
- // Reject: every other prototype, including class instances whose `constructor.name` is
31
- // renamed to 'Object' (trojan), built-ins (Date/Map/Set), cross-realm objects.
32
- // Cross-realm consumers can normalize with `Object.assign({}, opts)` before calling.
33
- const proto = Object.getPrototypeOf(opts);
34
- if (proto !== null && proto !== Object.prototype) {
35
- const ctorName = opts.constructor?.name ?? 'unknown';
36
- throw new BakerError(`Call options must be a plain object literal. Received instance of ${ctorName}.`);
37
- }
38
- for (const key of Object.keys(opts)) {
39
- if (CALL_OPTION_KEYS.has(key)) {
40
- continue;
41
- }
42
- if (SEAL_TIME_KEYS.has(key)) {
43
- throw new BakerError(`Option '${key}' is a seal-time setting and cannot be passed per-call. ` +
44
- `Move it to configure({ ${key}: ... }) at app startup. ` +
45
- `Per-call options: ${[...CALL_OPTION_KEYS].join(', ')}.`);
46
- }
47
- throw new BakerError(`Unknown per-call option '${key}'. Valid per-call options: ${[...CALL_OPTION_KEYS].join(', ')}. ` +
48
- `Seal-time options go to configure({...}).`);
49
- }
50
- return opts;
51
- }
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 configure({ ${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 configure({...}).`)}return d}