feat: implement getAppsByIds method

This commit is contained in:
StevanFreeborn
2023-02-02 20:24:20 -06:00
parent d00683ea3c
commit a38a6240bd
10 changed files with 334 additions and 23 deletions
+1 -1
View File
@@ -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"
}
+33 -5
View File
@@ -1,4 +1,5 @@
import { App } from './App';
import { CollectionResponse } from './CollectionResponse';
import { GetPagedAppsResponse } from './GetPagedAppsResponse';
/**
@@ -44,13 +45,13 @@ export class ApiResponse<T> {
* @returns {ApiResponse<GetPagedAppsResponse>} - An ApiResponse<GetPagedAppsResponse>.
*/
public AsGetPagedAppsResponseType(): ApiResponse<GetPagedAppsResponse> {
var apiResponse = this as ApiResponse<any>;
const apiResponse = this as ApiResponse<any>;
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<T> {
);
}
/**
* @method AsAppType - Converts the ApiResponse to an ApiResponse<App>.
* @returns {ApiResponse<App>} - An ApiResponse<App>.
*/
AsAppType(): ApiResponse<App> {
var apiResponse = this as ApiResponse<any>;
const apiResponse = this as ApiResponse<any>;
var app = new App(
const app = new App(
apiResponse.data.href,
apiResponse.data.id,
apiResponse.data.name
@@ -80,4 +85,27 @@ export class ApiResponse<T> {
app
);
}
/**
* @method AsAppCollectionType - Converts the ApiResponse to an ApiResponse<CollectionResponse<App>>.
* @returns {ApiResponse<CollectionResponse<App>>} - An ApiResponse<CollectionResponse<App>>.
*/
AsAppCollectionType(): ApiResponse<CollectionResponse<App>> {
const apiResponse = this as ApiResponse<any>;
const apps = apiResponse.data.items.map((item: any) => {
return new App(item.href, item.id, item.name);
});
const collectionResponse = new CollectionResponse<App>(
apiResponse.data.count,
apps
);
return new ApiResponse<CollectionResponse<App>>(
apiResponse.statusCode,
apiResponse.message,
collectionResponse
);
}
}
+4
View File
@@ -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.
+2 -2
View File
@@ -10,7 +10,7 @@ export class CollectionResponse<T> {
/**
* @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<T> {
* @param {T} items - The items in the collection.
* @returns {CollectionResponse<T>} - A new instance of the CollectionResponse.
*/
constructor(count: number, items: T) {
constructor(count: number, items: T[]) {
this.count = count;
this.items = items;
}
+22
View File
@@ -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.
*/
+12
View File
@@ -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<App> {
/**
* @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,
+44 -5
View File
@@ -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<ApiResponse<App>> {
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<App>.
*/
public async getAppsByIds(
getAppsByIds: number[]
): Promise<ApiResponse<CollectionResponse<App>>> {
const endpoint = EndpointFactory.getAppsByIdsEndpoint();
const apiResponse = await this.post<any>(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<ApiResponse<T>>} - A promise that resolves to an ApiResponse of type T.
*/
private async get<T>(
@@ -101,4 +123,21 @@ export class OnspringClient {
const apiResponse = ApiResponseFactory.getApiResponse<T>(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<T>(
endpoint: string,
data: any,
config: AxiosRequestConfig = {}
): Promise<ApiResponse<T>> {
const response = await this._client.post(endpoint, data, config);
const apiResponse = ApiResponseFactory.getApiResponse<T>(response);
return apiResponse;
}
}
+31
View File
@@ -1,10 +1,41 @@
/**
* @class PagedResponse - A generic paged response model
*/
export class PagedResponse<T> {
/**
* @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<T>} - A new instance of the PagedResponse class.
*/
constructor(
items: T[],
pageNumber: number,
+58 -9
View File
@@ -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<GetPagedAppsResponse> 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<any>(200, 'OK', mockApiResponse);
var appsPagedResponse = apiResponse.AsGetPagedAppsResponseType();
const apiResponse = new ApiResponse<any>(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<GetPagedAppsResponse> 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<any>(200, 'OK', mockApiResponse);
var appsPagedResponse = apiResponse.AsGetPagedAppsResponseType();
const apiResponse = new ApiResponse<any>(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<App> 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<any>(200, 'OK', mockApiResponse);
var appResponse = apiResponse.AsAppType();
const apiResponse = new ApiResponse<any>(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<CollectionResponse<App[]>> 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<any>(200, 'OK', mockResponseData);
const appCollectionResponse = apiResponse.AsAppCollectionType();
expect(appCollectionResponse).to.be.instanceOf(
ApiResponse<CollectionResponse<App>>
);
expect(appCollectionResponse.data).to.be.instanceOf(
CollectionResponse<App>
);
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');
});
}
});
});
});
+127 -1
View File
@@ -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<CollectionResponse<App[]>>);
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;
});
});
});