Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions examples/SubstitutionCast.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
// })
})
175 changes: 175 additions & 0 deletions examples/constraintsExample.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
roots: ['<rootDir>/src'],
roots: ['<rootDir>/src', '<rootDir>/examples'],
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
Expand Down
61 changes: 56 additions & 5 deletions src/utils/testing/SheetTester.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string, any>[]) {
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<string, any>[]) {
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<TRecordData<TPrimitive>, string | number>

export const removeUndefineds = (obj:Record<string, any>) => _.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()