diff --git a/src/models/ApiResponse.ts b/src/models/ApiResponse.ts index eaf7419..20967a4 100644 --- a/src/models/ApiResponse.ts +++ b/src/models/ApiResponse.ts @@ -10,6 +10,7 @@ import { FormulaField } from './FormulaField'; import { GetPagedAppsResponse } from './GetPagedAppsResponse'; import { GetPagedFieldsResponse } from './GetPagedFieldsResponse'; import { ListField } from './ListField'; +import { ListItemResponse } from './ListItemResponse'; import { ListValue } from './ListValue'; import { ReferenceField } from './ReferenceField'; @@ -258,6 +259,18 @@ export class ApiResponse { ); } + public asListItemResponseType(): ApiResponse { + const apiResponse = this as ApiResponse; + + const listItemResponse = new ListItemResponse(apiResponse.data.id); + + return new ApiResponse( + apiResponse.statusCode, + apiResponse.message, + listItemResponse + ); + } + /** * @method asFileCollectionType - Converts the field item to the appropriate field object based upon the field item's type. * @param fieldItem - The field item to convert. diff --git a/src/models/ListItemRequest.ts b/src/models/ListItemRequest.ts new file mode 100644 index 0000000..5af2f65 --- /dev/null +++ b/src/models/ListItemRequest.ts @@ -0,0 +1,52 @@ +/** + * @class ListItemRequest - Represents a list item request + */ +export class ListItemRequest { + /** + * @property {number} listId - The id for the list that the item belongs to. + */ + public listId: number; + + /** + * @property {string} id - The id for the list item. + */ + public id: string; + + /** + * @property {string} name - The name of the list item. + */ + public name: string; + + /** + * @property {number} numericValue - The numeric value of the list item. + */ + public numericValue: number; + + /** + * @property {string} color - The color of the list item. + */ + public color: string; + + /** + * @constructor - Creates a new instance of the ListItemRequest class. + * @param {number} listId - The id for the list that the item belongs to. + * @param {string} id - The id for the list item. + * @param {string} name - The name of the list item. + * @param {number} numericValue - The numeric value of the list item. + * @param {string} color - The color of the list item. + * @returns {ListItemRequest} - A new instance of the ListItemRequest class. + */ + constructor( + listId: number, + id: string, + name: string, + numericValue: number, + color: string + ) { + this.listId = listId; + this.id = id; + this.name = name; + this.numericValue = numericValue; + this.color = color; + } +} diff --git a/src/models/ListItemResponse.ts b/src/models/ListItemResponse.ts new file mode 100644 index 0000000..4fc8b8b --- /dev/null +++ b/src/models/ListItemResponse.ts @@ -0,0 +1,10 @@ +import { CreatedWithIdResponse } from './CreatedWithIdResponse'; + +/** + * @class ListItemResponse - Represents a respons when a list item is created or updated. + */ +export class ListItemResponse extends CreatedWithIdResponse { + constructor(id: number) { + super(id); + } +} diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index 193d792..ff6280f 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -14,6 +14,8 @@ import { type SaveFileRequest } from './SaveFileRequest'; import { type CreatedWithIdResponse } from './CreatedWithIdResponse'; import { type FileInfo } from './FileInfo'; import { type File } from './File'; +import { type ListItemResponse } from './ListItemResponse'; +import { type ListItemRequest } from './ListItemRequest'; /** * @class OnspringClient - A client that can communicate with the Onspring API. @@ -269,6 +271,22 @@ export class OnspringClient { return apiResponse; } + public async addOrUpdateListItem( + listItemRequest: ListItemRequest + ): Promise> { + const { listId, ...data } = listItemRequest; + + const endpoint = EndpointFactory.getAddOrUpdateListItemEndpoint(listId); + + const apiResponse = await this.put(endpoint, data); + + if (apiResponse.isSuccessful === false) { + return apiResponse; + } + + return apiResponse.asListItemResponseType(); + } + /** * @method get - Makes a GET request to the specified endpoint. * @param {string} endpoint - The endpoint that will be used to make the request. @@ -301,6 +319,29 @@ export class OnspringClient { return apiResponse; } + /** + * @method put - Makes a PUT request to the specified endpoint. + * @param {string} endpoint - The endpoint that will be used to make the request. + * @param {any} data - The data that will be sent with 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 put( + endpoint: string, + data: any, + config: AxiosRequestConfig = {} + ): Promise> { + const response = await this._client.put(endpoint, data, config); + const apiResponse = ApiResponseFactory.getApiResponse(response); + return apiResponse; + } + + /** + * @method delete - Makes a DELETE 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 delete( endpoint: string, config: AxiosRequestConfig = {} diff --git a/tests/ApiResponse.spec.ts b/tests/ApiResponse.spec.ts index 4f97f10..3f296d1 100644 --- a/tests/ApiResponse.spec.ts +++ b/tests/ApiResponse.spec.ts @@ -18,6 +18,7 @@ import { File } from '../src/models/File'; import fs from 'fs'; import { type AxiosResponse } from 'axios'; import path from 'path'; +import { ListItemResponse } from '../src/models/ListItemResponse'; describe('ApiResponse', function () { it('should be defined', function () { @@ -916,4 +917,29 @@ describe('ApiResponse', function () { expect(fileResponse.data).to.have.property('contentLength', 0); }); }); + + describe('asListItemResponseType', function () { + it('should be defined', function () { + expect(ApiResponse.prototype.asListItemResponseType).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(ApiResponse.prototype.asListItemResponseType).to.be.a('function'); + }); + it('should have no parameters', function () { + expect(ApiResponse.prototype.asListItemResponseType.length).to.equal(0); + }); + it('should return an ApiResponse', function () { + const apiResponse = new ApiResponse(200, 'OK', { + id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + }); + const listItemResponse = apiResponse.asListItemResponseType(); + expect(listItemResponse).to.be.an.instanceof(ApiResponse); + expect(listItemResponse.data).to.be.an.instanceof(ListItemResponse); + expect(listItemResponse.data).to.have.property( + 'id', + '3fa85f64-5717-4562-b3fc-2c963f66afa6' + ); + }); + }); }); diff --git a/tests/ListItemRequest.spec.ts b/tests/ListItemRequest.spec.ts new file mode 100644 index 0000000..1ff2d67 --- /dev/null +++ b/tests/ListItemRequest.spec.ts @@ -0,0 +1,33 @@ +import { ListItemRequest } from '../src/models/ListItemRequest'; +import { expect } from 'chai'; + +describe('ListItemRequest', function () { + it('should be defined', function () { + expect(ListItemRequest).to.not.be.undefined; + }); + + it('should have a constructor with 5 parameters', function () { + expect(ListItemRequest).to.have.property('constructor'); + expect(ListItemRequest).to.have.lengthOf(5); + }); + + it('should have a constructor that takes a listId, id, name, numericValue, and color and sets the listId, id, name, numericValue, and color properties', function () { + const listId = 1; + const id = '1'; + const name = 'name'; + const numericValue = 1; + const color = 'color'; + const listItemRequest = new ListItemRequest( + listId, + id, + name, + numericValue, + color + ); + expect(listItemRequest).to.have.property('listId', listId); + expect(listItemRequest).to.have.property('id', id); + expect(listItemRequest).to.have.property('name', name); + expect(listItemRequest).to.have.property('numericValue', numericValue); + expect(listItemRequest).to.have.property('color', color); + }); +}); diff --git a/tests/ListItemResponse.spec.ts b/tests/ListItemResponse.spec.ts new file mode 100644 index 0000000..9de321a --- /dev/null +++ b/tests/ListItemResponse.spec.ts @@ -0,0 +1,19 @@ +import { ListItemResponse } from '../src/models/ListItemResponse'; +import { expect } from 'chai'; + +describe('ListItemResponse', function () { + it('should be defined', function () { + expect(ListItemResponse).to.not.be.undefined; + }); + + it('should have a constructor with 1 parameter', function () { + expect(ListItemResponse).to.have.property('constructor'); + expect(ListItemResponse.constructor).to.have.lengthOf(1); + }); + + it('should have a constructor that takes an id and sets the id property', function () { + const id = 1; + const listItemResponse = new ListItemResponse(id); + expect(listItemResponse).to.have.property('id', id); + }); +}); diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index 2486ee6..071e809 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -20,6 +20,8 @@ import { FileInfo } from '../src/models/FileInfo'; import { File } from '../src/models/File'; import fs from 'fs'; import path from 'path'; +import { ListItemRequest } from '../src/models/ListItemRequest'; +import { ListItemResponse } from '../src/models/ListItemResponse'; describe('OnspringClient', function () { const baseUrl = 'https://api.onspring.dev'; @@ -2108,4 +2110,199 @@ describe('OnspringClient', function () { expect(result.data).to.be.null; }); }); + + describe('addOrUpdateListItem', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.addOrUpdateListItem).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.addOrUpdateListItem).to.be.a('function'); + }); + + it('should return a promise', function () { + expect( + new OnspringClient(baseUrl, apiKey).addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ) + ).to.be.instanceOf(Promise); + }); + + it('should return a promise that resolves to an api response when request is successful at updating a list item', 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, 'put').returns( + Promise.resolve({ + status: 200, + statusText: 'OK', + data: { id: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ); + + 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.data).to.be.instanceOf(ListItemResponse); + expect(result.data).to.have.property( + 'id', + '3fa85f64-5717-4562-b3fc-2c963f66afa6' + ); + }); + + it('should return a promise that resolves to an api response when request is successful at adding a list item', 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, 'put').returns( + Promise.resolve({ + status: 201, + statusText: 'OK', + data: { id: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ); + + expect(result).to.be.instanceOf(ApiResponse); + expect(result).to.have.property('statusCode', 201); + expect(result).to.have.property('isSuccessful', true); + expect(result).to.have.property('message', ''); + expect(result.data).to.be.instanceOf(ListItemResponse); + expect(result.data).to.have.property( + 'id', + '3fa85f64-5717-4562-b3fc-2c963f66afa6' + ); + }); + }); + + 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, 'put').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.addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ); + + 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, 'put').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.addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ); + + 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.data).to.be.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, 'put').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.addOrUpdateListItem( + new ListItemRequest(1, 'id', 'value', 1, 'color') + ); + + 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.data).to.be.null; + }); });