diff --git a/examples/SubstitutionCast.spec.ts b/examples/SubstitutionCast.spec.ts new file mode 100644 index 0000000..71c5e75 --- /dev/null +++ b/examples/SubstitutionCast.spec.ts @@ -0,0 +1,68 @@ +import { Sheet, TextField, Workbook, SubstitutionCast } from '@flatfile/configure' +import { SheetTester } from '../src/utils/testing/SheetTester' + + + + +const spanishNumeralSynonyms = [ + ['1', 'one', 'un'], + ['2', 'two', 'dos'], +] + + +const SpanishNumeralCast = SubstitutionCast( + spanishNumeralSynonyms, + 2, + (val) => `Couldn't convert '${val}' to a spanish number` +) + +// Substitution cast is used to coerce synonym values to a desired +// value. In this case '1', 'one', and 'un' are synonyms. If any of +// the three are found in a field using this cast, the value is set as +// the last item of the list. Substitution cast compares string case +// insensitive. + + +// note sheet must have same name as key in workbook it is shared as +const SubSheet = new Sheet( + 'SubSheet', + {numField: TextField({cast:SpanishNumeralCast})}) + +const TestWorkbook = new Workbook({ + name: `Test Workbook`, + namespace: 'test', + // saving SubSheet to workbook under key SubSheet + sheets: { SubSheet }, +}) + +describe('Workbook tests ->', () => { + // here we use Sheet tester + const testSheet = new SheetTester(TestWorkbook, 'SubSheet') + test('Spanish number word works', async () => { + // for this inputRow + const inputRow = { numField: 'un'} + // we expect this output row + const expectedOutputRow = { numField: 'un'} + const res = await testSheet.testRecord(inputRow) + //call the expectation here + expect(res).toMatchObject(expectedOutputRow) + }) + + test('Convert to spanish number word works', async () => { + const inputRow = { numField: 'un'} + const expectedOutputRow = { numField: 'un'} + const res = await testSheet.testRecord(inputRow) + expect(res).toMatchObject(expectedOutputRow) + + const res2 = await testSheet.testRecord({ numField: 'two'}) + expect(res).toMatchObject({ numField: 'dos' }) + }) + + // test('see how an error is handled ', async () => { + // // hold off for Paddy to fix + // const inputRow = { numField: 'not a number'} + // const expectedOutputRow = { numField: 'sadf'} + // const res = await testSheet.testRecord(inputRow) + // expect(res).toMatchObject(expectedOutputRow) + // }) +}) diff --git a/examples/constraintsExample.spec.ts b/examples/constraintsExample.spec.ts new file mode 100644 index 0000000..39e9737 --- /dev/null +++ b/examples/constraintsExample.spec.ts @@ -0,0 +1,175 @@ +import { FlatfileRecord } from '@flatfile/hooks' +import { Sheet, TextField, Workbook } from '@flatfile/configure' +import { SheetTester, matchSingleMessage } from '../src/utils/testing/SheetTester' + +/* + +1. Conditionally Null - Set field to null + throw warning if another field has a certain value +Field A: select either -> "Apples" or "Oranges" or "Bananas" +Field B: if "Apples" or "Bananas" (or N number of items) is selected on Field A -> set Field B to null and throw warning on Field B if there was data was cleared out of Field B. Warning message should contain the data that was cleared incase the user want to add it somewhere else. + +2. Conditionally Required - Required if a different field has a certain value, otherwise set it to null +Field A: select either -> "Apples" or "Oranges" or "Bananas" +Field B: if "Apples" or "Bananas" (or N number of items) on Field A and Field B is empty --> throw error if required. + if "Oranges" is selected on Field A --> Set Field B to null and throw a warning if there was data that was cleared out of Field B. + +Is there a way we can standardize this to avoid tons of nested conditionals in our sheet? + + */ + + + +const SetValWhen = ( + haystackField: string, needleValues: string | string[], targetField: string, val: any) => { + return (record: FlatfileRecord) => { + const [a, b] = [record.get(haystackField), record.get(targetField)] + let searchVals: string[]; + if (Array.isArray(needleValues)) { + searchVals = needleValues + } else { + searchVals = [needleValues] + } + //@ts-ignore + if (searchVals.includes(a)) { + record.set(targetField, val) + record.addWarning(targetField, `cleared '${targetField}', was ${b}`) + } + return record + } +} + +const RequiredWhen = ( + switchField: string, switchVals: string | string[], targetField: string) => { + return (record: FlatfileRecord) => { + const [a, b] = [record.get(switchField), record.get(targetField)] + let searchVals: string[]; + if (Array.isArray(switchVals)) { + searchVals = switchVals + } else { + searchVals = [switchVals] + } + //@ts-ignore + if (searchVals.includes(a)) { + if(b === null) { + record.addWarning(targetField, ` '${targetField}' required`) + } + } + return record + } +} + +const RCChain = (...funcs:any) => { + return (record: FlatfileRecord) => { + for (const func of funcs) { + func(record) + } + } +} + + +// note sheet must have same name as key in workbook it is shared as +const ConditionallyNullSheet = new Sheet( + 'ConditionallyNullSheet', + {a: TextField(), + b: TextField()}, + { + recordCompute: RCChain( + SetValWhen('a', 'b_must_be_null', 'b', null), + SetValWhen('a', 'b_to_10', 'b', 10)) + } +) + +const RequiredWhenSheet = new Sheet( + 'RequiredWhenSheet', + {a: TextField(), + b: TextField()}, + { + recordCompute: RequiredWhen('a', 'b_is_required', 'b') + } +) + +const TestWorkbook = new Workbook({ + name: `Test Workbook`, + namespace: 'test', + // saving SubSheet to workbook under key SubSheet + sheets: { ConditionallyNullSheet, RequiredWhenSheet}, +}) + +describe('Workbook tests ->', () => { + // here we use Sheet tester + const testSheet = new SheetTester(TestWorkbook, 'ConditionallyNullSheet') + + test('ConditionallyNullSheet test', async () => { + // for this inputRow + const inputRow = { a:'b_must_be_null', b:8 } + // we expect this output row + const expectedOutputRow = { a:'b_must_be_null', b:null } + const res = await testSheet.testRecord(inputRow) + const res2 = await testSheet.testMessage(inputRow) + + //use the match functions like + expect(matchSingleMessage(res2, 'b', "cleared 'b', was 8", 'warn')).toBeTruthy() + + expect(res).toMatchObject(expectedOutputRow) + }) + + test('ConditionallyNullSheet test', async () => { + // for this inputRow + const inputRow = { a:'b_must_be_null', b:8 } + // we expect this output row + const expectedOutputRow = { a:'b_must_be_null', b:null } + const res = await testSheet.testRecord(inputRow) + const res2 = await testSheet.testMessage(inputRow) + + //use the match functions like + expect(matchSingleMessage(res2, 'b', "cleared 'b', was 8", 'warn')).toBeTruthy() + + expect(res).toMatchObject(expectedOutputRow) + }) + + test('ConditionallyNullSheet test2', async () => { + // for this inputRow + const inputRow = { a:'anything_else', b:8 } + // we expect this output row + const expectedOutputRow = { a:'anything_else', b:8 } + const res = await testSheet.testRecord(inputRow) + expect(res).toMatchObject(expectedOutputRow) + }) + test('test set to 10 ', async () => { + // for this inputRow + const inputRow = { a:'b_to_10', b:10 } + // we expect this output row + const expectedOutputRow = { a:'b_to_10', b:10 } + const res = await testSheet.testRecord(inputRow) + expect(res).toMatchObject(expectedOutputRow) + }) + + const rqTestSheet = new SheetTester(TestWorkbook, 'RequiredWhenSheet') + test('RequiredWhen test1', async () => { + // for this inputRow + const inputRow = { a:'wont trigger', b:null } + // we expect this output row + const expectedOutputRow = { a:'wont trigger', b:null } + const res = await rqTestSheet.testRecord(inputRow) + const res2 = await rqTestSheet.testMessage(inputRow) + + //use the match functions like + expect(matchSingleMessage(res2, 'b')).toBeFalsy() + + expect(res).toMatchObject(expectedOutputRow) + }) + + test('RequiredWhen test2', async () => { + // for this inputRow + const inputRow = { a:'b_is_required', b:null } + // we expect this output row + const expectedOutputRow = { a:'b_is_required', b:null } + const res = await rqTestSheet.testRecord(inputRow) + const res2 = await rqTestSheet.testMessage(inputRow) + + //use the match functions like + expect(matchSingleMessage(res2, 'b', "'b' required")).toBeTruthy() + + expect(res).toMatchObject(expectedOutputRow) + }) +}) diff --git a/jest.config.js b/jest.config.js index 997df40..f5c3e51 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - roots: ['/src'], + roots: ['/src', '/examples'], testEnvironment: 'node', transform: { '^.+\\.tsx?$': 'ts-jest', diff --git a/src/utils/testing/SheetTester.ts b/src/utils/testing/SheetTester.ts index e30aefb..6f3acb9 100644 --- a/src/utils/testing/SheetTester.ts +++ b/src/utils/testing/SheetTester.ts @@ -1,6 +1,5 @@ import _ from 'lodash' - -import { FlatfileRecords, FlatfileSession, IPayload } from '@flatfile/hooks' +import { IRecordInfo, TRecordData, TPrimitive, FlatfileRecords, FlatfileSession, IPayload } from '@flatfile/hooks' import { Workbook } from '@flatfile/configure' export class SheetTester { @@ -97,14 +96,66 @@ export class SheetTester { return transform(pkey, value) } - public async testRecord(recordBatch: {}) { - const transformedRecords = await this.transformRecords([recordBatch]) + public async testRecord(record: {}) { + const transformedRecords = await this.transformRecords([record]) return transformedRecords.records[0].value } - public async testRecords(recordBatch: any[]) { + public async testRecords(recordBatch: Record[]) { const transformedRecords = await this.transformRecords(recordBatch) return transformedRecords.records.map((r) => r.value) } + public async testMessage(record: {}) { + const transformedRecords = await this.transformRecords([record]) + return transformedRecords.records.map((r) => r.toJSON().info)[0] + } + public async testMessages(recordBatch: Record[]) { + const transformedRecords = await this.transformRecords(recordBatch) + return transformedRecords.records.map((r) => r.toJSON().info) + } + +} + +// export interface InfoObj { +// field: string +// message: string +// level: TRecordStageLevel +// stage: 'validate' | 'compute' +// } + +export type InfoObj = IRecordInfo, string | number> + +export const removeUndefineds = (obj:Record) => _.pickBy(obj, _.identity) +export const matchMessages = (messages:InfoObj[], field?:string, message?:string, level?:string): false| any[] => { + + const results = _.filter(messages, removeUndefineds({field,message,level})) + if (results.length > 0) { + return results + } + return false } + +export const matchSingleMessage = ( + messages:InfoObj[], field?:string, message?:string, level?:string): false| any => { + const results = matchMessages(messages, field, message, level) + if (results === false) { + return false + } + if (results.length === 1) { + return results[0] + } + if (results.length > 1) { + throw new Error("more than one message returned") + } + if (results.length === 0) { + //unreachable + return false + } + //unreachable + return false +} + +//use the match functions like +// const res = await testSheet.testMessage(inputRow) +// expect(matchSingleMessage(res, 'numField', 'more than 5', 'error')).toBeTruthy()