From f01167595c7cc1a7cef019fe29e8423770951c40 Mon Sep 17 00:00:00 2001 From: Charles Pick Date: Tue, 18 Feb 2025 23:43:42 +0000 Subject: [PATCH 1/3] Immutable SeamPaginator --- src/lib/seam/connect/seam-http-request.ts | 34 +++++----- src/lib/seam/connect/seam-paginator.ts | 79 +++++++++++------------ test/seam/connect/seam-pagination.test.ts | 32 ++++++++- 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/lib/seam/connect/seam-http-request.ts b/src/lib/seam/connect/seam-http-request.ts index 41d343ca..fe37b388 100644 --- a/src/lib/seam/connect/seam-http-request.ts +++ b/src/lib/seam/connect/seam-http-request.ts @@ -38,8 +38,6 @@ export class SeamHttpRequest< readonly #parent: SeamHttpRequestParent readonly #config: SeamHttpRequestConfig - #pagination: Pagination | null = null - constructor( parent: SeamHttpRequestParent, config: SeamHttpRequestConfig, @@ -79,31 +77,24 @@ export class SeamHttpRequest< return this.#config.method } - public get body(): unknown { - return this.#config.body + public get params(): undefined | Record { + return this.#config.params } - public get pagination(): Pagination | null { - return this.#pagination + public get body(): unknown { + return this.#config.body } async execute(): Promise< TResponseKey extends keyof TResponse ? TResponse[TResponseKey] : undefined > { - const { client } = this.#parent - const response = await client.request({ - url: this.#config.path, - method: this.#config.method, - data: this.#config.body, - params: this.#config.params, - }) + const response = await this.fetchResponseData() if (this.responseKey === undefined) { return undefined as TResponseKey extends keyof TResponse ? TResponse[TResponseKey] : undefined } - if ('pagination' in response.data) this.#pagination = this.pagination - const data = response.data[this.responseKey] + const data = response[this.responseKey] as any if (this.responseKey === 'action_attempt') { const waitForActionAttempt = this.#config.options?.waitForActionAttempt ?? @@ -111,7 +102,7 @@ export class SeamHttpRequest< if (waitForActionAttempt !== false) { return await resolveActionAttempt( data, - SeamHttpActionAttempts.fromClient(client, { + SeamHttpActionAttempts.fromClient(this.#parent.client, { ...this.#parent.defaults, waitForActionAttempt: false, }), @@ -122,6 +113,17 @@ export class SeamHttpRequest< return data } + async fetchResponseData(): Promise { + const { client } = this.#parent + const response = await client.request({ + url: this.#config.path, + method: this.#config.method, + data: this.#config.body, + params: this.#config.params, + }) + return response.data + } + async then< TResult1 = TResponseKey extends keyof TResponse ? TResponse[TResponseKey] diff --git a/src/lib/seam/connect/seam-paginator.ts b/src/lib/seam/connect/seam-paginator.ts index 7ef6cdd1..9933d080 100644 --- a/src/lib/seam/connect/seam-paginator.ts +++ b/src/lib/seam/connect/seam-paginator.ts @@ -19,8 +19,6 @@ export class SeamPaginator< { readonly #request: SeamHttpRequest readonly #parent: SeamPaginatorParent - #page: Pagination | null - #items: EnsureReadonlyArray | null = null constructor( parent: SeamPaginatorParent, @@ -28,21 +26,17 @@ export class SeamPaginator< ) { this.#parent = parent this.#request = request - this.#page = request.pagination } - get hasNextPage(): boolean { - if (this.#page == null) return true - return this.#page.hasNextPage - } - - get nextPageCursor(): string | null { - return this.#page?.nextPageCursor ?? null + async first(): Promise< + [EnsureReadonlyArray, Pagination] + > { + return await this.fetch() } - async nextPage(): Promise> { - if (!this.hasNextPage) throw new Error('No next page') - + async fetch( + nextPageCursor?: Pagination['nextPageCursor'], + ): Promise<[EnsureReadonlyArray, Pagination]> { const responseKey = this.#request.responseKey if (typeof responseKey !== 'string') { throw new Error('Cannot paginate a response without a responseKey') @@ -52,53 +46,58 @@ export class SeamPaginator< path: this.#request.pathname, method: 'get', responseKey, - params: { page_cursor: this.#page?.nextPageCursor }, + params: { ...this.#request.params, next_page_cursor: nextPageCursor }, }) - const result = await request.execute() - this.#page = request.pagination ?? { - hasNextPage: false, - nextPageCursor: null, - } - if (!Array.isArray(result)) { + const response = await request.fetchResponseData() + const data = response[responseKey] + const pagination: Pagination = + response != null && + typeof response === 'object' && + 'pagination' in response + ? (response.pagination as Pagination) + : { + hasNextPage: false, + nextPageCursor: null, + } + if (!Array.isArray(data)) { throw new Error('Expected an array response') } - return result as EnsureReadonlyArray + return [ + data as EnsureReadonlyArray, + pagination, + ] as const } async toArray(): Promise> { - if (this.#items != null) return this.#items - - if (this.#page != null) { - throw new Error( - `${SeamPaginator.constructor.name}.toArray() may not be called after using other methods of iteration`, - ) - } - const items = [] as EnsureMutableArray - while (this.hasNextPage) { - for (const item of await this.nextPage()) { - items.push(item) - } + let [current, pagination] = await this.first() + items.push(...current) + while (pagination.hasNextPage) { + ;[current, pagination] = await this.fetch(pagination.nextPageCursor) + items.push(...current) } - this.#items = items as EnsureReadonlyArray return items as EnsureReadonlyArray } async *flatten(): AsyncGenerator< EnsureReadonlyArray > { - while (this.hasNextPage) { - for (const item of await this.nextPage()) { - yield item - } + let [current, pagination] = await this.first() + yield* current + while (pagination.hasNextPage) { + ;[current, pagination] = await this.fetch(pagination.nextPageCursor) + yield* current } } async *[Symbol.asyncIterator](): AsyncGenerator< EnsureReadonlyArray > { - while (this.hasNextPage) { - yield this.nextPage() + let [current, pagination] = await this.first() + yield current + while (pagination.hasNextPage) { + ;[current, pagination] = await this.fetch(pagination.nextPageCursor) + yield current } } } diff --git a/test/seam/connect/seam-pagination.test.ts b/test/seam/connect/seam-pagination.test.ts index a24542b3..85d90948 100644 --- a/test/seam/connect/seam-pagination.test.ts +++ b/test/seam/connect/seam-pagination.test.ts @@ -3,12 +3,42 @@ import { getTestServer } from 'fixtures/seam/connect/api.js' import { SeamHttp, SeamPaginator } from '@seamapi/http/connect' -test('SeamHttp: creates a SeamPaginator', async (t) => { +test('SeamPaginator: creates a SeamPaginator', async (t) => { const { seed, endpoint } = await getTestServer(t) const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) const pages = seam.createPaginator(seam.devices.list()) t.true(pages instanceof SeamPaginator) +}) + +test('SeamPaginator: fetches an array of devices', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) + const pages = seam.createPaginator(seam.devices.list()) const devices = await pages.toArray() t.true(devices.length > 1) }) + +test('SeamPaginator: flattens an array of devices', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) + const pages = seam.createPaginator(seam.devices.list()) + + const devices = [] + for await (const device of pages.flatten()) { + devices.push(device) + } + t.true(devices.length > 1) +}) + +test('SeamPaginator: Fetches an array of pages', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) + const pages = seam.createPaginator(seam.devices.list()) + + const devices = [] + for await (const page of pages) { + devices.push(page) + } + t.true(devices.length > 0) +}) From e344b58181148137e786f9483af13d04f1dcfadb Mon Sep 17 00:00:00 2001 From: Charles Pick Date: Tue, 18 Feb 2025 23:45:14 +0000 Subject: [PATCH 2/3] Fix unused type --- src/lib/seam/connect/seam-http-request.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib/seam/connect/seam-http-request.ts b/src/lib/seam/connect/seam-http-request.ts index fe37b388..cfec801c 100644 --- a/src/lib/seam/connect/seam-http-request.ts +++ b/src/lib/seam/connect/seam-http-request.ts @@ -20,11 +20,6 @@ interface SeamHttpRequestConfig { readonly options?: Pick } -interface Pagination { - readonly hasNextPage: boolean - readonly nextPageCursor: string | null -} - export class SeamHttpRequest< const TResponse, const TResponseKey extends keyof TResponse | undefined, From a1f8599907209a863a481368839ff8412a29e445 Mon Sep 17 00:00:00 2001 From: Charles Pick Date: Tue, 18 Feb 2025 23:52:06 +0000 Subject: [PATCH 3/3] Fix type error --- src/lib/seam/connect/seam-paginator.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/seam/connect/seam-paginator.ts b/src/lib/seam/connect/seam-paginator.ts index 9933d080..891f2e29 100644 --- a/src/lib/seam/connect/seam-paginator.ts +++ b/src/lib/seam/connect/seam-paginator.ts @@ -83,10 +83,14 @@ export class SeamPaginator< EnsureReadonlyArray > { let [current, pagination] = await this.first() - yield* current + for (const item of current) { + yield item + } while (pagination.hasNextPage) { ;[current, pagination] = await this.fetch(pagination.nextPageCursor) - yield* current + for (const item of current) { + yield item + } } }