Skip to content

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

@hotlong

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(). The schemaColumns branch maps each entry through columnIdentity(c). The schemaFields branch (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.ts then serializes $select with selectFields.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 $select into every per-group page request. The 17.5 server refuses unknown select keys 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

  1. self-assigned this
    on Sep 29, 2026
  2. hotlong commented on Sep 29, 2026

    @hotlong
    ContributorAuthor

    Claim: direct dispatch at the maintainer's request (not a PM loop round)
    Session: session_014VGCS11YUtYAiinRcdqQwL
    Account: hotlong (the session's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11105-grouped-select-identity
    Worktree: objectui-issue-11105
    Domain: 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/** (the getSelectFields() projection and a new test), optionally packages/data-objectstack/src/** (refuse a non-string $select entry), and one .changeset/*.md. The dev stops on a breach and explains it in the report.
    Container & model: S, mode:subagent, model: opus
    Clause-②: 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 inside getSelectFields() to keep the overlap minimal. No other in-flight claim was found on plugin-grid's projection.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    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) and columns. 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 drop fields when it sends columns. This is a producer fix, and it touches the handoff every child view reads. The inline-data path's !schemaFields gate would need re-measuring."
    ],
    "recommendation": "A for now. B is worth doing only when some other reader of object-grid fields is 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 fields to 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

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    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 release

    Triage 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 columns plus grouping, 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/console bundles 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:

    • both getSelectFields branches project through columnIdentity;
    • the data adapter refuses a non-string $select entry loudly instead of joining it.

    Pin it with a server-grouped grid over object columns.

  5. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions