diff --git a/codegen/layouts/partials/route-class-endpoint.hbs b/codegen/layouts/partials/route-class-endpoint.hbs index 4b8afcc1..24af5357 100644 --- a/codegen/layouts/partials/route-class-endpoint.hbs +++ b/codegen/layouts/partials/route-class-endpoint.hbs @@ -10,7 +10,8 @@ hasRequiredParameters: {{hasRequiredParameters}}, requiredParameterNames: [{{#each requiredParameterNames}}{{json .}}{{#unless @last}}, {{/unless}}{{/each}}], responseKey: {{#if returnsVoid}}undefined{{else}}'{{responseKey}}'{{/if}}, - options, + {{#if hasPagination}}hasPagination: true, + {{/if}}options, {{#if returnsActionAttempt}} actionAttempts: SeamHttpActionAttempts.fromClient(this.client, { ...this.defaults, diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index 509617c1..4f1532b9 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -28,6 +28,7 @@ export interface EndpointLayoutContext { className: string method: Method responseKey: string + hasPagination: boolean requestFormat: 'params' | 'body' parametersTypeName: string responseTypeName: string @@ -141,6 +142,7 @@ export const getEndpointLayoutContext = ( methodName, functionName: camelCase(prefix), method: endpoint.request.preferredMethod, + hasPagination: endpoint.hasPagination, className: getClassName(route.path), requestFormat, returnsActionAttempt, diff --git a/src/lib/routes/access-codes/access-codes.ts b/src/lib/routes/access-codes/access-codes.ts index 4aead41d..f619c116 100644 --- a/src/lib/routes/access-codes/access-codes.ts +++ b/src/lib/routes/access-codes/access-codes.ts @@ -294,6 +294,7 @@ export class SeamHttpAccessCodes { hasRequiredParameters: true, requiredParameterNames: [], responseKey: 'access_codes', + hasPagination: true, options, }) } diff --git a/src/lib/routes/access-codes/unmanaged/unmanaged.ts b/src/lib/routes/access-codes/unmanaged/unmanaged.ts index fee11d61..5c5d9070 100644 --- a/src/lib/routes/access-codes/unmanaged/unmanaged.ts +++ b/src/lib/routes/access-codes/unmanaged/unmanaged.ts @@ -237,6 +237,7 @@ export class SeamHttpAccessCodesUnmanaged { hasRequiredParameters: true, requiredParameterNames: ['device_id'], responseKey: 'access_codes', + hasPagination: true, options, }) } diff --git a/src/lib/routes/access-grants/access-grants.ts b/src/lib/routes/access-grants/access-grants.ts index 373116fb..9e65b024 100644 --- a/src/lib/routes/access-grants/access-grants.ts +++ b/src/lib/routes/access-grants/access-grants.ts @@ -257,6 +257,7 @@ export class SeamHttpAccessGrants { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'access_grants', + hasPagination: true, options, }) } diff --git a/src/lib/routes/access-grants/unmanaged/unmanaged.ts b/src/lib/routes/access-grants/unmanaged/unmanaged.ts index 1c184b2a..b3353dfd 100644 --- a/src/lib/routes/access-grants/unmanaged/unmanaged.ts +++ b/src/lib/routes/access-grants/unmanaged/unmanaged.ts @@ -192,6 +192,7 @@ export class SeamHttpAccessGrantsUnmanaged { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'access_grants', + hasPagination: true, options, }) } diff --git a/src/lib/routes/access-methods/access-methods.ts b/src/lib/routes/access-methods/access-methods.ts index 7a5fd6db..cdc051d4 100644 --- a/src/lib/routes/access-methods/access-methods.ts +++ b/src/lib/routes/access-methods/access-methods.ts @@ -286,6 +286,7 @@ export class SeamHttpAccessMethods { hasRequiredParameters: true, requiredParameterNames: [], responseKey: 'access_methods', + hasPagination: true, options, }) } diff --git a/src/lib/routes/acs/credentials/credentials.ts b/src/lib/routes/acs/credentials/credentials.ts index 804129e3..46eca4e8 100644 --- a/src/lib/routes/acs/credentials/credentials.ts +++ b/src/lib/routes/acs/credentials/credentials.ts @@ -250,6 +250,7 @@ export class SeamHttpAcsCredentials { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'acs_credentials', + hasPagination: true, options, }) } diff --git a/src/lib/routes/acs/encoders/encoders.ts b/src/lib/routes/acs/encoders/encoders.ts index 2ba60c79..97f33585 100644 --- a/src/lib/routes/acs/encoders/encoders.ts +++ b/src/lib/routes/acs/encoders/encoders.ts @@ -223,6 +223,7 @@ export class SeamHttpAcsEncoders { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'acs_encoders', + hasPagination: true, options, }) } diff --git a/src/lib/routes/acs/entrances/entrances.ts b/src/lib/routes/acs/entrances/entrances.ts index 4eeb87b2..5e288720 100644 --- a/src/lib/routes/acs/entrances/entrances.ts +++ b/src/lib/routes/acs/entrances/entrances.ts @@ -214,6 +214,7 @@ export class SeamHttpAcsEntrances { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'acs_entrances', + hasPagination: true, options, }) } diff --git a/src/lib/routes/acs/users/users.ts b/src/lib/routes/acs/users/users.ts index 69d2913f..368b37b3 100644 --- a/src/lib/routes/acs/users/users.ts +++ b/src/lib/routes/acs/users/users.ts @@ -251,6 +251,7 @@ export class SeamHttpAcsUsers { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'acs_users', + hasPagination: true, options, }) } diff --git a/src/lib/routes/action-attempts/action-attempts.ts b/src/lib/routes/action-attempts/action-attempts.ts index a67895bd..d7042d46 100644 --- a/src/lib/routes/action-attempts/action-attempts.ts +++ b/src/lib/routes/action-attempts/action-attempts.ts @@ -196,6 +196,7 @@ export class SeamHttpActionAttempts { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'action_attempts', + hasPagination: true, options, }) } diff --git a/src/lib/routes/connect-webviews/connect-webviews.ts b/src/lib/routes/connect-webviews/connect-webviews.ts index a334bb63..c05d77a8 100644 --- a/src/lib/routes/connect-webviews/connect-webviews.ts +++ b/src/lib/routes/connect-webviews/connect-webviews.ts @@ -240,6 +240,7 @@ export class SeamHttpConnectWebviews { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'connect_webviews', + hasPagination: true, options, }) } diff --git a/src/lib/routes/connected-accounts/connected-accounts.ts b/src/lib/routes/connected-accounts/connected-accounts.ts index e0303078..3149660f 100644 --- a/src/lib/routes/connected-accounts/connected-accounts.ts +++ b/src/lib/routes/connected-accounts/connected-accounts.ts @@ -225,6 +225,7 @@ export class SeamHttpConnectedAccounts { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'connected_accounts', + hasPagination: true, options, }) } diff --git a/src/lib/routes/devices/devices.ts b/src/lib/routes/devices/devices.ts index 4263b76e..be4a795e 100644 --- a/src/lib/routes/devices/devices.ts +++ b/src/lib/routes/devices/devices.ts @@ -207,6 +207,7 @@ export class SeamHttpDevices { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'devices', + hasPagination: true, options, }) } diff --git a/src/lib/routes/devices/unmanaged/unmanaged.ts b/src/lib/routes/devices/unmanaged/unmanaged.ts index e64cf8bc..99750a2e 100644 --- a/src/lib/routes/devices/unmanaged/unmanaged.ts +++ b/src/lib/routes/devices/unmanaged/unmanaged.ts @@ -199,6 +199,7 @@ export class SeamHttpDevicesUnmanaged { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'devices', + hasPagination: true, options, }) } diff --git a/src/lib/routes/spaces/spaces.ts b/src/lib/routes/spaces/spaces.ts index aaef4625..59d91f02 100644 --- a/src/lib/routes/spaces/spaces.ts +++ b/src/lib/routes/spaces/spaces.ts @@ -308,6 +308,7 @@ export class SeamHttpSpaces { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'spaces', + hasPagination: true, options, }) } diff --git a/src/lib/routes/user-identities/unmanaged/unmanaged.ts b/src/lib/routes/user-identities/unmanaged/unmanaged.ts index 90b88950..133b5ac7 100644 --- a/src/lib/routes/user-identities/unmanaged/unmanaged.ts +++ b/src/lib/routes/user-identities/unmanaged/unmanaged.ts @@ -195,6 +195,7 @@ export class SeamHttpUserIdentitiesUnmanaged { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'user_identities', + hasPagination: true, options, }) } diff --git a/src/lib/routes/user-identities/user-identities.ts b/src/lib/routes/user-identities/user-identities.ts index 47ce2d5d..ec6e8cba 100644 --- a/src/lib/routes/user-identities/user-identities.ts +++ b/src/lib/routes/user-identities/user-identities.ts @@ -306,6 +306,7 @@ export class SeamHttpUserIdentities { hasRequiredParameters: false, requiredParameterNames: [], responseKey: 'user_identities', + hasPagination: true, options, }) } diff --git a/src/lib/seam-http-request.ts b/src/lib/seam-http-request.ts index c094c73b..c391551e 100644 --- a/src/lib/seam-http-request.ts +++ b/src/lib/seam-http-request.ts @@ -21,6 +21,7 @@ interface SeamHttpRequestConfig { readonly body?: unknown readonly params?: undefined | Record readonly responseKey: TResponseKey + readonly hasPagination?: boolean readonly options?: Pick readonly actionAttempts?: ActionAttemptsClient readonly parameters?: unknown @@ -51,6 +52,10 @@ export class SeamHttpRequest< return this.#config.responseKey } + public get hasPagination(): boolean { + return this.#config.hasPagination ?? false + } + public get url(): URL { const { client } = this.#parent @@ -194,7 +199,9 @@ const getUrlPrefix = (input: string): string => { } if (globalThis.location != null) { const pathname = input.startsWith('/') ? input : `/${input}` - return new URL(`${globalThis.location.origin}${pathname}`).toString() + return new URL(`${globalThis.location.origin}${pathname}`) + .toString() + .replace(/\/$/, '') } throw new Error( `Cannot resolve origin from ${input} in a non-browser environment`, diff --git a/src/lib/seam-paginator.ts b/src/lib/seam-paginator.ts index 5d60b5a1..5fed0e9c 100644 --- a/src/lib/seam-paginator.ts +++ b/src/lib/seam-paginator.ts @@ -28,7 +28,7 @@ export class SeamPaginator< parent: SeamPaginatorParent, request: SeamHttpRequest, ) { - if (request.responseKey == null) { + if (!request.hasPagination) { throw new Error( `The ${request.pathname} endpoint does not support pagination`, ) diff --git a/test/seam/connect/seam-http-request.test.ts b/test/seam/connect/seam-http-request.test.ts index 084e71c9..00e69b5d 100644 --- a/test/seam/connect/seam-http-request.test.ts +++ b/test/seam/connect/seam-http-request.test.ts @@ -104,56 +104,73 @@ test('SeamHttpRequest: url is a URL when endpoint is a url with a path', async ( ) }) -test.failing( +test.serial( 'SeamHttpRequest: url is a URL when endpoint is path', async (t) => { const { seed } = await getTestServer(t) + Object.defineProperty(globalThis, 'location', { + configurable: true, + value: { origin: 'https://example.com' }, + }) + t.teardown(() => Reflect.deleteProperty(globalThis, 'location')) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint: '/some/sub/path', }) - const { url } = seam.devices.get({ device_id: 'abc123' }) t.true(url instanceof URL) t.deepEqual( toPlainUrlObject(url), toPlainUrlObject( - new URL('https://example.com/some/sub/path/devices/get'), + new URL( + 'https://example.com/some/sub/path/devices/get?device_id=abc123&_strict=true', + ), ), ) }, ) -test.failing( +test.serial( 'SeamHttpRequest: url is a URL when endpoint is empty', async (t) => { const { seed } = await getTestServer(t) + Object.defineProperty(globalThis, 'location', { + configurable: true, + value: { origin: 'https://example.com' }, + }) + t.teardown(() => Reflect.deleteProperty(globalThis, 'location')) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint: '', }) - - // TODO: Set globalThis.location.origin = 'https://example.com' - const { url } = seam.devices.get({ device_id: 'abc123' }) t.true(url instanceof URL) t.deepEqual( toPlainUrlObject(url), - toPlainUrlObject(new URL('https://example.com/devices/get')), + toPlainUrlObject( + new URL( + 'https://example.com/devices/get?device_id=abc123&_strict=true', + ), + ), ) }, ) -test('SeamHttpRequest: url throws if unable to resolve origin', async (t) => { - const { seed } = await getTestServer(t) - const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { - endpoint: '', - }) +test.serial( + 'SeamHttpRequest: url throws if unable to resolve origin', + async (t) => { + const { seed } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint: '', + }) - const request = seam.devices.get({ device_id: 'abc123' }) + const request = seam.devices.get({ device_id: 'abc123' }) - t.throws(() => request.url, { message: /Cannot resolve origin/ }) -}) + t.throws(() => request.url, { message: /Cannot resolve origin/ }) + }, +) const toPlainUrlObject = (url: URL): Omit => { return { diff --git a/test/seam/connect/seam-paginator.test.ts b/test/seam/connect/seam-paginator.test.ts index e3eba03a..2fca098c 100644 --- a/test/seam/connect/seam-paginator.test.ts +++ b/test/seam/connect/seam-paginator.test.ts @@ -24,18 +24,14 @@ test('SeamPaginator: cannot paginate a request with an empty response', async (t ) }) -// TODO: Validate the request supports pagination by extending SeamHttpRequest with this knowledge via codegen. -test.failing( - 'SeamPaginator: cannot paginate an request that does not return pagination data', - async (t) => { - const { seed, endpoint } = await getTestServer(t) - const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) +test('SeamPaginator: cannot paginate a request that does not return pagination data', async (t) => { + const { seed, endpoint } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint }) - t.throws(() => seam.createPaginator(seam.workspaces.list()), { - message: /does not support pagination/, - }) - }, -) + t.throws(() => seam.createPaginator(seam.workspaces.list()), { + message: /does not support pagination/, + }) +}) test('SeamPaginator: firstPage returns the first page', async (t) => { const { seed, endpoint } = await getTestServer(t)