Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable SeamPaginator #288

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 18 additions & 21 deletions src/lib/seam/connect/seam-http-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ interface SeamHttpRequestConfig<TResponseKey> {
readonly options?: Pick<SeamHttpRequestOptions, 'waitForActionAttempt'>
}

interface Pagination {
readonly hasNextPage: boolean
readonly nextPageCursor: string | null
}

export class SeamHttpRequest<
const TResponse,
const TResponseKey extends keyof TResponse | undefined,
Expand All @@ -38,8 +33,6 @@ export class SeamHttpRequest<
readonly #parent: SeamHttpRequestParent
readonly #config: SeamHttpRequestConfig<TResponseKey>

#pagination: Pagination | null = null

constructor(
parent: SeamHttpRequestParent,
config: SeamHttpRequestConfig<TResponseKey>,
Expand Down Expand Up @@ -79,39 +72,32 @@ export class SeamHttpRequest<
return this.#config.method
}

public get body(): unknown {
return this.#config.body
public get params(): undefined | Record<string, unknown> {
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 ??
this.#parent.defaults.waitForActionAttempt
if (waitForActionAttempt !== false) {
return await resolveActionAttempt(
data,
SeamHttpActionAttempts.fromClient(client, {
SeamHttpActionAttempts.fromClient(this.#parent.client, {
...this.#parent.defaults,
waitForActionAttempt: false,
}),
Expand All @@ -122,6 +108,17 @@ export class SeamHttpRequest<
return data
}

async fetchResponseData(): Promise<TResponse> {
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]
Expand Down
79 changes: 41 additions & 38 deletions src/lib/seam/connect/seam-paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,24 @@ export class SeamPaginator<
{
readonly #request: SeamHttpRequest<TResponse, TResponseKey>
readonly #parent: SeamPaginatorParent
#page: Pagination | null
#items: EnsureReadonlyArray<TResponse[TResponseKey]> | null = null

constructor(
parent: SeamPaginatorParent,
request: SeamHttpRequest<TResponse, TResponseKey>,
) {
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<TResponse[TResponseKey]>, Pagination]
> {
return await this.fetch()
}

async nextPage(): Promise<EnsureReadonlyArray<TResponse[TResponseKey]>> {
if (!this.hasNextPage) throw new Error('No next page')

async fetch(
nextPageCursor?: Pagination['nextPageCursor'],
): Promise<[EnsureReadonlyArray<TResponse[TResponseKey]>, Pagination]> {
const responseKey = this.#request.responseKey
if (typeof responseKey !== 'string') {
throw new Error('Cannot paginate a response without a responseKey')
Expand All @@ -52,43 +46,49 @@ 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<TResponse[TResponseKey]>
return [
data as EnsureReadonlyArray<TResponse[TResponseKey]>,
pagination,
] as const
}

async toArray(): Promise<EnsureReadonlyArray<TResponse[TResponseKey]>> {
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<TResponse[TResponseKey]>
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<TResponse[TResponseKey]>
return items as EnsureReadonlyArray<TResponse[TResponseKey]>
}

async *flatten(): AsyncGenerator<
EnsureReadonlyArray<TResponse[TResponseKey]>
> {
while (this.hasNextPage) {
for (const item of await this.nextPage()) {
let [current, pagination] = await this.first()
for (const item of current) {
yield item
}
while (pagination.hasNextPage) {
;[current, pagination] = await this.fetch(pagination.nextPageCursor)
for (const item of current) {
yield item
}
}
Expand All @@ -97,8 +97,11 @@ export class SeamPaginator<
async *[Symbol.asyncIterator](): AsyncGenerator<
EnsureReadonlyArray<TResponse[TResponseKey]>
> {
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
}
}
}
Expand Down
32 changes: 31 additions & 1 deletion test/seam/connect/seam-pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})