diff --git a/src/models/ApiResponse.ts b/src/models/ApiResponse.ts index 831b3a8..0aa6ded 100644 --- a/src/models/ApiResponse.ts +++ b/src/models/ApiResponse.ts @@ -15,6 +15,8 @@ import { ListItemResponse } from './ListItemResponse'; import { ListValue } from './ListValue'; import { ReferenceField } from './ReferenceField'; import { Report } from './Report'; +import { ReportData } from './ReportData'; +import { Row } from './Row'; /** * @class ApiResponse - A generic response object for API requests. @@ -303,6 +305,22 @@ export class ApiResponse { ); } + asReportDataType(): ApiResponse { + const apiResponse = this as ApiResponse; + + const rows = apiResponse.data.rows.map((row) => { + return new Row(row.recordId, row.cells); + }); + + const reportData = new ReportData(apiResponse.data.columns, rows); + + return new ApiResponse( + apiResponse.statusCode, + apiResponse.message, + reportData + ); + } + /** * @method asFileCollectionType - Converts the field item to the appropriate field object based upon the field item's type. * @param {any} fieldItem - The field item to convert. diff --git a/src/models/EndpointFactory.ts b/src/models/EndpointFactory.ts index 71d08d7..4947efc 100644 --- a/src/models/EndpointFactory.ts +++ b/src/models/EndpointFactory.ts @@ -1,3 +1,5 @@ +import { type DataFormat } from '../enums/DataFormat'; +import { type ReportDataType } from '../enums/ReportDataType'; import { type PagingRequest } from './PagingRequest'; /** @@ -210,10 +212,16 @@ export class EndpointFactory { /** * @method getReportByIdEndpoint - Gets the get report by id endpoint. * @param {number} reportId - The id of the report. + * @param {DataFormat} apiDataFormat - The data format that will be used to make the request. + * @param {ReportDataType} reportDataType - The report data type that will be used to make the request. * @returns {string} - The get report by id endpoint. */ - public static getReportByIdEndpoint(reportId: number): string { - return `/Reports/id/${reportId}`; + public static getReportByIdEndpoint( + reportId: number, + apiDataFormat: DataFormat, + reportDataType: ReportDataType + ): string { + return `/Reports/id/${reportId}?apiDataFormat=${apiDataFormat}&dataType=${reportDataType}`; } /** diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index c046c66..72fe110 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -3,6 +3,8 @@ import { PagingRequest } from './PagingRequest'; import { ArgumentValidator } from './ArgumentValidator'; import { EndpointFactory } from './EndpointFactory'; import { ApiResponseFactory } from './ApiResponseFactory'; +import { DataFormat } from '../enums/DataFormat'; +import { ReportDataType } from '../enums/ReportDataType'; import { type AxiosInstance, type AxiosRequestConfig } from 'axios'; import { type ApiResponse } from './ApiResponse'; import { type GetPagedAppsResponse } from './GetPagedAppsResponse'; @@ -17,6 +19,7 @@ import { type File } from './File'; import { type ListItemResponse } from './ListItemResponse'; import { type ListItemRequest } from './ListItemRequest'; import { type GetPagedReportsResponse } from './GetPagedReportsResponse'; +import { type ReportData } from './ReportData'; /** * @class OnspringClient - A client that can communicate with the Onspring API. @@ -331,6 +334,26 @@ export class OnspringClient { return apiResponse.asGetPagedReportsResponseType(); } + public async getReportById( + reportId: number, + apiDataFormat: DataFormat = DataFormat.Raw, + reportDataType: ReportDataType = ReportDataType.ReportData + ): Promise> { + const endpoint = EndpointFactory.getReportByIdEndpoint( + reportId, + apiDataFormat, + reportDataType + ); + + const apiResponse = await this.get(endpoint); + + if (apiResponse.isSuccessful === false) { + return apiResponse; + } + + return apiResponse.asReportDataType(); + } + /** * @method get - Makes a GET request to the specified endpoint. * @param {string} endpoint - The endpoint that will be used to make the request. diff --git a/src/models/ReportData.ts b/src/models/ReportData.ts new file mode 100644 index 0000000..1cee83f --- /dev/null +++ b/src/models/ReportData.ts @@ -0,0 +1,11 @@ +import { type Row } from './Row'; + +export class ReportData { + public columns: string[]; + public rows: Row[]; + + constructor(columns: string[], rows: Row[]) { + this.columns = columns; + this.rows = rows; + } +} diff --git a/src/models/Row.ts b/src/models/Row.ts new file mode 100644 index 0000000..80c9c08 --- /dev/null +++ b/src/models/Row.ts @@ -0,0 +1,9 @@ +export class Row { + public recordId: number; + public cells: object[]; + + constructor(recordId: number, cells: object[]) { + this.recordId = recordId; + this.cells = cells; + } +} diff --git a/tests/ApiResponse.spec.ts b/tests/ApiResponse.spec.ts index c4d378b..230e68b 100644 --- a/tests/ApiResponse.spec.ts +++ b/tests/ApiResponse.spec.ts @@ -21,6 +21,8 @@ import path from 'path'; import { ListItemResponse } from '../src/models/ListItemResponse'; import { GetPagedReportsResponse } from '../src/models/GetPagedReportsResponse'; import { Report } from '../src/models/Report'; +import { ReportData } from '../src/models/ReportData'; +import { Row } from '../src/models/Row'; describe('ApiResponse', function () { it('should be defined', function () { @@ -1014,4 +1016,54 @@ describe('ApiResponse', function () { } }); }); + + describe('asReportDataType', function () { + it('should be defined', function () { + expect(ApiResponse.prototype.asReportDataType).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(ApiResponse.prototype.asReportDataType).to.be.a('function'); + }); + + it('should have no parameters', function () { + expect(ApiResponse.prototype.asReportDataType).to.have.lengthOf(0); + }); + + it('should return an ApiResponse', function () { + const mockResponseData = { + columns: ['field 1', 'field 2'], + rows: [ + { + recordId: 1, + cells: ['field value 1', 'field value 2'], + }, + { + recordId: 2, + cells: ['field value 1', 'field value 2'], + }, + ], + }; + + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const reportData = apiResponse.asReportDataType(); + + expect(reportData).to.be.an.instanceof(ApiResponse); + expect(reportData.data).to.be.an.instanceof(ReportData); + expect(reportData.data).to.have.property('columns').that.is.an('array'); + expect(reportData.data).to.have.property('rows').that.is.an('array'); + expect(reportData.data).to.not.be.null; + + if (reportData.data != null) { + expect(reportData.data.columns).to.have.lengthOf(2); + reportData.data.columns.forEach((column) => { + expect(column).to.be.a('string'); + }); + expect(reportData.data.rows).to.have.lengthOf(2); + reportData.data.rows.forEach((row) => { + expect(row).to.be.an.instanceOf(Row); + }); + } + }); + }); }); diff --git a/tests/EndpointFactory.spec.ts b/tests/EndpointFactory.spec.ts index 4a81f66..fdd6c7e 100644 --- a/tests/EndpointFactory.spec.ts +++ b/tests/EndpointFactory.spec.ts @@ -1,6 +1,8 @@ import { EndpointFactory } from '../src/models/EndpointFactory'; import { expect } from 'chai'; import { PagingRequest } from '../src/models/PagingRequest'; +import { DataFormat } from '../src/enums/DataFormat'; +import { ReportDataType } from '../src/enums/ReportDataType'; describe('EndpointFactory', function () { describe('getPingEndpoint', function () { @@ -155,8 +157,14 @@ describe('EndpointFactory', function () { describe('getReportByIdEndpoint', function () { it('should return the correct report by id endpoint', function () { - const result = EndpointFactory.getReportByIdEndpoint(1); - expect(result).to.equal('/Reports/id/1'); + const result = EndpointFactory.getReportByIdEndpoint( + 1, + DataFormat.Raw, + ReportDataType.ReportData + ); + expect(result).to.equal( + '/Reports/id/1?apiDataFormat=Raw&dataType=ReportData' + ); }); }); diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index 133b606..7af9c31 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -24,6 +24,7 @@ import { ListItemRequest } from '../src/models/ListItemRequest'; import { ListItemResponse } from '../src/models/ListItemResponse'; import { Report } from '../src/models/Report'; import { GetPagedReportsResponse } from '../src/models/GetPagedReportsResponse'; +import { ReportData } from '../src/models/ReportData'; describe('OnspringClient', function () { const baseUrl = 'https://api.onspring.dev'; @@ -2631,4 +2632,194 @@ describe('OnspringClient', function () { expect(result).to.have.property('data', null); }); }); + + describe('getReportById', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.getReportById).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.getReportById).to.be.a('function'); + }); + + it('should return a promise', function () { + expect( + new OnspringClient(baseUrl, apiKey).getReportById(1) + ).to.be.instanceOf(Promise); + }); + + it('should return a promise that resolves to an api response when request is successful', 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').returns( + Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + columns: ['field 1', 'field 2'], + rows: [ + { + recordId: 1, + cells: ['field value 1', 'field value 2'], + }, + { + recordId: 2, + cells: ['field value 1', 'field value 2'], + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getReportById(1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 200); + expect(result).to.have.property('isSuccessful', true); + expect(result).to.have.property('message', ''); + expect(result).to.have.property('data'); + expect(result.data).to.be.instanceOf(ReportData); + expect(result.data).to.have.property('columns').that.is.an('array'); + expect(result.data).to.have.property('rows').that.is.an('array'); + }); + + it('should return a promise that resolves to an api response when request receives a 400 response', 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').returns( + Promise.resolve({ + status: 400, + statusText: 'Bad Request', + data: { message: 'Bad Request' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getReportById(1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 400); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', '{"message":"Bad Request"}'); + expect(result).to.have.property('data', null); + }); + + it('should return a promise that resolves to an api response when request receives a 401 response', 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').returns( + Promise.resolve({ + status: 401, + statusText: 'Unauthorized', + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getReportById(1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 401); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', undefined); + expect(result).to.have.property('data', null); + }); + + it('should return a promise that resolves to an api response when request receives a 403 response', 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').returns( + Promise.resolve({ + status: 403, + statusText: 'Forbidden', + data: { message: 'Forbidden' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getReportById(1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 403); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', 'Forbidden'); + expect(result).to.have.property('data', null); + }); + + it('should return a promise that resolves to an api response when request receives a 404 response', 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').returns( + Promise.resolve({ + status: 404, + statusText: 'Not Found', + data: { message: 'Not Found' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getReportById(1); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 404); + expect(result).to.have.property('isSuccessful', false); + expect(result).to.have.property('message', 'Not Found'); + expect(result).to.have.property('data', null); + }); + }); }); diff --git a/tests/ReportData.spec.ts b/tests/ReportData.spec.ts new file mode 100644 index 0000000..97e157c --- /dev/null +++ b/tests/ReportData.spec.ts @@ -0,0 +1,50 @@ +import { ReportData } from '../src/models/ReportData'; +import { expect } from 'chai'; + +describe('ReportData', function () { + it('should be defined', function () { + expect(ReportData).to.not.be.undefined; + }); + + it('should have a constructor with 2 parameters', function () { + expect(ReportData).to.have.property('constructor'); + expect(ReportData).to.have.lengthOf(2); + }); + + it('should have a construct that sets its properties correctly', function () { + const reportData = new ReportData( + ['a', 'b'], + [ + { + recordId: 1, + cells: [{}, {}], + }, + { + recordId: 2, + cells: [{}, {}], + }, + ] + ); + + expect(reportData) + .to.have.property('columns') + .that.is.an('array') + .with.lengthOf(2); + expect(reportData) + .to.have.property('rows') + .that.is.an('array') + .with.lengthOf(2); + + reportData.columns.forEach((column) => { + expect(column).to.be.a('string'); + }); + + reportData.rows.forEach((row) => { + expect(row).to.have.property('recordId').that.is.a('number'); + expect(row) + .to.have.property('cells') + .that.is.an('array') + .with.lengthOf(2); + }); + }); +}); diff --git a/tests/Row.spec.ts b/tests/Row.spec.ts new file mode 100644 index 0000000..58c7bc7 --- /dev/null +++ b/tests/Row.spec.ts @@ -0,0 +1,19 @@ +import { Row } from '../src/models/Row'; +import { expect } from 'chai'; + +describe('Row', function () { + it('should be defined', function () { + expect(Row).to.not.be.undefined; + }); + + it('should have a constructor with 2 parameters', function () { + expect(Row).to.have.property('constructor'); + expect(Row).to.have.lengthOf(2); + }); + + it('should have a construct that sets its properties correctly', function () { + const row = new Row(1, [{}, {}]); + expect(row).to.have.property('recordId', 1); + expect(row).to.have.property('cells').that.is.an('array').with.lengthOf(2); + }); +});