Skip to content

Commit 9cc82be

Browse files
authored
feat(core): tighten filter types per (breaking) (#453)
1 parent 367e521 commit 9cc82be

File tree

3 files changed

+34
-28
lines changed

3 files changed

+34
-28
lines changed

packages/hypergraph/src/entity/findMany.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -255,17 +255,9 @@ export function findMany<const S extends AnyNoContext>(
255255
const filtered: Array<Entity<S>> = [];
256256

257257
const evaluateFilter = <T>(fieldFilter: EntityFieldFilter<T>, fieldValue: T): boolean => {
258-
// Handle NOT operator
259-
if ('not' in fieldFilter && fieldFilter.not) {
260-
return !evaluateFilter(fieldFilter.not, fieldValue);
261-
}
262-
263-
// Handle OR operator
264-
if ('or' in fieldFilter) {
265-
const orFilters = fieldFilter.or;
266-
if (Array.isArray(orFilters)) {
267-
return orFilters.some((orFilter) => evaluateFilter(orFilter as EntityFieldFilter<T>, fieldValue));
268-
}
258+
const ff = fieldFilter as unknown as Record<string, unknown>;
259+
if ('not' in ff || 'or' in ff || 'and' in ff) {
260+
throw new Error("Logical operators 'not', 'or', 'and' are only allowed at the root (cross-field) level.");
269261
}
270262

271263
// Handle basic filters
@@ -324,14 +316,35 @@ export function findMany<const S extends AnyNoContext>(
324316
crossFieldFilter: CrossFieldFilter<Schema.Schema.Type<S>>,
325317
entity: Entity<S>,
326318
): boolean => {
319+
// Evaluate regular field filters with AND semantics
327320
for (const fieldName in crossFieldFilter) {
328-
const fieldFilter = crossFieldFilter[fieldName];
329-
const fieldValue = entity[fieldName];
321+
if (fieldName === 'or' || fieldName === 'not') continue;
322+
const fieldFilter = crossFieldFilter[fieldName] as unknown as EntityFieldFilter<unknown> | undefined;
323+
if (!fieldFilter) continue;
324+
const fieldValue = (entity as unknown as Record<string, unknown>)[fieldName] as unknown;
325+
if (!evaluateFilter(fieldFilter, fieldValue)) {
326+
return false;
327+
}
328+
}
329+
330+
// Evaluate nested OR at cross-field level (if present)
331+
const cf = crossFieldFilter as unknown as Record<string, unknown>;
332+
const maybeOr = cf.or;
333+
if (Array.isArray(maybeOr)) {
334+
const orFilters = maybeOr as Array<CrossFieldFilter<Schema.Schema.Type<S>>>;
335+
const orSatisfied = orFilters.some((orFilter) => evaluateCrossFieldFilter(orFilter, entity));
336+
if (!orSatisfied) return false;
337+
}
330338

331-
if (fieldFilter && !evaluateFilter(fieldFilter, fieldValue)) {
339+
// Evaluate nested NOT at cross-field level (if present)
340+
const maybeNot = cf.not;
341+
if (maybeNot) {
342+
const notFilter = maybeNot as CrossFieldFilter<Schema.Schema.Type<S>>;
343+
if (evaluateCrossFieldFilter(notFilter, entity)) {
332344
return false;
333345
}
334346
}
347+
335348
return true;
336349
};
337350

packages/hypergraph/src/entity/types.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,13 @@ export type EntityNumberFilter = {
5555
is?: number;
5656
greaterThan?: number;
5757
lessThan?: number;
58-
not?: EntityNumberFilter;
59-
or?: EntityNumberFilter[];
6058
};
6159

6260
export type EntityStringFilter = {
6361
is?: string;
6462
startsWith?: string;
6563
endsWith?: string;
6664
contains?: string;
67-
not?: EntityStringFilter;
68-
or?: EntityStringFilter[];
6965
};
7066

7167
export type CrossFieldFilter<T> = {
@@ -77,8 +73,6 @@ export type CrossFieldFilter<T> = {
7773

7874
export type EntityFieldFilter<T> = {
7975
is?: T;
80-
not?: EntityFieldFilter<T>;
81-
or?: Array<EntityFieldFilter<T>>;
8276
} & (T extends boolean
8377
? {
8478
is?: boolean;

packages/hypergraph/test/entity/findMany.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('findMany with filters', () => {
193193
handle,
194194
Person,
195195
{
196-
name: { not: { is: 'John' } },
196+
not: { name: { is: 'John' } },
197197
},
198198
undefined,
199199
);
@@ -212,7 +212,7 @@ describe('findMany with filters', () => {
212212
handle,
213213
Person,
214214
{
215-
age: { not: { is: 30 } },
215+
not: { age: { is: 30 } },
216216
},
217217
undefined,
218218
);
@@ -233,7 +233,7 @@ describe('findMany with filters', () => {
233233
handle,
234234
Person,
235235
{
236-
name: { or: [{ is: 'John' }, { is: 'Jane' }] },
236+
or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }],
237237
},
238238
undefined,
239239
);
@@ -252,7 +252,7 @@ describe('findMany with filters', () => {
252252
handle,
253253
Person,
254254
{
255-
age: { or: [{ is: 25 }, { is: 40 }] },
255+
or: [{ age: { is: 25 } }, { age: { is: 40 } }],
256256
},
257257
undefined,
258258
);
@@ -273,7 +273,7 @@ describe('findMany with filters', () => {
273273
handle,
274274
Person,
275275
{
276-
name: { not: { or: [{ is: 'John' }, { is: 'Jane' }] } },
276+
not: { or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }] },
277277
},
278278
undefined,
279279
);
@@ -292,7 +292,7 @@ describe('findMany with filters', () => {
292292
handle,
293293
Person,
294294
{
295-
name: { not: { or: [{ is: 'John' }, { is: 'Jane' }] } },
295+
not: { or: [{ name: { is: 'John' } }, { name: { is: 'Jane' } }] },
296296
},
297297
undefined,
298298
);
@@ -384,8 +384,7 @@ describe('findMany with filters', () => {
384384
handle,
385385
Person,
386386
{
387-
name: { not: { startsWith: 'J' } },
388-
age: { not: { greaterThan: 35 } },
387+
not: { or: [{ name: { startsWith: 'J' } }, { age: { greaterThan: 35 } }] },
389388
},
390389
undefined,
391390
);

0 commit comments

Comments
 (0)