diff --git a/.nycrc b/.nycrc index 80f4ea0..f4cb085 100644 --- a/.nycrc +++ b/.nycrc @@ -3,7 +3,7 @@ "check-coverage": true, "all": true, "include": ["src/**/*.ts"], - "exclude": ["src/index.ts", "**/*.spec.ts"], + "exclude": ["src/onspringApiSdk.ts", "**/*.spec.ts"], "reporter": ["html", "lcov", "text", "text-summary"], "report-dir": "coverage" } diff --git a/src/models/ApiResponse.ts b/src/models/ApiResponse.ts index cac1611..a098815 100644 --- a/src/models/ApiResponse.ts +++ b/src/models/ApiResponse.ts @@ -1,4 +1,5 @@ import { App } from './App'; +import { CollectionResponse } from './CollectionResponse'; import { GetPagedAppsResponse } from './GetPagedAppsResponse'; /** @@ -44,13 +45,13 @@ export class ApiResponse { * @returns {ApiResponse} - An ApiResponse. */ public AsGetPagedAppsResponseType(): ApiResponse { - var apiResponse = this as ApiResponse; + const apiResponse = this as ApiResponse; - var apps = apiResponse.data.items.map((item: any) => { + const apps = apiResponse.data.items.map((item: any) => { return new App(item.href, item.id, item.name); }); - var getAppsPagedResponse = new GetPagedAppsResponse( + const getAppsPagedResponse = new GetPagedAppsResponse( apps, apiResponse.data.pageNumber, apiResponse.data.pageSize, @@ -65,10 +66,14 @@ export class ApiResponse { ); } + /** + * @method AsAppType - Converts the ApiResponse to an ApiResponse. + * @returns {ApiResponse} - An ApiResponse. + */ AsAppType(): ApiResponse { - var apiResponse = this as ApiResponse; + const apiResponse = this as ApiResponse; - var app = new App( + const app = new App( apiResponse.data.href, apiResponse.data.id, apiResponse.data.name @@ -80,4 +85,27 @@ export class ApiResponse { app ); } + + /** + * @method AsAppCollectionType - Converts the ApiResponse to an ApiResponse>. + * @returns {ApiResponse>} - An ApiResponse>. + */ + AsAppCollectionType(): ApiResponse> { + const apiResponse = this as ApiResponse; + + const apps = apiResponse.data.items.map((item: any) => { + return new App(item.href, item.id, item.name); + }); + + const collectionResponse = new CollectionResponse( + apiResponse.data.count, + apps + ); + + return new ApiResponse>( + apiResponse.statusCode, + apiResponse.message, + collectionResponse + ); + } } diff --git a/src/models/ArgumentValidator.ts b/src/models/ArgumentValidator.ts index a27cf20..b7aceae 100644 --- a/src/models/ArgumentValidator.ts +++ b/src/models/ArgumentValidator.ts @@ -3,6 +3,7 @@ */ export class ArgumentValidator { /** + * @method isNullOrWhiteSpace - Determines if the value is null, undefined, or a string that contains only whitespace characters. * @param {string} value - The value to validate. * @returns {boolean} - True if the value is null, undefined, or a string that contains only whitespace characters; otherwise, false. */ @@ -11,6 +12,7 @@ export class ArgumentValidator { } /** + * @method isValidUrl - Determines if the value is a valid URL. * @param {string} value - The value to validate. * @returns {boolean} - True if the value is a valid URL; otherwise, false. */ @@ -31,6 +33,7 @@ export class ArgumentValidator { } /** + * @method isValidPageSize - Determines if the value is a valid page size. * @param {number} value - The value to validate. * @returns {boolean} - True if the value is a valid page size; otherwise, false. * @remarks - A valid page size is a number greater than 0 and less than or equal to 1000. @@ -44,6 +47,7 @@ export class ArgumentValidator { } /** + * @method isValidPageNumber - Determines if the value is a valid page number. * @param {number} value - The value to validate. * @returns {boolean} - True if the value is a valid page number; otherwise, false. * @remarks - A valid page number is a number greater than 0. diff --git a/src/models/CollectionResponse.ts b/src/models/CollectionResponse.ts index fcaaf9a..e7bde20 100644 --- a/src/models/CollectionResponse.ts +++ b/src/models/CollectionResponse.ts @@ -10,7 +10,7 @@ export class CollectionResponse { /** * @property {T} items - The items in the collection. */ - public items: T; + public items: T[]; /** * @constructor - Creates a new instance of the CollectionResponse class. @@ -18,7 +18,7 @@ export class CollectionResponse { * @param {T} items - The items in the collection. * @returns {CollectionResponse} - A new instance of the CollectionResponse. */ - constructor(count: number, items: T) { + constructor(count: number, items: T[]) { this.count = count; this.items = items; } diff --git a/src/models/EndpointFactory.ts b/src/models/EndpointFactory.ts index 4317b01..d8e6940 100644 --- a/src/models/EndpointFactory.ts +++ b/src/models/EndpointFactory.ts @@ -5,6 +5,7 @@ import { PagingRequest } from './PagingRequest'; */ export class EndpointFactory { /** + * @method getPingEndpoint - Gets the ping endpoint. * @returns {string} - The ping endpoint. */ public static getPingEndpoint(): string { @@ -12,6 +13,7 @@ export class EndpointFactory { } /** + * @method getAppsEndpoint - Gets the apps endpoint. * @param {PagingRequest} pagingRequest - Pagination information to use as query params in the endpoint string. * @returns {string} - The apps endpoint. */ @@ -20,6 +22,7 @@ export class EndpointFactory { } /** + * @method getAppByIdEndpoint - Gets the app by id endpoint. * @param {number} id - The id of the app. * @returns {string} - The app by id endpoint. */ @@ -28,6 +31,7 @@ export class EndpointFactory { } /** + * @method getAppsByIdsEndpoint - Gets the apps by ids endpoint. * @returns {string} - The apps by ids endpoint. */ public static getAppsByIdsEndpoint(): string { @@ -35,6 +39,7 @@ export class EndpointFactory { } /** + * @method getRecordsEndpoint - Gets the records endpoint. * @param {number} id - The id of the field. * @returns {string} - The field by id endpoint. */ @@ -43,6 +48,7 @@ export class EndpointFactory { } /** + * @method getFieldsByIdsEndpoint - Gets the fields by ids endpoint. * @returns {string} - The fields by ids endpoint. */ public static getFieldsByIdsEndpoint(): string { @@ -50,6 +56,7 @@ export class EndpointFactory { } /** + * @method getFieldsByAppIdEndpoint - Gets the fields by app id endpoint. * @param {number} id - The id of the app. * @returns {string} - The fields by app id endpoint. */ @@ -58,6 +65,7 @@ export class EndpointFactory { } /** + * @method getRecordsEndpoint - Gets the records endpoint. * @param {number} recordId - The id of the record. * @param {number} fieldId - The id of the field. * @param {number} fileId - The id of the file. @@ -72,6 +80,7 @@ export class EndpointFactory { } /** + * @method getDeleteFileByIdEndpoint - Gets the delete file by id endpoint. * @param {number} recordId - The id of the record. * @param {number} fieldId - The id of the field. * @param {number} fileId - The id of the file. @@ -86,6 +95,7 @@ export class EndpointFactory { } /** + * @method getFileByIdEndpoint - Gets the file by id endpoint. * @param {number} recordId - The id of the record. * @param {number} fieldId - The id of the field. * @param {number} fileId - The id of the file. @@ -100,6 +110,7 @@ export class EndpointFactory { } /** + * @method getSaveFileEndpoint - Gets the save file endpoint. * @returns {string} - The save file endpoint. */ public static getSaveFileEndpoint(): string { @@ -107,6 +118,7 @@ export class EndpointFactory { } /** + * @method getAddOrUpdateListItemEndpoint - Gets the add or update list item endpoint. * @param {number} listId - The id of the list. * @returns {string} - The add or update list item endpoint. */ @@ -115,6 +127,7 @@ export class EndpointFactory { } /** + * @method getDeleteListItemEndpoint - Gets the delete list item endpoint. * @param {number} listId - The id of the list. * @param {string} itemId - The id of the list item. * @returns {string} - The delete list item endpoint. @@ -127,6 +140,7 @@ export class EndpointFactory { } /** + * @method getRecordsByAppIdEndpoint - Gets the get records by app id endpoint. * @param {number} appId - The id of the app. * @returns {string} - The get records by app id endpoint. */ @@ -135,6 +149,7 @@ export class EndpointFactory { } /** + * @method getRecordByIdEndpoint - Gets the get record by id endpoint. * @param {number} appId - The id of the app. * @param {number} recordId - The id of the record. * @returns {string} - The get record by id endpoint. @@ -144,6 +159,7 @@ export class EndpointFactory { } /** + * @method getDeleteRecordByIdEndpoint - Gets the delete record by id endpoint. * @param {number} appId - The id of the app. * @param {number} recordId - The id of the record. * @returns {string} - The delete record by id endpoint. @@ -156,6 +172,7 @@ export class EndpointFactory { } /** + * @method getRecordsByIdsEndpoint - Gets the get records by ids endpoint. * @returns {string} - The get records by ids endpoint. */ public static getRecordsByIdsEndpoint(): string { @@ -163,6 +180,7 @@ export class EndpointFactory { } /** + * @method getQueryRecordsEndpoint - Gets the query records endpoint. * @returns {string} - The query records endpoint. */ public static getQueryRecordsEndpoint(): string { @@ -170,6 +188,7 @@ export class EndpointFactory { } /** + * @method getAddOrUpdateRecordEndpoint - Gets the add or update record endpoint. * @returns {string} - The add or update record endpoint. */ public static getAddOrUpdateRecordEndpoint(): string { @@ -177,6 +196,7 @@ export class EndpointFactory { } /** + * @method getDeleteRecordsByIdsEndpoint - Gets the delete records by ids endpoint. * @returns {string} - The delete records by ids endpoint. */ public static getDeleteRecordsByIdsEndpoint(): string { @@ -184,6 +204,7 @@ export class EndpointFactory { } /** + * @method getReportByIdEndpoint - Gets the get report by id endpoint. * @param {number} reportId - The id of the report. * @returns {string} - The get report by id endpoint. */ @@ -192,6 +213,7 @@ export class EndpointFactory { } /** + * @method getReportsByAppIdEndpoint - Gets the get reports by app id endpoint. * @param {number} appId - The id of the app. * @returns {string} - The get reports by app id endpoint. */ diff --git a/src/models/GetPagedAppsResponse.ts b/src/models/GetPagedAppsResponse.ts index 375dcf0..a187c87 100644 --- a/src/models/GetPagedAppsResponse.ts +++ b/src/models/GetPagedAppsResponse.ts @@ -1,7 +1,19 @@ import { PagedResponse } from './PagedResponse'; import { App } from './App'; +/** + * @class GetPagedAppsResponse - A paged response model for the GetApps method. + */ export class GetPagedAppsResponse extends PagedResponse { + /** + * @constructor - Creates a new instance of the GetPagedAppsResponse class. + * @param {App[]} items - The items in the collection. + * @param {number} pageNumber - The page number of the response. + * @param {number} pageSize - The page size of the response. + * @param {number} totalPages - The total number of pages in the response. + * @param {number} totalRecords - The total number of records in the response. + * @returns {GetPagedAppsResponse} - A new instance of the GetPagedAppsResponse class. + */ constructor( items: App[], pageNumber: number, diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index 46b41e4..5635410 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -7,14 +7,12 @@ import { ApiResponse } from './ApiResponse'; import { PagingRequest } from './PagingRequest'; import { GetPagedAppsResponse } from './GetPagedAppsResponse'; import { App } from './App'; +import { CollectionResponse } from './CollectionResponse'; /** * @class OnspringClient - A client that can communicate with the Onspring API. */ export class OnspringClient { - getAppsByIds(getAppsByIds: any) { - throw new Error('Method not implemented.'); - } /** * @readonly {AxiosInstance} client - The axios instance that will be used to make requests to the Onspring API. */ @@ -58,9 +56,9 @@ export class OnspringClient { } /** - * + * @method getApps - Gets a paged list of apps. * @param pagingRequest - The paging request that will be used to get the apps. - * @returns + * @returns - A promise that resolves to an ApiResponse of type GetPagedAppsResponse. */ public async getApps( pagingRequest: PagingRequest = new PagingRequest(1, 50) @@ -76,6 +74,11 @@ export class OnspringClient { return apiResponse.AsGetPagedAppsResponseType(); } + /** + * @method getAppById - Gets an app by its id. + * @param appId - The id of the app to get. + * @returns - A promise that resolves to an ApiResponse of type App. + */ public async getAppById(appId: number): Promise> { const endpoint = EndpointFactory.getAppByIdEndpoint(appId); @@ -88,9 +91,28 @@ export class OnspringClient { return apiResponse.AsAppType(); } + /** + * @method getAppsByIds - Gets a list of apps by their ids. + * @param getAppsByIds - The ids of the apps to get. + * @returns - A promise that resolves to an ApiResponse of type CollectionResponse. + */ + public async getAppsByIds( + getAppsByIds: number[] + ): Promise>> { + const endpoint = EndpointFactory.getAppsByIdsEndpoint(); + const apiResponse = await this.post(endpoint, getAppsByIds); + + if (apiResponse.isSuccessful === false) { + return apiResponse; + } + + return apiResponse.AsAppCollectionType(); + } + /** * @method get - Makes a GET request to the specified endpoint. * @param {string} endpoint - The endpoint that will be used to make the request. + * @param {AxiosRequestConfig} config - The configuration that will be used to make the request. * @returns {Promise>} - A promise that resolves to an ApiResponse of type T. */ private async get( @@ -101,4 +123,21 @@ export class OnspringClient { const apiResponse = ApiResponseFactory.getApiResponse(response); return apiResponse; } + + /** + * + * @param endpoint - The endpoint that will be used to make the request. + * @param data - The data that will be sent with the request. + * @param config - The configuration that will be used to make the request. + * @returns - A promise that resolves to an ApiResponse of type T. + */ + private async post( + endpoint: string, + data: any, + config: AxiosRequestConfig = {} + ): Promise> { + const response = await this._client.post(endpoint, data, config); + const apiResponse = ApiResponseFactory.getApiResponse(response); + return apiResponse; + } } diff --git a/src/models/PagedResponse.ts b/src/models/PagedResponse.ts index 7ed259e..63d6573 100644 --- a/src/models/PagedResponse.ts +++ b/src/models/PagedResponse.ts @@ -1,10 +1,41 @@ +/** + * @class PagedResponse - A generic paged response model + */ export class PagedResponse { + /** + * @property {T[]} items - The items in the collection. + */ public items: T[]; + + /** + * @property {number} pageNumber - The page number of the response. + */ public pageNumber: number; + + /** + * @property {number} pageSize - The page size of the response. + */ public pageSize: number; + + /** + * @property {number} totalPages - The total number of pages in the response. + */ public totalPages: number; + + /** + * @property {number} totalRecords - The total number of records in the response. + */ public totalRecords: number; + /** + * @constructor - Creates a new instance of the PagedResponse class. + * @param {T[]} items - The items in the collection. + * @param {number} pageNumber - The page number of the response. + * @param {number} pageSize - The page size of the response. + * @param {number} totalPages - The total number of pages in the response. + * @param {number} totalRecords - The total number of records in the response. + * @returns {PagedResponse} - A new instance of the PagedResponse class. + */ constructor( items: T[], pageNumber: number, diff --git a/tests/ApiResponse.spec.ts b/tests/ApiResponse.spec.ts index 199952b..be92250 100644 --- a/tests/ApiResponse.spec.ts +++ b/tests/ApiResponse.spec.ts @@ -2,6 +2,7 @@ import { ApiResponse } from '../src/models/ApiResponse'; import { expect } from 'chai'; import { GetPagedAppsResponse } from '../src/models/GetPagedAppsResponse'; import { App } from '../src/models/App'; +import { CollectionResponse } from '../src/models/CollectionResponse'; describe('ApiResponse', function () { it('should be defined', function () { @@ -85,7 +86,7 @@ describe('ApiResponse', function () { }); it('should return an ApiResponse when data contains app items', function () { - var mockApiResponse = { + const mockResponseData = { pageNumber: 1, pageSize: 2, totalPages: 1, @@ -104,8 +105,8 @@ describe('ApiResponse', function () { ], }; - var apiResponse = new ApiResponse(200, 'OK', mockApiResponse); - var appsPagedResponse = apiResponse.AsGetPagedAppsResponseType(); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const appsPagedResponse = apiResponse.AsGetPagedAppsResponseType(); expect(appsPagedResponse).to.be.instanceOf(ApiResponse); expect(appsPagedResponse.data).to.be.instanceOf(GetPagedAppsResponse); @@ -123,7 +124,7 @@ describe('ApiResponse', function () { }); it('should return an ApiResponse when data contains app items', function () { - var mockApiResponse = { + const mockResponseData = { pageNumber: 0, pageSize: 0, totalPages: 0, @@ -131,8 +132,8 @@ describe('ApiResponse', function () { items: [], }; - var apiResponse = new ApiResponse(200, 'OK', mockApiResponse); - var appsPagedResponse = apiResponse.AsGetPagedAppsResponseType(); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const appsPagedResponse = apiResponse.AsGetPagedAppsResponseType(); expect(appsPagedResponse).to.be.instanceOf(ApiResponse); expect(appsPagedResponse.data).to.be.instanceOf(GetPagedAppsResponse); @@ -158,14 +159,14 @@ describe('ApiResponse', function () { }); it('should return an ApiResponse when data contain an app', function () { - var mockApiResponse = { + const mockResponseData = { href: 'https://api.onspring.dev/apps/id/1', id: 1, name: 'Test App', }; - var apiResponse = new ApiResponse(200, 'OK', mockApiResponse); - var appResponse = apiResponse.AsAppType(); + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const appResponse = apiResponse.AsAppType(); expect(appResponse).to.be.instanceOf(ApiResponse); expect(appResponse.data).to.be.instanceOf(App); @@ -179,4 +180,52 @@ describe('ApiResponse', function () { } }); }); + + describe('AsAppCollectionType', function () { + it('should be defined', function () { + expect(ApiResponse.prototype.AsAppCollectionType).to.not.be.undefined; + }); + + it('should have no parameters', function () { + expect(ApiResponse.prototype.AsAppCollectionType).to.have.lengthOf(0); + }); + + it('should return an ApiResponse> when data contains app items', function () { + const mockResponseData = { + items: [ + { + href: 'https://api.onspring.dev/apps/id/1', + id: 1, + name: 'Test App 1', + }, + { + href: 'https://api.onspring.dev/apps/id/2', + id: 2, + name: 'Test App 2', + }, + ], + }; + + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const appCollectionResponse = apiResponse.AsAppCollectionType(); + + expect(appCollectionResponse).to.be.instanceOf( + ApiResponse> + ); + expect(appCollectionResponse.data).to.be.instanceOf( + CollectionResponse + ); + expect(appCollectionResponse.data).to.not.be.null; + if (appCollectionResponse.data != null) { + expect(appCollectionResponse.data.items).to.be.instanceOf(Array); + expect(appCollectionResponse.data.items).to.have.lengthOf(2); + appCollectionResponse.data.items.forEach((item) => { + expect(item).to.be.instanceOf(App); + expect(item).to.have.property('id'); + expect(item).to.have.property('name'); + expect(item).to.have.property('href'); + }); + } + }); + }); }); diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index b489081..183d2b8 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -5,6 +5,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { GetPagedAppsResponse } from '../src/models/GetPagedAppsResponse'; import { App } from '../src/models/App'; +import { CollectionResponse } from '../src/models/CollectionResponse'; describe('OnspringClient', function () { const baseUrl = 'https://api.onspring.dev'; @@ -454,7 +455,7 @@ describe('OnspringClient', function () { sinon.stub(mockAxiosClient, 'get').returns( Promise.resolve({ status: 403, - statusText: 'Unauthorized', + statusText: 'Forbidden', data: { message: 'Client does not have access to read app: 1', }, @@ -532,5 +533,130 @@ describe('OnspringClient', function () { }); // TODO: complete wriiting tests for getAppsByIds method + it('should return a promise that resolves to an api response of a collection of apps 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, 'post').returns( + Promise.resolve({ + status: 200, + statusText: 'OK', + data: { + count: 2, + items: [ + { + href: 'https://api.onspring.dev/Apps/id/1', + id: 1, + name: 'Test App 1', + }, + { + href: 'https://api.onspring.dev/Apps/id/2', + id: 2, + name: 'Test App 2', + }, + ], + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getAppsByIds([1, 2]); + 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'); + if (result.data != null) { + expect(result.data).to.be.instanceOf(CollectionResponse); + expect(result.data).to.have.property('count', 2); + expect(result.data).to.have.property('items'); + if (result.data.items != null) { + expect(result.data.items).to.be.an('array'); + expect(result.data.items).to.have.lengthOf(2); + result.data.items.forEach((item) => { + expect(item).to.be.instanceOf(App); + expect(item).to.have.property('id'); + expect(item).to.have.property('name'); + expect(item).to.have.property('href'); + }); + } + } + }); + + 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, 'post').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.getAppsByIds([1, 2]); + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 401); + expect(result).to.have.property('isSuccessful', false); + expect(result.message).to.be.undefined; + expect(result.data).to.be.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, 'post').returns( + Promise.resolve({ + status: 403, + statusText: 'Forbidden', + data: { + message: 'Client does not have access to read app: 1, 2', + }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.getAppsByIds([1, 2]); + 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', + 'Client does not have access to read app: 1, 2' + ); + expect(result.data).to.be.null; + }); }); });