fix: address Copilot review round 12 — prototype-safe default seeding
collectSchemaDefaults assigned by name, so a legal field called `__proto__` went to the legacy prototype setter instead of the result. A required one pinned by `const` would then render read-only and be seeded with nothing, leaving submit permanently disabled. Seeded through defineProperty now, the way the other schema-record paths already build their maps. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
a69cb58451
commit
41be9e7aa8
2 changed files with 26 additions and 3 deletions
|
|
@ -445,6 +445,18 @@ describe("root composition (#2123)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("seeds a field named __proto__", () => {
|
||||
// A plain assignment would invoke the legacy prototype setter, leaving a
|
||||
// required pinned field displayed read-only and seeded with nothing.
|
||||
const seeded = collectSchemaDefaults({
|
||||
type: "object",
|
||||
properties: Object.fromEntries([
|
||||
["__proto__", { type: "string", const: "kept" }],
|
||||
]),
|
||||
});
|
||||
expect(Object.hasOwn(seeded, "__proto__")).toBe(true);
|
||||
});
|
||||
|
||||
it("re-applies a nested object's constants", () => {
|
||||
// The overlay replaces the whole nested object rather than merging into it,
|
||||
// so a link naming `{ config: { kind: "sms" } }` would otherwise slip past
|
||||
|
|
|
|||
|
|
@ -145,6 +145,17 @@ export function collectSchemaDefaults(
|
|||
const selected = selectBranchIndex(branches, knownValues) ?? 0;
|
||||
const properties = (branches[selected]?.schema ?? base).properties ?? {};
|
||||
const result: Record<string, unknown> = {};
|
||||
// `properties` is a JSON record, so `__proto__` is a legal field name that a
|
||||
// plain assignment would drop into the legacy prototype setter rather than
|
||||
// keep — a required one pinned by `const` would then display read-only and
|
||||
// be seeded with nothing, leaving submit permanently disabled (#2123).
|
||||
const seed = (name: string, value: unknown) =>
|
||||
Object.defineProperty(result, name, {
|
||||
value,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
for (const [fieldName, rawSchema] of Object.entries(properties)) {
|
||||
// Collapse a nullable union first, for the same reason `SchemaForm` does:
|
||||
// a nested object's `properties` live on the union's surviving branch, so
|
||||
|
|
@ -161,13 +172,13 @@ export function collectSchemaDefaults(
|
|||
// `const` rejects, and seeding that would submit an invalid argument
|
||||
// through a field rendered read-only, leaving the user no way to correct
|
||||
// it.
|
||||
result[fieldName] = fieldSchema.const;
|
||||
seed(fieldName, fieldSchema.const);
|
||||
} else if (fieldSchema.default !== undefined) {
|
||||
result[fieldName] = fieldSchema.default;
|
||||
seed(fieldName, fieldSchema.default);
|
||||
} else if (fieldSchema.type === "object" && fieldSchema.properties) {
|
||||
const nested = collectSchemaDefaults(fieldSchema);
|
||||
if (Object.keys(nested).length > 0) {
|
||||
result[fieldName] = nested;
|
||||
seed(fieldName, nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue