Moved from objectstack-ai/objectstack#10470 by the triage seat (file-at-destination rule: every named site and the rule that should gate them live in this repo). Original filed 2026-08-21 by os-zhuang from the objectstack-ai/objectstack#10288 dev seat. Body reproduced verbatim below with bare issue refs qualified.
Census taken at objectui c40f3b8ca21ddc19e05682f1719fcf16d4ce7fba (objectstack pins 9a3daf8d37ad973a621e5edd276fe32467f90684; re-confirm at the pin before acting).
The class
QueryParams (packages/types/src/data.ts) declares only $-prefixed keys, and ObjectStackAdapter.convertQueryParams (packages/data-objectstack/src/index.ts) builds its outgoing options by copying exactly those. Any other key reaches no branch and is dropped — no throw, no warning. QueryParams also carries an index signature, so the type system accepts both spellings equally.
The consequence is an unbounded read, not a truncated one: the platform's GET list route has no default page size (objectstack packages/client/src/index.ts, pinned in its client.test.ts), so an absent top returns the entire match set. A dropped cap is therefore invisible until the object is large.
The four live sites
| file | call | dropped key |
|---|
apps/console/src/sdui-workbench-preview.tsx:65 | find(objectName, { top: 200 }) | top |
packages/app-shell/src/views/ObjectView.tsx:1561 | find(…, { limit: 0 }) | limit |
packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx:109 | find('sys_permission_set', { $filter: {…}, limit: 1 }) | limit |
content/docs/guide/react-pages.md:129 | find(…, { filters: [['status','=','open']] }) | filters |
ObjectView.tsx:1561's limit: 0 is worth reading twice: $top: 0 is honoured end to end as "no records" (objectstack-ai/objectstack#6485 pinned it), so if that call means what it says, dropping the key inverts it from no rows to every row.
AssignedUsersSection.tsx:109 is a one-line-away neighbour of three correct calls ($top: 500, $top: 200, $top: 1000 on L124/131/136) — the shape a reviewer's eye slides over.
sdui-workbench-preview.tsx:66 also reads all.records off the result, with no .data arm. find() resolves to a QueryResult (data, never records), so that preview lists nothing regardless of the cap. skills/objectui/guides/data-integration.md:268 publishes the same return result.records; to skill consumers.
Why a rule, and which one
eslint-rules/no-query-params-under-options.js already gates the sibling shape — find(obj, { options: { $top: 100 } }) — and its own header makes the argument for mechanising this family: the mistake type-checks, it publishes, the symptom looks like a data problem rather than a code problem, "so a review catches it once and then misses the next one." It cites two live instances by two authors (object-timeline, objectui#4009 / objectstack-ai/objectstack#7137; object-kanban, objectui#4025).
The rule bans a $-prefixed key under options. It does not look at a bare key at the top level, which is the other half of the same class — and the half with four live sites. Extending it (or adding a sibling rule) is the cheap move: the AST walk already lands on the params object literal of a find/findOne call, the fix is a second predicate, and the contract lives in the same repo as the rule.
Suggested shape, narrow on purpose: flag a static key that is a known query-option name without its $ (top, skip, filter, filters, select, orderby, sort, expand, search, count, limit, offset) in the second argument of an adapter.find / dataSource.find / …findOne call. Not "any unprefixed key" — adapters legitimately take adapter-specific params, which is why the index signature exists.
An objectstack-side gate was considered for objectstack-ai/objectstack#10288 and rejected: QueryParams is objectui's contract, so a checker there would have to hard-code another repo's key list and become a second source of truth for it. The rule belongs next to the type it enforces.
Related
Moved from objectstack-ai/objectstack#10470 by the triage seat (file-at-destination rule: every named site and the rule that should gate them live in this repo). Original filed 2026-08-21 by os-zhuang from the objectstack-ai/objectstack#10288 dev seat. Body reproduced verbatim below with bare issue refs qualified.
Census taken at objectui
c40f3b8ca21ddc19e05682f1719fcf16d4ce7fba(objectstack pins9a3daf8d37ad973a621e5edd276fe32467f90684; re-confirm at the pin before acting).The class
QueryParams(packages/types/src/data.ts) declares only$-prefixed keys, andObjectStackAdapter.convertQueryParams(packages/data-objectstack/src/index.ts) builds its outgoing options by copying exactly those. Any other key reaches no branch and is dropped — no throw, no warning.QueryParamsalso carries an index signature, so the type system accepts both spellings equally.The consequence is an unbounded read, not a truncated one: the platform's GET list route has no default page size (objectstack
packages/client/src/index.ts, pinned in itsclient.test.ts), so an absenttopreturns the entire match set. A dropped cap is therefore invisible until the object is large.The four live sites
apps/console/src/sdui-workbench-preview.tsx:65find(objectName, { top: 200 })toppackages/app-shell/src/views/ObjectView.tsx:1561find(…, { limit: 0 })limitpackages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx:109find('sys_permission_set', { $filter: {…}, limit: 1 })limitcontent/docs/guide/react-pages.md:129find(…, { filters: [['status','=','open']] })filtersObjectView.tsx:1561'slimit: 0is worth reading twice:$top: 0is honoured end to end as "no records" (objectstack-ai/objectstack#6485 pinned it), so if that call means what it says, dropping the key inverts it from no rows to every row.AssignedUsersSection.tsx:109is a one-line-away neighbour of three correct calls ($top: 500,$top: 200,$top: 1000on L124/131/136) — the shape a reviewer's eye slides over.sdui-workbench-preview.tsx:66also readsall.recordsoff the result, with no.dataarm.find()resolves to aQueryResult(data, neverrecords), so that preview lists nothing regardless of the cap.skills/objectui/guides/data-integration.md:268publishes the samereturn result.records;to skill consumers.Why a rule, and which one
eslint-rules/no-query-params-under-options.jsalready gates the sibling shape —find(obj, { options: { $top: 100 } })— and its own header makes the argument for mechanising this family: the mistake type-checks, it publishes, the symptom looks like a data problem rather than a code problem, "so a review catches it once and then misses the next one." It cites two live instances by two authors (object-timeline, objectui#4009 / objectstack-ai/objectstack#7137;object-kanban, objectui#4025).The rule bans a
$-prefixed key underoptions. It does not look at a bare key at the top level, which is the other half of the same class — and the half with four live sites. Extending it (or adding a sibling rule) is the cheap move: the AST walk already lands on the params object literal of afind/findOnecall, the fix is a second predicate, and the contract lives in the same repo as the rule.Suggested shape, narrow on purpose: flag a static key that is a known query-option name without its
$(top,skip,filter,filters,select,orderby,sort,expand,search,count,limit,offset) in the second argument of anadapter.find/dataSource.find/…findOnecall. Not "any unprefixed key" — adapters legitimately take adapter-specific params, which is why the index signature exists.An objectstack-side gate was considered for objectstack-ai/objectstack#10288 and rejected:
QueryParamsis objectui's contract, so a checker there would have to hard-code another repo's key list and become a second source of truth for it. The rule belongs next to the type it enforces.Related
top: 500toadapter.find, which is not a query option — the rollup silently reads only the first page objectstack#10288 — the same two defects inexamples/app-showcase, fixed there.result.records, which is always undefined — and its Callout misstates what a dropped option does objectstack#10469 — the same.recordsmisread in objectstack's own react-pages guide (queued there, devx lane).options: { $top: 100 }里 —— 这个键不属于 QueryParams,没有任何 adapter 读它,窗口从未上线 objectstack#7147 — theoptions: { $top }half, already gated.