From fc585052c4e1a75c5a44355837de16705056bc57 Mon Sep 17 00:00:00 2001 From: James Brooks <12275865+jamesobrooks@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:28:16 -0600 Subject: [PATCH] Let renderer handle $expr mongo queries Jira ticket: CAMS-421 Co-authored-by: Brian Posey <15091170+btposey@users.noreply.github.com> --- .../adapters/gateways/mongo/utils/foo.test.ts | 32 +++++++++++++ .../mongo/utils/mongo-query-renderer.ts | 30 +++++++----- backend/lib/query/query-builder.test.ts | 10 ++-- backend/lib/query/query-builder.ts | 46 +++++++++---------- 4 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 backend/lib/adapters/gateways/mongo/utils/foo.test.ts diff --git a/backend/lib/adapters/gateways/mongo/utils/foo.test.ts b/backend/lib/adapters/gateways/mongo/utils/foo.test.ts new file mode 100644 index 000000000..af6877946 --- /dev/null +++ b/backend/lib/adapters/gateways/mongo/utils/foo.test.ts @@ -0,0 +1,32 @@ +import { toMongoQuery } from './mongo-query-renderer'; +import QueryBuilder from '../../../../query/query-builder'; +import { SyncedCase } from '../../../../../../common/src/cams/cases'; + +describe('foo', () => { + const { greaterThan } = QueryBuilder; + test('should do the thing', () => { + const expected = { + $expr: { + $gt: ['$closedDate', '$reopenedDate'], + }, + }; + + expect( + toMongoQuery( + QueryBuilder.build(greaterThan('closedDate', 'reopenedDate')), + true, + ), + ).toEqual(expected); + + const expected2 = { + closedDate: { + $gt: '2025-01-01', + }, + }; + expect( + toMongoQuery( + QueryBuilder.build(greaterThan('closedDate', '2025-01-01')), + ), + ).toEqual(expected2); + }); +}); diff --git a/backend/lib/adapters/gateways/mongo/utils/mongo-query-renderer.ts b/backend/lib/adapters/gateways/mongo/utils/mongo-query-renderer.ts index 13252c175..7f3ceaaea 100644 --- a/backend/lib/adapters/gateways/mongo/utils/mongo-query-renderer.ts +++ b/backend/lib/adapters/gateways/mongo/utils/mongo-query-renderer.ts @@ -13,7 +13,7 @@ import { DocumentQuery } from '../../../../humble-objects/mongo-humble'; const isArray = Array.isArray; -const matchCondition: { [key: string]: string } = { +const mapCondition: { [key: string]: string } = { EXISTS: '$exists', EQUALS: '$eq', GREATER_THAN: '$gt', @@ -24,21 +24,29 @@ const matchCondition: { [key: string]: string } = { NOT_EQUAL: '$ne', NOT_CONTAINS: '$nin', REGEX: '$regex', - EXPR: '$expr', }; -function translateCondition(query: Condition) { - return { [query.attributeName]: { [matchCondition[query.condition]]: query.value } }; +// TODO: create new aggregate renderer +function translateCondition(query: Condition, isAggregateCondition: boolean = false) { + if (isAggregateCondition) { + return { + $expr: { + [mapCondition[query.condition]]: [`$${query.leftOperand}`, `$${query.rightOperand}`], + }, + }; + } else { + return { [query.leftOperand]: { [mapCondition[query.condition]]: query.rightOperand } }; + } } -const matchConjunction: { [key: string]: string } = { +const mapConjunction: { [key: string]: string } = { AND: '$and', OR: '$or', NOT: '$not', }; function translateConjunction(query: Conjunction) { - return { [matchConjunction[query.conjunction]]: renderQuery(query.values) }; + return { [mapConjunction[query.conjunction]]: renderQuery(query.values) }; } function translatePagination(query: Pagination) { @@ -69,20 +77,20 @@ function translatePagination(query: Pagination) { return result; } -function renderQuery(query: Query) { +function renderQuery(query: Query, isAggregateCondition: boolean = false) { if (isArray(query)) { - return query.map((q) => renderQuery(q)); + return query.map((q) => renderQuery(q, isAggregateCondition)); } else if (isPagination(query)) { return translatePagination(query); } else if (isConjunction(query)) { return translateConjunction(query); } else if (isCondition(query)) { - return translateCondition(query); + return translateCondition(query, isAggregateCondition); } } -export function toMongoQuery(query: Query): DocumentQuery { - return renderQuery(query); +export function toMongoQuery(query: Query, isAggregateCondition: boolean = false): DocumentQuery { + return renderQuery(query, isAggregateCondition); } export function toMongoSort(sort: Sort): MongoSort { diff --git a/backend/lib/query/query-builder.test.ts b/backend/lib/query/query-builder.test.ts index d4046ec0d..906ac8811 100644 --- a/backend/lib/query/query-builder.test.ts +++ b/backend/lib/query/query-builder.test.ts @@ -69,7 +69,11 @@ describe('Query Builder', () => { expect(actual).toEqual(expected); }); - const staticEqualCondition: Condition = { condition: 'EQUALS', attributeName: 'two', value: 45 }; + const staticEqualCondition: Condition = { + condition: 'EQUALS', + leftOperand: 'two', + rightOperand: 45, + }; const simpleQueryCases = [ { @@ -170,8 +174,8 @@ describe('Query Builder', () => { test('isCondition', () => { const condition: Condition = { condition: 'REGEX', - attributeName: '', - value: '', + leftOperand: '', + rightOperand: '', }; expect(isCondition(condition)).toBeTruthy(); expect(isCondition({})).toBeFalsy(); diff --git a/backend/lib/query/query-builder.ts b/backend/lib/query/query-builder.ts index f1bbd6fda..597bc2fd4 100644 --- a/backend/lib/query/query-builder.ts +++ b/backend/lib/query/query-builder.ts @@ -9,11 +9,9 @@ export type Condition = { | 'NOT_EQUAL' | 'NOT_CONTAINS' | 'EXISTS' - | 'MATCH' - | 'EXPR' | 'REGEX'; - attributeName: string; - value: unknown; + leftOperand: string; + rightOperand: unknown; }; export function isCondition(obj: unknown): obj is Condition { @@ -72,80 +70,80 @@ function not(...values: ConditionOrConjunction[]): Conjunction { function equals(attributeName: string, value: T): Condition { return { condition: 'EQUALS', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function notEqual(attributeName: string, value: T): Condition { return { condition: 'NOT_EQUAL', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function greaterThan(attributeName: string, value: T): Condition { return { condition: 'GREATER_THAN', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function greaterThanOrEqual(attributeName: string, value: T): Condition { return { condition: 'GREATER_THAN_OR_EQUAL', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function contains(attributeName: string, value: T | T[]): Condition { return { condition: 'CONTAINS', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function lessThan(attributeName: string, value: T): Condition { return { condition: 'LESS_THAN', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function lessThanOrEqual(attributeName: string, value: T): Condition { return { condition: 'LESS_THAN_OR_EQUAL', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function notContains(attributeName: string, value: T | T[]): Condition { return { condition: 'NOT_CONTAINS', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; } function exists(attributeName: keyof T, value: boolean): Condition { return { condition: 'EXISTS', - attributeName: attributeName as string, - value, + leftOperand: attributeName as string, + rightOperand: value, }; } function regex(attributeName: string, value: string): Condition { return { condition: 'REGEX', - attributeName, - value, + leftOperand: attributeName, + rightOperand: value, }; }