Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/public-picker-reference-key-prose.md
Original file line numberDiff line numberDiff line change
@@ -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.
4 changes: 2 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`

Expand All@@ -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]`

Expand Down
13 changes: 9 additions & 4 deletions packages/spec/src/ui/view.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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).'));

Expand Down
Loading