Uh oh!
There was an error while loading. Please reload this page.
fix(core,app-shell): project every declared recordIdField, and refuse an action that names no record - #4670
Merged
Conversation
… under-specified recordIdParam objectstack#8018
…read moved into objectstack#8018
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 15, 2026 06:31
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 15, 2026
This was referenced Aug 18, 2026
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 freeto 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.
Part of objectstack-ai/objectstack#8018
The objectui general half of the card. The silent-success half (
revoke-sessionanswering
{ status: true }while matching nothing) and thesys_session.revoke_sessiondeclaration are objectstack-side and are not addressed here, so the card stays open.
The defect, re-derived against
mainAn
apiaction declaringrecordIdParamidentifies the record it acts on by a rowfield —
recordIdField, defaultid. Two independent things were wrong:$selectis built from the listView columns,idand the predicate refs.
recordIdFieldwas harvested by nothing, so an action keyedon any other field asked the server for everything except the key naming its own
record.
if (rowValue != null) body[param] = rowValue;with an emptyelse— the requestwent out anyway, minus the parameter. A backend that reads a missing selector as
"match nothing" then answers success for having changed nothing.
Together: a record-scoped mutation that reports success and does nothing.
Anchors re-derived (the card's had drifted): the projection builder is
ObjectGrid.tsx:877-922, not:677-722;predicate-fields.ts:141-170was accurate.The card missed a second projection builder —
plugin-list/src/ListView.tsx:1503-1517feeds the same helper, so a fix landing only in
ObjectGridwould have left the classopen in the other half of the repo.
What changed
Projection (
@object-ui/core).listViewPredicatesnow also harvestsrecordIdFieldfromrowActionDefs,bulkActionDefsand the object'sactions,spelled as a synthetic
record.NAMEso the one existing harvester handles it — theidiom the function already uses for conditional formatting's native
{ field, operator, value }shape. Both projection builders read that function, soObjectGridandListViewgain the key with no call-site change; that is what makesthis a class fix rather than a per-surface one. Existing guards still apply: a name the
object does not declare is dropped by
isProjectableField, and a new identifier gatedrops a non-identifier declaration — without it the harvester's regex would read a
prefix of a malformed name (
record.not a fieldyieldsnot) and contribute aplausible wrong field instead of nothing.
Loud failure (
@object-ui/core+@object-ui/app-shell). NewresolveRecordIdParamSeedis the one definition of "can this row identify the record?".useConsoleActionRuntime's api handler now refuses the dispatch —{ success: false, error }, before the request — when the row lacks the key orholds
nullfor it. The two refusals are worded differently because they point atdifferent repairs: an absent key is a projection or read-visibility problem, a null
value is a data one. Falsy real values (
0,'',false) are values and stilldispatch.
The card allowed either half; both are here deliberately, because the projection half
alone closes only the common route. A row can still lack the key for reasons projection
cannot fix — a server-side read mask that strips the field regardless of
$select, apartial payload handed down from a host, a field the principal cannot read.
Every
recordIdFielddeclaration, enumeratedGrepped across both repos' metadata surfaces (
.ts/.tsx/.json/.yml/.yaml/.js/.mjs,excluding
node_modulesanddist).objectstack — shipped metadata: exactly one
platform-objects/src/identity/sys-session.object.ts:67—sys_session.revoke_sessiontokenmineshowsip_address,active_organization_id,created_at,expires_at)objectui — shipped metadata: none. Every hit is machinery or fixture: the spec key
itself (
ui/Action:recordIdField,action.zod.ts:1212, defaultid),ActionRunner/
actionKeys/serverActionHandlerplumbing, the forward-parity gate, and testfixtures (
recordIdField: 'code'inuseConsoleActionRuntime.test.tsxandserverActionHandler.test.ts). Non-shipped fixture in objectstack:runtime/src/http-dispatcher.actions-type-dispatch.test.ts:168(session_token).So the class has one live instance today, and the key is authorable by any customer
app — which is what makes the value of this change prospective as much as corrective,
and why a per-action patch would have been the wrong shape.
Which instances this covers, and what awaits objectstack-ai/objectstack#7823
recordIdFieldnaming a field the object declaresand the backend returns.
sys_session.revoke_sessionis NOT covered by the projection half. Measured, notassumed: on objectstack
origin/main(8ac2323),sys_session.tokenalready carriesinternal: true(sys-session.object.ts:219-227), and aninternalfield is strippedfrom result rows regardless of projection.
$selectwill now ask fortoken; the rowwill still arrive without it.
reporting success while revoking nothing, and refuses with a message naming the action
and the field. That is a strictly better failure, not a fix of the instance.
action that names the record by
idand lets the server resolve the token. That isobjectstack-side, and Check whether
sys_session.token— a live session credential — serializes over the data API (ADR-0100 channel 3 has no read protection) objectstack#7823 (the open ruling on whetherinternal: trueneeds a mint-path exemption) constrains it. Nothing here assumes thatruling either way, and no
sys_session-specific mechanism was added. #7823 remainsopen; #8018 remains open.
Reproduction and verification
Reproduction is component-level, stated honestly — no browser click-path was run
(the original measurement did not run one either). The grid test drives a real
ObjectGridagainst a stub dataSource and reads the$selectit emits; before the fixit was
['id', 'name']with the declaredrecordIdField: 'token'absent, and theruntime test shows the request dispatching with the parameter missing.
Reverse verification, both halves, run from the committed state, direction predicted
before running:
predicate-fields.tsonlyuseConsoleActionRuntime.tsxonlyBoth legs restored to a byte-clean tree (
git status --porcelainempty).The gate this change moved, and why it is a re-point rather than a deletion
check:action-forward-paritywent red — legitimately, and caused by this change.It derives its owed set from property accesses on the
actionbinding, and moving therecordIdFieldread behindresolveRecordIdParamSeed(action, rowRecord)took the keyout of the extractor's sight, which made three
JUSTIFIEDentries look stale.The gate's message offers "delete the stale entries". That would have been wrong here:
the entries are still true (those renderers never write
_rowRecord, so the key isunreachable on them, not dropped), and deleting them would record "no surface owes
recordIdField" while the runtime still reads it off every forwarded def. The helper issimply where the read lives now, so it is registered as a runtime consumer — the gate's
own "re-point this gate at it" instruction for a consumer that moved. The stale
line-number citations in the entry's reason text were corrected at the same time.
Evidence it restored the model exactly rather than papering over it: the runtime-read
union is 40 keys on
mainand 40 keys after, and every surface's owed count isunchanged (
24/24/24/24/12).Verification, all at
00738ed57(final commit)pnpm exec vitest runoverpackages/core/,packages/plugin-grid/,packages/plugin-list/,packages/app-shell/src/hooks/,scripts/__tests__/check-action-forward-parity.test.tsturbo run type-check— core, app-shell, plugin-grid, plugin-list (closure built via^build)type-checktasks confirmed executedturbo run lint(same four) +lint:rootcheck:action-forward-paritycheck:control-bytes,check:phantom-deps,check:spec-symbols,type-check:scripts,lint:coverage,type-check:coverage,check:i18n-keys,check:skills-pathscheck-changeset-presence,check-changeset-no-majorChangeset:
.changeset/nervous-pugs-worry.md(minor — core, app-shell, plugin-grid,plugin-list; never
major, per the fixed-group rule).Deliberate scope boundaries
packages/app-shell/src/views/RecordDetailView.tsx:617-621carries the samesilent-drop shape and was left untouched: it is held by an in-flight sibling
(
userActions.edit.visibleWhen对详情页页头内建【编辑】不生效 —— 同一份声明里delete生效、edit不生效(17.0.0-rc.6) objectstack#8499). It can adoptresolveRecordIdParamSeedin oneline later, which is why the guard was put in
@object-ui/corerather than inlined.Filed as RecordDetailView drops a
recordIdParamsilently when the record cannot supply the key #4669.Generated by Claude Code