@myapihq/cli 2.20.0 → 2.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// requestAll follows a keyset cursor to the end.
|
|
2
|
+
//
|
|
3
|
+
// The backend started paging ten list endpoints on 2026-08-20. Callers using
|
|
4
|
+
// `request` kept working and quietly began receiving only the first page — 50
|
|
5
|
+
// rows where they used to get the collection, with nothing in the response
|
|
6
|
+
// saying so. A list that silently stops is worse than a slow one: the caller
|
|
7
|
+
// acts on a partial answer believing it is complete.
|
|
8
|
+
//
|
|
9
|
+
// So these pin the two ways this helper could reintroduce that: stopping early,
|
|
10
|
+
// and looping forever on a server that never says it is done.
|
|
11
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
12
|
+
import { requestAll } from '@myapihq/sdk';
|
|
13
|
+
/** A stub that pages `rows` and records every URL it was asked for. */
|
|
14
|
+
function pagingServer(rows, pageSize) {
|
|
15
|
+
const seen = [];
|
|
16
|
+
const fetchMock = vi.fn(async (url) => {
|
|
17
|
+
seen.push(url);
|
|
18
|
+
const u = new URL(url);
|
|
19
|
+
const cursor = u.searchParams.get('cursor');
|
|
20
|
+
const start = cursor ? rows.findIndex(r => r.id === cursor) + 1 : 0;
|
|
21
|
+
const slice = rows.slice(start, start + pageSize);
|
|
22
|
+
const last = start + pageSize >= rows.length;
|
|
23
|
+
return new Response(JSON.stringify({
|
|
24
|
+
success: true,
|
|
25
|
+
data: slice,
|
|
26
|
+
error: null,
|
|
27
|
+
meta: last ? { has_more: false } : { has_more: true, next_cursor: slice[slice.length - 1]?.id },
|
|
28
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
29
|
+
});
|
|
30
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
31
|
+
return { seen };
|
|
32
|
+
}
|
|
33
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
34
|
+
describe('requestAll', () => {
|
|
35
|
+
it('returns every row across pages, in order, exactly once', async () => {
|
|
36
|
+
const rows = Array.from({ length: 23 }, (_, i) => ({ id: `r${i}` }));
|
|
37
|
+
pagingServer(rows, 5);
|
|
38
|
+
const got = await requestAll('https://api.test/things', 'k', { pageSize: 5 });
|
|
39
|
+
expect(got.map(r => r.id)).toEqual(rows.map(r => r.id));
|
|
40
|
+
expect(new Set(got.map(r => r.id)).size).toBe(23);
|
|
41
|
+
});
|
|
42
|
+
it('passes the cursor on, so it is not just asking page one repeatedly', async () => {
|
|
43
|
+
const rows = Array.from({ length: 7 }, (_, i) => ({ id: `r${i}` }));
|
|
44
|
+
const { seen } = pagingServer(rows, 3);
|
|
45
|
+
await requestAll('https://api.test/things', 'k', { pageSize: 3 });
|
|
46
|
+
expect(seen).toHaveLength(3);
|
|
47
|
+
expect(seen[0]).toContain('limit=3');
|
|
48
|
+
expect(seen[0]).not.toContain('cursor=');
|
|
49
|
+
expect(seen[1]).toContain('cursor=r2');
|
|
50
|
+
expect(seen[2]).toContain('cursor=r5');
|
|
51
|
+
});
|
|
52
|
+
it('stops on the page that says has_more is false', async () => {
|
|
53
|
+
const rows = Array.from({ length: 4 }, (_, i) => ({ id: `r${i}` }));
|
|
54
|
+
const { seen } = pagingServer(rows, 10);
|
|
55
|
+
const got = await requestAll('https://api.test/things', 'k', { pageSize: 10 });
|
|
56
|
+
expect(got).toHaveLength(4);
|
|
57
|
+
expect(seen).toHaveLength(1);
|
|
58
|
+
});
|
|
59
|
+
it('throws rather than returning a partial list when the server never finishes', async () => {
|
|
60
|
+
// A server that always says has_more with a fresh cursor. Returning what
|
|
61
|
+
// was collected would hand back a truncated list that looks complete —
|
|
62
|
+
// exactly the failure this helper exists to prevent, so it must fail loudly.
|
|
63
|
+
let n = 0;
|
|
64
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
|
|
65
|
+
success: true, data: [{ id: `x${n}` }], error: null,
|
|
66
|
+
meta: { has_more: true, next_cursor: `c${n++}` },
|
|
67
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } })));
|
|
68
|
+
await expect(requestAll('https://api.test/things', 'k', { maxPages: 5 }))
|
|
69
|
+
.rejects.toThrow(/pagination_runaway|still reported more rows/);
|
|
70
|
+
});
|
|
71
|
+
it('throws when the cursor does not advance', async () => {
|
|
72
|
+
// A stuck cursor is the ascending/descending mix-up seen from the client
|
|
73
|
+
// side: the server keeps answering with the same next_cursor, so a naive
|
|
74
|
+
// loop spins forever on the same rows.
|
|
75
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
|
|
76
|
+
success: true, data: [{ id: 'same' }], error: null,
|
|
77
|
+
meta: { has_more: true, next_cursor: 'stuck' },
|
|
78
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } })));
|
|
79
|
+
await expect(requestAll('https://api.test/things', 'k', { maxPages: 50 }))
|
|
80
|
+
.rejects.toThrow(/pagination_stalled|same cursor twice/);
|
|
81
|
+
});
|
|
82
|
+
it('unwraps a wrapped payload when told how', async () => {
|
|
83
|
+
// {queues: […]}, {clients: […]} — the wrappers kept so paging did not break
|
|
84
|
+
// existing callers.
|
|
85
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
|
|
86
|
+
success: true, data: { queues: [{ id: 'q1' }, { id: 'q2' }] }, error: null,
|
|
87
|
+
meta: { has_more: false },
|
|
88
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } })));
|
|
89
|
+
const got = await requestAll('https://api.test/queues', 'k', {
|
|
90
|
+
select: d => (d?.queues ?? []),
|
|
91
|
+
});
|
|
92
|
+
expect(got.map(r => r.id)).toEqual(['q1', 'q2']);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.20.
|
|
4
|
+
"version": "2.20.1",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@myapihq/sdk": "^2.20.
|
|
49
|
+
"@myapihq/sdk": "^2.20.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|