fix(web): merge defaults in parseSettings/parseSchema (IDEA-1487) (#564)

* fix(web): merge defaults in parseSettings/parseSchema on successful parse (IDEA-1487)

parseSettings and parseSchema only merged defaults in the catch branch.
Post-PR #562 migration backfilled NULL collections.settings to '{}', so
JSON.parse succeeds and returns a bare object — downstream consumers
read settings.layout as undefined (rendering 'layout-undefined') and
schema.fields.find as a TypeError on any collection with bare '{}'.

Merge SETTINGS_DEFAULTS / SCHEMA_DEFAULTS into the parsed object in both
branches. Explicit user-supplied fields still override defaults.

Note: QuickActionsMenu spreads parseSettings() back to the wire on edit,
so first quick-action save on a previously-bare collection now persists
{layout:'balanced', default_view:'list'} alongside quick_actions. Left
as-is — defaults migrating to wire is harmless and matches what the UI
was already rendering. Reviewer flag, not a regression.

* fix(web): fresh defaults per parse call to avoid shared mutable state (IDEA-1487 R1)

The module-level SCHEMA_DEFAULTS / SETTINGS_DEFAULTS consts introduced in
8c177d0 hold a `fields: []` array that is copied by reference under shallow
spread. Any caller that mutates `.fields` in place (push/splice/sort) on a
parsed result that fell through to the default would pollute the shared
array for every subsequent parseSchema call.

No current caller mutates, so this is latent — but defense-in-depth at the
exact boundary IDEA-1487 exists to harden. Switch to factory functions that
return a fresh object (with a fresh nested array) per call.

* fix(web): fresh array on getTerminalOptions fallback (IDEA-1487 R2)

getTerminalOptions returned the module-level DEFAULT_TERMINAL_STATUSES
array by reference on the fallback path. Same shared-mutable-state hazard
as R1's parseSchema fix — latent today (only consumer iterates), but a
defense-in-depth gap at the same boundary. Spread on return so each
caller gets a fresh array.
This commit is contained in:
xarmian
2026-05-15 21:44:18 -04:00
committed by GitHub
parent 7e37dfc34e
commit 312cf06ce5
+9 -5
View File
@@ -1072,19 +1072,23 @@ export function parseFields(item: Item): Record<string, any> {
}
}
const schemaDefaults = (): CollectionSchema => ({ fields: [] });
export function parseSchema(collection: Collection): CollectionSchema {
try {
return JSON.parse(collection.schema);
return { ...schemaDefaults(), ...JSON.parse(collection.schema) };
} catch {
return { fields: [] };
return schemaDefaults();
}
}
const settingsDefaults = (): CollectionSettings => ({ layout: 'balanced', default_view: 'list' });
export function parseSettings(collection: Collection): CollectionSettings {
try {
return JSON.parse(collection.settings);
return { ...settingsDefaults(), ...JSON.parse(collection.settings) };
} catch {
return { layout: 'balanced', default_view: 'list' };
return settingsDefaults();
}
}
@@ -1111,7 +1115,7 @@ const DEFAULT_TERMINAL_STATUSES = [
export function getTerminalOptions(collection: Collection): string[] {
const schema = parseSchema(collection);
const statusField = schema.fields.find((f) => f.key === 'status');
return statusField?.terminal_options ?? DEFAULT_TERMINAL_STATUSES;
return statusField?.terminal_options ?? [...DEFAULT_TERMINAL_STATUSES];
}
/** Check if a status value is terminal (finalized) for a given collection. */