fix(housekeeping): close schema reflection escapes
This commit is contained in:
1 parent
7895188b56
commit
01dceac073
3 files changed
+539
-37
No files matched your search
@@ -5,6 +5,7 @@ import type { HousekeepingCapabilityContext } from "../contracts";
|
|||||||
import { anyCapability, fail, ok } from "../contracts";
|
import { anyCapability, fail, ok } from "../contracts";
|
||||||
import { dispatchHousekeepingCommand } from "./dispatcher";
|
import { dispatchHousekeepingCommand } from "./dispatcher";
|
||||||
import {
|
import {
|
||||||
|
getHousekeepingCommand,
|
||||||
type HousekeepingCommand,
|
type HousekeepingCommand,
|
||||||
registerHousekeepingCommand,
|
registerHousekeepingCommand,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
@@ -150,6 +151,129 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
expect(executed).toBe(false);
|
expect(executed).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses unchanged private validation after public descriptor mutation attempts", async () => {
|
||||||
|
const commandId = "system.dispatch.reflective-schema-mutation";
|
||||||
|
let executions = 0;
|
||||||
|
register(
|
||||||
|
baseCommand(commandId, {
|
||||||
|
input: z.object({
|
||||||
|
objectDescriptor: z.string().min(3),
|
||||||
|
objectDescriptors: z.string().min(3),
|
||||||
|
reflectDescriptor: z.string().min(3),
|
||||||
|
}),
|
||||||
|
execute: async (context) => {
|
||||||
|
executions += 1;
|
||||||
|
return ok(null, context.correlationId);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const registered = getHousekeepingCommand(commandId);
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
|
||||||
|
const readers = [
|
||||||
|
["objectDescriptor", Object.getOwnPropertyDescriptor],
|
||||||
|
[
|
||||||
|
"objectDescriptors",
|
||||||
|
(target: object, property: PropertyKey) =>
|
||||||
|
Reflect.get(Object.getOwnPropertyDescriptors(target), property) as
|
||||||
|
| PropertyDescriptor
|
||||||
|
| undefined,
|
||||||
|
],
|
||||||
|
["reflectDescriptor", Reflect.getOwnPropertyDescriptor],
|
||||||
|
] as const;
|
||||||
|
const descriptorValue = (
|
||||||
|
target: object,
|
||||||
|
property: PropertyKey,
|
||||||
|
readDescriptor: (
|
||||||
|
target: object,
|
||||||
|
property: PropertyKey,
|
||||||
|
) => PropertyDescriptor | undefined,
|
||||||
|
): unknown => {
|
||||||
|
const descriptor = readDescriptor(target, property);
|
||||||
|
if (!descriptor) {
|
||||||
|
throw new Error(`missing descriptor: ${String(property)}`);
|
||||||
|
}
|
||||||
|
if ("value" in descriptor) return descriptor.value;
|
||||||
|
if (descriptor.get) return Reflect.apply(descriptor.get, target, []);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [field, readDescriptor] of readers) {
|
||||||
|
const definition = descriptorValue(
|
||||||
|
registered.input,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const shape = descriptorValue(
|
||||||
|
definition,
|
||||||
|
"shape",
|
||||||
|
readDescriptor,
|
||||||
|
) as Record<string, z.ZodType>;
|
||||||
|
const child = shape[field];
|
||||||
|
if (!child) throw new Error(`public child missing: ${field}`);
|
||||||
|
const childDefinition = descriptorValue(
|
||||||
|
child,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const checks = descriptorValue(
|
||||||
|
childDefinition,
|
||||||
|
"checks",
|
||||||
|
readDescriptor,
|
||||||
|
) as readonly object[];
|
||||||
|
const internal = descriptorValue(
|
||||||
|
checks[0] as object,
|
||||||
|
"_zod",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const checkDefinition = descriptorValue(
|
||||||
|
internal,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as { minimum: number };
|
||||||
|
try {
|
||||||
|
shape[field] = z.number();
|
||||||
|
} catch {
|
||||||
|
// A readonly facade may reject the assignment.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
checkDefinition.minimum = 0;
|
||||||
|
} catch {
|
||||||
|
// A readonly facade may reject the assignment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forged = await dispatchHousekeepingCommand(
|
||||||
|
{
|
||||||
|
commandId,
|
||||||
|
input: {
|
||||||
|
objectDescriptor: 7,
|
||||||
|
objectDescriptors: 7,
|
||||||
|
reflectDescriptor: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies(),
|
||||||
|
);
|
||||||
|
expect(forged).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: "VALIDATION" },
|
||||||
|
});
|
||||||
|
expect(executions).toBe(0);
|
||||||
|
|
||||||
|
const valid = await dispatchHousekeepingCommand(
|
||||||
|
{
|
||||||
|
commandId,
|
||||||
|
input: {
|
||||||
|
objectDescriptor: "valid",
|
||||||
|
objectDescriptors: "valid",
|
||||||
|
reflectDescriptor: "valid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies(),
|
||||||
|
);
|
||||||
|
expect(valid).toMatchObject({ ok: true });
|
||||||
|
expect(executions).toBe(1);
|
||||||
|
});
|
||||||
it("rejects a missing required reason before the command can execute", async () => {
|
it("rejects a missing required reason before the command can execute", async () => {
|
||||||
let executed = false;
|
let executed = false;
|
||||||
const audit = auditRecorder();
|
const audit = auditRecorder();
|
||||||
|
|||||||
@@ -25,6 +25,52 @@ function command(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DescriptorReader = (
|
||||||
|
target: object,
|
||||||
|
property: PropertyKey,
|
||||||
|
) => PropertyDescriptor | undefined;
|
||||||
|
|
||||||
|
const descriptorReaders = [
|
||||||
|
[
|
||||||
|
"Object.getOwnPropertyDescriptor",
|
||||||
|
"object-descriptor",
|
||||||
|
Object.getOwnPropertyDescriptor,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Object.getOwnPropertyDescriptors",
|
||||||
|
"object-descriptors",
|
||||||
|
(target: object, property: PropertyKey) =>
|
||||||
|
Reflect.get(Object.getOwnPropertyDescriptors(target), property) as
|
||||||
|
| PropertyDescriptor
|
||||||
|
| undefined,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Reflect.getOwnPropertyDescriptor",
|
||||||
|
"reflect-descriptor",
|
||||||
|
Reflect.getOwnPropertyDescriptor,
|
||||||
|
],
|
||||||
|
] as const satisfies readonly (readonly [string, string, DescriptorReader])[];
|
||||||
|
|
||||||
|
function readDescriptorValue(
|
||||||
|
target: object,
|
||||||
|
property: PropertyKey,
|
||||||
|
readDescriptor: DescriptorReader,
|
||||||
|
): unknown {
|
||||||
|
const descriptor = readDescriptor(target, property);
|
||||||
|
if (!descriptor) throw new Error(`missing descriptor: ${String(property)}`);
|
||||||
|
if ("value" in descriptor) return descriptor.value;
|
||||||
|
if (descriptor.get) return Reflect.apply(descriptor.get, target, []);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attemptMutation(mutate: () => void): void {
|
||||||
|
try {
|
||||||
|
mutate();
|
||||||
|
} catch {
|
||||||
|
// Readonly public views may reject the mutation directly.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("housekeeping command registry", () => {
|
describe("housekeeping command registry", () => {
|
||||||
it("returns the validated snapshot registered under its global ID", () => {
|
it("returns the validated snapshot registered under its global ID", () => {
|
||||||
const registered = command("people.registry.lookup");
|
const registered = command("people.registry.lookup");
|
||||||
@@ -296,6 +342,170 @@ describe("housekeeping command registry", () => {
|
|||||||
});
|
});
|
||||||
expect(String(thrown)).not.toContain("caller lazy secret");
|
expect(String(thrown)).not.toContain("caller lazy secret");
|
||||||
});
|
});
|
||||||
|
it.each(descriptorReaders)(
|
||||||
|
"keeps private validation unchanged after nested mutation through %s",
|
||||||
|
(_api, suffix, readDescriptor) => {
|
||||||
|
const input = z.object({ value: z.string().min(3) });
|
||||||
|
const id = `people.registry.${suffix}`;
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command(id),
|
||||||
|
input,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||||
|
const registered = getHousekeepingCommand(id);
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
|
||||||
|
const definition = readDescriptorValue(
|
||||||
|
registered.input,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const shape = readDescriptorValue(
|
||||||
|
definition,
|
||||||
|
"shape",
|
||||||
|
readDescriptor,
|
||||||
|
) as { value: z.ZodType };
|
||||||
|
const child = shape.value;
|
||||||
|
const childDefinition = readDescriptorValue(
|
||||||
|
child,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const checks = readDescriptorValue(
|
||||||
|
childDefinition,
|
||||||
|
"checks",
|
||||||
|
readDescriptor,
|
||||||
|
) as readonly object[];
|
||||||
|
const internal = readDescriptorValue(
|
||||||
|
checks[0] as object,
|
||||||
|
"_zod",
|
||||||
|
readDescriptor,
|
||||||
|
) as object;
|
||||||
|
const checkDefinition = readDescriptorValue(
|
||||||
|
internal,
|
||||||
|
"def",
|
||||||
|
readDescriptor,
|
||||||
|
) as { minimum: number };
|
||||||
|
|
||||||
|
attemptMutation(() => {
|
||||||
|
shape.value = z.number();
|
||||||
|
});
|
||||||
|
attemptMutation(() => {
|
||||||
|
checkDefinition.minimum = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(registered.input.safeParse({ value: "ab" }).success).toBe(false);
|
||||||
|
expect(registered.input.safeParse({ value: "abcd" }).success).toBe(true);
|
||||||
|
expect(registered.input.safeParse({ value: 7 }).success).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("keeps own-key and symbol iteration detached from private checks", () => {
|
||||||
|
const input = z.object({ value: z.string().min(3) });
|
||||||
|
const id = "people.registry.symbol-iteration";
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command(id),
|
||||||
|
input,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||||
|
const registered = getHousekeepingCommand(id);
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
|
||||||
|
const publicInput = registered.input as z.ZodObject<{
|
||||||
|
value: z.ZodString;
|
||||||
|
}>;
|
||||||
|
const shape = publicInput.shape;
|
||||||
|
expect(Reflect.ownKeys(publicInput.def)).toContain("shape");
|
||||||
|
expect(Object.keys(shape)).toEqual(["value"]);
|
||||||
|
const child = Object.entries(shape)[0]?.[1];
|
||||||
|
if (!child) throw new Error("public child schema missing");
|
||||||
|
const checks = child.def.checks ?? [];
|
||||||
|
const spreadChecks = [...checks];
|
||||||
|
const iterator = checks[Symbol.iterator]();
|
||||||
|
const iteratedCheck = iterator.next().value;
|
||||||
|
if (!iteratedCheck) throw new Error("public check missing");
|
||||||
|
expect(spreadChecks[0]).toBe(iteratedCheck);
|
||||||
|
|
||||||
|
attemptMutation(() => {
|
||||||
|
(
|
||||||
|
iteratedCheck as unknown as {
|
||||||
|
_zod: { def: { minimum: number } };
|
||||||
|
}
|
||||||
|
)._zod.def.minimum = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(registered.input.safeParse({ value: "ab" }).success).toBe(false);
|
||||||
|
expect(registered.input.safeParse({ value: "abcd" }).success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps prototype methods operational without passing the private schema to callbacks", async () => {
|
||||||
|
const input = z.object({ value: z.string().min(3) });
|
||||||
|
const id = "people.registry.prototype-methods";
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command(id),
|
||||||
|
input,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||||
|
const registered = getHousekeepingCommand(id);
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
const publicInput = registered.input as z.ZodObject<{
|
||||||
|
value: z.ZodString;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const publicPrototype = Object.getPrototypeOf(publicInput);
|
||||||
|
expect(publicPrototype).toBe(Reflect.getPrototypeOf(publicInput));
|
||||||
|
const prototypeMutation = Symbol("prototype-mutation");
|
||||||
|
Object.defineProperty(publicPrototype, prototypeMutation, {
|
||||||
|
configurable: true,
|
||||||
|
value(this: z.ZodObject<{ value: z.ZodString }>): unknown {
|
||||||
|
attemptMutation(() => {
|
||||||
|
this.def.shape.value = z.number() as never;
|
||||||
|
});
|
||||||
|
return this.def;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
(
|
||||||
|
publicInput as typeof publicInput & {
|
||||||
|
[prototypeMutation](): unknown;
|
||||||
|
}
|
||||||
|
)[prototypeMutation]();
|
||||||
|
} finally {
|
||||||
|
Reflect.deleteProperty(publicPrototype, prototypeMutation);
|
||||||
|
}
|
||||||
|
expect(publicInput.safeParse({ value: "abcd" }).success).toBe(true);
|
||||||
|
expect(publicInput.safeParse({ value: 7 }).success).toBe(false);
|
||||||
|
|
||||||
|
let appliedSchema: z.ZodType | undefined;
|
||||||
|
const applied = publicInput.apply((schema) => {
|
||||||
|
appliedSchema = schema;
|
||||||
|
return schema;
|
||||||
|
});
|
||||||
|
expect(appliedSchema).toBe(publicInput);
|
||||||
|
expect(applied).toBe(publicInput);
|
||||||
|
|
||||||
|
let externallyRegistered: z.ZodType | undefined;
|
||||||
|
const externalRegistry = {
|
||||||
|
add(schema: z.ZodType): void {
|
||||||
|
externallyRegistered = schema;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
publicInput.register(externalRegistry as never, undefined as never),
|
||||||
|
).toBe(publicInput);
|
||||||
|
expect(externallyRegistered).toBe(publicInput);
|
||||||
|
|
||||||
|
const partial = publicInput.partial();
|
||||||
|
const picked = publicInput.pick({ value: true });
|
||||||
|
expect(partial.safeParse({}).success).toBe(true);
|
||||||
|
expect(picked.safeParse({ value: "abcd" }).success).toBe(true);
|
||||||
|
expect((await publicInput.safeParseAsync({ value: "ab" })).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect((await publicInput.safeParseAsync({ value: "abcd" })).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
it("seals the global registry after deterministic bootstrap", () => {
|
it("seals the global registry after deterministic bootstrap", () => {
|
||||||
sealHousekeepingCommandRegistry();
|
sealHousekeepingCommandRegistry();
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,22 @@ function isZodInternalNode(value: object): value is ZodInternalNode {
|
|||||||
|
|
||||||
function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
||||||
const views = new WeakMap<object, unknown>();
|
const views = new WeakMap<object, unknown>();
|
||||||
|
const parseResultMethods = new Set<PropertyKey>([
|
||||||
|
"parse",
|
||||||
|
"safeParse",
|
||||||
|
"parseAsync",
|
||||||
|
"safeParseAsync",
|
||||||
|
"spa",
|
||||||
|
"encode",
|
||||||
|
"decode",
|
||||||
|
"encodeAsync",
|
||||||
|
"decodeAsync",
|
||||||
|
"safeEncode",
|
||||||
|
"safeDecode",
|
||||||
|
"safeEncodeAsync",
|
||||||
|
"safeDecodeAsync",
|
||||||
|
"toJSONSchema",
|
||||||
|
]);
|
||||||
|
|
||||||
function readonlyView(value: unknown): unknown {
|
function readonlyView(value: unknown): unknown {
|
||||||
if (
|
if (
|
||||||
@@ -369,56 +385,208 @@ function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
|||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
if (value instanceof z.ZodType) return schemaFacade(value);
|
if (value instanceof z.ZodType) return schemaFacade(value);
|
||||||
if (typeof value === "function") {
|
if (typeof value === "function") {
|
||||||
const wrapped = (...args: unknown[]) =>
|
return functionFacade(value as (...args: never[]) => unknown);
|
||||||
readonlyView(Reflect.apply(value, undefined, args));
|
|
||||||
views.set(value, wrapped);
|
|
||||||
return Object.freeze(wrapped);
|
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(value)) return arrayFacade(value);
|
||||||
const view = new Proxy(value, {
|
if (value instanceof Map) return mapFacade(value);
|
||||||
defineProperty: () => false,
|
if (value instanceof Set) return setFacade(value);
|
||||||
deleteProperty: () => false,
|
if (value instanceof RegExp) {
|
||||||
get: (target, property, receiver) => {
|
const detached = new RegExp(value.source, value.flags);
|
||||||
const raw = Reflect.get(target, property, receiver);
|
views.set(value, detached);
|
||||||
if (typeof raw !== "function") return readonlyView(raw);
|
return Object.freeze(detached);
|
||||||
return (...args: unknown[]) =>
|
}
|
||||||
readonlyView(Reflect.apply(raw, receiver, args));
|
if (value instanceof Date) {
|
||||||
},
|
const detached = new Date(value.getTime());
|
||||||
set: () => false,
|
views.set(value, detached);
|
||||||
setPrototypeOf: () => false,
|
return Object.freeze(detached);
|
||||||
});
|
}
|
||||||
views.set(value, view);
|
return objectFacade(value);
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function functionFacade(value: (...args: never[]) => unknown): unknown {
|
||||||
|
const wrapped = function (this: unknown, ...args: unknown[]) {
|
||||||
|
const result = new.target
|
||||||
|
? Reflect.construct(value, args)
|
||||||
|
: Reflect.apply(value, this, args);
|
||||||
|
return readonlyView(result);
|
||||||
|
};
|
||||||
|
views.set(value, wrapped);
|
||||||
|
return Object.freeze(wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayFacade(value: readonly unknown[]): readonly unknown[] {
|
||||||
|
const detached: unknown[] = [];
|
||||||
|
views.set(value, detached);
|
||||||
|
for (const item of value) detached.push(readonlyView(item));
|
||||||
|
return Object.freeze(detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFacade(
|
||||||
|
value: ReadonlyMap<unknown, unknown>,
|
||||||
|
): ReadonlyMap<unknown, unknown> {
|
||||||
|
const detached = new Map<unknown, unknown>();
|
||||||
|
views.set(value, detached);
|
||||||
|
for (const [key, item] of value) {
|
||||||
|
detached.set(readonlyView(key), readonlyView(item));
|
||||||
|
}
|
||||||
|
return Object.freeze(detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFacade(value: ReadonlySet<unknown>): ReadonlySet<unknown> {
|
||||||
|
const detached = new Set<unknown>();
|
||||||
|
views.set(value, detached);
|
||||||
|
for (const item of value) detached.add(readonlyView(item));
|
||||||
|
return Object.freeze(detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectFacade(value: object): object {
|
||||||
|
const detached = Object.create(Object.getPrototypeOf(value)) as Record<
|
||||||
|
PropertyKey,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
views.set(value, detached);
|
||||||
|
copyReadonlyProperties(value, detached, (method) =>
|
||||||
|
boundFunctionFacade(method, value),
|
||||||
|
);
|
||||||
|
return Object.freeze(detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundFunctionFacade(
|
||||||
|
method: (...args: never[]) => unknown,
|
||||||
|
receiver: object,
|
||||||
|
): (...args: unknown[]) => unknown {
|
||||||
|
return Object.freeze((...args: unknown[]) =>
|
||||||
|
readonlyView(Reflect.apply(method, receiver, args)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyReadonlyProperties(
|
||||||
|
source: object,
|
||||||
|
destination: Record<PropertyKey, unknown>,
|
||||||
|
wrapMethod: (
|
||||||
|
method: (...args: never[]) => unknown,
|
||||||
|
property: PropertyKey,
|
||||||
|
) => unknown,
|
||||||
|
): void {
|
||||||
|
for (const key of Reflect.ownKeys(source)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
||||||
|
if (!descriptor) continue;
|
||||||
|
if ("value" in descriptor) {
|
||||||
|
Object.defineProperty(destination, key, {
|
||||||
|
configurable: false,
|
||||||
|
enumerable: descriptor.enumerable,
|
||||||
|
value:
|
||||||
|
typeof descriptor.value === "function"
|
||||||
|
? wrapMethod(descriptor.value, key)
|
||||||
|
: readonlyView(descriptor.value),
|
||||||
|
writable: false,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getter = descriptor.get;
|
||||||
|
const safeGetter = getter
|
||||||
|
? () => readonlyView(Reflect.apply(getter, source, []))
|
||||||
|
: undefined;
|
||||||
|
Object.defineProperty(destination, key, {
|
||||||
|
configurable: false,
|
||||||
|
enumerable: descriptor.enumerable,
|
||||||
|
get: safeGetter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function materializeSchemaPrototypeMethods(target: z.ZodType): void {
|
||||||
|
let prototype = Object.getPrototypeOf(target);
|
||||||
|
while (prototype && prototype !== Object.prototype) {
|
||||||
|
for (const property of Reflect.ownKeys(prototype)) {
|
||||||
|
if (Object.hasOwn(target, property)) continue;
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
|
||||||
|
if (
|
||||||
|
!descriptor?.get ||
|
||||||
|
!descriptor.set ||
|
||||||
|
!descriptor.configurable ||
|
||||||
|
descriptor.enumerable
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Reflect.get(target, property, target);
|
||||||
|
}
|
||||||
|
prototype = Object.getPrototypeOf(prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
function schemaFacade<S extends z.ZodType>(target: S): S {
|
function schemaFacade<S extends z.ZodType>(target: S): S {
|
||||||
const cached = views.get(target);
|
const cached = views.get(target);
|
||||||
if (cached !== undefined) return cached as S;
|
if (cached !== undefined) return cached as S;
|
||||||
|
|
||||||
const facade = Object.create(Object.getPrototypeOf(target)) as Record<
|
materializeSchemaPrototypeMethods(target);
|
||||||
|
const shell = Object.create(Object.getPrototypeOf(target)) as Record<
|
||||||
PropertyKey,
|
PropertyKey,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
let facade: S;
|
||||||
|
facade = new Proxy(shell, {
|
||||||
|
defineProperty: () => false,
|
||||||
|
deleteProperty: () => false,
|
||||||
|
get: (publicShell, property, receiver) => {
|
||||||
|
if (Object.hasOwn(publicShell, property)) {
|
||||||
|
return Reflect.get(publicShell, property, receiver);
|
||||||
|
}
|
||||||
|
const inherited = Reflect.get(publicShell, property, receiver);
|
||||||
|
return readonlyView(inherited);
|
||||||
|
},
|
||||||
|
set: () => false,
|
||||||
|
setPrototypeOf: () => false,
|
||||||
|
}) as unknown as S;
|
||||||
views.set(target, facade);
|
views.set(target, facade);
|
||||||
for (const key of Reflect.ownKeys(target)) {
|
copyReadonlyProperties(target, shell, (method, property) =>
|
||||||
const raw = Reflect.get(target, key);
|
schemaMethodFacade(method, property, target, facade),
|
||||||
const exposed =
|
);
|
||||||
typeof raw === "function"
|
Object.freeze(shell);
|
||||||
? (...args: unknown[]) => {
|
return facade;
|
||||||
const result = Reflect.apply(raw, target, args);
|
}
|
||||||
return result instanceof z.ZodType
|
|
||||||
? isolateValidationGraph(result, "derived-schema")
|
function schemaMethodFacade<S extends z.ZodType>(
|
||||||
: result;
|
method: (...args: never[]) => unknown,
|
||||||
}
|
property: PropertyKey,
|
||||||
: readonlyView(raw);
|
target: S,
|
||||||
Object.defineProperty(facade, key, {
|
facade: S,
|
||||||
configurable: false,
|
): (...args: unknown[]) => unknown {
|
||||||
enumerable: Object.prototype.propertyIsEnumerable.call(target, key),
|
if (property === "apply") {
|
||||||
value: exposed,
|
return Object.freeze((...args: unknown[]) => {
|
||||||
writable: false,
|
const callback = args[0];
|
||||||
|
if (typeof callback !== "function") {
|
||||||
|
throw new TypeError("schema apply callback missing");
|
||||||
|
}
|
||||||
|
return Reflect.apply(callback, undefined, [facade]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Object.freeze(facade) as S;
|
if (property === "register") {
|
||||||
|
return Object.freeze((...args: unknown[]) => {
|
||||||
|
const registry = args[0];
|
||||||
|
if (
|
||||||
|
typeof registry !== "object" ||
|
||||||
|
registry === null ||
|
||||||
|
typeof (registry as { add?: unknown }).add !== "function"
|
||||||
|
) {
|
||||||
|
throw new TypeError("schema registry missing");
|
||||||
|
}
|
||||||
|
(
|
||||||
|
registry as {
|
||||||
|
add(schema: S, metadata?: unknown): unknown;
|
||||||
|
}
|
||||||
|
).add(facade, args[1]);
|
||||||
|
return facade;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze((...args: unknown[]) => {
|
||||||
|
const result = Reflect.apply(method, target, args);
|
||||||
|
if (result instanceof z.ZodType) {
|
||||||
|
return isolateValidationGraph(result, "derived-schema");
|
||||||
|
}
|
||||||
|
return parseResultMethods.has(property) ? result : readonlyView(result);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return schemaFacade(schema);
|
return schemaFacade(schema);
|
||||||
|
|||||||
Reference in new issue
Block a user