Skip to content

Commit 5ea0ad9

Browse files
authored
filter for relation existence (#559)
1 parent f0d8b3d commit 5ea0ad9

File tree

6 files changed

+132
-15
lines changed

6 files changed

+132
-15
lines changed

.changeset/fast-geese-mate.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@graphprotocol/hypergraph": patch
3+
"@graphprotocol/hypergraph-react": patch
4+
---
5+
6+
extend filter capability to check for existing relation e.g. `filter: { cover: { exists: true } }`
7+

apps/events/src/routes/podcasts.lazy.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEntities } from '@graphprotocol/hypergraph-react';
22
import { createLazyFileRoute } from '@tanstack/react-router';
3-
import { Podcast } from '@/schema';
3+
import { Podcast, Topic } from '@/schema';
44

55
export const Route = createLazyFileRoute('/podcasts')({
66
component: RouteComponent,
@@ -53,6 +53,23 @@ function RouteComponent() {
5353
orderBy: { property: 'dateFounded', direction: 'asc' },
5454
backlinksTotalCountsTypeId1: '972d201a-d780-4568-9e01-543f67b26bee',
5555
});
56+
57+
const { data: topics } = useEntities(Topic, {
58+
mode: 'public',
59+
first: 10,
60+
space: space,
61+
filter: {
62+
cover: {
63+
exists: true,
64+
},
65+
},
66+
include: {
67+
cover: {},
68+
},
69+
});
70+
71+
console.log({ topics });
72+
5673
console.log({ data, isLoading, isError });
5774
return (
5875
<>

packages/hypergraph/src/entity/find-many-public.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,6 @@ export const parseResult = <S extends Schema.Schema.AnyNoContext>(
127127
...Utils.convertRelations(queryEntity, ast, relationInfoLevel1),
128128
};
129129

130-
console.log('rawEntity', rawEntity);
131-
132130
const decodeResult = decode({
133131
...rawEntity,
134132
__deleted: false,

packages/hypergraph/src/entity/types.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,34 @@ export type CrossFieldFilter<T> = {
6464
not?: CrossFieldFilter<T>;
6565
};
6666

67-
export type EntityFieldFilter<T> = {
68-
is?: T;
69-
} & (T extends boolean
67+
type RelationExistsFilter<T> = [T] extends [readonly unknown[] | undefined]
7068
? {
71-
is?: boolean;
69+
exists?: boolean;
7270
}
73-
: T extends number
71+
: Record<never, never>;
72+
73+
type ScalarFieldFilter<T> = [T] extends [readonly unknown[] | undefined]
74+
? Record<never, never>
75+
: T extends boolean
7476
? {
75-
greaterThan?: number;
76-
lessThan?: number;
77+
is?: boolean;
7778
}
78-
: T extends string
79+
: T extends number
7980
? {
80-
startsWith?: string;
81-
endsWith?: string;
82-
contains?: string;
81+
greaterThan?: number;
82+
lessThan?: number;
8383
}
84-
: Record<string, never>);
84+
: T extends string
85+
? {
86+
startsWith?: string;
87+
endsWith?: string;
88+
contains?: string;
89+
}
90+
: Record<string, never>;
91+
92+
export type EntityFieldFilter<T> = {
93+
is?: T;
94+
} & RelationExistsFilter<T> &
95+
ScalarFieldFilter<T>;
8596

8697
export type EntityFilter<T> = CrossFieldFilter<T>;

packages/hypergraph/src/utils/translate-filter-to-graphql.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ type GraphqlFilterEntry =
3131
| {
3232
and: GraphqlFilterEntry[];
3333
}
34+
| {
35+
relations: {
36+
some: {
37+
typeId: { is: string };
38+
};
39+
};
40+
}
3441
| { [k: string]: never };
3542

3643
/**
@@ -47,6 +54,14 @@ export function translateFilterToGraphql<S extends Schema.Schema.AnyNoContext>(
4754

4855
const graphqlFilter: GraphqlFilterEntry[] = [];
4956

57+
const buildRelationExistsFilter = (propertyId: string): GraphqlFilterEntry => ({
58+
relations: {
59+
some: {
60+
typeId: { is: propertyId },
61+
},
62+
},
63+
});
64+
5065
for (const [fieldName, fieldFilter] of Object.entries(filter)) {
5166
if (fieldName === 'or') {
5267
graphqlFilter.push({
@@ -77,6 +92,23 @@ export function translateFilterToGraphql<S extends Schema.Schema.AnyNoContext>(
7792

7893
if (!Option.isSome(propertyId) || !Option.isSome(propertyType)) continue;
7994

95+
if (propertyType.value === 'relation') {
96+
const relationFilter = fieldFilter as { exists?: boolean };
97+
98+
if (relationFilter.exists === true) {
99+
graphqlFilter.push(buildRelationExistsFilter(propertyId.value));
100+
continue;
101+
}
102+
103+
if (relationFilter.exists === false) {
104+
const existsFilter = buildRelationExistsFilter(propertyId.value);
105+
graphqlFilter.push({
106+
not: existsFilter,
107+
});
108+
continue;
109+
}
110+
}
111+
80112
if (
81113
propertyType.value === 'string' &&
82114
(fieldFilter.is || fieldFilter.startsWith || fieldFilter.endsWith || fieldFilter.contains)

packages/hypergraph/test/utils/translate-filter-to-graphql.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,32 @@ import type * as Schema from 'effect/Schema';
44
import { describe, expect, it } from 'vitest';
55
import { translateFilterToGraphql } from '../../src/utils/translate-filter-to-graphql.js';
66

7+
export const User = Entity.Schema(
8+
{
9+
username: Type.String,
10+
},
11+
{
12+
types: [Id('f6fa5a6a-7dbf-4c31-aba5-7b4cd0a9b2de')],
13+
properties: {
14+
username: Id('f0dfb5c0-3c90-4d30-98a3-6a139c8b5943'),
15+
},
16+
},
17+
);
18+
719
export const Todo = Entity.Schema(
820
{
921
name: Type.String,
1022
completed: Type.Boolean,
1123
priority: Type.Number,
24+
assignees: Type.Relation(User),
1225
},
1326
{
1427
types: [Id('a288444f-06a3-4037-9ace-66fe325864d0')],
1528
properties: {
1629
name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
1730
completed: Id('d2d64cd3-a337-4784-9e30-25bea0349471'),
1831
priority: Id('ee920534-42ce-4113-a63b-8f3c889dd772'),
32+
assignees: Id('f399677c-2bf9-40c3-9622-815be7b83344'),
1933
},
2034
},
2135
);
@@ -147,6 +161,44 @@ describe('translateFilterToGraphql number filters', () => {
147161
});
148162
});
149163

164+
describe('translateFilterToGraphql relation filters', () => {
165+
it('should translate relation `exists` filter correctly', () => {
166+
const filter: TodoFilter = {
167+
// @ts-expect-error - this is a test
168+
assignees: { exists: true },
169+
};
170+
171+
const result = translateFilterToGraphql(filter, Todo);
172+
173+
expect(result).toEqual({
174+
relations: {
175+
some: {
176+
typeId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' },
177+
},
178+
},
179+
});
180+
});
181+
182+
it('should translate relation `exists: false` filter correctly', () => {
183+
const filter: TodoFilter = {
184+
// @ts-expect-error - this is a test
185+
assignees: { exists: false },
186+
};
187+
188+
const result = translateFilterToGraphql(filter, Todo);
189+
190+
expect(result).toEqual({
191+
not: {
192+
relations: {
193+
some: {
194+
typeId: { is: 'f399677c-2bf9-40c3-9622-815be7b83344' },
195+
},
196+
},
197+
},
198+
});
199+
});
200+
});
201+
150202
describe('translateFilterToGraphql multiple filters', () => {
151203
it('should translate multiple filters correctly', () => {
152204
const filter: TodoFilter = {

0 commit comments

Comments
 (0)