Files
pad/internal/models/schema_uniqueness.go
T
xarmian 47ca9e521a fix(server): refuse a collection schema that repeats or empties a field key or option (TASK-3539, PR B) (#2009)
* fix(server): refuse a collection schema that repeats or empties a field key or option (TASK-3539, PR B)

Lead ruling: a repeated or empty field key or select option is a broken
schema, and the web renders fields and options as lists keyed by them, so
one repeat blanked the view (BUG-3538). PR A made the web render such a
schema; this stops new ones being written.

- Collection create and update refuse, with 400 validation_error naming
  it, a schema in which a field has no key, two fields share a key, or a
  select field lists an empty option or the same option twice
  (models.ValidateSchemaKeysAndOptions, beside ValidateAbandonedOptions).
  WRITE check only: a stored schema with a repeat still reads, and an update
  that sends no schema still applies. Live census on :7777: 0 of 213
  collections affected.
- Dynamic Client Registration registers a repeated redirect URI once (it is
  unauthenticated, and the connected-apps page lists URIs keyed by value).
- ListWorkspaceTags groups tags as TEXT: on SQLite json_each kept [1] and
  ["1"] apart while both scanned to "1".
- Pins that convention commands and surfaces are deduped on read: they are
  already (normalizeItemConventionMetadata); the census verdict that said
  otherwise was wrong.

Not done, deliberately: refusing repeated app-manifest event names or
companion field keys. appmanifest.Parse also re-reads a STORED manifest on
upgrade, so a new refusal there would fail a read of an installed app; PR A
made both web lists render repeats.

ToolSurfaceVersion 0.82 -> 0.83: pad_collection create/update refuse calls
they used to accept (the v0.43 / v0.48 grounds). docs/api.md states the 400.

* fix(server): hold companion and template schemas to the key/option rule (TASK-3539)

Codex round 1: writers that bypass the collection handlers could still store
a schema that repeats or empties a key or option.
- An app's companion collection schemas are checked in the install/upgrade
  preview, on the NEW manifest beside the reserved-key check, and refused
  422 invalid_manifest. Not in appmanifest.Parse, which also re-reads an
  installed app's stored manifest.
- Every template and default collection schema the repo ships is checked by
  a test (they seed through the store, not the handlers).
- The legacy-schema test also reads and creates items in a collection stored
  with a repeat.
Workspace and artifact import, and the library option widening (which
already dedupes), are unchanged: refuse on write only.

* docs(mcp): the v0.83 entry in docs/mcp.md's version history (TASK-3539)

Codex round 2: the current-version labels said v0.83, the history began at
v0.82.

* docs(mcp): docs/mcp.md's intro names the current catalog, v0.83 (TASK-3539)

Codex round 3: the intro still said v0.81, stale since before this change.

* fix(server): the option check covers select and multi_select only (TASK-3539)

Codex round 4: it refused empty or repeated options on every field type, but
only a select or multi_select's options are a rendered list. Another type can
carry a legacy options list (the old field DSL put a third part there), and
refusing it would block an unrelated schema save. Tested both ways.

* test(collections): the template schema check is independent of the validator (TASK-3539)

Codex round 5: calling ValidateSchemaKeysAndOptions from the template test was
circular. It now uses its own check. Also records, at the write, why the
library option widening is not held to the validator: it only appends missing
words (never a repeat), and refusing would fail an item create over a legacy
schema.
2026-10-10 03:04:20 -04:00

47 lines
1.8 KiB
Go

package models
import "fmt"
// ValidateSchemaKeysAndOptions refuses a collection schema in which a field
// has no key, two fields share a key, or a select or multi_select field's
// options include an empty value or the same value twice (TASK-3539). Every client renders a
// collection's fields and options as lists keyed by those values, and a
// repeated key throws during render and blanks the view (BUG-3538); a field
// key named twice is also ambiguous for every reader that looks one up.
//
// It is a WRITE check: called on collection create and update, never on a
// read, an import or a migration, so a schema already stored with a repeat
// keeps working (the web normalises it for rendering) until someone edits it,
// and the error then names exactly what to fix. Values compare exactly, as
// stored.
func ValidateSchemaKeysAndOptions(schema CollectionSchema) error {
seen := make(map[string]bool, len(schema.Fields))
for i, f := range schema.Fields {
if f.Key == "" {
return fmt.Errorf("field %d (label %q) has no key", i+1, f.Label)
}
if seen[f.Key] {
return fmt.Errorf("field key %q is defined more than once", f.Key)
}
seen[f.Key] = true
// Options are a list the clients render only for a select or a
// multi_select. Another type can carry a legacy `options` list (the old
// field DSL put a third part there for every type), and refusing that
// would block an unrelated schema save.
if f.Type != "select" && f.Type != "multi_select" {
continue
}
options := make(map[string]bool, len(f.Options))
for _, o := range f.Options {
if o == "" {
return fmt.Errorf("field %q has an empty option", f.Key)
}
if options[o] {
return fmt.Errorf("field %q lists the option %q more than once", f.Key, o)
}
options[o] = true
}
}
return nil
}