Repository navigation
plugin-grid: server-grouped grid sends select=id,[object Object],… when the view's columns are objects — every group shows INVALID_FIELD (17.5.0 regression) #11105
Description
Found while upgrading HotCRM from @objectstack/* 17.4.0 to 17.5.0 (objectstack-ai/hotcrm#1970) and checking the console in a browser.
Symptom
A grid list view that declares grouping and writes its columns as objects ({ field, width, … }) renders its group headers with correct counts. Every expanded group then shows this instead of rows:
Unknown field '[object Object]' on object 'crm_product' (also: [object Object], …). 'select' chooses which fields to return; …
In HotCRM this breaks the default list of Products, Knowledge Articles and Forecasts. All three are grouping plus object columns.
Wire evidence (17.5.0 console, objectstack dev)
200 GET /api/v1/data/crm_product?top=50&sort=name&select=id,product_code,name,category,family,sku,list_price,cost,is_active
400 GET /api/v1/data/crm_product?top=50&skip=0&sort=name+asc&select=id,[object+Object],[object+Object],…&filter=["and",["category","=","service"]]
{"code":"INVALID_FIELD","field":"[object Object]","object":"crm_product", …}
The 400 repeats once per group.
Control: the same app, view and data on 17.4.0 makes only the flat request, with a correct select, and renders all 13 products.
Mechanism (objectui main @ 88fbd793)
packages/plugin-grid/src/ObjectGrid.tsx,getSelectFields(). TheschemaColumnsbranch maps each entry throughcolumnIdentity(c). TheschemaFieldsbranch (return withHarvestedFields(ensureId((schemaFields as any[]).filter(passesProjectionGate))), around line 2424) returns the entries unmapped. When the view's fields are column objects, the objects themselves end up in$select.packages/data-objectstack/src/index.tsthen serializes$selectwithselectFields.join(','), which produces[object Object].- Before objectui#7189 / feat(plugin-grid): server-side grid grouping — group headers from the query, rows paged per group (objectui#7189) #10878 (server-side grid grouping), a grouped ObjectGrid under ListView got its rows as inline
data, so this path never reached the wire. With server grouping,setGroupRowQuery(groupQuery)carries this$selectinto every per-group page request. The 17.5 server refuses unknownselectkeys instead of widening the projection, so the latent bug became a visible failure.
Reproduce
defineView({ list: {
type: 'grid',
data: { provider: 'object', object: 'crm_product' },
columns: [{ field: 'name' }, { field: 'category' }],
grouping: { fields: [{ field: 'category', order: 'asc' }] },
} })Open the list. Every group shows the INVALID_FIELD message. With collapsed: true the error appears only when a group is expanded.
Suggested direction (not a ruling)
Map the schemaFields branch through columnIdentity the same way the schemaColumns branch does, so both branches project field names. It would also help to make the data adapter refuse a non-string $select entry rather than join it. Add a test with a server-grouped grid and object columns.
HotCRM will not work around this in app metadata. Its views are valid, and rewriting columns as strings would drop widths, pinning and summaries.
Activity
hotlong commentedon Sep 29, 2026
Claim: direct dispatch at the maintainer's request (not a PM loop round)
Session:
session_014VGCS11YUtYAiinRcdqQwLAccount:
hotlong(the session's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11105-grouped-select-identityWorktree:
objectui-issue-11105Domain: none (the card carries no
domain:*label; the maintainer asked for it to be dispatched directly from the HotCRM 17.5.0 upgrade, objectstack-ai/hotcrm#1970)File surface:
packages/plugin-grid/src/**(thegetSelectFields()projection and a new test), optionallypackages/data-objectstack/src/**(refuse a non-string$selectentry), and one.changeset/*.md. The dev stops on a breach and explains it in the report.Container & model: S,
mode:subagent,model: opusClause-②: no (no published contract or accept-set change; this projects field names where the code already meant to)
Thread-read: none
Serial constraints cleared: objectui#10278 (draft, also edits
ObjectGrid.tsx's fetch effect). This fix stays insidegetSelectFields()to keep the overlap minimal. No other in-flight claim was found onplugin-grid's projection.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{
"issue": 11105,
"status": "done",
"branch": "claude/issue-11105-grouped-select-identity",
"pr": "#11119",
"session": "session_014VGCS11YUtYAiinRcdqQwL",
"premise_still_valid": true,
"summary": "Draft PR #11119, head 3541d68, based on d1e683f (origin/main had moved on from 88fbd79, the commit the issue was read on). I re-checked the premise on d1e683f: the unmapped schemaFields line was still there word for word, and ListView's grid node still carries the same column entries under both
fields(from baseProps) andcolumns. Inside getSelectFields() only, both branches now go through one helper, projectFieldNames: passesProjectionGate, then columnIdentity, then drop entries with no identity. So $select carries names only. String projections produce the same $select as before. The ungrouped fetch the grid makes for itself had the same bug and is fixed by the same change. The adapter hardening was deferred (see findings). Conflict I want to flag rather than settle quietly: ObjectGridSchema.fields is declared string[] in both the TS interface and the zod mirror (z.array(z.string())), and the producer (ListView baseProps) sends column objects under that key. The dispatch chose the consumer-side read, and I followed it. For contract-valid input that read is the identity, and after it no grid reader of schema.fields gives a wrong answer. The producer-side option is listed in open_questions.","tests": "Proving test: packages/plugin-grid/src/tests/serverGroupedSelectIdentity-11105.test.tsx, describe 'a server-grouped grid with object columns asks for field names (objectui#11105)'. It has 4 cases: grouped object columns; an entry with no identity is dropped; the ungrouped self-fetch; and a CONTROL with string columns. The double refuses any $select entry that is not a declared name, as 17.5 does. With the fix: 'Tests 4 passed (4)'. Reverse verification, with the fix committed first (a014b30): node ../objectstack/scripts/ablation-replace.mjs swapped the new schemaFields return back to the BASE line. Output: 'anchor x1 → x0', 'blob 2088977ee1ae → 568c722bce4f', 'ok mutation landed'. The run then read '× every group page selects names only…', '× an entry with no field identity…', '× the ungrouped fetch…', 'AssertionError: expected [ id, …(3) ] to deeply equal [ id, name, list_price, …(1) ]', 'Tests 3 failed | 1 passed (4)', with the CONTROL green. Restore: 'blob after restore 2088977ee1ae == blob at HEAD', 'git diff HEAD empty'. The subject is imported from ../ObjectGrid source (vitest aliases every @object-ui/* to src), so there is no dist leg. At head 3541d68, all under os-verify-lock: 'pnpm exec vitest run packages/plugin-grid/ --maxWorkers=2' → 'Test Files 168 passed (168) / Tests 1557 passed (1557)', VERDICT command-exit 0. 'turbo run build --filter=@object-ui/plugin-grid^... --concurrency=2' → 13 successful; then 'pnpm --filter @object-ui/plugin-grid type-check' (it echoed 'tsc --noEmit && tsc -p tsconfig.test.json') → exit 0; 'tsc -p tsconfig.test.json --listFiles' lists the new test file once. ESLint on the 2 touched TS files (count read from --format json): 0 errors, test 0 warnings, ObjectGrid.tsx 207 warnings against 209 on BASE (two any removed). Type-aware linting is not configured (eslint.config.js has no parserOptions.project or projectService), so the diff cannot change a verdict in an untouched file. Exit 0 on: check-changeset-presence, changeset:check, check:new-line-citations ('0 new citation(s)'), check:control-bytes, check:phantom-deps, check:test-path-roots, check:unreferenced-sources, check:pending-changeset-literals. check-governed-queue-guard --test on the 3 paths: 'NOT GOVERNED'. check:changeset-claims is report-only: it lists 5 pending changesets that name ObjectGrid.tsx. I read each paragraph. None describes the $select projection, so none is made false. Declared to CI, not run locally: repo-wide pnpm lint, full pnpm test, and e2e. CI status: in_progress (not awaited).",
"mcp_calls": "0 — no MCP GitHub tool used; reads went over REST (issue, comments, PR 10278 diff, PR list/readback).",
"api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls (draft), run 36591781058 success; (2) label-write --assign hotlong → POST /repos//issues/11119/assignees, run 36591900772 success, read back: assignees [hotlong], labels [plugin, tests] (both from labeler.yml, not mine); (3) this os-dev-report comment → POST /repos//issues/11105/comments. Plus 3 git pushes (empty-branch probe, fix, test tidy), which are not REST writes. No label written by me: the dispatch named none, and objectui path labels belong to labeler.yml.",
"open_questions": [
{
"question": "Should the producer also stop handing column OBJECTS to object-grid under
fields, which ObjectGridSchema declares string[] in both TS and zod? ListView baseProps set fields: effectiveFields for every child view, and its grid case adds columns: effectiveFields.","options": [
"A Leave it. This PR reads names on the consumer side. After it, every grid reader of schema.fields either prefers columns or projects names. No wrong answer is left that I measured.",
"B Change ListView's grid node to send names under
fields, or to dropfieldswhen it sendscolumns. This is a producer fix, and it touches the handoff every child view reads. The inline-data path's!schemaFieldsgate would need re-measuring."],
"recommendation": "A for now. B is worth doing only when some other reader of object-grid
fieldsis shown to break on the object entries. No reader of that kind was measured in this run."}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed · adapter hardening deferred as the dispatch allowed: @object-ui/data-objectstack serializes $select without a type check at two sites (rawFindWithPopulate's selectFields.join(','), and convertQueryParams passing options.select through). Skipping a non-string silently would be the lenient fallback AGENTS.md #0.1 rules out. Refusing it adds a new client-side error surface (code and wording to decide) and changes behaviour for any caller that relied on an ignore-unknown server. After this PR no producer is known to send a non-string. · dedupe words: data-objectstack $select join, non-string select entry, object Object select",
"carrier: 承接者:无 · noted, not filed (dormant after this PR, so no reach) · ListView baseProps hand column objects under
fieldsto every child view, and ObjectGridSchema.fields is z.array(z.string()) · recorded in the PR's Acceptance notes and in open_questions · dedupe words: ListView baseProps fields effectiveFields, ObjectGridSchema fields string array, column objects under fields"]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Triage: route —
bug·priority:p1·domain:ui·area:records·pm:dispatched(in flight:hotlong, PR objectui#11119). A 17.5.0 regression that also gates objectstack's next releaseTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T15:57Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugin-grid/src/ObjectGrid.tsx(getSelectFields) ⇒domain:ui. The card came in claimed by a direct dispatch at the maintainer's request (5893155566), with no lane or state. This comment only completes routing; the claim, the assignee and PR objectui#11119 stand.Why p1. A road step is broken: with object
columnsplusgrouping, the default list view shows an error in every expanded group. It is measured on hotcrm's Products, Knowledge Articles and Forecasts, with a 17.4.0 control.Release note (objectstack).
@objectstack/consolebundles objectui at objectstack's.objectui-sha, so this fix reaches objectstack's next release only through the pin bump objectstack-ai/objectstack#20638, taken after PR objectui#11119 merges. The triage seat carries that ordering on the release checklist.On the PR: the card's two asks both stand:
getSelectFieldsbranches project throughcolumnIdentity;$selectentry loudly instead of joining it.Pin it with a server-grouped grid over object columns.
fix(plugin-grid): a server-grouped grid selects field names, never co…