From 76caf84101ceeb3fd291d8ba836c8574aa853194 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 9 Aug 2025 15:20:55 +0200 Subject: [PATCH 1/5] remove casting from filter logic, rethrow filter error and improve tests --- packages/hypergraph/src/entity/findMany.ts | 43 ++++++------ .../hypergraph/test/entity/findMany.test.ts | 67 +++++++++++++++---- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index afca4b8b..04250255 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -3,7 +3,7 @@ import * as Schema from 'effect/Schema'; import { deepMerge } from '../utils/internal/deep-merge.js'; import { isRelationField } from '../utils/isRelationField.js'; import { canonicalize } from '../utils/jsc.js'; -import { type DecodedEntitiesCacheEntry, decodedEntitiesCache, type QueryEntry } from './decodedEntitiesCache.js'; +import { decodedEntitiesCache, type DecodedEntitiesCacheEntry, type QueryEntry } from './decodedEntitiesCache.js'; import { entityRelationParentsMap } from './entityRelationParentsMap.js'; import { getEntityRelations } from './getEntityRelations.js'; import { hasValidTypesProperty } from './hasValidTypesProperty.js'; @@ -255,12 +255,11 @@ export function findMany( const filtered: Array> = []; const evaluateFilter = (fieldFilter: EntityFieldFilter, fieldValue: T): boolean => { - 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."); + if ('not' in fieldFilter || 'or' in fieldFilter) { + throw new Error("Logical operators 'not', 'or' are only allowed at the root (cross-field) level."); } - // Handle basic filters + // handle basic filters if ('is' in fieldFilter) { if (typeof fieldValue === 'boolean') { return fieldValue === fieldFilter.is; @@ -316,33 +315,26 @@ export function findMany( crossFieldFilter: CrossFieldFilter>, entity: Entity, ): boolean => { - // Evaluate regular field filters with AND semantics + // evaluate regular field filters with AND semantics for (const fieldName in crossFieldFilter) { if (fieldName === 'or' || fieldName === 'not') continue; - const fieldFilter = crossFieldFilter[fieldName] as unknown as EntityFieldFilter | undefined; + const fieldFilter = crossFieldFilter[fieldName]; if (!fieldFilter) continue; - const fieldValue = (entity as unknown as Record)[fieldName] as unknown; + const fieldValue = entity[fieldName]; 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)); + // evaluate nested OR at cross-field level (if present) + if (Array.isArray(crossFieldFilter.or)) { + const orSatisfied = crossFieldFilter.or.some((orFilter) => evaluateCrossFieldFilter(orFilter, entity)); if (!orSatisfied) return false; } - // 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; - } + // evaluate nested NOT at cross-field level (if present) + if (crossFieldFilter.not && evaluateCrossFieldFilter(crossFieldFilter.not, entity)) { + return false; } return true; @@ -380,7 +372,14 @@ export function findMany( decoded.__schema = type; filtered.push(decoded); } - } catch (_error) { + } catch (error) { + // rethrow in case it's filter error + if ( + error instanceof Error && + error.message.includes("Logical operators 'not', 'or' are only allowed at the root (cross-field) level") + ) { + throw error; + } corruptEntityIds.push(id); } } diff --git a/packages/hypergraph/test/entity/findMany.test.ts b/packages/hypergraph/test/entity/findMany.test.ts index 811426e2..de36fccf 100644 --- a/packages/hypergraph/test/entity/findMany.test.ts +++ b/packages/hypergraph/test/entity/findMany.test.ts @@ -201,7 +201,27 @@ describe('findMany with filters', () => { expect(result.entities.map((e) => e.name).sort()).toEqual(['Bob', 'Jane']); }); - it('should filter entities using NOT operator with number fields', () => { + it.only('should throw an error if NOT operator is used at the field level', () => { + // Create test entities + Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); + Entity.create(handle, Person)({ name: 'Jane', age: 25, isActive: true }); + Entity.create(handle, Person)({ name: 'Bob', age: 40, isActive: false }); + + expect(() => { + // Filter by name NOT equal to 'John' + Entity.findMany( + handle, + Person, + { + // @ts-expect-error + name: { not: { is: 'John' } }, + }, + undefined, + ); + }).toThrowError("Logical operators 'not', 'or' are only allowed at the root (cross-field) level."); + }); + + it.skip('should filter entities using NOT operator with number fields', () => { // Create test entities Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); Entity.create(handle, Person)({ name: 'Jane', age: 25, isActive: true }); @@ -241,6 +261,26 @@ describe('findMany with filters', () => { expect(result.entities.map((e) => e.name).sort()).toEqual(['Jane', 'John']); }); + it('should throw an error if OR operator is used at the field level', () => { + // Create test entities + Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); + Entity.create(handle, Person)({ name: 'Jane', age: 25, isActive: true }); + Entity.create(handle, Person)({ name: 'Bob', age: 40, isActive: false }); + + expect(() => { + // Filter by name NOT equal to 'John' + Entity.findMany( + handle, + Person, + { + // @ts-expect-error + name: { or: [{ is: 'John' }, { is: 'Jane' }] }, + }, + undefined, + ); + }).toThrowError("Logical operators 'not', 'or' are only allowed at the root (cross-field) level."); + }); + it('should filter entities using OR operator with number fields', () => { // Create test entities Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); @@ -281,23 +321,24 @@ describe('findMany with filters', () => { expect(result.entities[0].name).toBe('Bob'); }); - it('should filter entities using NOT with OR operator', () => { + it('should throw an error if NOT operator is used at the field level', () => { // Create test entities Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); Entity.create(handle, Person)({ name: 'Jane', age: 25, isActive: true }); Entity.create(handle, Person)({ name: 'Bob', age: 40, isActive: false }); - // Filter by NOT (name equal to 'John' OR 'Jane') - const result = Entity.findMany( - handle, - Person, - { - not: { or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }] }, - }, - undefined, - ); - expect(result.entities).toHaveLength(1); - expect(result.entities[0].name).toBe('Bob'); + expect(() => { + // Filter by name NOT equal to 'John' + Entity.findMany( + handle, + Person, + { + // @ts-expect-error + name: { not: { or: [{ is: 'John' }, { is: 'Jane' }] } }, + }, + undefined, + ); + }).toThrowError("Logical operators 'not', 'or' are only allowed at the root (cross-field) level."); }); }); From 62f6db60b31ce2c1d604a4d5d45f02e05890c239 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 9 Aug 2025 15:22:27 +0200 Subject: [PATCH 2/5] add changeset --- .changeset/mighty-numbers-run.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/mighty-numbers-run.md diff --git a/.changeset/mighty-numbers-run.md b/.changeset/mighty-numbers-run.md new file mode 100644 index 00000000..c612d810 --- /dev/null +++ b/.changeset/mighty-numbers-run.md @@ -0,0 +1,6 @@ +--- +"@graphprotocol/hypergraph": minor +--- + +rework filter logic to match public filter logic - logic operators are only allowed at the cross-field level + \ No newline at end of file From 55d04c70fde8b612c5e3a9ce2435a5daae7912c7 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 9 Aug 2025 15:25:47 +0200 Subject: [PATCH 3/5] fix import order --- packages/hypergraph/src/entity/findMany.ts | 2 +- packages/hypergraph/test/entity/findMany.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts index 04250255..297d0190 100644 --- a/packages/hypergraph/src/entity/findMany.ts +++ b/packages/hypergraph/src/entity/findMany.ts @@ -3,7 +3,7 @@ import * as Schema from 'effect/Schema'; import { deepMerge } from '../utils/internal/deep-merge.js'; import { isRelationField } from '../utils/isRelationField.js'; import { canonicalize } from '../utils/jsc.js'; -import { decodedEntitiesCache, type DecodedEntitiesCacheEntry, type QueryEntry } from './decodedEntitiesCache.js'; +import { type DecodedEntitiesCacheEntry, decodedEntitiesCache, type QueryEntry } from './decodedEntitiesCache.js'; import { entityRelationParentsMap } from './entityRelationParentsMap.js'; import { getEntityRelations } from './getEntityRelations.js'; import { hasValidTypesProperty } from './hasValidTypesProperty.js'; diff --git a/packages/hypergraph/test/entity/findMany.test.ts b/packages/hypergraph/test/entity/findMany.test.ts index de36fccf..85cfc5b9 100644 --- a/packages/hypergraph/test/entity/findMany.test.ts +++ b/packages/hypergraph/test/entity/findMany.test.ts @@ -201,7 +201,7 @@ describe('findMany with filters', () => { expect(result.entities.map((e) => e.name).sort()).toEqual(['Bob', 'Jane']); }); - it.only('should throw an error if NOT operator is used at the field level', () => { + it('should throw an error if NOT operator is used at the field level', () => { // Create test entities Entity.create(handle, Person)({ name: 'John', age: 30, isActive: true }); Entity.create(handle, Person)({ name: 'Jane', age: 25, isActive: true }); From 2ac1974a39a064acc089a18cc5b9ded3d547c63b Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 9 Aug 2025 15:39:02 +0200 Subject: [PATCH 4/5] Update packages/hypergraph/test/entity/findMany.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/hypergraph/test/entity/findMany.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hypergraph/test/entity/findMany.test.ts b/packages/hypergraph/test/entity/findMany.test.ts index 85cfc5b9..112f42d9 100644 --- a/packages/hypergraph/test/entity/findMany.test.ts +++ b/packages/hypergraph/test/entity/findMany.test.ts @@ -268,7 +268,7 @@ describe('findMany with filters', () => { Entity.create(handle, Person)({ name: 'Bob', age: 40, isActive: false }); expect(() => { - // Filter by name NOT equal to 'John' + // Attempt to use OR operator at the field level (should throw) Entity.findMany( handle, Person, From e08beb7b08f8315f98df56ad870da09af61b59c8 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 9 Aug 2025 15:39:12 +0200 Subject: [PATCH 5/5] Update packages/hypergraph/test/entity/findMany.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/hypergraph/test/entity/findMany.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hypergraph/test/entity/findMany.test.ts b/packages/hypergraph/test/entity/findMany.test.ts index 112f42d9..23c639a3 100644 --- a/packages/hypergraph/test/entity/findMany.test.ts +++ b/packages/hypergraph/test/entity/findMany.test.ts @@ -328,7 +328,7 @@ describe('findMany with filters', () => { Entity.create(handle, Person)({ name: 'Bob', age: 40, isActive: false }); expect(() => { - // Filter by name NOT equal to 'John' + // Filter by name NOT (name is 'John' OR name is 'Jane') Entity.findMany( handle, Person,