From 70ae194e4e183cfd70dcc4d00866ea41e6915668 Mon Sep 17 00:00:00 2001 From: Jon Eubank Date: Fri, 18 Oct 2024 10:03:02 -0400 Subject: [PATCH 1/3] ReduceSQON should not combine `in` filters within `and`/`not` combos --- src/utils/reduceSQON.ts | 19 ++++++++-- test/SQONBuilder.spec.ts | 8 ++-- test/utils/reduceSQON.spec.ts | 69 ++++++++++++++++++++++++----------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/utils/reduceSQON.ts b/src/utils/reduceSQON.ts index 3ff26be..ef7448b 100644 --- a/src/utils/reduceSQON.ts +++ b/src/utils/reduceSQON.ts @@ -11,6 +11,11 @@ import { import asArray from './asArray'; import { createFilter } from './createFilter'; import filterDuplicates from './filterDuplicates'; +/** + * For an ArrayFilter, remove duplicate entries from the array of values. + * @param filter + * @returns + */ const deduplicateValues = (filter: FilterOperator): FilterOperator => { if (isArrayFilter(filter)) { const value = asArray(filter.content.value).filter(filterDuplicates); @@ -53,7 +58,7 @@ const reduceSQON = (sqon: SQON): SQON => { * - 2. multiple GT on the same 'or' combo can be a single with the lesser value * - 3. multiple LT on the same 'and'/'not' combo can be the lesser value * - 4. multiple GT on the same 'or' combo can be the greater value - * - 5. multiple IN on the same 'or'/'and'/'not' combo can be combined into a list + * - 5. multiple IN on the same 'or' combo can be combined into a single list */ // In this if/else chain we check both the match and the innersqon match. we know this is true thanks to the .find that found the match, but this is needed for the type checker if (match.op === FilterKeys.GreaterThan && innerSqon.op === FilterKeys.GreaterThan) { @@ -68,7 +73,7 @@ const reduceSQON = (sqon: SQON): SQON => { if (match.op === FilterKeys.LesserThan && innerSqon.op === FilterKeys.LesserThan) { if (output.op === CombinationKeys.And || output.op === CombinationKeys.Not) { - // 3. multiple LT on the same 'and'/'not combo can be the lesser value + // 3. multiple LT on the same 'and'/'not' combo can be the lesser value match.content.value = Math.min(match.content.value, innerSqon.content.value); } else { // 4. multiple LT on the same 'or' combo can be the greater value @@ -77,8 +82,14 @@ const reduceSQON = (sqon: SQON): SQON => { } if (match.op === FilterKeys.In && innerSqon.op === FilterKeys.In) { - // 5. multiple IN on the same 'or'/'and'/'not' combo can be combined into a list - match.content.value = [...asArray(match.content.value), ...asArray(innerSqon.content.value)]; + if (output.op === CombinationKeys.Or) { + // 5. multiple IN on the same 'or' combo can be combined into a list + match.content.value = [...asArray(match.content.value), ...asArray(innerSqon.content.value)]; + // Note that we cannot reduce 'and'/'not' combos since there are cases for testing inclusion + // in multiple separate lists when the tested property has an array of values. + } else { + output.content.push(innerSqon); + } } } else { // Did not find a matching filter in the existing output, so we add this one diff --git a/test/SQONBuilder.spec.ts b/test/SQONBuilder.spec.ts index aebbace..a6204cf 100644 --- a/test/SQONBuilder.spec.ts +++ b/test/SQONBuilder.spec.ts @@ -591,7 +591,7 @@ describe('SQONBuilder', () => { value: ['Jim', 'Bob', 'Greg'], }, }; - const output = SQONBuilder.in('name', 'Jim').in('name', ['Bob', 'Greg']); + const output = SQONBuilder.in('name', 'Jim').or(SQONBuilder.in('name', ['Bob', 'Greg'])); expect(output).deep.contains(expectedSqon); }); it('in(a).in(b) on different names combines with and', () => { @@ -619,7 +619,7 @@ describe('SQONBuilder', () => { }); it('in(a).in(b).in(a) collects like names and combines in and', () => { const expectedSqon: SQON = { - op: CombinationKeys.And, + op: CombinationKeys.Or, content: [ { op: FilterKeys.In, @@ -637,7 +637,9 @@ describe('SQONBuilder', () => { }, ], }; - const output = SQONBuilder.in('name', 'Jim').in('class', ['Bio']).in('name', 'Bob'); + const output = SQONBuilder.in('name', 'Jim') + .or(SQONBuilder.in('class', ['Bio'])) + .or(SQONBuilder.in('name', 'Bob')); expect(output).deep.contains(expectedSqon); }); }); diff --git a/test/utils/reduceSQON.spec.ts b/test/utils/reduceSQON.spec.ts index 5715504..50692d4 100644 --- a/test/utils/reduceSQON.spec.ts +++ b/test/utils/reduceSQON.spec.ts @@ -14,21 +14,42 @@ import { describe('utils/reduceSQON', () => { describe('filters', () => { // Filters of the same type and same name in the same combination operator can combine into a single filter - it('combines multiple `in` filters', () => { - const filterA: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; - const filterB: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Bob', 'May'] } }; + it('`in` filter within `and` is not reduced ', () => { + /** + * There is a use case where we want to filter data on a field with an array of values, + * in that situation the logic of running a test against two separate arrays is possible. We should not reduce + * two `in` filters in an `and` operator. + */ + const filterA: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue', 'May'] } }; + const filterB: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Bob', 'May'] } }; const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB] }; - const expected = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue', 'Bob', 'May'] } }; + const expected = input; const output = reduceSQON(input); expect(output).deep.equal(expected); }); - it('removes duplicates in array filter', () => { + it('`in` filter within `not` is not reduced ', () => { + /** + * There is a use case where we want to filter data on a field with an array of values, + * in that situation the logic of running a test against two separate arrays is possible. We should not reduce + * two `in` filters in a `not` operator. + */ const filterA: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue', 'May'] } }; const filterB: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Bob', 'May'] } }; - const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB] }; + const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB] }; + + const expected = input; + + const output = reduceSQON(input); + + expect(output).deep.equal(expected); + }); + it('`in` filters within `or` is combined into single array with duplicates removed', () => { + const filterA: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue', 'May'] } }; + const filterB: InFilter = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Bob', 'May'] } }; + const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB] }; const expected = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue', 'May', 'Bob'] } }; @@ -36,35 +57,38 @@ describe('utils/reduceSQON', () => { expect(output).deep.equal(expected); }); - it('combines multiple `greaterThan` within `and` using max', () => { + it('`greaterThan` filters within `and` are combined using max', () => { const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB] }; + const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB, filterC] }; - const expected = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; + const expected = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; const output = reduceSQON(input); expect(output).deep.equal(expected); }); - it('combines multiple `greaterThan` within `not` using max', () => { + it('`greaterThan` fitlers within `not` are combined using max', () => { const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB] }; + const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB, filterC] }; const expected = { op: CombinationKeys.Not, - content: [{ op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }], + content: [{ op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }], }; const output = reduceSQON(input); expect(output).deep.equal(expected); }); - it('combines multiple `greaterThan` within `or` using min', () => { + it('`greaterThan` filters within `or` are combined using min', () => { const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB] }; + const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB, filterC] }; const expected = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; @@ -72,10 +96,11 @@ describe('utils/reduceSQON', () => { expect(output).deep.equal(expected); }); - it('combines multiple `lesserThan` within `and` using min', () => { + it('`lesserThan` filters within `and` are combined using min', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB] }; + const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB, filterC] }; const expected = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; @@ -83,10 +108,11 @@ describe('utils/reduceSQON', () => { expect(output).deep.equal(expected); }); - it('combines multiple `lesserThan` within `not` using min', () => { + it('`lesserThan` filters within `not` are combined using min', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB] }; + const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB, filterC] }; const expected = { op: CombinationKeys.Not, @@ -97,12 +123,13 @@ describe('utils/reduceSQON', () => { expect(output).deep.equal(expected); }); - it('combines multiple `lesserThan` within `or` using max', () => { + it('`lesserThan` filters within `or` are combined using max', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; - const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB] }; + const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; + const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB, filterC] }; - const expected = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; + const expected = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; const output = reduceSQON(input); From 1689b1b7cde0c509552a97d5f449d2f4c9a15b98 Mon Sep 17 00:00:00 2001 From: Jon Eubank Date: Sat, 1 Feb 2025 17:42:23 -0500 Subject: [PATCH 2/3] Example formatting for new filters --- src/SQONBuilder.ts | 30 +++++------ src/types/index.ts | 2 + src/types/sqon.ts | 101 ++++--------------------------------- src/types/sqonFilters.ts | 106 +++++++++++++++++++++++++++++++++++++++ src/types/util.ts | 4 +- src/utils/reduceSQON.ts | 9 ++-- 6 files changed, 140 insertions(+), 112 deletions(-) create mode 100644 src/types/index.ts create mode 100644 src/types/sqonFilters.ts diff --git a/src/SQONBuilder.ts b/src/SQONBuilder.ts index d875e98..0f09b97 100644 --- a/src/SQONBuilder.ts +++ b/src/SQONBuilder.ts @@ -1,24 +1,24 @@ import { - ArrayFilter, - ArrayFilterValue, - CombinationKey, CombinationKeys, - CombinationOperator, - FilterKey, FilterKeys, - FilterOperator, - FilterValue, - FilterTypeMap, - GreaterThanFilter, - InFilter, - LesserThanFilter, - Operator, - SQON, - ScalarFilterValue, isArrayFilter, isCombination, isFilter, -} from './types/sqon'; + SQON, + type ArrayFilter, + type ArrayFilterValue, + type CombinationKey, + type CombinationOperator, + type FilterKey, + type FilterOperator, + type FilterTypeMap, + type FilterValue, + type GreaterThanFilter, + type InFilter, + type LesserThanFilter, + type Operator, + type ScalarFilterValue, +} from './types'; import asArray from './utils/asArray'; import checkMatchingFilter, { checkMatchingArrays } from './utils/checkMatchingFilter'; import cloneDeepValues from './utils/cloneDeepValues'; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..95b215f --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './sqon'; +export * from './sqonFilters'; diff --git a/src/types/sqon.ts b/src/types/sqon.ts index b15f83d..7bfbfc1 100644 --- a/src/types/sqon.ts +++ b/src/types/sqon.ts @@ -1,23 +1,20 @@ import { z as zod } from 'zod'; +import { + ArrayFilter, + ArrayFilterKeys, + ArrayFilterValue, + FilterOperator, + ScalarFilter, + ScalarFilterKeys, + ScalarFilterValue, + type ArrayFilterKey, + type ScalarFilterKey, +} from './sqonFilters'; import { Clean, Values } from './util'; /* **** * * Keys * * **** */ -export const ArrayFilterKeys = { - In: 'in', -} as const; -export type ArrayFilterKey = Values; - -export const ScalarFilterKeys = { - GreaterThan: 'gt', - LesserThan: 'lt', -} as const; -export type ScalarFilterKey = Values; - -export type FilterKey = ScalarFilterKey | ArrayFilterKey; -export const FilterKeys = Object.assign({}, ArrayFilterKeys, ScalarFilterKeys); - export const CombinationKeys = { And: 'and', Or: 'or', @@ -27,82 +24,6 @@ export type CombinationKey = Values; export const Keys = Object.assign({}, ArrayFilterKeys, ScalarFilterKeys, CombinationKeys); -/* ****************** * - * Filters * - * - Filter Values * - * - Specific Filters * - * ****************** */ - -/* ===== Filter Values ==== */ - -// The array value wants to be able to accept a single value or an array of values -// Arranger also doesnt care if the values are mixed numbers and strings, that will be sorted out by elasticsearch -// and in practice won't be mixed, so to simlpify type validation we use this nested union structure: -// string | number | (string | number)[] -export type ArrayFilterValue = zod.infer; -export const ArrayFilterValue = zod.union([ - zod.union([zod.string(), zod.number()]).array(), - zod.string(), - zod.number(), -]); - -export type ScalarFilterValue = zod.infer; -export const ScalarFilterValue = zod.number(); - -export type FilterValue = zod.infer; -export const FilterValue = zod.union([ArrayFilterValue, ScalarFilterValue]); - -export type FilterTypeMap = { - [ArrayFilterKeys.In]: InFilter; - [ScalarFilterKeys.GreaterThan]: GreaterThanFilter; - [ScalarFilterKeys.LesserThan]: LesserThanFilter; -}; - -/* ===== Specific Filters ==== */ - -export type InFilterContent = zod.infer; -export const InFilterContent = zod.object({ - fieldName: zod.string(), - value: ArrayFilterValue, -}); -export type InFilter = zod.infer; -export const InFilter = zod.object({ - op: zod.literal(ArrayFilterKeys.In), - content: InFilterContent, -}); - -export type GreaterThanFilterContent = zod.infer; -export const GreaterThanFilterContent = zod.object({ - fieldName: zod.string(), - value: ScalarFilterValue, -}); -export type GreaterThanFilter = zod.infer; -export const GreaterThanFilter = zod.object({ - op: zod.literal(ScalarFilterKeys.GreaterThan), - content: GreaterThanFilterContent, -}); - -export type LesserThanFilterContent = zod.infer; -export const LesserThanFilterContent = zod.object({ - fieldName: zod.string(), - value: ScalarFilterValue, -}); -export type LesserThanFilter = zod.infer; -export const LesserThanFilter = zod.object({ - op: zod.literal(ScalarFilterKeys.LesserThan), - content: LesserThanFilterContent, -}); - -export type ArrayFilter = zod.infer; -export const ArrayFilter = InFilter; // zod.union([InFilter]); // If other arrays are added, expand this to be a union -export type ScalarFilter = zod.infer; -export const ScalarFilter = zod.union([GreaterThanFilter, LesserThanFilter]); // If other arrays are added, expand this to be a union - -export type FilterContent = zod.infer; -export const FilterContent = zod.union([InFilterContent, GreaterThanFilterContent, LesserThanFilterContent]); -export type FilterOperator = zod.infer; -export const FilterOperator = zod.discriminatedUnion('op', [InFilter, GreaterThanFilter, LesserThanFilter]); - /* ************ * * Combinations * * ************ */ diff --git a/src/types/sqonFilters.ts b/src/types/sqonFilters.ts new file mode 100644 index 0000000..bb00906 --- /dev/null +++ b/src/types/sqonFilters.ts @@ -0,0 +1,106 @@ +import { z as zod, type ZodSchema } from 'zod'; +import type { Values } from './util'; + +/* *********** * + * Filter Keys * + * *********** */ +export const ArrayFilterKeys = { + In: 'in', +} as const; +export type ArrayFilterKey = Values; + +export const ScalarFilterKeys = { + GreaterThan: 'gt', + GreaterThanAlias: '>', + LesserThan: 'lt', +} as const; +export type ScalarFilterKey = Values; + +export type FilterKey = ScalarFilterKey | ArrayFilterKey; +export const FilterKeys = Object.assign({}, ArrayFilterKeys, ScalarFilterKeys); + +export type FilterSchema = ReturnType< + typeof zod.object<{ op: zod.ZodLiteral; content: Content }> +>; + +/* ===== Filter Values ==== */ + +/** + * Utility to create a zod schema for the value of a filter, formatting consistently as: + * `{op, content}` + * @param op A string literal that will define when the + * @param content + * @returns + */ +const defineFilter = ( + op: Op, + content: Content, +): FilterSchema => { + return zod.object({ + op: zod.literal(op), + content, + }); +}; + +// The array value wants to be able to accept a single value or an array of values +// Arranger also doesnt care if the values are mixed numbers and strings, that will be sorted out by elasticsearch +// and in practice won't be mixed, so to simlpify type validation we use this nested union structure: +// string | number | (string | number)[] +export const ArrayFilterValue = zod.union([ + zod.union([zod.string(), zod.number()]).array(), + zod.string(), + zod.number(), +]); +export type ArrayFilterValue = zod.infer; + +export const ArrayFilterContent = zod.object({ + fieldName: zod.string(), + value: ArrayFilterValue, +}); +export type ArrayFilterContent = zod.infer; + +// Scalar filter value differs from the array filter because it only accepts numbers or arrays of numbers +export const ScalarFilterValue = zod.number(); +export type ScalarFilterValue = zod.infer; + +export const ScalarFilterContent = zod.object({ + fieldName: zod.string(), + value: ScalarFilterValue, +}); +export type ScalarFilterContent = zod.infer; + +export const InFilter = defineFilter(ArrayFilterKeys.In, ArrayFilterContent); +export type InFilter = zod.infer; + +export const ArrayFilter = zod.discriminatedUnion('op', [InFilter]); +export type ArrayFilter = zod.infer; + +export const GreaterThanFilter = defineFilter(ScalarFilterKeys.GreaterThan, ScalarFilterContent); +export type GreaterThanFilter = zod.infer; +export const GreaterThanAliasFilter = defineFilter(ScalarFilterKeys.GreaterThanAlias, ScalarFilterContent); +export type GreaterThanAliasFilter = zod.infer; + +export const LesserThanFilter = defineFilter(ScalarFilterKeys.LesserThan, ScalarFilterContent); +export type LesserThanFilter = zod.infer; + +export const ScalarFilter = zod.discriminatedUnion('op', [GreaterThanFilter, GreaterThanAliasFilter, LesserThanFilter]); +export type ScalarFilter = zod.infer; + +export const FilterValue = zod.union([ScalarFilterValue, ArrayFilterValue]); +export type FilterValue = zod.infer; +export const FilterContent = zod.union([InFilter, GreaterThanFilter, GreaterThanAliasFilter, LesserThanFilter]); +export type FilterContent = zod.infer; +export const FilterOperator = zod.discriminatedUnion('op', [ + InFilter, + GreaterThanFilter, + GreaterThanAliasFilter, + LesserThanFilter, +]); +export type FilterOperator = zod.infer; + +export type FilterTypeMap = { + [ArrayFilterKeys.In]: InFilter; + [ScalarFilterKeys.GreaterThan]: GreaterThanFilter; + [ScalarFilterKeys.GreaterThanAlias]: GreaterThanAliasFilter; + [ScalarFilterKeys.LesserThan]: LesserThanFilter; +}; diff --git a/src/types/util.ts b/src/types/util.ts index f1effc6..fcb7646 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -22,10 +22,10 @@ export type Keys = T extends infer U ? keyof U : never; * type ModelAsConstValues = Values; // 'hello' | 100 * ``` */ -export type Values = T extends infer U ? U[keyof U] : never; +export type Values = T[keyof T]; /** * Strip out aliases from the TS reported type, to one level. * This will display type as an object with key: value pairs instead as an alias name. */ -export type Clean = T extends infer U ? { [K in keyof U]: U[K] } : never; +export type Clean = { [K in keyof T]: T[K] }; diff --git a/src/utils/reduceSQON.ts b/src/utils/reduceSQON.ts index ef7448b..e9bd9c2 100644 --- a/src/utils/reduceSQON.ts +++ b/src/utils/reduceSQON.ts @@ -1,13 +1,12 @@ import { CombinationKeys, - CombinationOperator, FilterKeys, - FilterOperator, - SQON, - ScalarFilterKeys, isArrayFilter, isFilter, -} from '../types/sqon'; + type CombinationOperator, + type FilterOperator, + type SQON, +} from '../types'; import asArray from './asArray'; import { createFilter } from './createFilter'; import filterDuplicates from './filterDuplicates'; From 674b46d752030f7fee4f2f334a17c70edec180f8 Mon Sep 17 00:00:00 2001 From: Jon Eubank Date: Sat, 1 Feb 2025 19:13:02 -0500 Subject: [PATCH 3/3] Fix keys exports and add comparison between symbol and letter ops in SQON reducer - adds `isSameFilter()` function to compare if `gt` and `>` are the same - updates tests to use mix of Symbol (`>`) and Letter (`gt`) op keys --- src/index.ts | 2 +- src/types/sqon.ts | 25 ++++++------- src/types/sqonFilters.ts | 61 ++++++++++++++++++-------------- src/utils/checkMatchingFilter.ts | 2 +- src/utils/createFilter.ts | 5 +-- src/utils/isSameFilter.ts | 37 +++++++++++++++++++ src/utils/matchesSchema.ts | 5 +++ src/utils/reduceSQON.ts | 18 +++++++--- test/types/sqon.spec.ts | 6 ++-- test/utils/isSameFilter.spec.ts | 48 +++++++++++++++++++++++++ test/utils/reduceSQON.spec.ts | 28 +++++++-------- 11 files changed, 168 insertions(+), 69 deletions(-) create mode 100644 src/utils/isSameFilter.ts create mode 100644 src/utils/matchesSchema.ts create mode 100644 test/utils/isSameFilter.spec.ts diff --git a/src/index.ts b/src/index.ts index 7f1d9ee..70c9085 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from './types/sqon'; +export * from './types'; export { default as checkMatchingFilter } from './utils/checkMatchingFilter'; export { default as reduceSQON } from './utils/reduceSQON'; export { emptySQON } from './SQONBuilder'; diff --git a/src/types/sqon.ts b/src/types/sqon.ts index 7bfbfc1..9dfb981 100644 --- a/src/types/sqon.ts +++ b/src/types/sqon.ts @@ -1,14 +1,14 @@ import { z as zod } from 'zod'; +import { matchesSchema } from '../utils/matchesSchema'; import { ArrayFilter, ArrayFilterKeys, ArrayFilterValue, + FilterKeys, FilterOperator, ScalarFilter, ScalarFilterKeys, ScalarFilterValue, - type ArrayFilterKey, - type ScalarFilterKey, } from './sqonFilters'; import { Clean, Values } from './util'; @@ -22,7 +22,7 @@ export const CombinationKeys = { } as const; export type CombinationKey = Values; -export const Keys = Object.assign({}, ArrayFilterKeys, ScalarFilterKeys, CombinationKeys); +export const Keys = Object.assign({}, FilterKeys, CombinationKeys); /* ************ * * Combinations * @@ -49,24 +49,21 @@ export type SQON = Clean; /* ===== Convenient Type Guards ===== */ export const isCombination = (operator: Operator): operator is CombinationOperator => - CombinationOperator.safeParse(operator).success; + matchesSchema(CombinationOperator, operator); -export const isFilter = (operator: Operator): operator is FilterOperator => FilterOperator.safeParse(operator).success; +export const isFilter = (operator: Operator): operator is FilterOperator => matchesSchema(FilterOperator, operator); -export const isArrayFilter = (operator: Operator): operator is ArrayFilter => ArrayFilter.safeParse(operator).success; +export const isArrayFilter = (operator: Operator): operator is ArrayFilter => matchesSchema(ArrayFilter, operator); -export const isScalarFilter = (operator: Operator): operator is ScalarFilter => - ScalarFilter.safeParse(operator).success; +export const isScalarFilter = (operator: Operator): operator is ScalarFilter => matchesSchema(ScalarFilter, operator); const arrayFilterKeys: string[] = Object.values(ArrayFilterKeys); -export const isArrayFilterKey = (input: unknown): input is ArrayFilterKey => +export const isArrayFilterKey = (input: unknown): input is ArrayFilterKeys => typeof input === 'string' && arrayFilterKeys.includes(input); - const scalarFilterKeys: string[] = Object.values(ScalarFilterKeys); -export const isScalarFilterKey = (input: unknown): input is ScalarFilterKey => +export const isScalarFilterKey = (input: unknown): input is ScalarFilterKeys => typeof input === 'string' && scalarFilterKeys.includes(input); -export const isArrayFilterValue = (value: unknown): value is ArrayFilterValue => - ArrayFilterValue.safeParse(value).success; +export const isArrayFilterValue = (value: unknown): value is ArrayFilterValue => matchesSchema(ArrayFilterValue, value); export const isScalarFilterValue = (value: unknown): value is ScalarFilterValue => - ScalarFilterValue.safeParse(value).success; + matchesSchema(ScalarFilterValue, value); diff --git a/src/types/sqonFilters.ts b/src/types/sqonFilters.ts index bb00906..9e99402 100644 --- a/src/types/sqonFilters.ts +++ b/src/types/sqonFilters.ts @@ -4,27 +4,31 @@ import type { Values } from './util'; /* *********** * * Filter Keys * * *********** */ -export const ArrayFilterKeys = { +export const FilterKeys = { In: 'in', -} as const; -export type ArrayFilterKey = Values; - -export const ScalarFilterKeys = { GreaterThan: 'gt', - GreaterThanAlias: '>', + GreaterThanSymbol: '>', LesserThan: 'lt', + LesserThanSymbol: '<', } as const; -export type ScalarFilterKey = Values; +export type FilterKey = Values; -export type FilterKey = ScalarFilterKey | ArrayFilterKey; -export const FilterKeys = Object.assign({}, ArrayFilterKeys, ScalarFilterKeys); +export const InFilterKeys = [FilterKeys.In]; +export const GreaterThanFilterKeys = [FilterKeys.GreaterThan, FilterKeys.GreaterThanSymbol]; +export const LesserThanFilterKeys = [FilterKeys.LesserThan, FilterKeys.LesserThanSymbol]; -export type FilterSchema = ReturnType< - typeof zod.object<{ op: zod.ZodLiteral; content: Content }> ->; +export const ArrayFilterKeys = [...InFilterKeys]; +export type ArrayFilterKeys = (typeof ArrayFilterKeys)[number]; +export const ScalarFilterKeys = [...GreaterThanFilterKeys, ...LesserThanFilterKeys]; +export type ScalarFilterKeys = (typeof ScalarFilterKeys)[number]; + +export const FilterKeySets = [InFilterKeys, GreaterThanFilterKeys, LesserThanFilterKeys]; /* ===== Filter Values ==== */ +export type FilterSchema = ReturnType< + typeof zod.object<{ op: zod.ZodLiteral; content: Content }> +>; /** * Utility to create a zod schema for the value of a filter, formatting consistently as: * `{op, content}` @@ -69,38 +73,41 @@ export const ScalarFilterContent = zod.object({ }); export type ScalarFilterContent = zod.infer; -export const InFilter = defineFilter(ArrayFilterKeys.In, ArrayFilterContent); +export const InFilter = defineFilter(FilterKeys.In, ArrayFilterContent); export type InFilter = zod.infer; export const ArrayFilter = zod.discriminatedUnion('op', [InFilter]); export type ArrayFilter = zod.infer; -export const GreaterThanFilter = defineFilter(ScalarFilterKeys.GreaterThan, ScalarFilterContent); +export const GreaterThanFilter = zod.discriminatedUnion('op', [ + defineFilter(FilterKeys.GreaterThan, ScalarFilterContent), + defineFilter(FilterKeys.GreaterThanSymbol, ScalarFilterContent), +]); export type GreaterThanFilter = zod.infer; -export const GreaterThanAliasFilter = defineFilter(ScalarFilterKeys.GreaterThanAlias, ScalarFilterContent); -export type GreaterThanAliasFilter = zod.infer; - -export const LesserThanFilter = defineFilter(ScalarFilterKeys.LesserThan, ScalarFilterContent); +export const LesserThanFilter = zod.discriminatedUnion('op', [ + defineFilter(FilterKeys.LesserThan, ScalarFilterContent), + defineFilter(FilterKeys.LesserThanSymbol, ScalarFilterContent), +]); export type LesserThanFilter = zod.infer; -export const ScalarFilter = zod.discriminatedUnion('op', [GreaterThanFilter, GreaterThanAliasFilter, LesserThanFilter]); +export const ScalarFilter = zod.discriminatedUnion('op', [...GreaterThanFilter.options, ...LesserThanFilter.options]); export type ScalarFilter = zod.infer; export const FilterValue = zod.union([ScalarFilterValue, ArrayFilterValue]); export type FilterValue = zod.infer; -export const FilterContent = zod.union([InFilter, GreaterThanFilter, GreaterThanAliasFilter, LesserThanFilter]); +export const FilterContent = zod.union([InFilter, ...GreaterThanFilter.options, LesserThanFilter]); export type FilterContent = zod.infer; export const FilterOperator = zod.discriminatedUnion('op', [ InFilter, - GreaterThanFilter, - GreaterThanAliasFilter, - LesserThanFilter, + ...GreaterThanFilter.options, + ...LesserThanFilter.options, ]); export type FilterOperator = zod.infer; export type FilterTypeMap = { - [ArrayFilterKeys.In]: InFilter; - [ScalarFilterKeys.GreaterThan]: GreaterThanFilter; - [ScalarFilterKeys.GreaterThanAlias]: GreaterThanAliasFilter; - [ScalarFilterKeys.LesserThan]: LesserThanFilter; + [FilterKeys.In]: InFilter; + [FilterKeys.GreaterThan]: GreaterThanFilter; + [FilterKeys.GreaterThanSymbol]: GreaterThanFilter; + [FilterKeys.LesserThan]: LesserThanFilter; + [FilterKeys.LesserThanSymbol]: LesserThanFilter; }; diff --git a/src/utils/checkMatchingFilter.ts b/src/utils/checkMatchingFilter.ts index 3ca242f..7ccf24e 100644 --- a/src/utils/checkMatchingFilter.ts +++ b/src/utils/checkMatchingFilter.ts @@ -1,4 +1,4 @@ -import { FilterOperator, isArrayFilter } from '../types/sqon'; +import { FilterOperator } from '../types'; import asArray from './asArray'; import filterDuplicates from './filterDuplicates'; diff --git a/src/utils/createFilter.ts b/src/utils/createFilter.ts index f171991..b51d9bf 100644 --- a/src/utils/createFilter.ts +++ b/src/utils/createFilter.ts @@ -3,10 +3,9 @@ import { FilterOperator, FilterTypeMap, isArrayFilterKey, - isArrayFilterValue, isScalarFilterKey, isScalarFilterValue, -} from '../types/sqon'; +} from '../types'; import asArray from './asArray'; export const createFilter = ( @@ -24,5 +23,3 @@ export const createFilter = ( throw new TypeError(`Cannot assign the value "${value}" to a filter of type "${op}".`); } }; - -createFilter('a', 'in', ['1']); diff --git a/src/utils/isSameFilter.ts b/src/utils/isSameFilter.ts new file mode 100644 index 0000000..d72986f --- /dev/null +++ b/src/utils/isSameFilter.ts @@ -0,0 +1,37 @@ +import { FilterKeySets, GreaterThanFilterKeys, type FilterOperator } from '../types'; +import filterDuplicates from './filterDuplicates'; + +/** + * Determine if every value in the values array is found in the options array. + * + * @param options + * @param values + * @returns + */ +function allIncluded(options: T[], values: T[]): boolean { + return values.filter((value) => options.includes(value)).length === values.length; + + // let count = 0; + // for (const option of options) { + // count += values.filter((i) => i === option).length; + // if (count >= values.length) { + // return true; + // } + // } + // return false; +} + +export function isSameFilter(filterA: FilterOperator, filterB: FilterOperator): boolean { + if (filterA.op === filterB.op) { + return true; + } + + const operations: string[] = [filterA.op, filterB.op]; + for (const keySet of FilterKeySets) { + if (allIncluded(keySet, operations)) { + return true; + } + } + + return false; +} diff --git a/src/utils/matchesSchema.ts b/src/utils/matchesSchema.ts new file mode 100644 index 0000000..1831ebc --- /dev/null +++ b/src/utils/matchesSchema.ts @@ -0,0 +1,5 @@ +import type { ZodSchema } from 'zod'; + +export function matchesSchema(schema: ZodSchema, value: unknown): value is T { + return schema.safeParse(value).success; +} diff --git a/src/utils/reduceSQON.ts b/src/utils/reduceSQON.ts index e9bd9c2..7d75535 100644 --- a/src/utils/reduceSQON.ts +++ b/src/utils/reduceSQON.ts @@ -1,8 +1,11 @@ import { CombinationKeys, FilterKeys, + GreaterThanFilter, + InFilter, isArrayFilter, isFilter, + LesserThanFilter, type CombinationOperator, type FilterOperator, type SQON, @@ -10,6 +13,8 @@ import { import asArray from './asArray'; import { createFilter } from './createFilter'; import filterDuplicates from './filterDuplicates'; +import { isSameFilter } from './isSameFilter'; +import { matchesSchema } from './matchesSchema'; /** * For an ArrayFilter, remove duplicate entries from the array of values. * @param filter @@ -46,9 +51,12 @@ const reduceSQON = (sqon: SQON): SQON => { for (const innerSqon of sqon.content) { // Filters are added to output content if (isFilter(innerSqon)) { - // Check for duplicate filter in this operator + // Check for duplicate filter already stored in our output operator const match = output.content.find( - (content) => content.op === innerSqon.op && content.content.fieldName === innerSqon.content.fieldName, + (content) => + isFilter(content) && + isSameFilter(content, innerSqon) && + content.content.fieldName === innerSqon.content.fieldName, ); if (match !== undefined) { /** @@ -60,7 +68,7 @@ const reduceSQON = (sqon: SQON): SQON => { * - 5. multiple IN on the same 'or' combo can be combined into a single list */ // In this if/else chain we check both the match and the innersqon match. we know this is true thanks to the .find that found the match, but this is needed for the type checker - if (match.op === FilterKeys.GreaterThan && innerSqon.op === FilterKeys.GreaterThan) { + if (matchesSchema(GreaterThanFilter, match) && matchesSchema(GreaterThanFilter, innerSqon)) { if (output.op === CombinationKeys.And || output.op === CombinationKeys.Not) { // 1. multiple GT on the same 'and'/'not' combo can be a single with the greater value match.content.value = Math.max(match.content.value, innerSqon.content.value); @@ -70,7 +78,7 @@ const reduceSQON = (sqon: SQON): SQON => { } } - if (match.op === FilterKeys.LesserThan && innerSqon.op === FilterKeys.LesserThan) { + if (matchesSchema(LesserThanFilter, match) && matchesSchema(LesserThanFilter, innerSqon)) { if (output.op === CombinationKeys.And || output.op === CombinationKeys.Not) { // 3. multiple LT on the same 'and'/'not' combo can be the lesser value match.content.value = Math.min(match.content.value, innerSqon.content.value); @@ -80,7 +88,7 @@ const reduceSQON = (sqon: SQON): SQON => { } } - if (match.op === FilterKeys.In && innerSqon.op === FilterKeys.In) { + if (matchesSchema(InFilter, match) && matchesSchema(InFilter, innerSqon)) { if (output.op === CombinationKeys.Or) { // 5. multiple IN on the same 'or' combo can be combined into a list match.content.value = [...asArray(match.content.value), ...asArray(innerSqon.content.value)]; diff --git a/test/types/sqon.spec.ts b/test/types/sqon.spec.ts index 656f277..ec11adc 100644 --- a/test/types/sqon.spec.ts +++ b/test/types/sqon.spec.ts @@ -10,7 +10,7 @@ import { isFilter, isScalarFilter, isScalarFilterKey, -} from '../../src/types/sqon'; +} from '../../src/types'; const combo: CombinationOperator = { op: CombinationKeys.And, content: [] }; const arrayFilter: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'a', value: ['b', 'c'] } }; @@ -52,10 +52,10 @@ describe('types/sqon', () => { expect(isScalarFilter(scalarFilter)).true; }); it('rejects combination operator', () => { - expect(isScalarFilter(arrayFilter)).false; + expect(isScalarFilter(combo)).false; }); it('rejects array filter', () => { - expect(isScalarFilter(combo)).false; + expect(isScalarFilter(arrayFilter)).false; }); }); describe('isArrayFilterKey', () => { diff --git a/test/utils/isSameFilter.spec.ts b/test/utils/isSameFilter.spec.ts new file mode 100644 index 0000000..41cd4ef --- /dev/null +++ b/test/utils/isSameFilter.spec.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import { GreaterThanFilter, type InFilter, type LesserThanFilter } from '../../src/types/sqonFilters'; +import { isSameFilter } from '../../src/utils/isSameFilter'; + +const inFilter: InFilter = { + op: 'in', + content: { value: 0, fieldName: 'anything' }, +}; +const gtFilter: GreaterThanFilter = { + op: 'gt', + content: { value: 0, fieldName: 'anything' }, +}; +const gtSymbolFilter: GreaterThanFilter = { + op: '>', + content: { value: 0, fieldName: 'anything' }, +}; +const ltFilter: LesserThanFilter = { + op: 'lt', + content: { value: 0, fieldName: 'anything' }, +}; +const ltSymbolFilter: LesserThanFilter = { + op: '<', + content: { value: 0, fieldName: 'anything' }, +}; + +describe('utils/isSameFilter', () => { + it('same op - returns true', () => { + expect(isSameFilter(gtFilter, gtFilter)).true; + expect(isSameFilter(gtSymbolFilter, gtSymbolFilter)).true; + expect(isSameFilter(ltFilter, ltFilter)).true; + expect(isSameFilter(ltSymbolFilter, ltSymbolFilter)).true; + expect(isSameFilter(inFilter, inFilter)).true; + }); + it('greaterthan - alt op - returns true', () => { + expect(isSameFilter(gtFilter, gtSymbolFilter)).true; + expect(isSameFilter(gtSymbolFilter, gtFilter)).true; + }); + it('lesserthan - different op values - returns true', () => { + expect(isSameFilter(ltFilter, ltSymbolFilter)).true; + expect(isSameFilter(ltSymbolFilter, ltFilter)).true; + }); + it('non-matching - returns false', () => { + expect(isSameFilter(ltFilter, gtSymbolFilter)).false; + expect(isSameFilter(ltSymbolFilter, gtFilter)).false; + expect(isSameFilter(gtSymbolFilter, inFilter)).false; + expect(isSameFilter(inFilter, ltFilter)).false; + }); +}); diff --git a/test/utils/reduceSQON.spec.ts b/test/utils/reduceSQON.spec.ts index 50692d4..b5c7378 100644 --- a/test/utils/reduceSQON.spec.ts +++ b/test/utils/reduceSQON.spec.ts @@ -60,7 +60,7 @@ describe('utils/reduceSQON', () => { it('`greaterThan` filters within `and` are combined using max', () => { const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; + const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB, filterC] }; const expected = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; @@ -71,7 +71,7 @@ describe('utils/reduceSQON', () => { }); it('`greaterThan` fitlers within `not` are combined using max', () => { const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; - const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; + const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 2 } }; const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB, filterC] }; @@ -85,12 +85,12 @@ describe('utils/reduceSQON', () => { expect(output).deep.equal(expected); }); it('`greaterThan` filters within `or` are combined using min', () => { - const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; + const filterA: GreaterThanFilter = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 1 } }; const filterB: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; const filterC: GreaterThanFilter = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB, filterC] }; - const expected = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 1 } }; + const expected = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 1 } }; const output = reduceSQON(input); @@ -98,7 +98,7 @@ describe('utils/reduceSQON', () => { }); it('`lesserThan` filters within `and` are combined using min', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; - const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; + const filterB: LesserThanFilter = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'num', value: 2 } }; const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.And, content: [filterA, filterB, filterC] }; @@ -111,7 +111,7 @@ describe('utils/reduceSQON', () => { it('`lesserThan` filters within `not` are combined using min', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; - const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; + const filterC: LesserThanFilter = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.Not, content: [filterA, filterB, filterC] }; const expected = { @@ -125,8 +125,8 @@ describe('utils/reduceSQON', () => { }); it('`lesserThan` filters within `or` are combined using max', () => { const filterA: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 1 } }; - const filterB: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 2 } }; - const filterC: LesserThanFilter = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; + const filterB: LesserThanFilter = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'num', value: 2 } }; + const filterC: LesserThanFilter = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'num', value: 4 } }; const input: SQON = { op: CombinationKeys.Or, content: [filterA, filterB, filterC] }; const expected = { op: FilterKeys.LesserThan, content: { fieldName: 'num', value: 4 } }; @@ -165,7 +165,7 @@ describe('utils/reduceSQON', () => { }); it('nested `and` combinations are removed', () => { const filterA: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; - const filterB: FilterOperator = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; + const filterB: FilterOperator = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 2 } }; const filterC: FilterOperator = { op: FilterKeys.LesserThan, content: { fieldName: 'score', value: 10 } }; const comboA: CombinationOperator = { op: CombinationKeys.And, content: [filterA, filterB] }; const comboB: CombinationOperator = { op: CombinationKeys.And, content: [filterC] }; @@ -176,7 +176,7 @@ describe('utils/reduceSQON', () => { it('nested `or` combinations are removed', () => { const filterA: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; const filterB: FilterOperator = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const filterC: FilterOperator = { op: FilterKeys.LesserThan, content: { fieldName: 'score', value: 10 } }; + const filterC: FilterOperator = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'score', value: 10 } }; const comboA: CombinationOperator = { op: CombinationKeys.Or, content: [filterA, filterB] }; const comboB: CombinationOperator = { op: CombinationKeys.Or, content: [filterC] }; const input: CombinationOperator = { op: CombinationKeys.Or, content: [comboA, comboB] }; @@ -187,7 +187,7 @@ describe('utils/reduceSQON', () => { // Not optimal behaviour, but simplest way to not introduce errors with negation const filterA: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; const filterB: FilterOperator = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const filterC: FilterOperator = { op: FilterKeys.LesserThan, content: { fieldName: 'score', value: 10 } }; + const filterC: FilterOperator = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'score', value: 10 } }; const comboA: CombinationOperator = { op: CombinationKeys.Not, content: [filterA, filterB] }; const comboB: CombinationOperator = { op: CombinationKeys.Not, content: [filterC] }; const input: CombinationOperator = { op: CombinationKeys.Not, content: [comboA, comboB] }; @@ -233,7 +233,7 @@ describe('utils/reduceSQON', () => { it('does not combine operators with different pivots', () => { const filterA: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; const filterB: FilterOperator = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const filterC: FilterOperator = { op: FilterKeys.LesserThan, content: { fieldName: 'score', value: 10 } }; + const filterC: FilterOperator = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'score', value: 10 } }; const comboA: CombinationOperator = { op: CombinationKeys.And, content: [filterA, filterB], pivot: 'pilot' }; const comboB: CombinationOperator = { op: CombinationKeys.And, content: [filterC], pivot: 'actor' }; const input: CombinationOperator = { op: CombinationKeys.And, content: [comboA, comboB], pivot: 'doctor' }; @@ -244,8 +244,8 @@ describe('utils/reduceSQON', () => { }); it('combines operators with same pivots', () => { const filterA: FilterOperator = { op: FilterKeys.In, content: { fieldName: 'name', value: ['Jim', 'Sue'] } }; - const filterB: FilterOperator = { op: FilterKeys.GreaterThan, content: { fieldName: 'num', value: 2 } }; - const filterC: FilterOperator = { op: FilterKeys.LesserThan, content: { fieldName: 'score', value: 10 } }; + const filterB: FilterOperator = { op: FilterKeys.GreaterThanSymbol, content: { fieldName: 'num', value: 2 } }; + const filterC: FilterOperator = { op: FilterKeys.LesserThanSymbol, content: { fieldName: 'score', value: 10 } }; const comboA: CombinationOperator = { op: CombinationKeys.And, content: [filterA, filterB], pivot: 'user' }; const comboB: CombinationOperator = { op: CombinationKeys.And, content: [filterC], pivot: 'user' }; const input: CombinationOperator = { op: CombinationKeys.And, content: [comboA, comboB], pivot: 'user' };