From 757e773f9fca10cbe3bfc6448934f830c2e6ad42 Mon Sep 17 00:00:00 2001 From: StevanFreeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 13 Feb 2023 20:21:28 -0600 Subject: [PATCH] feat: added saveRecord method --- src/models/ApiResponse.ts | 14 ++ src/models/OnspringClient.ts | 22 ++- src/models/Record.ts | 4 +- src/models/SaveRecordResponse.ts | 10 ++ tests/ApiResponse.spec.ts | 40 +++++ tests/OnspringClient.spec.ts | 262 +++++++++++++++++++++++++++++++ tests/SaveRecordResponse.spec.ts | 36 +++++ 7 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 src/models/SaveRecordResponse.ts create mode 100644 tests/SaveRecordResponse.spec.ts diff --git a/src/models/ApiResponse.ts b/src/models/ApiResponse.ts index 7744668..804a3d4 100644 --- a/src/models/ApiResponse.ts +++ b/src/models/ApiResponse.ts @@ -45,6 +45,7 @@ import { StringRecordValue } from './StringRecordValue'; import { TimeSpanData } from './TimeSpanData'; import { TimeSpanRecordValue } from './TimeSpanRecordValue'; import { type RecordValue } from './RecordValue'; +import { SaveRecordResponse } from './SaveRecordResponse'; /** * @class ApiResponse - A generic response object for API requests. @@ -431,6 +432,19 @@ export class ApiResponse { ); } + public asSaveRecordResponseType(): ApiResponse { + const apiResponse = this as ApiResponse; + const response = new SaveRecordResponse( + apiResponse.data.id, + apiResponse.data.warnings + ); + return new ApiResponse( + apiResponse.statusCode, + apiResponse.message, + response + ); + } + private static getRecordValueByType(recordValueItem: any): RecordValue { const type = RecordValueType[recordValueItem.type]; diff --git a/src/models/OnspringClient.ts b/src/models/OnspringClient.ts index 1d03f1d..fbab5e2 100644 --- a/src/models/OnspringClient.ts +++ b/src/models/OnspringClient.ts @@ -5,6 +5,7 @@ import { EndpointFactory } from './EndpointFactory'; import { ApiResponseFactory } from './ApiResponseFactory'; import { DataFormat } from '../enums/DataFormat'; import { ReportDataType } from '../enums/ReportDataType'; +import { Record } from './Record'; import { type AxiosInstance, type AxiosRequestConfig } from 'axios'; import { type ApiResponse } from './ApiResponse'; import { type GetPagedAppsResponse } from './GetPagedAppsResponse'; @@ -20,12 +21,13 @@ import { type ListItemResponse } from './ListItemResponse'; import { type ListItemRequest } from './ListItemRequest'; import { type GetPagedReportsResponse } from './GetPagedReportsResponse'; import { type ReportData } from './ReportData'; -import { type Record } from './Record'; import { type GetRecordRequest } from './GetRecordRequest'; import { type GetRecordsByAppIdRequest } from './GetRecordsByAppIdRequest'; import { type GetPagedRecordsResponse } from './GetPagedRecordsResponse'; import { type GetRecordsRequest } from './GetRecordsRequest'; import { type QueryRecordsRequest } from './QueryRecordsRequest'; +import { type SaveRecordResponse } from './SaveRecordResponse'; +import { type SaveRecordRequest } from './SaveRecordRequest'; /** * @class OnspringClient - A client that can communicate with the Onspring API. @@ -397,6 +399,24 @@ export class OnspringClient { return apiResponse.asGetPagedRecordsResponseType(); } + public async saveRecord( + request: Record | SaveRecordRequest + ): Promise> { + request = + request instanceof Record + ? request.convertToSaveRecordRequest() + : request; + + const endpoint = EndpointFactory.getAddOrUpdateRecordEndpoint(); + const apiResponse = await this.put(endpoint, request); + + if (apiResponse.isSuccessful === false) { + return apiResponse; + } + + return apiResponse.asSaveRecordResponseType(); + } + /** * @method deleteRecordById - Deletes a record by its id. * @param {number} appId - The id of the app that the record belongs to. diff --git a/src/models/Record.ts b/src/models/Record.ts index e201518..92a804b 100644 --- a/src/models/Record.ts +++ b/src/models/Record.ts @@ -13,7 +13,7 @@ export class Record { /** * @property {number} recordId - The id of the record. */ - public recordId: number; + public recordId: number | null; /** * @property {RecordValue[]} fieldData - The data for the fields in the record. @@ -29,7 +29,7 @@ export class Record { */ constructor( appId: number, - recordId: number, + recordId: number | null, fieldData: Array> = [] ) { this.appId = appId; diff --git a/src/models/SaveRecordResponse.ts b/src/models/SaveRecordResponse.ts new file mode 100644 index 0000000..0c3dcec --- /dev/null +++ b/src/models/SaveRecordResponse.ts @@ -0,0 +1,10 @@ +import { CreatedWithIdResponse } from './CreatedWithIdResponse'; + +export class SaveRecordResponse extends CreatedWithIdResponse { + public warnings: string[]; + + constructor(id: number, warnings: string[] = []) { + super(id); + this.warnings = warnings; + } +} diff --git a/tests/ApiResponse.spec.ts b/tests/ApiResponse.spec.ts index 49e5b6b..2a48785 100644 --- a/tests/ApiResponse.spec.ts +++ b/tests/ApiResponse.spec.ts @@ -27,6 +27,7 @@ import { testFieldData } from './testData/testFieldData'; import { type AxiosResponse } from 'axios'; import path from 'path'; import fs from 'fs'; +import { SaveRecordResponse } from '../src/models/SaveRecordResponse'; describe('ApiResponse', function () { it('should be defined', function () { @@ -1801,4 +1802,43 @@ describe('ApiResponse', function () { }).to.throw(); }); }); + + describe('asSaveRecordResponseType', function () { + it('should be defined', function () { + expect(ApiResponse.prototype.asSaveRecordResponseType).to.not.be + .undefined; + }); + + it('should be a function', function () { + expect(ApiResponse.prototype.asSaveRecordResponseType).to.be.a( + 'function' + ); + }); + + it('should take no parameters', function () { + expect(ApiResponse.prototype.asSaveRecordResponseType).to.have.lengthOf( + 0 + ); + }); + + it('should return a SaveRecordResponse', function () { + const mockResponseData = { + id: 1, + warnings: ['warning 1', 'warning 2'], + }; + + const apiResponse = new ApiResponse(200, 'OK', mockResponseData); + const saveRecordResponse = apiResponse.asSaveRecordResponseType(); + + expect(saveRecordResponse).to.be.an.instanceof( + ApiResponse + ); + expect(saveRecordResponse.data).to.be.an.instanceof(SaveRecordResponse); + expect(saveRecordResponse.data).to.have.property('id', 1); + expect(saveRecordResponse.data) + .to.have.property('warnings') + .that.is.an('array') + .that.has.lengthOf(2); + }); + }); }); diff --git a/tests/OnspringClient.spec.ts b/tests/OnspringClient.spec.ts index 8c305aa..07fcea4 100644 --- a/tests/OnspringClient.spec.ts +++ b/tests/OnspringClient.spec.ts @@ -32,6 +32,7 @@ import fs from 'fs'; import path from 'path'; import * as sinon from 'sinon'; import { QueryRecordsRequest } from '../src/models/QueryRecordsRequest'; +import { SaveRecordRequest } from '../src/models/SaveRecordRequest'; describe('OnspringClient', function () { const baseUrl = 'https://api.onspring.dev'; @@ -3757,4 +3758,265 @@ describe('OnspringClient', function () { expect(result).to.have.property('data', null); }); }); + + describe('saveRecord', function () { + it('should be defined', function () { + expect(OnspringClient.prototype.saveRecord).to.not.be.undefined; + }); + + it('should be a function', function () { + expect(OnspringClient.prototype.saveRecord).to.be.a('function'); + }); + + it('should return a promise', function () { + expect( + new OnspringClient(baseUrl, apiKey).saveRecord( + new SaveRecordRequest(1, 1) + ) + ).to.be.instanceOf(Promise); + }); + + it('should return a promise that resolves to an api response when request is successful at creating a record using a SaveRecordRequest', 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: 'Created', + data: { id: 1, warnings: ['warning'] }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.saveRecord(new SaveRecordRequest(1, null)); + + 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).to.have.property('data'); + expect(result.data).to.have.property('id', 1); + expect(result.data) + .to.have.property('warnings') + .that.is.an('array') + .that.has.lengthOf(1); + }); + + it('should return a promise that resolves to an api response when request is successful at updating a record using a SaveRecordRequest', 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: 1, warnings: ['warning'] }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.saveRecord(new SaveRecordRequest(1, 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.have.property('id', 1); + expect(result.data) + .to.have.property('warnings') + .that.is.an('array') + .that.has.lengthOf(1); + }); + + it('should return a promise that resolves to an api response when request is successful at creating a record using a Record', 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: 'Created', + data: { id: 1, warnings: ['warning'] }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.saveRecord(new Record(1, null)); + + 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).to.have.property('data'); + expect(result.data).to.have.property('id', 1); + expect(result.data) + .to.have.property('warnings') + .that.is.an('array') + .that.has.lengthOf(1); + }); + + it('should return a promise that resolves to an api response when request is successful at updating a record using a Record', 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: 1, warnings: ['warning'] }, + headers: {}, + config: {} as InternalAxiosRequestConfig, + } as AxiosResponse) + ); + + sinon.stub(client, '_client' as any).value(mockAxiosClient); + + const result = await client.saveRecord(new Record(1, 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.have.property('id', 1); + expect(result.data) + .to.have.property('warnings') + .that.is.an('array') + .that.has.lengthOf(1); + }); + + 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.saveRecord(new SaveRecordRequest(1, 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, '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.saveRecord(new SaveRecordRequest(1, 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, '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.saveRecord(new SaveRecordRequest(1, 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/SaveRecordResponse.spec.ts b/tests/SaveRecordResponse.spec.ts new file mode 100644 index 0000000..ffd6935 --- /dev/null +++ b/tests/SaveRecordResponse.spec.ts @@ -0,0 +1,36 @@ +import { SaveRecordResponse } from '../src/models/SaveRecordResponse'; +import { expect } from 'chai'; + +describe('SaveRecordResponse', function () { + it('should be defined', function () { + expect(SaveRecordResponse).to.not.be.undefined; + }); + + it('should have a constructor', function () { + expect(SaveRecordResponse).to.have.property('constructor'); + }); + + it('should have 1 parameter', function () { + expect(SaveRecordResponse).to.have.lengthOf(1); + }); + + it('should have a id property', function () { + expect(new SaveRecordResponse(1)).to.have.property('id'); + }); + + it('should have a warnings property', function () { + expect(new SaveRecordResponse(1)).to.have.property('warnings'); + }); + + it('should have a constructor that sets its properties correctly', function () { + const response = new SaveRecordResponse(1); + expect(response.id).to.equal(1); + expect(response.warnings).to.be.an('array').that.has.lengthOf(0); + }); + + it('should have a constructor that sets its properties correctly when passed warnings', function () { + const response = new SaveRecordResponse(1, ['warning1', 'warning2']); + expect(response.id).to.equal(1); + expect(response.warnings).to.be.an('array').that.has.lengthOf(2); + }); +});