@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
@@ -0,0 +1,8 @@
1
+ /** Null/undefined guard strategy selected per field from its optional/nullable/defined flags. */
2
+ export declare enum GuardKey {
3
+ NullableOptional = "nullable+optional",
4
+ Nullable = "nullable",
5
+ Defined = "defined",
6
+ Optional = "optional",
7
+ Default = "default"
8
+ }
@@ -0,0 +1 @@
1
+ export var GuardKey;((l)=>{l.NullableOptional="nullable+optional";l.Nullable="nullable";l.Defined="defined";l.Optional="optional";l.Default="default"})(GuardKey||={});
@@ -1,65 +1 @@
1
- import { BakerError } from '../errors.js';
2
- /**
3
- * Static validation of @Expose stacks (§4.1, §3.3)
4
- *
5
- * Check 1: same @Expose entry has deserializeOnly: true + serializeOnly: true → excluded from both directions
6
- * Check 2: if 2+ @Expose entries in the same direction have overlapping groups → BakerError
7
- * - both groups=[] (ungrouped) → overlap
8
- * - both non-empty groups with intersection → overlap
9
- * - one ungrouped + one grouped → no overlap (different scope)
10
- */
11
- function validateExposeStacks(merged, className) {
12
- const prefix = className ? `${className}.` : '';
13
- for (const [key, meta] of Object.entries(merged)) {
14
- // ① single-entry check: deserializeOnly + serializeOnly cannot coexist
15
- for (const exp of meta.expose) {
16
- if (exp.deserializeOnly && exp.serializeOnly) {
17
- throw new BakerError(`Invalid @Expose on field '${prefix}${key}': cannot have both deserializeOnly:true and serializeOnly:true on the same @Expose entry. Use separate @Expose decorators for each direction.`);
18
- }
19
- // Reserved output keys would corrupt the serialized object (e.g. a '__proto__' key sets the
20
- // prototype instead of an own property) — reject them as wire names, matching banned field names.
21
- if (exp.name === '__proto__' || exp.name === 'constructor' || exp.name === 'prototype') {
22
- throw new BakerError(`Invalid @Field name on '${prefix}${key}': '${exp.name}' is a reserved property name and cannot be used as a serialized key.`);
23
- }
24
- }
25
- // ② multi-entry check per direction
26
- // deserialize direction: !serializeOnly (includes bidirectional + deserializeOnly)
27
- const desEntries = meta.expose.filter(e => !e.serializeOnly);
28
- // serialize direction: !deserializeOnly (includes bidirectional + serializeOnly)
29
- const serEntries = meta.expose.filter(e => !e.deserializeOnly);
30
- checkDirectionOverlap(prefix + key, desEntries, 'deserialize');
31
- checkDirectionOverlap(prefix + key, serEntries, 'serialize');
32
- }
33
- }
34
- /**
35
- * Check for groups overlap between each pair of @Expose entries within the same direction
36
- */
37
- function checkDirectionOverlap(key, entries, direction) {
38
- for (let i = 0; i < entries.length; i++) {
39
- for (let j = i + 1; j < entries.length; j++) {
40
- const aGroups = entries[i].groups ?? [];
41
- const bGroups = entries[j].groups ?? [];
42
- if (groupsOverlap(aGroups, bGroups)) {
43
- const bSet = new Set(bGroups);
44
- const overlapping = aGroups.length === 0 ? [] : aGroups.filter(g => bSet.has(g));
45
- throw new BakerError(`@Expose conflict on '${key}': 2 @Expose stacks with '${direction}' direction and overlapping groups [${overlapping.join(', ')}]. Each direction must have at most one @Expose per group set.`);
46
- }
47
- }
48
- }
49
- }
50
- /**
51
- * Determine whether two groups arrays overlap.
52
- * - both empty → overlap (same ungrouped scope)
53
- * - both non-empty with intersection → overlap
54
- * - one empty + one non-empty → no overlap (different filter scopes)
55
- */
56
- function groupsOverlap(a, b) {
57
- if (a.length === 0 && b.length === 0) {
58
- return true;
59
- }
60
- if (a.length === 0 || b.length === 0) {
61
- return false;
62
- }
63
- return a.some(g => b.includes(g));
64
- }
65
- export { validateExposeStacks };
1
+ import{Direction as L}from"../enums.js";import{BakerError as K}from"../errors.js";function validateExposeStacks(F,z){const C=z?`${z}.`:"";for(const[A,H]of Object.entries(F)){for(const q of H.expose){if(q.deserializeOnly&&q.serializeOnly)throw new K(`Invalid @Expose on field '${C}${A}': cannot have both deserializeOnly:true and serializeOnly:true on the same @Expose entry. Use separate @Expose decorators for each direction.`);if(q.name==="__proto__"||q.name==="constructor"||q.name==="prototype")throw new K(`Invalid @Field name on '${C}${A}': '${q.name}' is a reserved property name and cannot be used as a serialized key.`)}const I=H.expose.filter((q)=>!q.serializeOnly),J=H.expose.filter((q)=>!q.deserializeOnly);M(C+A,I,L.Deserialize);M(C+A,J,L.Serialize)}}function M(F,z,C){for(let A=0;A<z.length;A++)for(let H=A+1;H<z.length;H++){const I=z[A].groups??[],J=z[H].groups??[];if(R(I,J)){const q=new Set(J),P=I.length===0?[]:I.filter((Q)=>q.has(Q));throw new K(`@Expose conflict on '${F}': 2 @Expose stacks with '${C}' direction and overlapping groups [${P.join(", ")}]. Each direction must have at most one @Expose per group set.`)}}}function R(F,z){if(F.length===0&&z.length===0)return!0;if(F.length===0||z.length===0)return!1;return F.some((C)=>z.includes(C))}export{validateExposeStacks};
@@ -1,18 +1 @@
1
- /**
2
- * @internal — shared seal state, extracted so `configure.ts` can read `isSealed()`
3
- * without importing `seal.ts` (which would create a cycle: seal → configure → seal).
4
- */
5
- let sealed = false;
6
- /** List of sealed classes — used by unseal to remove SEALED */
7
- export const sealedClasses = new Set();
8
- export function isSealed() {
9
- return sealed;
10
- }
11
- export function markSealed() {
12
- sealed = true;
13
- }
14
- /** @internal — used by unseal() in test helpers */
15
- export function resetForTesting() {
16
- sealed = false;
17
- sealedClasses.clear();
18
- }
1
+ let c=!1;export const sealedClasses=new Set;export function isSealed(){return c}export function markSealed(){c=!0}export function resetForTesting(){c=!1;sealedClasses.clear()}
@@ -1,3 +1,4 @@
1
+ import type { SealOptions } from '../interfaces';
1
2
  import type { RawClassMeta, SealedExecutors } from '../types';
