From 6167441d8fec5f5b375f3c09d6ce897b1168bfd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 19:13:30 +0000 Subject: [PATCH] fix: Send the page cursor when the request has no parameters The paginator only attached page_cursor to the next page request when the original request had non-null params or body. Every generated list route has optional parameters, so paginating a route called with no arguments re-sent the first page request forever: an infinite request loop and unbounded memory growth in flattenToArray. Build the next page request data unconditionally, choosing params or body by the request method. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01B8xeJm2Hd923k8uo6eoFd2 --- src/lib/seam-paginator.ts | 24 ++++++++++------ test/seam/connect/seam-paginator.test.ts | 35 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/lib/seam-paginator.ts b/src/lib/seam-paginator.ts index 0741f46f..b3ac0328 100644 --- a/src/lib/seam-paginator.ts +++ b/src/lib/seam-paginator.ts @@ -81,18 +81,21 @@ export class SeamPaginator< throw new Error('Cannot paginate a response without a responseKey') } + const method = this.#request.method + + const requestData = { + ...(usesParams(method) + ? (this.#request.params ?? {}) + : ((this.#request.body as Record | null) ?? {})), + page_cursor: nextPageCursor, + } + const request = new SeamHttpRequest(this.#parent, { pathname: this.#request.pathname, - method: this.#request.method, + method, responseKey, - params: - this.#request.params != null - ? { ...this.#request.params, page_cursor: nextPageCursor } - : undefined, - body: - this.#request.body != null - ? { ...this.#request.body, page_cursor: nextPageCursor } - : undefined, + params: usesParams(method) ? requestData : undefined, + body: usesParams(method) ? undefined : requestData, }) const response = await request.fetchResponse() @@ -172,6 +175,9 @@ export class SeamPaginator< } } +const usesParams = (method: string): boolean => + ['GET', 'DELETE'].includes(method.toUpperCase()) + type EnsureReadonlyArray = T extends readonly any[] ? T : never type EnsureMutableArray = T extends any[] ? T : never diff --git a/test/seam/connect/seam-paginator.test.ts b/test/seam/connect/seam-paginator.test.ts index 2fca098c..a25d3386 100644 --- a/test/seam/connect/seam-paginator.test.ts +++ b/test/seam/connect/seam-paginator.test.ts @@ -1,5 +1,6 @@ import test from 'ava' import { getTestServer } from 'fixtures/seam/connect/api.js' +import nock from 'nock' import { SeamHttp, SeamPaginator } from '@seamapi/http/connect' @@ -88,6 +89,40 @@ test('SeamPaginator: flatten allows iteration over all devices', async (t) => { t.is(devices.length, allDevices.length) }) +test('SeamPaginator: sends the page cursor when the request has no parameters', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) + + nock(endpoint) + .get('/devices/list') + .reply(200, { + devices: [{ device_id: 'device-1' }], + pagination: { + has_next_page: true, + next_page_cursor: 'page-cursor-1', + next_page_url: `${endpoint}/devices/list?page_cursor=page-cursor-1`, + }, + }) + .get('/devices/list') + .query({ page_cursor: 'page-cursor-1', _strict: 'true' }) + .reply(200, { + devices: [{ device_id: 'device-2' }], + pagination: { + has_next_page: false, + next_page_cursor: null, + next_page_url: null, + }, + }) + + const pages = seam.createPaginator(seam.devices.list()) + const devices = await pages.flattenToArray() + + t.deepEqual( + devices.map(({ device_id: deviceId }) => deviceId), + ['device-1', 'device-2'], + ) +}) + test('SeamPaginator: instance allows iteration over all pages', async (t) => { const { seed, endpoint } = await getTestServer(t) const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })