fix(spec,objectql,metadata-protocol): a user field carries its target in the TYPE — bare {type:'user'} is not targetless - #4438
Merged
Conversation
…et in the TYPE — bare `{type:'user'}` is not targetless
`field.zod` defines `user` as "a lookup specialized to the `sys_user` system
object … target fixed to the `sys_user` system object", and `Field.user()` —
unlike `Field.lookup(reference, …)` / `Field.masterDetail(reference, …)` —
takes NO target argument and writes `reference: 'sys_user'` itself. The target
is a CONSTANT OF THE TYPE. `reference` on a `user` field materializes that
constant; it does not supply it.
Two callers read `field.reference` raw and so disagreed with that definition:
the protocol's expand gate refused `?expand=<a bare user field>` with
`400 INVALID_FIELD … declares no target object`, and objectql's expand loop
skipped it. Metadata authored without the redundant `reference` — hand-written
JSON, an AI author, a Studio form — was therefore read as under-specified when
it was complete.
Live capture (cloud#983): an AI-built equipment app modelled 负责人 as
`{ type: 'user' }`; objectui's default list expanded that column (its
`EXPANDABLE_FIELD_TYPES` keys on the TYPE, deliberately ignoring the target);
the very first screen of the brand-new app rendered "该视图的查询被拒绝" over
that 400.
`referenceTargetOf` in `@objectstack/spec/data` is now the single arbiter of
"what does this reference field point at", next to `REFERENCE_VALUE_TYPES` —
the set the same two callers already share for "is this a reference at all".
Both halves of the expand path read it, which is what stops the gate from
refusing a field the engine would have expanded, or blessing one it skips.
Fixing only the gate would be worse than not fixing it: the request would be
admitted and the engine would still skip the field, answering 200 with a raw
user id in the cell — the "client renders raw ids where names belong" failure
the expand axis exists to close. The conformance test pins BOTH halves (each
was verified to fail alone).
Deliberately unchanged: `seed-loader`'s reference resolution still requires an
explicit `reference`. An unresolvable seed reference is a HARD failure there,
so folding implicit targets in would turn seeds that today write a raw string
into failed loads — a different subsystem's contract question, not this one.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The contract question, settled first
type: 'user'— does it imply the target object, or must it carry an explicitreference?It implies it. The spec says so in its own words:
field.zodtype list: "User reference — a lookup specialized to thesys_usersystem object … Stored IDENTICALLY to 'lookup' (FK string column → sys_user.id)".Field.user(): "target fixed to thesys_usersystem object" — and unlikeField.lookup(reference, …)/Field.masterDetail(reference, …)it takes no target argument; it writesreference: 'sys_user'itself.reference's own docstring names the types that require it: "Used bylookupandmaster_detailfield types".buildExpandFieldsexpands on TYPE alone ("thereference/reference_totarget is irrelevant to the decision"), andlookupTargetObject({type:'user'})→sys_user.So the target is a constant of the type.
referenceon auserfield materializes that constant; it does not supply it. Metadata authored without it — hand-written JSON, an AI author, a Studio form — is complete, not under-specified.What was broken
Two callers read
field.referenceraw and so disagreed with that definition:protocol.assertExpandTargetsExistrefused?expand=<bare user field>→400 INVALID_FIELD … declares no target objectengine.expandRelatedRecordsskipped it (if (!fieldDef.reference) continue)Live capture (cloud#983): an AI-built equipment app modelled 负责人 as
{ type: 'user' }, objectui's default list expanded that column, and the very first screen of the brand-new app rendered 「该视图的查询被拒绝」:The fix
referenceTargetOfin@objectstack/spec/data, next toREFERENCE_VALUE_TYPES— the set those same two callers already share for "is this a reference at all". It answers the companion question "what does it point at", folding the type-implied targets. Both halves of the expand path read it, which is what stops the gate from refusing a field the engine would have expanded, or blessing one it skips.A gate-only fix would have been worse than none: the request would be admitted and the engine would still skip the field, answering
200with a raw user id in the cell — the "client renders raw ids where names belong" failure the whole expand axis exists to close. The conformance test pins both halves; each was verified to fail on its own (engine reverted →expected 'usr_1' to match object {id:'usr_1', name:'Ada'}; gate reverted → the issue's exact 400).Deliberately unchanged
seed-loader's reference resolution still requires an explicitreference. An unresolvable seed reference is a hard failure there, so folding implicit targets in would turn seeds that today write a raw string into failed loads. That is a different subsystem's contract question, not this one.Tests
spec—referenceTargetOfunit tests, plus a drift guard assertinguseris the only reference type with an implied targetobjectql/query-expression-conformance— a bare{type:'user'}field is admitted and actually expands ({id:'usr_1', name:'Ada'}, not the raw id)@objectstack/spec7230 ·metadata-protocol139 ·objectql1507 — all green;spec+objectqltypecheck clean🤖 Generated with Claude Code