Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
dist
node_modules

.nyc_output/
.nyc_output/
coverage/
6 changes: 3 additions & 3 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extension": ["ts"],
"require": "ts-node/register",
"spec": "test/**/*.spec.ts"
"extension": ["ts"],
"node-option": ["import=tsx"],
"spec": "test/**/*.spec.ts"
}
6,105 changes: 2,465 additions & 3,640 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
"version": "1.1.0",
"description": "SQON creation and manipulation library",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"files": [
"dist/"
],
"scripts": {
"build": "rm -rf dist && tsc",
"test": "nyc mocha"
"build": "rimraf ./dist && tsc -p tsconfig.build.json",
"test": "c8 --clean mocha"
},
"author": "OICR",
"license": "AGPL-3.0-or-later",
Expand All @@ -28,10 +29,12 @@
"devDependencies": {
"@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1",
"chai": "^4.3.7",
"c8": "^10.1.2",
"chai": "^5",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"ts-node": "^10.9.1",
"nyc": "^17",
"rimraf": "^6.0.1",
"tsx": "^4.19.1",
"typescript": "^5"
},
"prettier": {
Expand Down
12 changes: 6 additions & 6 deletions src/SQONBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import {
isArrayFilter,
isCombination,
isFilter,
} from './types/sqon';
import asArray from './utils/asArray';
import checkMatchingFilter, { checkMatchingArrays } from './utils/checkMatchingFilter';
import cloneDeepValues from './utils/cloneDeepValues';
import { createFilter } from './utils/createFilter';
import reduceSQON from './utils/reduceSQON';
} from './types/sqon.js';
import asArray from './utils/asArray.js';
import checkMatchingFilter, { checkMatchingArrays } from './utils/checkMatchingFilter.js';
import cloneDeepValues from './utils/cloneDeepValues.js';
import { createFilter } from './utils/createFilter.js';
import reduceSQON from './utils/reduceSQON.js';

type SQONBuilder = {
and: (content: SQON | SQON[], pivot?: string) => SQONBuilder;
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './types/sqon';
export { default as checkMatchingFilter } from './utils/checkMatchingFilter';
export { default as reduceSQON } from './utils/reduceSQON';
export { emptySQON } from './SQONBuilder';
export * from './types/sqon.js';
export { default as checkMatchingFilter } from './utils/checkMatchingFilter.js';
export { default as reduceSQON } from './utils/reduceSQON.js';
export { emptySQON } from './SQONBuilder.js';

import { default as SQONBuilder } from './SQONBuilder';
import { default as SQONBuilder } from './SQONBuilder.js';
export default SQONBuilder;
2 changes: 1 addition & 1 deletion src/types/sqon.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z as zod } from 'zod';
import { Clean, Values } from './util';
import { Clean, Values } from './util.js';

/* **** *
* Keys *
Expand Down
6 changes: 3 additions & 3 deletions src/utils/checkMatchingFilter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FilterOperator, isArrayFilter } from '../types/sqon';
import asArray from './asArray';
import filterDuplicates from './filterDuplicates';
import { FilterOperator, isArrayFilter } from '../types/sqon.js';
import asArray from './asArray.js';
import filterDuplicates from './filterDuplicates.js';

/**
* Compare two arrays ensuring they have the same elements. This removes duplicates and compares them
Expand Down
5 changes: 2 additions & 3 deletions src/utils/createFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import {
FilterOperator,
FilterTypeMap,
isArrayFilterKey,
isArrayFilterValue,
isScalarFilterKey,
isScalarFilterValue,
} from '../types/sqon';
import asArray from './asArray';
} from '../types/sqon.js';
import asArray from './asArray.js';

export const createFilter = <Key extends FilterKey>(
fieldName: string,
Expand Down
28 changes: 19 additions & 9 deletions src/utils/reduceSQON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import {
FilterKeys,
FilterOperator,
SQON,
ScalarFilterKeys,
isArrayFilter,
isFilter,
} from '../types/sqon';
import asArray from './asArray';
import { createFilter } from './createFilter';
import filterDuplicates from './filterDuplicates';
} from '../types/sqon.js';
import asArray from './asArray.js';
import { createFilter } from './createFilter.js';
import filterDuplicates from './filterDuplicates.js';
/**
* 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);
Expand Down Expand Up @@ -53,7 +57,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) {
Expand All @@ -68,7 +72,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
Expand All @@ -77,8 +81,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
Expand Down
14 changes: 8 additions & 6 deletions test/SQONBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import SQONBuilder, {
FilterOperator,
SQON,
ScalarFilterKeys,
} from '../src';
import reduceSQON from '../src/utils/reduceSQON';
import { emptySQON } from '../src/SQONBuilder';
} from '../src/index.js';
import reduceSQON from '../src/utils/reduceSQON.js';
import { emptySQON } from '../src/SQONBuilder.js';

describe('SQONBuilder', () => {
describe('Root', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
});
});
Expand Down
2 changes: 1 addition & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import * as Exports from '../src';
import * as Exports from '../src/index.js';

describe('index', () => {
describe('exports', () => {
Expand Down
4 changes: 2 additions & 2 deletions test/utils/checkMatchingFilter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import { ArrayFilter, FilterKeys, FilterOperator, ScalarFilter } from '../../src';
import checkMatchingFilter, { checkMatchingArrays } from '../../src/utils/checkMatchingFilter';
import { ArrayFilter, FilterKeys, FilterOperator, ScalarFilter } from '../../src/index.js';
import checkMatchingFilter, { checkMatchingArrays } from '../../src/utils/checkMatchingFilter.js';

describe('utils/checkMatchingFilter', () => {
describe('checkMatchingArrays', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/utils/cloneDeepValues.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import cloneDeepValues from '../../src/utils/cloneDeepValues';
import cloneDeepValues from '../../src/utils/cloneDeepValues.js';

describe('utils/cloneDeepPojo', () => {
it('clones nested objects', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/utils/filterDuplicates.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import filterDuplicates from '../../src/utils/filterDuplicates';
import filterDuplicates from '../../src/utils/filterDuplicates.js';

describe('utils/filterDuplicates', () => {
it('no duplicates - not modified', () => {
Expand Down
71 changes: 49 additions & 22 deletions test/utils/reduceSQON.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,84 +9,110 @@ import {
LesserThanFilter,
SQON,
reduceSQON,
} from '../../src';
} from '../../src/index.js';

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'] } };

const output = reduceSQON(input);

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 } };

const output = reduceSQON(input);

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 } };

const output = reduceSQON(input);

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,
Expand All @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["test/**/*.ts"]
}
Loading