anyvali 0.3.1 → 0.3.3
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 +60 -44
- package/README.md +370 -370
- package/dist/schemas/optional.d.ts.map +1 -1
- package/dist/schemas/optional.js +4 -3
- package/dist/schemas/optional.js.map +1 -1
- package/package.json +40 -40
- package/sdk/js/CHANGELOG.md +13 -13
- package/src/format/validators.ts +71 -71
- package/src/index.ts +285 -285
- package/src/infer.ts +12 -12
- package/src/interchange/importer.ts +285 -285
- package/src/issue-codes.ts +19 -19
- package/src/schemas/base.ts +322 -322
- package/src/schemas/intersection.ts +81 -81
- package/src/schemas/object.ts +203 -203
- package/src/schemas/optional.ts +4 -3
- package/src/schemas/record.ts +55 -55
- package/src/schemas/string.ts +192 -192
- package/src/schemas/union.ts +53 -53
- package/src/types.ts +239 -239
- package/tests/unit/collections.test.ts +99 -99
- package/tests/unit/date-format.test.ts +18 -18
- package/tests/unit/default-mutation.test.ts +32 -32
- package/tests/unit/defaults.test.ts +70 -1
- package/tests/unit/inference.test.ts +306 -306
- package/tests/unit/interchange.test.ts +191 -191
- package/tests/unit/object.test.ts +208 -208
- package/tests/unit/security-recursion.test.ts +105 -105
- package/tests/unit/security.test.ts +945 -945
- package/tests/unit/shared-ref-falsepos.test.ts +33 -33
- package/tests/unit/string-pattern-redos.test.ts +46 -46
- package/tests/unit/string.test.ts +147 -147
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
-
import { BaseSchema } from "./base.js";
|
|
3
|
-
|
|
4
|
-
/** Convert a union of types to an intersection. */
|
|
5
|
-
type UnionToIntersection<U> = (
|
|
6
|
-
U extends any ? (x: U) => void : never
|
|
7
|
-
) extends (x: infer I) => void
|
|
8
|
-
? I
|
|
9
|
-
: never;
|
|
10
|
-
|
|
11
|
-
/** Flatten an intersection into a clean single-level type. */
|
|
12
|
-
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
13
|
-
|
|
14
|
-
export class IntersectionSchema<
|
|
15
|
-
T extends BaseSchema<any, any>[] = BaseSchema[],
|
|
16
|
-
> extends BaseSchema<
|
|
17
|
-
unknown,
|
|
18
|
-
Prettify<UnionToIntersection<T[number]["_output"]>>
|
|
19
|
-
> {
|
|
20
|
-
private _schemas: T;
|
|
21
|
-
|
|
22
|
-
constructor(schemas: [...T]) {
|
|
23
|
-
super();
|
|
24
|
-
this._schemas = schemas as T;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
28
|
-
let result: unknown = input;
|
|
29
|
-
let anyFailed = false;
|
|
30
|
-
|
|
31
|
-
for (const schema of this._schemas) {
|
|
32
|
-
const innerCtx: ParseContext = {
|
|
33
|
-
path: [...ctx.path],
|
|
34
|
-
issues: [],
|
|
35
|
-
// Propagate recursion depth and circular-reference tracking so the
|
|
36
|
-
// depth guard is not reset (and bypassed) at each intersection member.
|
|
37
|
-
definitions: ctx.definitions,
|
|
38
|
-
seen: ctx.seen,
|
|
39
|
-
depth: ctx.depth,
|
|
40
|
-
};
|
|
41
|
-
const validated = schema._runPipeline(input, innerCtx);
|
|
42
|
-
|
|
43
|
-
if (innerCtx.issues.length > 0) {
|
|
44
|
-
ctx.issues.push(...innerCtx.issues);
|
|
45
|
-
anyFailed = true;
|
|
46
|
-
} else {
|
|
47
|
-
// Merge object results
|
|
48
|
-
if (
|
|
49
|
-
typeof result === "object" &&
|
|
50
|
-
result !== null &&
|
|
51
|
-
typeof validated === "object" &&
|
|
52
|
-
validated !== null &&
|
|
53
|
-
!Array.isArray(result) &&
|
|
54
|
-
!Array.isArray(validated)
|
|
55
|
-
) {
|
|
56
|
-
result = {
|
|
57
|
-
...(result as Record<string, unknown>),
|
|
58
|
-
...(validated as Record<string, unknown>),
|
|
59
|
-
};
|
|
60
|
-
} else {
|
|
61
|
-
result = validated;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (anyFailed) {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
_toNode(): SchemaNode {
|
|
74
|
-
const node = {
|
|
75
|
-
kind: "intersection" as const,
|
|
76
|
-
allOf: this._schemas.map((s) => s._toNode()),
|
|
77
|
-
};
|
|
78
|
-
this._addDefault(node as unknown as SchemaNode);
|
|
79
|
-
return node as unknown as SchemaNode;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
|
|
4
|
+
/** Convert a union of types to an intersection. */
|
|
5
|
+
type UnionToIntersection<U> = (
|
|
6
|
+
U extends any ? (x: U) => void : never
|
|
7
|
+
) extends (x: infer I) => void
|
|
8
|
+
? I
|
|
9
|
+
: never;
|
|
10
|
+
|
|
11
|
+
/** Flatten an intersection into a clean single-level type. */
|
|
12
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
13
|
+
|
|
14
|
+
export class IntersectionSchema<
|
|
15
|
+
T extends BaseSchema<any, any>[] = BaseSchema[],
|
|
16
|
+
> extends BaseSchema<
|
|
17
|
+
unknown,
|
|
18
|
+
Prettify<UnionToIntersection<T[number]["_output"]>>
|
|
19
|
+
> {
|
|
20
|
+
private _schemas: T;
|
|
21
|
+
|
|
22
|
+
constructor(schemas: [...T]) {
|
|
23
|
+
super();
|
|
24
|
+
this._schemas = schemas as T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
28
|
+
let result: unknown = input;
|
|
29
|
+
let anyFailed = false;
|
|
30
|
+
|
|
31
|
+
for (const schema of this._schemas) {
|
|
32
|
+
const innerCtx: ParseContext = {
|
|
33
|
+
path: [...ctx.path],
|
|
34
|
+
issues: [],
|
|
35
|
+
// Propagate recursion depth and circular-reference tracking so the
|
|
36
|
+
// depth guard is not reset (and bypassed) at each intersection member.
|
|
37
|
+
definitions: ctx.definitions,
|
|
38
|
+
seen: ctx.seen,
|
|
39
|
+
depth: ctx.depth,
|
|
40
|
+
};
|
|
41
|
+
const validated = schema._runPipeline(input, innerCtx);
|
|
42
|
+
|
|
43
|
+
if (innerCtx.issues.length > 0) {
|
|
44
|
+
ctx.issues.push(...innerCtx.issues);
|
|
45
|
+
anyFailed = true;
|
|
46
|
+
} else {
|
|
47
|
+
// Merge object results
|
|
48
|
+
if (
|
|
49
|
+
typeof result === "object" &&
|
|
50
|
+
result !== null &&
|
|
51
|
+
typeof validated === "object" &&
|
|
52
|
+
validated !== null &&
|
|
53
|
+
!Array.isArray(result) &&
|
|
54
|
+
!Array.isArray(validated)
|
|
55
|
+
) {
|
|
56
|
+
result = {
|
|
57
|
+
...(result as Record<string, unknown>),
|
|
58
|
+
...(validated as Record<string, unknown>),
|
|
59
|
+
};
|
|
60
|
+
} else {
|
|
61
|
+
result = validated;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (anyFailed) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_toNode(): SchemaNode {
|
|
74
|
+
const node = {
|
|
75
|
+
kind: "intersection" as const,
|
|
76
|
+
allOf: this._schemas.map((s) => s._toNode()),
|
|
77
|
+
};
|
|
78
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
79
|
+
return node as unknown as SchemaNode;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/schemas/object.ts
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
import type { ParseContext, SchemaNode, UnknownKeyMode } from "../types.js";
|
|
2
|
-
import { BaseSchema, ABSENT } from "./base.js";
|
|
3
|
-
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
-
import { describeType } from "../util.js";
|
|
5
|
-
import type { OptionalSchema } from "./optional.js";
|
|
6
|
-
|
|
7
|
-
/** Flatten an intersection into a clean single-level type. */
|
|
8
|
-
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
9
|
-
|
|
10
|
-
/** Map a shape record to its inferred output type, separating required from optional fields. */
|
|
11
|
-
export type InferShape<
|
|
12
|
-
T extends Record<string, BaseSchema<any, any>>,
|
|
13
|
-
> = Prettify<
|
|
14
|
-
{
|
|
15
|
-
[K in keyof T as T[K] extends OptionalSchema<any> ? never : K]: T[K]["_output"];
|
|
16
|
-
} & {
|
|
17
|
-
[K in keyof T as T[K] extends OptionalSchema<any>
|
|
18
|
-
? K
|
|
19
|
-
: never]?: T[K]["_output"];
|
|
20
|
-
}
|
|
21
|
-
>;
|
|
22
|
-
|
|
23
|
-
interface PropertyDef {
|
|
24
|
-
schema: BaseSchema;
|
|
25
|
-
required: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class ObjectSchema<
|
|
29
|
-
TShape extends Record<string, BaseSchema<any, any>> = Record<
|
|
30
|
-
string,
|
|
31
|
-
BaseSchema
|
|
32
|
-
>,
|
|
33
|
-
> extends BaseSchema<Record<string, unknown>, InferShape<TShape>> {
|
|
34
|
-
private _properties: Map<string, PropertyDef>;
|
|
35
|
-
private _unknownKeys: UnknownKeyMode;
|
|
36
|
-
private _unknownKeysExplicit: boolean;
|
|
37
|
-
|
|
38
|
-
constructor(
|
|
39
|
-
shape: TShape,
|
|
40
|
-
options?: { unknownKeys?: UnknownKeyMode },
|
|
41
|
-
) {
|
|
42
|
-
super();
|
|
43
|
-
this._properties = new Map();
|
|
44
|
-
this._unknownKeys = options?.unknownKeys ?? "strip";
|
|
45
|
-
this._unknownKeysExplicit = options?.unknownKeys !== undefined;
|
|
46
|
-
|
|
47
|
-
for (const [key, schema] of Object.entries(shape)) {
|
|
48
|
-
// Check if the schema is an OptionalSchema wrapper
|
|
49
|
-
const isOptional = (schema as any)._isOptionalWrapper === true;
|
|
50
|
-
this._properties.set(key, {
|
|
51
|
-
schema,
|
|
52
|
-
required: !isOptional,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
unknownKeys(mode: UnknownKeyMode): ObjectSchema<TShape> {
|
|
58
|
-
const clone = this._clone();
|
|
59
|
-
clone._unknownKeys = mode;
|
|
60
|
-
clone._unknownKeysExplicit = true;
|
|
61
|
-
return clone;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private _effectiveUnknownKeys(): UnknownKeyMode {
|
|
65
|
-
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private _exportUnknownKeys(): UnknownKeyMode {
|
|
69
|
-
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
73
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
74
|
-
ctx.issues.push({
|
|
75
|
-
code: ISSUE_CODES.INVALID_TYPE,
|
|
76
|
-
message: `Expected object, received ${describeType(input)}`,
|
|
77
|
-
path: [...ctx.path],
|
|
78
|
-
expected: "object",
|
|
79
|
-
received: describeType(input),
|
|
80
|
-
});
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Circular reference detection
|
|
85
|
-
if (!ctx.seen) ctx.seen = new WeakSet();
|
|
86
|
-
if (ctx.seen.has(input as object)) {
|
|
87
|
-
ctx.issues.push({
|
|
88
|
-
code: ISSUE_CODES.INVALID_TYPE,
|
|
89
|
-
message: "Circular reference detected",
|
|
90
|
-
path: [...ctx.path],
|
|
91
|
-
expected: "object",
|
|
92
|
-
received: "circular",
|
|
93
|
-
});
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
ctx.seen.add(input as object);
|
|
97
|
-
|
|
98
|
-
const obj = input as Record<string, unknown>;
|
|
99
|
-
const result: Record<string, unknown> = Object.create(null);
|
|
100
|
-
const inputKeys = new Set(Object.keys(obj));
|
|
101
|
-
|
|
102
|
-
// Detect __proto__ via hasOwnProperty (Object.keys skips it)
|
|
103
|
-
if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
|
|
104
|
-
inputKeys.add("__proto__");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Validate declared properties
|
|
108
|
-
for (const [key, prop] of this._properties) {
|
|
109
|
-
ctx.path.push(key);
|
|
110
|
-
const hasKey = Object.prototype.hasOwnProperty.call(obj, key);
|
|
111
|
-
inputKeys.delete(key);
|
|
112
|
-
|
|
113
|
-
if (!hasKey) {
|
|
114
|
-
// Check if required
|
|
115
|
-
if (prop.required && prop.schema._defaultValue === ABSENT) {
|
|
116
|
-
const expectedKind = prop.schema._toNode().kind;
|
|
117
|
-
ctx.issues.push({
|
|
118
|
-
code: ISSUE_CODES.REQUIRED,
|
|
119
|
-
message: `Required property "${key}" is missing`,
|
|
120
|
-
path: [...ctx.path],
|
|
121
|
-
expected: expectedKind,
|
|
122
|
-
received: "undefined",
|
|
123
|
-
});
|
|
124
|
-
ctx.path.pop();
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const rawValue = hasKey ? obj[key] : undefined;
|
|
130
|
-
const val = prop.schema._runPipeline(rawValue, ctx);
|
|
131
|
-
|
|
132
|
-
// Only include in result if value is not undefined or it was explicitly present
|
|
133
|
-
if (val !== undefined || hasKey || prop.schema._defaultValue !== ABSENT) {
|
|
134
|
-
Object.defineProperty(result, key, {
|
|
135
|
-
value: val,
|
|
136
|
-
writable: true,
|
|
137
|
-
enumerable: true,
|
|
138
|
-
configurable: true,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
ctx.path.pop();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Handle unknown keys
|
|
146
|
-
for (const key of inputKeys) {
|
|
147
|
-
switch (this._effectiveUnknownKeys()) {
|
|
148
|
-
case "reject":
|
|
149
|
-
ctx.issues.push({
|
|
150
|
-
code: ISSUE_CODES.UNKNOWN_KEY,
|
|
151
|
-
message: `Unknown key "${key}"`,
|
|
152
|
-
path: [...ctx.path, key],
|
|
153
|
-
expected: "undefined",
|
|
154
|
-
received: key,
|
|
155
|
-
});
|
|
156
|
-
break;
|
|
157
|
-
case "allow":
|
|
158
|
-
Object.defineProperty(result, key, {
|
|
159
|
-
value: obj[key],
|
|
160
|
-
writable: true,
|
|
161
|
-
enumerable: true,
|
|
162
|
-
configurable: true,
|
|
163
|
-
});
|
|
164
|
-
break;
|
|
165
|
-
case "strip":
|
|
166
|
-
// Just ignore it
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Remove from the ancestor set now that this subtree is fully processed.
|
|
172
|
-
// The guard tracks the current ancestor chain (true cycles), not every
|
|
173
|
-
// object ever seen — otherwise a shared/repeated non-circular reference in
|
|
174
|
-
// sibling positions would be falsely rejected as circular.
|
|
175
|
-
ctx.seen.delete(input as object);
|
|
176
|
-
|
|
177
|
-
// Restore normal prototype so result behaves like a standard object
|
|
178
|
-
// while preventing __proto__ pollution via Object.create(null) above
|
|
179
|
-
Object.setPrototypeOf(result, Object.prototype);
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
_toNode(): SchemaNode {
|
|
184
|
-
const properties: Record<string, SchemaNode> = {};
|
|
185
|
-
const required: string[] = [];
|
|
186
|
-
|
|
187
|
-
for (const [key, prop] of this._properties) {
|
|
188
|
-
properties[key] = prop.schema._toNode();
|
|
189
|
-
if (prop.required) {
|
|
190
|
-
required.push(key);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const node = {
|
|
195
|
-
kind: "object" as const,
|
|
196
|
-
properties,
|
|
197
|
-
required,
|
|
198
|
-
unknownKeys: this._exportUnknownKeys(),
|
|
199
|
-
};
|
|
200
|
-
this._addDefault(node as unknown as SchemaNode);
|
|
201
|
-
return node as unknown as SchemaNode;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
1
|
+
import type { ParseContext, SchemaNode, UnknownKeyMode } from "../types.js";
|
|
2
|
+
import { BaseSchema, ABSENT } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
import type { OptionalSchema } from "./optional.js";
|
|
6
|
+
|
|
7
|
+
/** Flatten an intersection into a clean single-level type. */
|
|
8
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
9
|
+
|
|
10
|
+
/** Map a shape record to its inferred output type, separating required from optional fields. */
|
|
11
|
+
export type InferShape<
|
|
12
|
+
T extends Record<string, BaseSchema<any, any>>,
|
|
13
|
+
> = Prettify<
|
|
14
|
+
{
|
|
15
|
+
[K in keyof T as T[K] extends OptionalSchema<any> ? never : K]: T[K]["_output"];
|
|
16
|
+
} & {
|
|
17
|
+
[K in keyof T as T[K] extends OptionalSchema<any>
|
|
18
|
+
? K
|
|
19
|
+
: never]?: T[K]["_output"];
|
|
20
|
+
}
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
interface PropertyDef {
|
|
24
|
+
schema: BaseSchema;
|
|
25
|
+
required: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ObjectSchema<
|
|
29
|
+
TShape extends Record<string, BaseSchema<any, any>> = Record<
|
|
30
|
+
string,
|
|
31
|
+
BaseSchema
|
|
32
|
+
>,
|
|
33
|
+
> extends BaseSchema<Record<string, unknown>, InferShape<TShape>> {
|
|
34
|
+
private _properties: Map<string, PropertyDef>;
|
|
35
|
+
private _unknownKeys: UnknownKeyMode;
|
|
36
|
+
private _unknownKeysExplicit: boolean;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
shape: TShape,
|
|
40
|
+
options?: { unknownKeys?: UnknownKeyMode },
|
|
41
|
+
) {
|
|
42
|
+
super();
|
|
43
|
+
this._properties = new Map();
|
|
44
|
+
this._unknownKeys = options?.unknownKeys ?? "strip";
|
|
45
|
+
this._unknownKeysExplicit = options?.unknownKeys !== undefined;
|
|
46
|
+
|
|
47
|
+
for (const [key, schema] of Object.entries(shape)) {
|
|
48
|
+
// Check if the schema is an OptionalSchema wrapper
|
|
49
|
+
const isOptional = (schema as any)._isOptionalWrapper === true;
|
|
50
|
+
this._properties.set(key, {
|
|
51
|
+
schema,
|
|
52
|
+
required: !isOptional,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
unknownKeys(mode: UnknownKeyMode): ObjectSchema<TShape> {
|
|
58
|
+
const clone = this._clone();
|
|
59
|
+
clone._unknownKeys = mode;
|
|
60
|
+
clone._unknownKeysExplicit = true;
|
|
61
|
+
return clone;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private _effectiveUnknownKeys(): UnknownKeyMode {
|
|
65
|
+
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _exportUnknownKeys(): UnknownKeyMode {
|
|
69
|
+
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
73
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
74
|
+
ctx.issues.push({
|
|
75
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
76
|
+
message: `Expected object, received ${describeType(input)}`,
|
|
77
|
+
path: [...ctx.path],
|
|
78
|
+
expected: "object",
|
|
79
|
+
received: describeType(input),
|
|
80
|
+
});
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Circular reference detection
|
|
85
|
+
if (!ctx.seen) ctx.seen = new WeakSet();
|
|
86
|
+
if (ctx.seen.has(input as object)) {
|
|
87
|
+
ctx.issues.push({
|
|
88
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
89
|
+
message: "Circular reference detected",
|
|
90
|
+
path: [...ctx.path],
|
|
91
|
+
expected: "object",
|
|
92
|
+
received: "circular",
|
|
93
|
+
});
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
ctx.seen.add(input as object);
|
|
97
|
+
|
|
98
|
+
const obj = input as Record<string, unknown>;
|
|
99
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
100
|
+
const inputKeys = new Set(Object.keys(obj));
|
|
101
|
+
|
|
102
|
+
// Detect __proto__ via hasOwnProperty (Object.keys skips it)
|
|
103
|
+
if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
|
|
104
|
+
inputKeys.add("__proto__");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate declared properties
|
|
108
|
+
for (const [key, prop] of this._properties) {
|
|
109
|
+
ctx.path.push(key);
|
|
110
|
+
const hasKey = Object.prototype.hasOwnProperty.call(obj, key);
|
|
111
|
+
inputKeys.delete(key);
|
|
112
|
+
|
|
113
|
+
if (!hasKey) {
|
|
114
|
+
// Check if required
|
|
115
|
+
if (prop.required && prop.schema._defaultValue === ABSENT) {
|
|
116
|
+
const expectedKind = prop.schema._toNode().kind;
|
|
117
|
+
ctx.issues.push({
|
|
118
|
+
code: ISSUE_CODES.REQUIRED,
|
|
119
|
+
message: `Required property "${key}" is missing`,
|
|
120
|
+
path: [...ctx.path],
|
|
121
|
+
expected: expectedKind,
|
|
122
|
+
received: "undefined",
|
|
123
|
+
});
|
|
124
|
+
ctx.path.pop();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rawValue = hasKey ? obj[key] : undefined;
|
|
130
|
+
const val = prop.schema._runPipeline(rawValue, ctx);
|
|
131
|
+
|
|
132
|
+
// Only include in result if value is not undefined or it was explicitly present
|
|
133
|
+
if (val !== undefined || hasKey || prop.schema._defaultValue !== ABSENT) {
|
|
134
|
+
Object.defineProperty(result, key, {
|
|
135
|
+
value: val,
|
|
136
|
+
writable: true,
|
|
137
|
+
enumerable: true,
|
|
138
|
+
configurable: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ctx.path.pop();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle unknown keys
|
|
146
|
+
for (const key of inputKeys) {
|
|
147
|
+
switch (this._effectiveUnknownKeys()) {
|
|
148
|
+
case "reject":
|
|
149
|
+
ctx.issues.push({
|
|
150
|
+
code: ISSUE_CODES.UNKNOWN_KEY,
|
|
151
|
+
message: `Unknown key "${key}"`,
|
|
152
|
+
path: [...ctx.path, key],
|
|
153
|
+
expected: "undefined",
|
|
154
|
+
received: key,
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
case "allow":
|
|
158
|
+
Object.defineProperty(result, key, {
|
|
159
|
+
value: obj[key],
|
|
160
|
+
writable: true,
|
|
161
|
+
enumerable: true,
|
|
162
|
+
configurable: true,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
case "strip":
|
|
166
|
+
// Just ignore it
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove from the ancestor set now that this subtree is fully processed.
|
|
172
|
+
// The guard tracks the current ancestor chain (true cycles), not every
|
|
173
|
+
// object ever seen — otherwise a shared/repeated non-circular reference in
|
|
174
|
+
// sibling positions would be falsely rejected as circular.
|
|
175
|
+
ctx.seen.delete(input as object);
|
|
176
|
+
|
|
177
|
+
// Restore normal prototype so result behaves like a standard object
|
|
178
|
+
// while preventing __proto__ pollution via Object.create(null) above
|
|
179
|
+
Object.setPrototypeOf(result, Object.prototype);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_toNode(): SchemaNode {
|
|
184
|
+
const properties: Record<string, SchemaNode> = {};
|
|
185
|
+
const required: string[] = [];
|
|
186
|
+
|
|
187
|
+
for (const [key, prop] of this._properties) {
|
|
188
|
+
properties[key] = prop.schema._toNode();
|
|
189
|
+
if (prop.required) {
|
|
190
|
+
required.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const node = {
|
|
195
|
+
kind: "object" as const,
|
|
196
|
+
properties,
|
|
197
|
+
required,
|
|
198
|
+
unknownKeys: this._exportUnknownKeys(),
|
|
199
|
+
};
|
|
200
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
201
|
+
return node as unknown as SchemaNode;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/schemas/optional.ts
CHANGED
|
@@ -25,9 +25,10 @@ export class OptionalSchema<
|
|
|
25
25
|
_runPipeline(input: unknown, ctx: ParseContext): unknown {
|
|
26
26
|
const isAbsent = input === undefined || input === ABSENT;
|
|
27
27
|
|
|
28
|
-
// If absent and we have a default
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// If absent and we have a default on this wrapper, use the normal
|
|
29
|
+
// BaseSchema default path so validation and cloning still run.
|
|
30
|
+
if (isAbsent && this._defaultValue !== ABSENT) {
|
|
31
|
+
return super._runPipeline(input, ctx);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
if (isAbsent) {
|