Skip to content

Commit aee1fd9

Browse files
Delete the unreachable Array.isArray limb at all three adapter.find() sites (#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites `ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight `return existing` that hands back a promise from that same set. Both of `normalizeQueryResult`'s branches return an object literal with exactly `data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array response into it. So no `Array.isArray(<find result>)` limb can ever be taken. Behaviour-preserving, like the `?? records` deletion beside it: `.data` was already read first and always won. What goes is a shape the producer cannot emit, in the sample a customer (and a coding agent) copies from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC * Add the changeset for the docs sample change `@objectstack/docs` patch, following #13955 (the most recent `content/docs/**` diff, which named that package). `@objectstack/example-showcase` is deliberately not named: it is private and appears in 0 of the repo's changesets, and #13705 — the immediately preceding repair at two of these same three sites — carried no changeset at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd8791f commit aee1fd9

4 files changed

Lines changed: 45 additions & 6 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/docs": patch
3+
---
4+
5+
docs(react-pages): delete the unreachable `Array.isArray(result)` limb from the live-data sample
6+
7+
`ObjectStackAdapter.find()` cannot resolve to an array, so the `Array.isArray(result)`
8+
arm the live-data sample carried could never be taken. Re-derived against objectui at
9+
the sha this repo pins (`9602dc82`) and again at objectui `origin/main`, which agree
10+
line for line:
11+
12+
- `find()` returns from five points — `{ data: [], total: 0 }` for a resource already
13+
memoized as missing, `{ data: [], total: 0 }` for a fresh 404 that is not an
14+
`enable`-block denial, two `normalizeQueryResult(...)` calls (the `$expand`/`$search`
15+
raw-GET path and the client-SDK path), and `return existing`, which hands back a
16+
promise produced by that same set.
17+
- Both branches of `normalizeQueryResult()` return an object literal with exactly
18+
`data`, `total`, `page`, `pageSize`, `hasMore`. The first branch is the one that
19+
makes the limb dead: it tests `Array.isArray(result)` on the *transport* response and
20+
**wraps** a bare array into that envelope. The array case is folded before any caller
21+
sees it.
22+
23+
The sample now reads `result.data` directly, and a new paragraph under it states the
24+
envelope contract so the reason survives the next edit. The two `kind:'react'` pages in
25+
`examples/app-showcase` carrying the same dead limb — `crm-workbench` and
26+
`renewals-pipeline` — were repaired in the same edit, with the derivation recorded in the
27+
comment that already explains the neighbouring `.records` trap.
28+
29+
Behaviour-preserving: `.data` was read first and always won. What goes is a shape the
30+
producer cannot emit, sitting in the page a customer — and a coding agent — copies from.

content/docs/ui/react-pages.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ function Page() {
176176
$filter: ['status', '!=', 'paid'],
177177
$top: 200,
178178
});
179-
const records = result?.data ?? (Array.isArray(result) ? result : []);
180-
if (alive) setRows(records);
179+
if (alive) setRows(result.data);
181180
})();
182181
return () => { alive = false; };
183182
}, [adapter]);
@@ -186,6 +185,11 @@ function Page() {
186185
}
187186
```
188187

188+
`find()` always resolves to the same envelope — a `QueryResult` carrying the rows
189+
under `data`. A backend that answers with a bare array is folded into that envelope
190+
before your code sees it, so `result.data` is the only row shape a page is ever handed:
191+
read it directly, with no fallback for a shape the adapter cannot produce.
192+
189193
<Callout type="warn">
190194
The `$` prefixes are load-bearing. An unprefixed `top:` or a `filters:` key is not a
191195
query option — it is silently dropped, and the query runs as if you had not written

examples/app-showcase/src/ui/pages/crm-workbench.page.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ function Page() {
3737
// silently stuck at 0 even though the ListView beside them showed the same
3838
// rows. Read .data: it is the only row shape QueryResult declares, so a
3939
// tolerant '.data || .records' alias would render correctly while
40-
// teaching a spelling the producer cannot emit.
40+
// teaching a spelling the producer cannot emit. There is no array
41+
// shape to test for either: every find() path resolves to that same
42+
// object envelope, and normalizeQueryResult WRAPS a bare array
43+
// response into it, so an 'Array.isArray(all)' limb can never be taken.
4144
//
4245
// The cap is $top, not 'limit': QueryParams declares only $-prefixed keys
4346
// and the adapter copies only those, so a bare 'limit' is dropped without
@@ -48,7 +51,7 @@ function Page() {
4851
// "Active" stays a per-row verdict over the 200 rows actually fetched;
4952
// an exact one would need its own filtered count query.
5053
const all = await adapter.find('showcase_project', { $top: 200 });
51-
const rows = Array.isArray(all) ? all : (all && all.data) || [];
54+
const rows = all?.data ?? [];
5255
const total = typeof (all && all.total) === 'number' ? all.total : rows.length;
5356
setStats({ total, active: rows.filter((r) => r.status === 'active').length });
5457
} catch (e) { console.warn('[CRM Workbench] failed to refresh stats', e); }

examples/app-showcase/src/ui/pages/renewals-pipeline.page.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,16 @@ function Page() {
7575
// applied, is the server's real count over the same $filter rather than
7676
// the page length. Counting data.length under a cap is how a KPI starts
7777
// under-reporting in silence; count 'total' and the cap stays a fetch
78-
// bound instead of a lie about the business.
78+
// bound instead of a lie about the business. That envelope is the ONLY
79+
// shape find() resolves to -- normalizeQueryResult WRAPS a bare array
80+
// response into it -- so there is no array form to test for either.
7981
React.useEffect(() => {
8082
let alive = true;
8183
(async () => {
8284
if (!adapter || !sel) { setRelated({ projects: 0, invoices: 0, openInvoices: 0, capped: false }); return; }
8385
const pr = await adapter.find('showcase_project', { $filter: ['account', '=', sel], $top: 500 });
8486
const iv = await adapter.find('showcase_invoice', { $filter: ['account', '=', sel], $top: 500 });
85-
const rows = (res) => (Array.isArray(res) ? res : (res && res.data) || []);
87+
const rows = (res) => res?.data ?? [];
8688
const count = (res, list) => (typeof (res && res.total) === 'number' ? res.total : list.length);
8789
const projects = rows(pr);
8890
const invoices = rows(iv);

0 commit comments

Comments
 (0)