From 9be25c672ea2af833efb464c6d7de12a6db43c5f Mon Sep 17 00:00:00 2001 From: 0xTaneja Date: Fri, 8 Aug 2025 15:49:36 +0000 Subject: [PATCH] feat(core): tighten filter types --- packages/hypergraph/src/entity/findMany.ts | 41 ++++++++++++------- packages/hypergraph/src/entity/types.ts | 6 --- .../hypergraph/test/entity/findMany.test.ts | 15 ++++--- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index 85195d7c..afca4b8b 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -255,17 +255,9 @@ export function findMany( const filtered: Array> = []; const evaluateFilter = (fieldFilter: EntityFieldFilter, fieldValue: T): boolean => { - // Handle NOT operator - if ('not' in fieldFilter && fieldFilter.not) { - return !evaluateFilter(fieldFilter.not, fieldValue); - } - - // Handle OR operator - if ('or' in fieldFilter) { - const orFilters = fieldFilter.or; - if (Array.isArray(orFilters)) { - return orFilters.some((orFilter) => evaluateFilter(orFilter as EntityFieldFilter, fieldValue)); - } + const ff = fieldFilter as unknown as Record; + if ('not' in ff || 'or' in ff || 'and' in ff) { + throw new Error("Logical operators 'not', 'or', 'and' are only allowed at the root (cross-field) level."); } // Handle basic filters @@ -324,14 +316,35 @@ export function findMany( crossFieldFilter: CrossFieldFilter>, entity: Entity, ): boolean => { + // Evaluate regular field filters with AND semantics for (const fieldName in crossFieldFilter) { - const fieldFilter = crossFieldFilter[fieldName]; - const fieldValue = entity[fieldName]; + if (fieldName === 'or' || fieldName === 'not') continue; + const fieldFilter = crossFieldFilter[fieldName] as unknown as EntityFieldFilter | undefined; + if (!fieldFilter) continue; + const fieldValue = (entity as unknown as Record)[fieldName] as unknown; + if (!evaluateFilter(fieldFilter, fieldValue)) { + return false; + } + } + + // Evaluate nested OR at cross-field level (if present) + const cf = crossFieldFilter as unknown as Record; + const maybeOr = cf.or; + if (Array.isArray(maybeOr)) { + const orFilters = maybeOr as Array>>; + const orSatisfied = orFilters.some((orFilter) => evaluateCrossFieldFilter(orFilter, entity)); + if (!orSatisfied) return false; + } - if (fieldFilter && !evaluateFilter(fieldFilter, fieldValue)) { + // Evaluate nested NOT at cross-field level (if present) + const maybeNot = cf.not; + if (maybeNot) { + const notFilter = maybeNot as CrossFieldFilter>; + if (evaluateCrossFieldFilter(notFilter, entity)) { return false; } } + return true; }; diff --git a/packages/hypergraph/src/entity/types.ts b/packages/hypergraph/src/entity/types.ts index 10171577..df9449a4 100644 --- a/packages/hypergraph/src/entity/types.ts +++ b/packages/hypergraph/src/entity/types.ts @@ -55,8 +55,6 @@ export type EntityNumberFilter = { is?: number; greaterThan?: number; lessThan?: number; - not?: EntityNumberFilter; - or?: EntityNumberFilter[]; }; export type EntityStringFilter = { @@ -64,8 +62,6 @@ export type EntityStringFilter = { startsWith?: string; endsWith?: string; contains?: string; - not?: EntityStringFilter; - or?: EntityStringFilter[]; }; export type CrossFieldFilter = { @@ -77,8 +73,6 @@ export type CrossFieldFilter = { export type EntityFieldFilter = { is?: T; - not?: EntityFieldFilter; - or?: Array>; } & (T extends boolean ? { is?: boolean; diff --git a/packages/hypergraph/test/entity/findMany.test.ts b/packages/hypergraph/test/entity/findMany.test.ts index 7e90962a..811426e2 100644 --- a/packages/hypergraph/test/entity/findMany.test.ts +++ b/packages/hypergraph/test/entity/findMany.test.ts @@ -193,7 +193,7 @@ describe('findMany with filters', () => { handle, Person, { - name: { not: { is: 'John' } }, + not: { name: { is: 'John' } }, }, undefined, ); @@ -212,7 +212,7 @@ describe('findMany with filters', () => { handle, Person, { - age: { not: { is: 30 } }, + not: { age: { is: 30 } }, }, undefined, ); @@ -233,7 +233,7 @@ describe('findMany with filters', () => { handle, Person, { - name: { or: [{ is: 'John' }, { is: 'Jane' }] }, + or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }], }, undefined, ); @@ -252,7 +252,7 @@ describe('findMany with filters', () => { handle, Person, { - age: { or: [{ is: 25 }, { is: 40 }] }, + or: [{ age: { is: 25 } }, { age: { is: 40 } }], }, undefined, ); @@ -273,7 +273,7 @@ describe('findMany with filters', () => { handle, Person, { - name: { not: { or: [{ is: 'John' }, { is: 'Jane' }] } }, + not: { or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }] }, }, undefined, ); @@ -292,7 +292,7 @@ describe('findMany with filters', () => { handle, Person, { - name: { not: { or: [{ is: 'John' }, { is: 'Jane' }] } }, + not: { or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }] }, }, undefined, ); @@ -384,8 +384,7 @@ describe('findMany with filters', () => { handle, Person, { - name: { not: { startsWith: 'J' } }, - age: { not: { greaterThan: 35 } }, + not: { or: [{ name: { startsWith: 'J' } }, { age: { greaterThan: 35 } }] }, }, undefined, );