2
3
  /** @internal Placeholder executor for circular dependency detection during seal */
3
4
  declare function circularPlaceholder(className: string): SealedExecutors<unknown>;
@@ -6,6 +7,19 @@ declare function circularPlaceholder(className: string): SealedExecutors<unknown
6
7
  * Throws if the class was never sealed. Users must call `seal()` at app startup.
7
8
  */
8
9
  declare function ensureSealed(Class: Function): SealedExecutors<unknown>;
10
+ /**
11
+ * Seal every class in `registry` with `options`. Shared core of both the global default `seal()`
12
+ * and per-scope `createBaker().seal()`. Transactional: on any failure every class sealed by this
13
+ * call is rolled back. Clears `registry` on success.
14
+ *
15
+ * A class already sealed (e.g. a shared value-type DTO reached from another scope's roots) is
16
+ * reused as-is — class identity is the isolation boundary, so a shared class carries one sealed
17
+ * behaviour. Distinct classes stay fully isolated because each is sealed with its scope's options.
18
+ *
19
+ * @param track Optional set recording successfully-sealed classes. The default seal passes the
20
+ * global `sealedClasses` so `unseal()` can restore them; instances pass nothing.
21
+ */
22
+ declare function sealRegistry(registry: Set<Function>, options: SealOptions, track?: Set<Function>): void;
9
23
  /**
10
24
  * Seal a single class (and its nested DTOs). Not part of the public API — `seal()` (argless)
11
25
  * is the only public entry. Exposed via `__testing__.sealClass` so tests can seal one class in
@@ -39,5 +53,5 @@ declare const __testing__: {
39
53
  circularPlaceholder: typeof circularPlaceholder;
40
54
  sealClass: typeof sealOneClass;
41
55
  };
42
- export { ensureSealed, seal, mergeInheritance, __testing__ };
56
+ export { ensureSealed, seal, sealRegistry, mergeInheritance, __testing__ };
43
57
  export { sealedClasses, resetForTesting } from './seal-state';
@@ -1,431 +1 @@
1
- import { getGlobalOptions } from '../configure.js';
2
- import { BakerError } from '../errors.js';
3
- import { deleteSealed, freezeRaw, getRaw, getSealed, hasRawOwn, hasSealedOwn, setSealed } from '../meta-access.js';
4
- import { globalRegistry } from '../registry.js';
5
- import { isAsyncFunction } from '../utils.js';
6
- import { analyzeCircular } from './circular-analyzer.js';
7
- import { buildDeserializeCode, buildValidateCode } from './deserialize-builder.js';
8
- import { validateExposeStacks } from './expose-validator.js';
9
- import { sealedClasses, isSealed, markSealed } from './seal-state.js';
10
- import { buildSerializeCode } from './serialize-builder.js';
11
- import { validateMeta } from './validate-meta.js';
12
- const BANNED_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
13
- const PRIMITIVE_CTORS = new Set([Number, String, Boolean, Date]);
14
- /** @internal Placeholder executor for circular dependency detection during seal */
15
- function circularPlaceholder(className) {
16
- const msg = `Circular dependency during seal: ${className} is still being sealed`;
17
- return {
18
- deserialize() {
19
- throw new BakerError(msg);
20
- },
21
- serialize() {
22
- throw new BakerError(msg);
23
- },
24
- validate() {
25
- throw new BakerError(msg);
26
- },
27
- isAsync: false,
28
- isSerializeAsync: false,
29
- };
30
- }
31
- // ─────────────────────────────────────────────────────────────────────────────
32
- // analyzeAsync — static analysis to determine if a sealed DTO requires an async executor (C1)
33
- // ─────────────────────────────────────────────────────────────────────────────
34
- function analyzeAsync(merged, direction, visited) {
35
- const flag = direction === 'deserialize' ? 'isAsync' : 'isSerializeAsync';
36
- const seen = visited ?? new Set();
37
- // sealOne seals every nested DTO (step 4) before this runs (step 5). For a fully-sealed nested
38
- // class its `isAsync`/`isSerializeAsync` flag is authoritative and already accounts for ITS nested
39
- // classes — so trusting the flag propagates async through any nesting depth (re-deriving from
40
- // metadata would lose `resolvedClass` past depth 1). A class still being sealed carries a
41
- // placeholder executor (no `merged`); that only happens on a circular back-edge, where the flag
42
- // is not yet known — there we recurse into the class's own metadata, guarded by `seen`.
43
- const nestedIsAsync = (cls) => {
44
- if (seen.has(cls)) {
45
- return false;
46
- }
47
- seen.add(cls);
48
- const sealed = getSealed(cls);
49
- if (sealed?.merged) {
50
- return sealed[flag] === true;
51
- }
52
- return analyzeAsync(mergeInheritance(cls), direction, seen);
53
- };
54
- for (const meta of Object.values(merged)) {
55
- // 1. createRule may return Promise<boolean> even without `async` syntax (deserialize only).
56
- if (direction === 'deserialize' && meta.validation.some(rd => rd.rule.isAsync)) {
57
- return true;
58
- }
59
- // 2. @Transform async — single-pass scan, avoids intermediate filter[] allocation
60
- for (const td of meta.transform) {
61
- if (direction === 'deserialize' ? td.options?.serializeOnly : td.options?.deserializeOnly) {
62
- continue;
63
- }
64
- if (td.isAsync ?? isAsyncFunction(td.fn)) {
65
- return true;
66
- }
67
- }
68
- // 3. nested DTOs (direct, Set/Map value, discriminator subtypes)
69
- if (nestedClassesOf(meta).some(nestedIsAsync)) {
70
- return true;
71
- }
72
- }
73
- return false;
74
- }
75
- /**
76
- * Nested DTO classes referenced by a field's type. Prefers normalized `resolved*` slots, but
77
- * falls back to resolving the raw `type.fn()` thunk — needed when `analyzeAsync` recurses into a
78
- * still-being-sealed class on a circular back-edge whose metadata was never normalized.
79
- */
80
- function nestedClassesOf(meta) {
81
- const t = meta.type;
82
- if (!t) {
83
- return [];
84
- }
85
- const out = [];
86
- if (t.resolvedClass) {
87
- out.push(t.resolvedClass);
88
- }
89
- if (t.resolvedCollectionValue) {
90
- out.push(t.resolvedCollectionValue);
91
- }
92
- if (t.discriminator) {
93
- for (const sub of t.discriminator.subTypes) {
94
- out.push(sub.value);
95
- }
96
- }
97
- if (out.length === 0 && t.fn) {
98
- const result = t.fn();
99
- if (result === Map || result === Set) {
100
- const cv = t.collectionValue?.();
101
- if (typeof cv === 'function' && !PRIMITIVE_CTORS.has(cv)) {
102
- out.push(cv);
103
- }
104
- }
105
- else {
106
- const resolved = Array.isArray(result) ? result[0] : result;
107
- if (typeof resolved === 'function' && !PRIMITIVE_CTORS.has(resolved)) {
108
- out.push(resolved);
109
- }
110
- }
111
- }
112
- return out;
113
- }
114
- // Seal state lives in ./seal-state so `configure.ts` can read it without importing this file
115
- // (which would form a cycle: seal → configure → seal). Re-export the test helpers used by `unseal()`.
116
- /**
117
- * @internal — used by serialize/deserialize. Returns the sealed executor.
118
- * Throws if the class was never sealed. Users must call `seal()` at app startup.
119
- */
120
- function ensureSealed(Class) {
121
- const sealed = getSealed(Class);
122
- if (!sealed) {
123
- const name = Class.name || '<anonymous class>';
124
- throw new BakerError(`${name} is not sealed. Call seal() at app startup before deserialize/validate/serialize. ` +
125
- `(If ${name} has no @Field decorators, decorate at least one property.)`);
126
- }
127
- return sealed;
128
- }
129
- /**
130
- * Seal every class in the decorator registry, then clear the registry.
131
- */
132
- function sealAllRegistered() {
133
- if (isSealed()) {
134
- return;
135
- }
136
- const options = getGlobalOptions();
137
- const sealed = new Set();
138
- try {
139
- for (const Class of globalRegistry) {
140
- sealOne(Class, options, sealed);
141
- }
142
- }
143
- catch (e) {
144
- // On failure, roll back every class sealed so far (including nested DTOs) — prevent
145
- // partial seal state. The failed class self-cleaned its own placeholder in sealOne.
146
- for (const Class of sealed) {
147
- deleteSealed(Class);
148
- }
149
- throw e;
150
- }
151
- for (const Class of sealed) {
152
- sealedClasses.add(Class);
153
- freezeRaw(Class);
154
- }
155
- globalRegistry.clear();
156
- markSealed();
157
- }
158
- /**
159
- * Seal a single class (and its nested DTOs). Not part of the public API — `seal()` (argless)
160
- * is the only public entry. Exposed via `__testing__.sealClass` so tests can seal one class in
161
- * isolation. Class[Symbol.metadata][RAW] must exist; Class[SEALED] must not.
162
- * Transactional: on failure, every placeholder installed by this call (the class and any
163
- * nested DTO reached by recursion) is removed so a future seal attempt can re-run cleanly.
164
- */
165
- function sealOneClass(Class) {
166
- if (hasSealedOwn(Class)) {
167
- return;
168
- }
169
- const options = getGlobalOptions();
170
- const sealed = new Set();
171
- try {
172
- sealOne(Class, options, sealed);
173
- }
174
- catch (e) {
175
- // Roll back every class sealed during this call (the failed class self-cleaned in sealOne).
176
- for (const C of sealed) {
177
- deleteSealed(C);
178
- }
179
- throw e;
180
- }
181
- // Freeze + track + drop from the registry every class sealed by this call (incl. nested).
182
- for (const C of sealed) {
183
- sealedClasses.add(C);
184
- freezeRaw(C);
185
- globalRegistry.delete(C);
186
- }
187
- }
188
- /**
189
- * Public — call once at app startup. Seals every @Recipe-decorated class (and its nested DTOs)
190
- * and clears the registry. Idempotent: a second call is a no-op.
191
- *
192
- * Baker requires this call before any deserialize/serialize/validate. There is no implicit seal.
193
- * All DTOs must be imported before this call — baker has no lazy/on-demand sealing.
194
- */
195
- function seal() {
196
- sealAllRegistered();
197
- }
198
- // ─────────────────────────────────────────────────────────────────────────────
199
- // sealOne() — seal an individual class (§4.1)
200
- // ─────────────────────────────────────────────────────────────────────────────
201
- function sealOne(Class, options, sealedAcc) {
202
- if (hasSealedOwn(Class)) {
203
- return;
204
- } // already sealed (prevent recursion during circular references)
205
- // 0. Register placeholder — prevent infinite recursion on circular references
206
- const placeholder = circularPlaceholder(Class.name);
207
- setSealed(Class, placeholder);
208
- try {
209
- // 1. Merge inheritance metadata
210
- const merged = mergeInheritance(Class);
211
- // 1a. Banned field name check — prevent prototype pollution (C5)
212
- for (const key of Object.keys(merged)) {
213
- if (BANNED_FIELD_NAMES.has(key)) {
214
- throw new BakerError(`${Class.name}: field name '${key}' is not allowed (reserved property name)`);
215
- }
216
- }
217
- // 1b. TypeDef normalization — resolve @Type/@Field type fn(), detect arrays, auto-infer nested DTOs
218
- // Prevent original RAW mutation: copy type/flags before mutating (C-16 root fix)
219
- for (const [key, meta] of Object.entries(merged)) {
220
- if (!meta.type?.fn) {
221
- continue;
222
- }
223
- let typeResult;
224
- try {
225
- typeResult = meta.type.fn();
226
- }
227
- catch (e) {
228
- throw new BakerError(`${Class.name}.${key}: type function threw: ${e.message}`, { cause: e });
229
- }
230
- // Detect Map/Set collection
231
- if (typeResult === Map || typeResult === Set) {
232
- const collection = typeResult === Map ? 'Map' : 'Set';
233
- const typeCopy = { ...meta.type, collection, isArray: false };
234
- // collectionValue thunk → cache resolvedCollectionValue
235
- if (meta.type.collectionValue) {
236
- let valCls;
237
- try {
238
- valCls = meta.type.collectionValue();
239
- }
240
- catch (e) {
241
- throw new BakerError(`${Class.name}.${key}: collectionValue function threw: ${e.message}`, { cause: e });
242
- }
243
- if (valCls != null && typeof valCls === 'function' && !PRIMITIVE_CTORS.has(valCls)) {
244
- typeCopy.resolvedCollectionValue = valCls;
245
- }
246
- }
247
- merged[key] = { ...meta, type: typeCopy };
248
- continue;
249
- }
250
- const isArray = Array.isArray(typeResult);
251
- const resolved = isArray ? typeResult[0] : typeResult;
252
- if (resolved == null || typeof resolved !== 'function') {
253
- throw new BakerError(`${Class.name}: @Type/@Field type must return a constructor or [constructor], got ${String(resolved)}`);
254
- }
255
- // Copy type object before mutating — preserve original RAW type reference
256
- const typeCopy = { ...meta.type, isArray };
257
- if (!PRIMITIVE_CTORS.has(resolved)) {
258
- typeCopy.resolvedClass = resolved;
259
- // Automatically set validateNested flags for DTO classes
260
- if (!meta.flags.validateNested || !meta.flags.validateNestedEach) {
261
- meta.flags = { ...meta.flags };
262
- if (!meta.flags.validateNested) {
263
- meta.flags.validateNested = true;
264
- }
265
- if (isArray && !meta.flags.validateNestedEach) {
266
- meta.flags.validateNestedEach = true;
267
- }
268
- }
269
- }
270
- merged[key] = { ...meta, type: typeCopy };
271
- }
272
- // 2. Static validation of @Expose stacks (throws BakerError on failure)
273
- validateExposeStacks(merged, Class.name);
274
- // 2b. W2: seal-time invariant checks (D7 discriminator/Set·Map + D9 async-in-sync)
275
- validateMeta(Class, merged);
276
- // 3. Static analysis for circular references
277
- const needsCircularCheck = analyzeCircular(Class);
278
- // 4. Seal nested @Type referenced DTOs first (recursive) — uses resolvedClass / resolvedCollectionValue
279
- for (const meta of Object.values(merged)) {
280
- if (meta.type?.resolvedClass) {
281
- sealOne(meta.type.resolvedClass, options, sealedAcc);
282
- }
283
- if (meta.type?.resolvedCollectionValue) {
284
- sealOne(meta.type.resolvedCollectionValue, options, sealedAcc);
285
- }
286
- if (meta.type?.discriminator) {
287
- for (const sub of meta.type.discriminator.subTypes) {
288
- sealOne(sub.value, options, sealedAcc);
289
- }
290
- }
291
- }
292
- // 5. Async analysis
293
- const isAsync = analyzeAsync(merged, 'deserialize');
294
- const isSerializeAsync = analyzeAsync(merged, 'serialize');
295
- // 6. Generate deserialize executor code
296
- const deserializeExecutor = buildDeserializeCode(Class, merged, options, needsCircularCheck, isAsync);
297
- // 6b. Generate validate-only executor code (no Object.create, no assignments)
298
- const validateExecutor = buildValidateCode(Class, merged, options, needsCircularCheck, isAsync);
299
- // 7. Generate serialize executor code
300
- const serializeExecutor = buildSerializeCode(Class, merged, options, isSerializeAsync);
301
- // 8. Replace placeholder with actual executor in-place (Object.assign preserves reference integrity)
302
- Object.assign(placeholder, {
303
- deserialize: deserializeExecutor,
304
- serialize: serializeExecutor,
305
- validate: validateExecutor,
306
- isAsync: isAsync,
307
- isSerializeAsync: isSerializeAsync,
308
- merged: merged,
309
- });
310
- }
311
- catch (e) {
312
- // Self-clean this class's placeholder so a failed seal leaves no broken state —
313
- // including nested DTOs reached by recursion that are not in the registry.
314
- deleteSealed(Class);
315
- throw e;
316
- }
317
- // Record success so the caller can freeze + track every sealed class (including nested
318
- // DTOs reached by recursion) once the whole operation succeeds. Freezing here would be
319
- // premature: a later failure must roll back, and a frozen RAW cannot be re-sealed.
320
- sealedAcc?.add(Class);
321
- }
322
- // ─────────────────────────────────────────────────────────────────────────────
323
- // mergeInheritance() — merge inheritance metadata (§4.2)
324
- // ─────────────────────────────────────────────────────────────────────────────
325
- /**
326
- * Merges RAW metadata child-first along the prototype chain of Class.
327
- *
328
- * Merge rules:
329
- * - validation: union merge (both parent and child apply, duplicate rules removed)
330
- * - transform: child takes priority, inherits from parent if absent in child
331
- * - expose: child takes priority, inherits from parent if absent in child
332
- * - exclude: child takes priority, inherits from parent if absent in child
333
- * - type: child takes priority, inherits from parent if absent in child
334
- * - flags: child takes priority, only missing flags are supplemented from parent
335
- */
336
- function mergeInheritance(Class) {
337
- // Collect classes with RAW along the prototype chain (array order: child first)
338
- const chain = [];
339
- let current = Class;
340
- while (current && current !== Object) {
341
- if (hasRawOwn(current)) {
342
- chain.push(current);
343
- }
344
- const proto = Object.getPrototypeOf(current);
345
- current = proto === current ? null : proto;
346
- }
347
- // child-first merge
348
- const merged = Object.create(null);
349
- // When the prototype chain has only the class itself (no decorated parent), no merging happens
350
- // and we never mutate the metadata arrays — skip the shallow copy entirely.
351
- const needsCopy = chain.length > 1;
352
- for (const ctor of chain) {
353
- const raw = getRaw(ctor);
354
- for (const [key, meta] of Object.entries(raw)) {
355
- if (!merged[key]) {
356
- // First occurrence of field → copy only when subsequent ancestors might mutate
357
- merged[key] = needsCopy
358
- ? {
359
- validation: [...meta.validation],
360
- transform: [...meta.transform],
361
- expose: [...meta.expose],
362
- exclude: meta.exclude,
363
- type: meta.type,
364
- flags: { ...meta.flags },
365
- }
366
- : meta;
367
- }
368
- else {
369
- // Already exists in child → independent merge per category (§4.2)
370
- const m = merged[key];
371
- const p = meta;
372
- // validation: union merge by ruleName — child overrides parent for the same rule name (N-6)
373
- for (const rd of p.validation) {
374
- if (!m.validation.some(d => d.rule.ruleName === rd.rule.ruleName)) {
375
- m.validation.push(rd);
376
- }
377
- }
378
- // transform: inherit from parent if absent in child
379
- if (m.transform.length === 0 && p.transform.length > 0) {
380
- m.transform = [...p.transform];
381
- }
382
- // expose: inherit from parent if absent in child
383
- if (m.expose.length === 0 && p.expose.length > 0) {
384
- m.expose = [...p.expose];
385
- }
386
- // exclude: inherit from parent if absent in child
387
- if (m.exclude === null && p.exclude !== null) {
388
- m.exclude = p.exclude;
389
- }
390
- // type: inherit from parent if absent in child
391
- if (m.type === null && p.type !== null) {
392
- m.type = p.type;
393
- }
394
- // flags: child takes priority, only supplement missing flags from parent
395
- const mf = m.flags;
396
- const pf = p.flags;
397
- if (pf.isOptional !== undefined && mf.isOptional === undefined) {
398
- mf.isOptional = pf.isOptional;
399
- }
400
- if (pf.isDefined !== undefined && mf.isDefined === undefined) {
401
- mf.isDefined = pf.isDefined;
402
- }
403
- if (pf.validateIf !== undefined && mf.validateIf === undefined) {
404
- mf.validateIf = pf.validateIf;
405
- }
406
- if (pf.isNullable !== undefined && mf.isNullable === undefined) {
407
- mf.isNullable = pf.isNullable;
408
- }
409
- if (pf.validateNested !== undefined && mf.validateNested === undefined) {
410
- mf.validateNested = pf.validateNested;
411
- }
412
- if (pf.validateNestedEach !== undefined && mf.validateNestedEach === undefined) {
413
- mf.validateNestedEach = pf.validateNestedEach;
414
- }
415
- }
416
- }
417
- }
418
- return merged;
419
- }
420
- // ─────────────────────────────────────────────────────────────────────────────
421
- // __testing__ — test-only export (TST-ACCESS compliant)
422
- // ─────────────────────────────────────────────────────────────────────────────
423
- const __testing__ = {
424
- mergeInheritance,
425
- circularPlaceholder,
426
- // Targeted single-class seal — test-only. Production code uses argless seal() exclusively;
427
- // this exists so tests can seal one class in isolation (e.g. error-path assertions).
428
- sealClass: sealOneClass,
429
- };
430
- export { ensureSealed, seal, mergeInheritance, __testing__ };
431
- export { sealedClasses, resetForTesting } from './seal-state.js';
1
+ import{getGlobalOptions as h}from"../configure.js";import{CollectionType as O,Direction as M}from"../enums.js";import{BakerError as G}from"../errors.js";import{deleteSealed as T,freezeRaw as I,getRaw as R,getSealed as E,hasRawOwn as f,hasSealedOwn as S,setSealed as g}from"../meta-access.js";import{globalRegistry as k}from"../registry.js";import{isAsyncFunction as u}from"../utils.js";import{analyzeCircular as y}from"./circular-analyzer.js";import{buildDeserializeCode as p,buildValidateCode as C}from"./deserialize-builder.js";import{validateExposeStacks as m}from"./expose-validator.js";import{sealedClasses as v,isSealed as n,markSealed as c}from"./seal-state.js";import{buildSerializeCode as d}from"./serialize-builder.js";import{validateMeta as o}from"./validate-meta.js";const i=new Set(["__proto__","constructor","prototype"]);const D=new Set([Number,String,Boolean,Date]);function A(H){const q=`Circular dependency during seal: ${H} is still being sealed`;return{deserialize(){throw new G(q)},serialize(){throw new G(q)},validate(){throw new G(q)},isAsync:!1,isSerializeAsync:!1}}function B(H,q,J){const Q=q===M.Deserialize?"isAsync":"isSerializeAsync",j=J??new Set,$=(X)=>{if(j.has(X))return!1;j.add(X);const W=E(X);if(W?.merged)return W[Q]===!0;return B(mergeInheritance(X),q,j)};for(const X of Object.values(H)){if(q===M.Deserialize&&X.validation.some((W)=>W.rule.isAsync))return!0;for(const W of X.transform){if(q===M.Deserialize?W.options?.serializeOnly:W.options?.deserializeOnly)continue;if(W.isAsync??u(W.fn))return!0}if(r(X).some($))return!0}return!1}function r(H){const q=H.type;if(!q)return[];const J=[];if(q.resolvedClass)J.push(q.resolvedClass);if(q.resolvedCollectionValue)J.push(q.resolvedCollectionValue);if(q.discriminator)for(const Q of q.discriminator.subTypes)J.push(Q.value);if(J.length===0&&q.fn){const Q=q.fn();if(Q===Map||Q===Set){const j=q.collectionValue?.();if(typeof j==="function"&&!D.has(j))J.push(j)}else{const j=Array.isArray(Q)?Q[0]:Q;if(typeof j==="function"&&!D.has(j))J.push(j)}}return J}function ensureSealed(H){const q=E(H);if(!q){const J=H.name||"<anonymous class>";throw new G(`${J} is not sealed. Call seal() at app startup before deserialize/validate/serialize. (If ${J} has no @Field decorators, decorate at least one property.)`)}return q}function t(){if(n())return;sealRegistry(k,h(),v);c()}function sealRegistry(H,q,J){const Q=new Set;try{for(const j of H)P(j,q,Q)}catch(j){for(const $ of Q)T($);throw j}for(const j of Q){J?.add(j);I(j)}H.clear()}function l(H){if(S(H))return;const q=h(),J=new Set;try{P(H,q,J)}catch(Q){for(const j of J)T(j);throw Q}for(const Q of J){v.add(Q);I(Q);k.delete(Q)}}function seal(){t()}function P(H,q,J){if(S(H))return;const Q=A(H.name);g(H,Q);try{const j=mergeInheritance(H);for(const U of Object.keys(j))if(i.has(U))throw new G(`${H.name}: field name '${U}' is not allowed (reserved property name)`);for(const[U,K]of Object.entries(j)){if(!K.type?.fn)continue;let F;try{F=K.type.fn()}catch(w){throw new G(`${H.name}.${U}: type function threw: ${w.message}`,{cause:w})}if(F===Map||F===Set){const w=F===Map?O.Map:O.Set,b={...K.type,collection:w,isArray:!1};if(K.type.collectionValue){let x;try{x=K.type.collectionValue()}catch(z){throw new G(`${H.name}.${U}: collectionValue function threw: ${z.message}`,{cause:z})}if(x!=null&&typeof x==="function"&&!D.has(x))b.resolvedCollectionValue=x}j[U]={...K,type:b};continue}const N=Array.isArray(F),V=N?F[0]:F;if(V==null||typeof V!=="function")throw new G(`${H.name}: @Type/@Field type must return a constructor or [constructor], got ${String(V)}`);const _={...K.type,isArray:N};if(!D.has(V)){_.resolvedClass=V;if(!K.flags.validateNested||!K.flags.validateNestedEach){K.flags={...K.flags};if(!K.flags.validateNested)K.flags.validateNested=!0;if(N&&!K.flags.validateNestedEach)K.flags.validateNestedEach=!0}}j[U]={...K,type:_}}m(j,H.name);o(H,j);const $=y(H);for(const U of Object.values(j)){if(U.type?.resolvedClass)P(U.type.resolvedClass,q,J);if(U.type?.resolvedCollectionValue)P(U.type.resolvedCollectionValue,q,J);if(U.type?.discriminator)for(const K of U.type.discriminator.subTypes)P(K.value,q,J)}const X=B(j,M.Deserialize),W=B(j,M.Serialize),L=p(H,j,q,$,X),Y=C(H,j,q,$,X),Z=d(H,j,q,W);Object.assign(Q,{deserialize:L,serialize:Z,validate:Y,isAsync:X,isSerializeAsync:W,merged:j})}catch(j){T(H);throw j}J?.add(H)}function mergeInheritance(H){const q=[];let J=H;while(J&&J!==Object){if(f(J))q.push(J);const $=Object.getPrototypeOf(J);J=$===J?null:$}const Q=Object.create(null),j=q.length>1;for(const $ of q){const X=R($);for(const[W,L]of Object.entries(X))if(!Q[W])Q[W]=j?{validation:[...L.validation],transform:[...L.transform],expose:[...L.expose],exclude:L.exclude,type:L.type,flags:{...L.flags}}:L;else{const Y=Q[W],Z=L;for(const F of Z.validation)if(!Y.validation.some((N)=>N.rule.ruleName===F.rule.ruleName))Y.validation.push(F);if(Y.transform.length===0&&Z.transform.length>0)Y.transform=[...Z.transform];if(Y.expose.length===0&&Z.expose.length>0)Y.expose=[...Z.expose];if(Y.exclude===null&&Z.exclude!==null)Y.exclude=Z.exclude;if(Y.type===null&&Z.type!==null)Y.type=Z.type;const U=Y.flags,K=Z.flags;if(K.isOptional!==void 0&&U.isOptional===void 0)U.isOptional=K.isOptional;if(K.isDefined!==void 0&&U.isDefined===void 0)U.isDefined=K.isDefined;if(K.validateIf!==void 0&&U.validateIf===void 0)U.validateIf=K.validateIf;if(K.isNullable!==void 0&&U.isNullable===void 0)U.isNullable=K.isNullable;if(K.validateNested!==void 0&&U.validateNested===void 0)U.validateNested=K.validateNested;if(K.validateNestedEach!==void 0&&U.validateNestedEach===void 0)U.validateNestedEach=K.validateNestedEach}}return Q}const __testing__={mergeInheritance,circularPlaceholder:A,sealClass:l};export{ensureSealed,seal,sealRegistry,mergeInheritance,__testing__};export{sealedClasses,resetForTesting}from"./seal-state.js";