From 27a4295000d3b86b5a20173c57ea1f9756721d25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:40:45 +0000 Subject: [PATCH 1/2] Initial plan From a5aceeb8683b22fd6f99c616976da638914c1874 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:00:20 +0000 Subject: [PATCH 2/2] feat: add async iterable methods for paged responses --- src/models/OnspringClient.ts | 208 ++++++++- tests/OnspringClient.spec.ts | 795 +++++++++++++++++++++++++++++++++++ 2 files changed, 1001 insertions(+), 2 deletions(-) diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index a957bba..9df47a5 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -20,14 +20,16 @@ import { type GetPagedFieldsResponse } from './GetPagedFieldsResponse.js'; import { type GetPagedRecordsResponse } from './GetPagedRecordsResponse.js'; import { type GetPagedReportsResponse } from './GetPagedReportsResponse.js'; import { type GetRecordRequest } from './GetRecordRequest.js'; -import { type GetRecordsByAppIdRequest } from './GetRecordsByAppIdRequest.js'; +import { GetRecordsByAppIdRequest } from './GetRecordsByAppIdRequest.js'; import { type GetRecordsRequest } from './GetRecordsRequest.js'; import { type ListItemRequest } from './ListItemRequest.js'; import { type ListItemResponse } from './ListItemResponse.js'; import { PagingRequest } from './PagingRequest.js'; -import { type QueryRecordsRequest } from './QueryRecordsRequest.js'; +import { type QueryFilter } from './QueryFilter.js'; +import { QueryRecordsRequest } from './QueryRecordsRequest.js'; import { Record } from './Record.js'; import { type ReportData } from './ReportData.js'; +import { type Report } from './Report.js'; import { type SaveFileRequest } from './SaveFileRequest.js'; import { type SaveRecordRequest } from './SaveRecordRequest.js'; import { type SaveRecordResponse } from './SaveRecordResponse.js'; @@ -111,6 +113,39 @@ export class OnspringClient { return apiResponse.asGetPagedAppsResponseType(); } + /** + * @method getAppsIterable - Gets an async iterable of apps that automatically pages through all results. + * @param {number} pageSize - The page size to use for each request. Default is 50. + * @returns {AsyncIterable} - An async iterable of apps. + * @example + * ```typescript + * for await (const app of client.getAppsIterable()) { + * console.log(app); + * } + * ``` + */ + public async *getAppsIterable(pageSize = 50): AsyncIterable { + let pageNumber = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await this.getApps( + new PagingRequest(pageNumber, pageSize) + ); + + if (response.isSuccessful === false || response.data === null) { + break; + } + + for (const app of response.data.items) { + yield app; + } + + hasMorePages = pageNumber < response.data.totalPages; + pageNumber++; + } + } + /** * @method getAppById - Gets an app by its id. * @param {number} appId - The id of the app to get. @@ -203,6 +238,44 @@ export class OnspringClient { return apiResponse.asGetPagedFieldsResponseType(); } + /** + * @method getFieldsByAppIdIterable - Gets an async iterable of fields that automatically pages through all results. + * @param {number} appId - The id of the app to get the fields for. + * @param {number} pageSize - The page size to use for each request. Default is 50. + * @returns {AsyncIterable} - An async iterable of fields. + * @example + * ```typescript + * for await (const field of client.getFieldsByAppIdIterable(132)) { + * console.log(field); + * } + * ``` + */ + public async *getFieldsByAppIdIterable( + appId: number, + pageSize = 50 + ): AsyncIterable { + let pageNumber = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await this.getFieldsByAppId( + appId, + new PagingRequest(pageNumber, pageSize) + ); + + if (response.isSuccessful === false || response.data === null) { + break; + } + + for (const field of response.data.items) { + yield field; + } + + hasMorePages = pageNumber < response.data.totalPages; + pageNumber++; + } + } + /** * @method getFileInfoById - Gets a file's information by its id. * @param {number} recordId - The id of the record that the file is attached to. @@ -364,6 +437,51 @@ export class OnspringClient { return apiResponse.asGetPagedRecordsResponseType(); } + /** + * @method getRecordsByAppIdIterable - Gets an async iterable of records that automatically pages through all results. + * @param {number} appId - The id of the app to get the records for. + * @param {number[]} fieldIds - The ids of the fields to include in the response. Default is empty array (all fields). + * @param {DataFormat} dataFormat - The format of the data in the response. Default is Raw. + * @param {number} pageSize - The page size to use for each request. Default is 50. + * @returns {AsyncIterable} - An async iterable of records. + * @example + * ```typescript + * for await (const record of client.getRecordsByAppIdIterable(130)) { + * console.log(record); + * } + * ``` + */ + public async *getRecordsByAppIdIterable( + appId: number, + fieldIds: number[] = [], + dataFormat: DataFormat = DataFormat.Raw, + pageSize = 50 + ): AsyncIterable { + let pageNumber = 1; + let hasMorePages = true; + + while (hasMorePages) { + const request = new GetRecordsByAppIdRequest( + appId, + fieldIds, + dataFormat, + new PagingRequest(pageNumber, pageSize) + ); + const response = await this.getRecordsByAppId(request); + + if (response.isSuccessful === false || response.data === null) { + break; + } + + for (const record of response.data.items) { + yield record; + } + + hasMorePages = pageNumber < response.data.totalPages; + pageNumber++; + } + } + /** * @method getRecordById - Gets a record by its id. * @param {GetRecordRequest} request - The request that will be used to get the record. @@ -423,6 +541,54 @@ export class OnspringClient { return apiResponse.asGetPagedRecordsResponseType(); } + /** + * @method queryRecordsIterable - Gets an async iterable of records from a query that automatically pages through all results. + * @param {number} appId - The id of the app to query records from. + * @param {string | QueryFilter} filter - The filter to use to query for records. + * @param {number[]} fieldIds - The ids of the fields to include in the response. Default is empty array (all fields). + * @param {DataFormat} dataFormat - The format of the data in the response. Default is Raw. + * @param {number} pageSize - The page size to use for each request. Default is 50. + * @returns {AsyncIterable} - An async iterable of records. + * @example + * ```typescript + * for await (const record of client.queryRecordsIterable(130, "status eq 'Active'")) { + * console.log(record); + * } + * ``` + */ + public async *queryRecordsIterable( + appId: number, + filter: string | QueryFilter, + fieldIds: number[] = [], + dataFormat: DataFormat = DataFormat.Raw, + pageSize = 50 + ): AsyncIterable { + let pageNumber = 1; + let hasMorePages = true; + + while (hasMorePages) { + const request = new QueryRecordsRequest( + appId, + filter, + fieldIds, + dataFormat, + new PagingRequest(pageNumber, pageSize) + ); + const response = await this.queryRecords(request); + + if (response.isSuccessful === false || response.data === null) { + break; + } + + for (const record of response.data.items) { + yield record; + } + + hasMorePages = pageNumber < response.data.totalPages; + pageNumber++; + } + } + /** * @method saveRecord - Saves a record. * @param {Record | SaveRecordRequest} request - The record or request that will be used to save the record. @@ -503,6 +669,44 @@ export class OnspringClient { return apiResponse.asGetPagedReportsResponseType(); } + /** + * @method getReportsByAppIdIterable - Gets an async iterable of reports that automatically pages through all results. + * @param {number} appId - The id of the app to get the reports for. + * @param {number} pageSize - The page size to use for each request. Default is 50. + * @returns {AsyncIterable} - An async iterable of reports. + * @example + * ```typescript + * for await (const report of client.getReportsByAppIdIterable(130)) { + * console.log(report); + * } + * ``` + */ + public async *getReportsByAppIdIterable( + appId: number, + pageSize = 50 + ): AsyncIterable { + let pageNumber = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await this.getReportsByAppId( + appId, + new PagingRequest(pageNumber, pageSize) + ); + + if (response.isSuccessful === false || response.data === null) { + break; + } + + for (const report of response.data.items) { + yield report; + } + + hasMorePages = pageNumber < response.data.totalPages; + pageNumber++; + } + } + /** * @method getReportById - Gets a report by its id. * @param {number} reportId - The id of the report to get. diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index 9411448..7c4ac15 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -7,6 +7,7 @@ import fs from 'fs'; import path from 'path'; import * as sinon from 'sinon'; import { Readable } from 'stream'; +import { DataFormat } from '../src/enums/DataFormat'; import { FieldStatus } from '../src/enums/FieldStatus'; import { FieldType } from '../src/enums/FieldType'; import { ApiResponse } from '../src/models/ApiResponse'; @@ -4200,4 +4201,798 @@ describe('OnspringClient', function () { expect(result).to.have.property('data', null); }); }); + + describe('getAppsIterable', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getAppsIterable).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getAppsIterable).to.be.a('function'); + }); + + it('should yield all apps across multiple pages', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + let callCount = 0; + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + callCount++; + if (callCount === 1) { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { + href: 'https://api.onspring.dev/Apps/id/1', + id: '1', + name: 'App 1', + }, + { + href: 'https://api.onspring.dev/Apps/id/2', + id: '2', + name: 'App 2', + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } else { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 2, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { + href: 'https://api.onspring.dev/Apps/id/3', + id: '3', + name: 'App 3', + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const apps: App[] = []; + for await (const app of client.getAppsIterable(2)) { + apps.push(app); + } + + expect(apps).to.have.lengthOf(3); + expect(apps[0]).to.be.instanceOf(App); + expect(apps[0]).to.have.property('id', '1'); + expect(apps[1]).to.have.property('id', '2'); + expect(apps[2]).to.have.property('id', '3'); + }); + + it('should stop iteration when request fails', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 500, + statusText: 'Internal Server Error', + data: null, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const apps: App[] = []; + for await (const app of client.getAppsIterable(2)) { + apps.push(app); + } + + expect(apps).to.have.lengthOf(0); + }); + + it('should use default page size when not specified', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 50, + totalPages: 1, + totalRecords: 1, + items: [ + { + href: 'https://api.onspring.dev/Apps/id/1', + id: '1', + name: 'App 1', + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const apps: App[] = []; + for await (const app of client.getAppsIterable()) { + apps.push(app); + } + + expect(apps).to.have.lengthOf(1); + }); + }); + + describe('getFieldsByAppIdIterable', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getFieldsByAppIdIterable).to.not.be + .undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getFieldsByAppIdIterable).to.be.a( + 'function' + ); + }); + + it('should yield all fields across multiple pages', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + let callCount = 0; + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + callCount++; + if (callCount === 1) { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { + id: 1, + appId: 100, + name: 'Field 1', + type: FieldType.Text, + status: FieldStatus.Enabled, + isRequired: false, + isUnique: false, + }, + { + id: 2, + appId: 100, + name: 'Field 2', + type: FieldType.Text, + status: FieldStatus.Enabled, + isRequired: false, + isUnique: false, + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } else { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 2, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { + id: 3, + appId: 100, + name: 'Field 3', + type: FieldType.Text, + status: FieldStatus.Enabled, + isRequired: false, + isUnique: false, + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const fields: Field[] = []; + for await (const field of client.getFieldsByAppIdIterable(100, 2)) { + fields.push(field); + } + + expect(fields).to.have.lengthOf(3); + expect(fields[0]).to.be.instanceOf(Field); + expect(fields[0]).to.have.property('id', 1); + expect(fields[1]).to.have.property('id', 2); + expect(fields[2]).to.have.property('id', 3); + }); + + it('should stop iteration when request fails', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 500, + statusText: 'Internal Server Error', + data: null, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const fields: Field[] = []; + for await (const field of client.getFieldsByAppIdIterable(100, 2)) { + fields.push(field); + } + + expect(fields).to.have.lengthOf(0); + }); + + it('should use default page size when not specified', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 50, + totalPages: 1, + totalRecords: 1, + items: [ + { + id: 1, + appId: 100, + name: 'Field 1', + type: FieldType.Text, + status: FieldStatus.Enabled, + isRequired: false, + isUnique: false, + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const fields: Field[] = []; + for await (const field of client.getFieldsByAppIdIterable(100)) { + fields.push(field); + } + + expect(fields).to.have.lengthOf(1); + }); + }); + + describe('getRecordsByAppIdIterable', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getRecordsByAppIdIterable).to.not.be + .undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getRecordsByAppIdIterable).to.be.a( + 'function' + ); + }); + + it('should yield all records across multiple pages', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + let callCount = 0; + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + callCount++; + if (callCount === 1) { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { appId: 100, recordId: 1, fieldData: [] }, + { appId: 100, recordId: 2, fieldData: [] }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } else { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 2, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [{ appId: 100, recordId: 3, fieldData: [] }], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.getRecordsByAppIdIterable( + 100, + [], + DataFormat.Raw, + 2 + )) { + records.push(record); + } + + expect(records).to.have.lengthOf(3); + expect(records[0]).to.be.instanceOf(Record); + expect(records[0]).to.have.property('recordId', 1); + expect(records[1]).to.have.property('recordId', 2); + expect(records[2]).to.have.property('recordId', 3); + }); + + it('should stop iteration when request fails', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 500, + statusText: 'Internal Server Error', + data: null, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.getRecordsByAppIdIterable( + 100, + [], + DataFormat.Raw, + 2 + )) { + records.push(record); + } + + expect(records).to.have.lengthOf(0); + }); + + it('should use default parameters when not specified', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 50, + totalPages: 1, + totalRecords: 1, + items: [{ appId: 100, recordId: 1, fieldData: [] }], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.getRecordsByAppIdIterable(100)) { + records.push(record); + } + + expect(records).to.have.lengthOf(1); + }); + }); + + describe('queryRecordsIterable', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.queryRecordsIterable).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.queryRecordsIterable).to.be.a('function'); + }); + + it('should yield all records across multiple pages', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + let callCount = 0; + sinon.stub(mockAxiosClient, 'post').callsFake(async () => { + callCount++; + if (callCount === 1) { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { appId: 100, recordId: 1, fieldData: [] }, + { appId: 100, recordId: 2, fieldData: [] }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } else { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 2, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [{ appId: 100, recordId: 3, fieldData: [] }], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.queryRecordsIterable( + 100, + "status eq 'Active'", + [], + DataFormat.Raw, + 2 + )) { + records.push(record); + } + + expect(records).to.have.lengthOf(3); + expect(records[0]).to.be.instanceOf(Record); + expect(records[0]).to.have.property('recordId', 1); + expect(records[1]).to.have.property('recordId', 2); + expect(records[2]).to.have.property('recordId', 3); + }); + + it('should stop iteration when request fails', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'post').callsFake(async () => { + return await Promise.resolve({ + status: 500, + statusText: 'Internal Server Error', + data: null, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.queryRecordsIterable( + 100, + "status eq 'Active'", + [], + DataFormat.Raw, + 2 + )) { + records.push(record); + } + + expect(records).to.have.lengthOf(0); + }); + + it('should use default parameters when not specified', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'post').callsFake(async () => { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 50, + totalPages: 1, + totalRecords: 1, + items: [{ appId: 100, recordId: 1, fieldData: [] }], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const records: Record[] = []; + for await (const record of client.queryRecordsIterable( + 100, + "status eq 'Active'" + )) { + records.push(record); + } + + expect(records).to.have.lengthOf(1); + }); + }); + + describe('getReportsByAppIdIterable', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getReportsByAppIdIterable).to.not.be + .undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getReportsByAppIdIterable).to.be.a( + 'function' + ); + }); + + it('should yield all reports across multiple pages', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + let callCount = 0; + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + callCount++; + if (callCount === 1) { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { appId: 100, id: 1, name: 'Report 1', description: 'Test' }, + { appId: 100, id: 2, name: 'Report 2', description: 'Test' }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } else { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 2, + pageSize: 2, + totalPages: 2, + totalRecords: 3, + items: [ + { appId: 100, id: 3, name: 'Report 3', description: 'Test' }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + } + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const reports: Report[] = []; + for await (const report of client.getReportsByAppIdIterable(100, 2)) { + reports.push(report); + } + + expect(reports).to.have.lengthOf(3); + expect(reports[0]).to.be.instanceOf(Report); + expect(reports[0]).to.have.property('id', 1); + expect(reports[1]).to.have.property('id', 2); + expect(reports[2]).to.have.property('id', 3); + }); + + it('should stop iteration when request fails', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 500, + statusText: 'Internal Server Error', + data: null, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const reports: Report[] = []; + for await (const report of client.getReportsByAppIdIterable(100, 2)) { + reports.push(report); + } + + expect(reports).to.have.lengthOf(0); + }); + + it('should use default page size when not specified', async function () { + const client = new OnspringClient(baseUrl, apiKey); + + const mockAxiosClient = axios.create({ + baseURL: baseUrl, + headers: { + 'x-apikey': apiKey, + 'x-api-version': '2', + }, + }); + + sinon.stub(mockAxiosClient, 'get').callsFake(async () => { + return await Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + pageNumber: 1, + pageSize: 50, + totalPages: 1, + totalRecords: 1, + items: [ + { appId: 100, id: 1, name: 'Report 1', description: 'Test' }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse); + }); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const reports: Report[] = []; + for await (const report of client.getReportsByAppIdIterable(100)) { + reports.push(report); + } + + expect(reports).to.have.lengthOf(1); + }); + }); });