diff --git a/.changeset/public-picker-reference-key-prose.md b/.changeset/public-picker-reference-key-prose.md new file mode 100644 index 0000000000..c5eba669ce --- /dev/null +++ b/.changeset/public-picker-reference-key-prose.md @@ -0,0 +1,32 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): `FormFieldPublicPickerSchema.object` now names `reference` — the key `FieldSchema` actually accepts — instead of the rejected `referenceTo` alias (#13138) + +Both sentences on the `object` key told authors the picker target resolves from +`referenceTo` on the parent object's field definition. `referenceTo` is not a +key `FieldSchema` accepts: it is a **rejected alias**, listed in the field +schema's alias map only so `strictUnknownKeyError` can offer a rename hint when +a parse fails on it. An author who followed the sentence and wrote `referenceTo` +on the parent object's field got their whole object metadata refused at parse — +a failure, not a degraded render. + +Measured against the built `packages/spec/dist/data/index.mjs` with a three-level +control (a two-level one cannot separate "rejected" from "not measured"): + +- `reference: 'sys_user'` (positive control) parses, `parsed.reference` is `'sys_user'` +- `zzz_not_a_key` (negative control) is refused `unrecognized_keys`, with no rename hint +- `referenceTo` is refused `unrecognized_keys`, **with** the hint ``Did you mean `referenceTo` → `reference`?`` + +The `.describe()` half is the load-bearing one: it is published, flowing into the +generated JSON Schema and `content/docs/references/ui/view.mdx`, so the wrong key +name reached authors who never open this file — and AI authors reading the +generated schema as ground truth. The reference docs are regenerated with the +repo's own `gen:docs` in the same change. + +Prose only: no schema shape, no accept/reject movement, no new or removed keys — +every previously-valid input still parses byte-identically. This takes no +position on whether the REST route's legacy-spelling fallback chain survives: +the sentence was wrong under either outcome, because a conformant authored field +carries `reference` in both worlds, and the route already reads `reference` first. diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 4e0c8b2343..74d3e853dc 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -235,7 +235,7 @@ Form-view select option — the object-field option shape minus the per-option ` | **displayFields** | `string[]` | optional | Fields projected into each picker result (with `id`); the visitor's search matches `contains` on the first entry. At most 5 (the route projects no more); omitted → ['name']. | | **maxResults** | `integer` | optional | Maximum rows a lookup returns (default 20, hard ceiling 50 — the route clamps; anonymous visitors cannot paginate past it). | | **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Static pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same `{ field, operator, value }` dialect as list-view filters. | -| **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the field definition (`referenceTo`). | +| **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the `reference` key on the field definition. | ### Nested Shape: `FormField.keyField` @@ -262,7 +262,7 @@ Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on a | **displayFields** | `string[]` | optional | Fields projected into each picker result (with `id`); the visitor's search matches `contains` on the first entry. At most 5 (the route projects no more); omitted → ['name']. | | **maxResults** | `integer` | optional | Maximum rows a lookup returns (default 20, hard ceiling 50 — the route clamps; anonymous visitors cannot paginate past it). | | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Static pre-filter rows ANDed ahead of the visitor's search (e.g. only active records are searchable). Same `{ field, operator, value }` dialect as list-view filters. | -| **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the field definition (`referenceTo`). | +| **object** | `string` | optional | Referenced-object override for the picker search; omitted → resolved from the `reference` key on the field definition. | ### Nested Shape: `FormFieldPublicPicker.filter[number]` diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 59b9d67419..747f1132f9 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1907,12 +1907,17 @@ export const FormFieldPublicPickerSchema = lazySchema(() => strictObject({ ), /** * Referenced-object override. Omitted, the route resolves the target from - * the field definition on the parent object (`referenceTo`); set it only - * when that resolution is wrong for this form. + * the field definition on the parent object (`reference`); set it only when + * that resolution is wrong for this form. + * + * `reference` is the key `FieldSchema` accepts — `referenceTo` is only a + * rejected alias it lists so a failed parse can offer a rename hint, so an + * author following the old spelling of this sentence had their whole object + * metadata refused at parse. */ object: z.string().optional().describe( - 'Referenced-object override for the picker search; omitted → resolved from the field ' - + 'definition (`referenceTo`).', + 'Referenced-object override for the picker search; omitted → resolved from the `reference` ' + + 'key on the field definition.', ), }).describe('Public-lookup opt-in: enables GET /forms/:slug/lookup/:field for this field on an anonymous public form (without it the route answers 403 LOOKUP_NOT_PUBLIC).'));