Skip to content

extractRecords reads records AHEAD of data on DataSource.find() answers — the shared-helper copy of the #5945/#6726 inversion, live in 9 renderers #6839

Description

@os-sales

Found while implementing #6726 (out of that card's scope — it names exactly seven direct call sites and does not mention this helper).

The shape

packages/core/src/utils/extract-records.ts is a shared normaliser:

exportfunctionextractRecords(results: unknown): any[]{if(Array.isArray(results))returnresults;if(results&&typeofresults==='object'){if(Array.isArray((resultsasany).records))return(resultsasany).records;// <- FIRSTif(Array.isArray((resultsasany).data))return(resultsasany).data;// <- secondif(Array.isArray((resultsasany).value))return(resultsasany).value;}return[];}

records is tried beforedata — the same precedence inversion #5945 was filed about and #6726 repaired in seven modules by hand. QueryResult (packages/types/src/data.ts) declares data, total, page, pageSize, hasMore, cursor, metadata; records is not a member of it.

Why this is bigger than any one of #6726's seven

#6726's sites each carry their own copy of the read. This one is a helper, and nine renderers call it directly on a DataSource.find() answer:

modulecall site
packages/plugin-charts/src/ObjectChart.tsx:196, on dataSource.find(referenceTo, …)
packages/plugin-kanban/src/ObjectKanban.tsx:298
packages/plugin-tree/src/ObjectTree.tsx:456, on dataSource.find(dataObjectName, …)
packages/plugin-map/src/ObjectMap.tsx:714
packages/plugin-timeline/src/ObjectTimeline.tsx:213
packages/plugin-gantt/src/ObjectGantt.tsx:680, :1132
packages/plugin-dashboard/src/ObjectDataTable.tsx:680
packages/plugin-dashboard/src/ObjectPivotTable.tsx:157
packages/plugin-calendar/src/ObjectCalendar.tsx:357

Measured on 26896c689 (#6726's base) with a repo-wide sweep of every tracked file for an object-literal records: key: no producer emits records at the DataSource.find() seam. ObjectStackAdapter.normalizeQueryResult CONSUMES the server/SDK records envelope and returns { data, total, page, pageSize, hasMore }; ApiDataSource, ValueDataSource, the runner and example mocks, and the @object-ui/types REST example all return data or a bare array. Control for that zero: the same sweep DOES find the legitimate records producers on other seams (ViewDataProvider's ResolvedData, the raw Cloud HTTP payloads, the client-SDK doubles below normalizeQueryResult), so a producer at this seam would have been caught.

What makes this NOT a copy-paste of #6726's fix

⚠️ The helper is genuinely dual-seam, and that is why it was left alone rather than repaired on #6726's PR:

  • packages/core/src/data-scope/ViewDataProvider.ts also calls it, and its own ResolvedData / DataFetcher.fetchRecords interfaces declare recordslegitimately. On that seam the records arm is load-bearing.
  • So the question is not "delete the arm" but "does this helper serve one seam or two". Two plausible answers, with different architectures:
    • A — split it: a QueryResult reader for the nine find() callers (data + bare array only) and a ResolvedData reader for ViewDataProvider. Each seam then has one contract and no tolerance.
    • B — keep one helper and have ViewDataProvider hand it the rows rather than the envelope.

Per-module measurement is still owed before any deletion: each of the nine call sites needs its own producer check, and any pin has to be per module (a single "nothing reads records" assertion passes even if one module was repaired wrongly). The value arm is on the same footing and is recorded in the sibling finding.

Why it is worth recording

AGENTS.md #0.1: every extra arm is a place a non-conforming producer keeps working unrejected. A helper multiplies that — nine renderers inherit a tolerance none of them chose, and the one place where the tolerance is correct is exactly what stops anyone deleting it.

Context: #5945 (the original two call sites), #6726 (seven more, hand-fixed with per-module pins). No PR here; recording only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions