@zipbul/baker 3.4.0 → 3.4.1
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 +15 -0
- package/dist/index.js +1 -10
- package/dist/src/collect.js +1 -26
- package/dist/src/configure.js +1 -43
- package/dist/src/create-rule.js +1 -41
- package/dist/src/decorators/field.js +1 -277
- package/dist/src/decorators/index.js +1 -2
- package/dist/src/decorators/recipe.js +1 -23
- package/dist/src/errors.js +1 -52
- package/dist/src/functions/check-call-options.js +1 -51
- package/dist/src/functions/deserialize.js +1 -57
- package/dist/src/functions/serialize.js +1 -52
- package/dist/src/functions/validate.js +1 -49
- package/dist/src/interfaces.js +0 -4
- package/dist/src/meta-access.js +1 -75
- package/dist/src/registry.js +1 -8
- package/dist/src/rule-metadata.js +1 -17
- package/dist/src/rule-plan.js +1 -117
- package/dist/src/rules/array.js +1 -96
- package/dist/src/rules/binary.js +3 -51
- package/dist/src/rules/combinators.js +1 -111
- package/dist/src/rules/common.js +1 -77
- package/dist/src/rules/date.js +1 -35
- package/dist/src/rules/index.js +1 -10
- package/dist/src/rules/locales.js +1 -249
- package/dist/src/rules/number.js +1 -79
- package/dist/src/rules/object.js +1 -49
- package/dist/src/rules/string.js +10 -2033
- package/dist/src/rules/typechecker.js +5 -171
- package/dist/src/seal/circular-analyzer.js +1 -63
- package/dist/src/seal/codegen-utils.js +1 -18
- package/dist/src/seal/deserialize-builder.js +265 -1564
- package/dist/src/seal/expose-validator.js +1 -65
- package/dist/src/seal/seal-state.js +1 -18
- package/dist/src/seal/seal.js +1 -431
- package/dist/src/seal/serialize-builder.js +66 -370
- package/dist/src/seal/validate-meta.js +1 -61
- package/dist/src/symbols.js +1 -13
- package/dist/src/transformers/collection.transformer.js +1 -25
- package/dist/src/transformers/date.transformer.js +1 -18
- package/dist/src/transformers/index.js +1 -6
- package/dist/src/transformers/luxon.transformer.js +1 -34
- package/dist/src/transformers/moment.transformer.js +1 -32
- package/dist/src/transformers/number.transformer.js +1 -8
- package/dist/src/transformers/string.transformer.js +1 -12
- package/dist/src/types.js +0 -1
- package/dist/src/utils.js +1 -10
- package/package.json +2 -2
|
@@ -1,65 +1 @@
|
|
|
1
|
-
import
|
|
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{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);L(C+A,I,"deserialize");L(C+A,J,"serialize")}}function L(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(Q(I,J)){const q=new Set(J),M=I.length===0?[]:I.filter((P)=>q.has(P));throw new K(`@Expose conflict on '${F}': 2 @Expose stacks with '${C}' direction and overlapping groups [${M.join(", ")}]. Each direction must have at most one @Expose per group set.`)}}}function Q(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()}
|
package/dist/src/seal/seal.js
CHANGED
|
@@ -1,431 +1 @@
|
|
|
1
|
-
import {
|
|
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 O}from"../configure.js";import{BakerError as G}from"../errors.js";import{deleteSealed as B,freezeRaw as h,getRaw as v,getSealed as I,hasRawOwn as R,hasSealedOwn as E,setSealed as A}from"../meta-access.js";import{globalRegistry as w}from"../registry.js";import{isAsyncFunction as f}from"../utils.js";import{analyzeCircular as g}from"./circular-analyzer.js";import{buildDeserializeCode as u,buildValidateCode as y}from"./deserialize-builder.js";import{validateExposeStacks as p}from"./expose-validator.js";import{sealedClasses as S,isSealed as C,markSealed as n}from"./seal-state.js";import{buildSerializeCode as c}from"./serialize-builder.js";import{validateMeta as m}from"./validate-meta.js";const o=new Set(["__proto__","constructor","prototype"]);const P=new Set([Number,String,Boolean,Date]);function k(H){const j=`Circular dependency during seal: ${H} is still being sealed`;return{deserialize(){throw new G(j)},serialize(){throw new G(j)},validate(){throw new G(j)},isAsync:!1,isSerializeAsync:!1}}function T(H,j,q){const U=j==="deserialize"?"isAsync":"isSerializeAsync",J=q??new Set,F=(X)=>{if(J.has(X))return!1;J.add(X);const W=I(X);if(W?.merged)return W[U]===!0;return T(mergeInheritance(X),j,J)};for(const X of Object.values(H)){if(j==="deserialize"&&X.validation.some((W)=>W.rule.isAsync))return!0;for(const W of X.transform){if(j==="deserialize"?W.options?.serializeOnly:W.options?.deserializeOnly)continue;if(W.isAsync??f(W.fn))return!0}if(i(X).some(F))return!0}return!1}function i(H){const j=H.type;if(!j)return[];const q=[];if(j.resolvedClass)q.push(j.resolvedClass);if(j.resolvedCollectionValue)q.push(j.resolvedCollectionValue);if(j.discriminator)for(const U of j.discriminator.subTypes)q.push(U.value);if(q.length===0&&j.fn){const U=j.fn();if(U===Map||U===Set){const J=j.collectionValue?.();if(typeof J==="function"&&!P.has(J))q.push(J)}else{const J=Array.isArray(U)?U[0]:U;if(typeof J==="function"&&!P.has(J))q.push(J)}}return q}function ensureSealed(H){const j=I(H);if(!j){const q=H.name||"<anonymous class>";throw new G(`${q} is not sealed. Call seal() at app startup before deserialize/validate/serialize. (If ${q} has no @Field decorators, decorate at least one property.)`)}return j}function d(){if(C())return;const H=O(),j=new Set;try{for(const q of w)x(q,H,j)}catch(q){for(const U of j)B(U);throw q}for(const q of j){S.add(q);h(q)}w.clear();n()}function r(H){if(E(H))return;const j=O(),q=new Set;try{x(H,j,q)}catch(U){for(const J of q)B(J);throw U}for(const U of q){S.add(U);h(U);w.delete(U)}}function seal(){d()}function x(H,j,q){if(E(H))return;const U=k(H.name);A(H,U);try{const J=mergeInheritance(H);for(const Q of Object.keys(J))if(o.has(Q))throw new G(`${H.name}: field name '${Q}' is not allowed (reserved property name)`);for(const[Q,K]of Object.entries(J)){if(!K.type?.fn)continue;let L;try{L=K.type.fn()}catch(M){throw new G(`${H.name}.${Q}: type function threw: ${M.message}`,{cause:M})}if(L===Map||L===Set){const M=L===Map?"Map":"Set",b={...K.type,collection:M,isArray:!1};if(K.type.collectionValue){let V;try{V=K.type.collectionValue()}catch(z){throw new G(`${H.name}.${Q}: collectionValue function threw: ${z.message}`,{cause:z})}if(V!=null&&typeof V==="function"&&!P.has(V))b.resolvedCollectionValue=V}J[Q]={...K,type:b};continue}const D=Array.isArray(L),N=D?L[0]:L;if(N==null||typeof N!=="function")throw new G(`${H.name}: @Type/@Field type must return a constructor or [constructor], got ${String(N)}`);const _={...K.type,isArray:D};if(!P.has(N)){_.resolvedClass=N;if(!K.flags.validateNested||!K.flags.validateNestedEach){K.flags={...K.flags};if(!K.flags.validateNested)K.flags.validateNested=!0;if(D&&!K.flags.validateNestedEach)K.flags.validateNestedEach=!0}}J[Q]={...K,type:_}}p(J,H.name);m(H,J);const F=g(H);for(const Q of Object.values(J)){if(Q.type?.resolvedClass)x(Q.type.resolvedClass,j,q);if(Q.type?.resolvedCollectionValue)x(Q.type.resolvedCollectionValue,j,q);if(Q.type?.discriminator)for(const K of Q.type.discriminator.subTypes)x(K.value,j,q)}const X=T(J,"deserialize"),W=T(J,"serialize"),$=u(H,J,j,F,X),Y=y(H,J,j,F,X),Z=c(H,J,j,W);Object.assign(U,{deserialize:$,serialize:Z,validate:Y,isAsync:X,isSerializeAsync:W,merged:J})}catch(J){B(H);throw J}q?.add(H)}function mergeInheritance(H){const j=[];let q=H;while(q&&q!==Object){if(R(q))j.push(q);const F=Object.getPrototypeOf(q);q=F===q?null:F}const U=Object.create(null),J=j.length>1;for(const F of j){const X=v(F);for(const[W,$]of Object.entries(X))if(!U[W])U[W]=J?{validation:[...$.validation],transform:[...$.transform],expose:[...$.expose],exclude:$.exclude,type:$.type,flags:{...$.flags}}:$;else{const Y=U[W],Z=$;for(const L of Z.validation)if(!Y.validation.some((D)=>D.rule.ruleName===L.rule.ruleName))Y.validation.push(L);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 Q=Y.flags,K=Z.flags;if(K.isOptional!==void 0&&Q.isOptional===void 0)Q.isOptional=K.isOptional;if(K.isDefined!==void 0&&Q.isDefined===void 0)Q.isDefined=K.isDefined;if(K.validateIf!==void 0&&Q.validateIf===void 0)Q.validateIf=K.validateIf;if(K.isNullable!==void 0&&Q.isNullable===void 0)Q.isNullable=K.isNullable;if(K.validateNested!==void 0&&Q.validateNested===void 0)Q.validateNested=K.validateNested;if(K.validateNestedEach!==void 0&&Q.validateNestedEach===void 0)Q.validateNestedEach=K.validateNestedEach}}return U}const __testing__={mergeInheritance,circularPlaceholder:k,sealClass:r};export{ensureSealed,seal,mergeInheritance,__testing__};export{sealedClasses,resetForTesting}from"./seal-state.js";
|