From d2d3687356c57bde97b41675402784b78d4f91f0 Mon Sep 17 00:00:00 2001 From: Mikael Vesavuori Date: Tue, 14 May 2024 11:13:51 +0200 Subject: [PATCH] feat: add DatasetRequests --- package.json | 2 +- src/DatasetRequests.ts | 96 ++++++++++++++++++++++++ src/Utils.ts | 21 ++++-- src/index.ts | 1 + src/interfaces/DatasetRequests.ts | 16 ++++ testdata/dataset.ts | 55 ++++++++++++++ tests/DatasetRequests.test.ts | 117 ++++++++++++++++++++++++++++++ tests/Utils.dataset.test.ts | 25 ++++++- 8 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 src/DatasetRequests.ts create mode 100644 src/interfaces/DatasetRequests.ts create mode 100644 tests/DatasetRequests.test.ts diff --git a/package.json b/package.json index 8ae79d3..2cfbf70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getpopcorn/utils", - "version": "0.0.20", + "version": "0.0.21", "description": "Various common utilities and tools", "author": "Popcorn Cloud", "license": "SEE LICENSE IN LICENSE", diff --git a/src/DatasetRequests.ts b/src/DatasetRequests.ts new file mode 100644 index 0000000..002bff9 --- /dev/null +++ b/src/DatasetRequests.ts @@ -0,0 +1,96 @@ +import { Utils } from './Utils.js'; +import { + datasetAddRequestObject, + datasetDeleteRequestObject, + datasetGetRequestObject, + datasetUpdateRequestObject +} from './RequestObjects.js'; + +import { + DatasetCreateUpdateOptions, + DatasetDeleteOptions, + DatasetGetOptions +} from './interfaces/DatasetRequests.js'; + +/** + * @description TODO + */ +export class DatasetRequests { + utils: Utils; + + constructor() { + this.utils = new Utils(); + } + + /** + * @description Get the metadata, headers, and first items from the specified Dataset. + * @todo Support queries + * + * @example + * dataset.get(); + */ + public async get(options: DatasetGetOptions) { + const { datasetApiBaseUrl, datasetId } = options; + + return await this.utils.request(datasetGetRequestObject(datasetApiBaseUrl, datasetId)); + } + + /** + * @description Delete an item from the specified Dataset. + * + * @example + * dataset.delete(); + */ + public async delete(options: DatasetDeleteOptions) { + const { datasetApiBaseUrl, datasetId, id } = options; + + await this.utils.request(datasetDeleteRequestObject(datasetApiBaseUrl, datasetId, 'item', id)); + + return true; + } + + /** + * @description Create an item in the Dataset. + * + * @example + * dataset.create(input); + */ + public async create(options: DatasetCreateUpdateOptions) { + const { datasetApiBaseUrl, datasetId, id, input, properties } = options; + + const resourcePath = id ? `item/${id}` : `item`; + + const { success, errors } = this.utils.inputMatchesDatasetConfig(input, properties); + if (!success) return { success, errors }; + + const payload = this.utils.inputToDatasetPayload(input, properties); + + await this.utils.request( + datasetAddRequestObject(datasetApiBaseUrl, datasetId, resourcePath, payload) + ); + + return true; + } + + /** + * @description Update an item in the Dataset. + * The deletion is based on the `itemId` set in the settings. + * + * @example + * dataset.update(input); + */ + public async update(options: DatasetCreateUpdateOptions) { + const { datasetApiBaseUrl, datasetId, id, input, properties } = options; + + const { success, errors } = this.utils.inputMatchesDatasetConfig(input, properties); + if (!success) return { success, errors }; + + const payload = this.utils.inputToDatasetPayload(input, properties); + + await this.utils.request( + datasetUpdateRequestObject(datasetApiBaseUrl, datasetId, 'item', id, payload) + ); + + return true; + } +} diff --git a/src/Utils.ts b/src/Utils.ts index 7db56d7..33bca69 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -170,6 +170,9 @@ export class Utils { /** * @description Check if an input object matches a Dataset configuration. * + * Note that it does not fail on extraneous properties, as it picks only + * what the configuration expects (as references) and/or provides as directly assigned values. + * * @example * const input = { * name: 'Sam Person', @@ -226,7 +229,7 @@ export class Utils { const value = this.getReferencedValue(item.value, input); const exists = value !== '__KEY_NOT_FOUND__'; - if (!exists && item.isRequired) errors.push(`Missing value for "${value}"`); + if (!exists && item.isRequired) errors.push(`Missing value for "${item.value}"`); const isValidType = validateType(item.headerType, value); if (!isValidType) errors.push(`Invalid type for "${value}"`); @@ -268,12 +271,16 @@ export class Utils { /** * @description Get the referenced value, either literally or if used within a variable-type format. */ - private getReferencedValue(value: string, input: Record) { - const isReferenceValue = value.startsWith('{input.'); - return this.getNestedValue( - isReferenceValue ? value.replace('{input.', '').replace('}', '') : value, - input - ); + private getReferencedValue(value: unknown, input: Record) { + const isString = typeof value === 'string'; + const isReferenceValue = isString ? value.startsWith('{input.') : false; + const fixedValue = + isString && isReferenceValue ? value.replace('{input.', '').replace('}', '') : value; + + if (typeof fixedValue === 'string' && isString && isReferenceValue) + return this.getNestedValue(fixedValue, input); + + return fixedValue; } /** diff --git a/src/index.ts b/src/index.ts index bc3b550..acf828e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ //export * from './Compressor.js'; // TODO +export * from './DatasetRequests.js'; export * from './ItemOptimizer.js'; export * from './RequestObjects.js'; export * from './Utils.js'; diff --git a/src/interfaces/DatasetRequests.ts b/src/interfaces/DatasetRequests.ts new file mode 100644 index 0000000..3175c61 --- /dev/null +++ b/src/interfaces/DatasetRequests.ts @@ -0,0 +1,16 @@ +type DatasetOptionsBase = { + datasetApiBaseUrl: string; + datasetId: string; +}; + +export type DatasetGetOptions = DatasetOptionsBase; + +export type DatasetDeleteOptions = DatasetOptionsBase & { + id: string; +}; + +export type DatasetCreateUpdateOptions = DatasetOptionsBase & + DatasetDeleteOptions & { + input: Record; + properties: Record[]; + }; diff --git a/testdata/dataset.ts b/testdata/dataset.ts index d70bbb4..1132648 100644 --- a/testdata/dataset.ts +++ b/testdata/dataset.ts @@ -40,3 +40,58 @@ export const validDatasetConfig = [ isRequired: true } ]; + +export const validDatasetConfigDirectAssignment = [ + { + headerRef: 'j2d8y22d', + headerType: 'short_text', + value: 'Sam Person', + isRequired: true + }, + { + headerRef: 'kjhf298y', + headerType: 'short_text', + value: '10:00', + isRequired: true + }, + { + headerRef: 'f2oifh9q', + headerType: 'short_text', + value: 'Central', + isRequired: true + }, + { + headerRef: 'fb1891g2', + headerType: 'number', + value: 2, + isRequired: false + }, + { + headerRef: 'mbhwf8ax', + headerType: 'number', + value: 46, + isRequired: true + } +]; + +export const datasetGetResponse = { + metadata: { deletedAt: '' }, + headers: { + u: '100000000', + h: [ + { i: 'k392dg', t: 'short_text', r: true, n: 'First Header', p: 0, l: 'user123' }, + { i: 'o2ufj2', t: 'short_text', r: false, n: 'Second Header', p: 1, l: 'user123' } + ] + }, + items: [ + { i: 'aj2831', f: [{ h: 'k392dg', v: 'something here' }] }, + { i: 'vjc923', f: [{ h: 'k392dg', v: 'whoops' }] }, + { + i: 'k473nd', + f: [ + { h: 'k392dg', v: 'more there' }, + { h: 'o2ufj2', v: 'whoa' } + ] + } + ] +}; diff --git a/tests/DatasetRequests.test.ts b/tests/DatasetRequests.test.ts new file mode 100644 index 0000000..83dd558 --- /dev/null +++ b/tests/DatasetRequests.test.ts @@ -0,0 +1,117 @@ +import { test, expect } from 'vitest'; + +import { DatasetRequests } from '../src/DatasetRequests.js'; + +import { datasetGetResponse } from '../testdata/dataset.js'; + +const datasetApiBaseUrl = 'https://www.mockachino.com/6e5778ca-638a-4c'; +const datasetId = 'asdf1234'; +const id = 'abc123'; +const input = { + something: 'my value here' +}; +const properties = [ + { + headerRef: 'abc123', + headerType: 'short_text', + value: '{input.something}', + isRequired: true + } +]; + +/** + * POSITIVE TESTS + */ +test('It should make a Dataset create request', async () => { + const expected = true; + + const result = await new DatasetRequests().create({ + datasetApiBaseUrl, + datasetId, + id, + input, + properties + }); + + expect(result).toBe(expected); +}); + +test('It should make a Dataset create request for a specific ID', async () => { + const expected = true; + + const result = await new DatasetRequests().create({ + datasetApiBaseUrl, + datasetId, + id: '', + input, + properties + }); + + expect(result).toBe(expected); +}); + +test('It should make a Dataset update request', async () => { + const expected = true; + + const result = await new DatasetRequests().update({ + datasetApiBaseUrl, + datasetId, + id, + input, + properties + }); + + expect(result).toBe(expected); +}); + +test('It should make a Dataset delete request', async () => { + const expected = true; + + const result = await new DatasetRequests().delete({ + datasetApiBaseUrl, + datasetId, + id + }); + + expect(result).toBe(expected); +}); + +test('It should make a Dataset get request', async () => { + const result = await new DatasetRequests().get({ + datasetApiBaseUrl, + datasetId + }); + + expect(result).toMatchObject(datasetGetResponse); +}); + +/** + * NEGATIVE TESTS + */ +test('It should not make a Dataset create request if the input does not match the configuration', async () => { + const expected = { success: false, errors: ['Missing value for "{input.something}"'] }; + + const result = await new DatasetRequests().create({ + datasetApiBaseUrl, + datasetId, + id, + input: { x: 1 }, + properties + }); + + expect(result).toMatchObject(expected); +}); + +test('It should not make a Dataset update request if the input does not match the configuration', async () => { + const expected = { success: false, errors: ['Missing value for "{input.something}"'] }; + + const result = await new DatasetRequests().update({ + datasetApiBaseUrl, + datasetId, + id, + input: { x: 1 }, + properties + }); + + expect(result).toMatchObject(expected); +}); diff --git a/tests/Utils.dataset.test.ts b/tests/Utils.dataset.test.ts index edb3f71..b0d6c51 100644 --- a/tests/Utils.dataset.test.ts +++ b/tests/Utils.dataset.test.ts @@ -2,7 +2,11 @@ import { test, expect } from 'vitest'; import { Utils } from '../src/index.js'; -import { validDatasetInput, validDatasetConfig } from '../testdata/dataset.js'; +import { + validDatasetInput, + validDatasetConfig, + validDatasetConfigDirectAssignment +} from '../testdata/dataset.js'; /** * POSITIVE TESTS @@ -26,7 +30,7 @@ test('It should return true for an input that is missing optional properties, bu expect(success).toBe(expected); }); -test('It should return a Dataset payload (create/update) from a valid input and Dataset configuration', () => { +test('It should return a Dataset payload (create/update) from a valid input (using a reference value) and Dataset configuration', () => { const expected = [ { headerRef: 'j2d8y22d', value: 'Sam Person' }, { headerRef: 'kjhf298y', value: '10:00' }, @@ -40,6 +44,23 @@ test('It should return a Dataset payload (create/update) from a valid input and expect(result).toMatchObject(expected); }); +test('It should return a Dataset payload (create/update) from a valid input (using a direct-assigned value) and Dataset configuration', () => { + const expected = [ + { headerRef: 'j2d8y22d', value: 'Sam Person' }, + { headerRef: 'kjhf298y', value: '10:00' }, + { headerRef: 'f2oifh9q', value: 'Central' }, + { headerRef: 'fb1891g2', value: 2 }, + { headerRef: 'mbhwf8ax', value: 46 } + ]; + + const result = new Utils().inputToDatasetPayload( + validDatasetInput, + validDatasetConfigDirectAssignment + ); + + expect(result).toMatchObject(expected); +}); + test('It should not handle incomplete configs', () => { const expected = [{ headerRef: 'kjhf298y', value: '10:00' }];