Skip to content

Commit c1efd89

Browse files
committed
add multi spaces filter
1 parent b8bae14 commit c1efd89

14 files changed

Lines changed: 271 additions & 51 deletions

File tree

.changeset/shy-bugs-obey.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@graphprotocol/hypergraph-react": patch
3+
"@graphprotocol/hypergraph": patch
4+
---
5+
6+
Add support for querying public entities across multiple spaces (including an `all` scope) and expose the new API through the React hooks
7+

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

Lines changed: 12 additions & 2 deletions
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, Topic } from '@/schema';
3+
import { Podcast, Space, Topic } from '@/schema';
44

55
export const Route = createLazyFileRoute('/podcasts')({
66
component: RouteComponent,
@@ -54,6 +54,8 @@ function RouteComponent() {
5454
backlinksTotalCountsTypeId1: '972d201a-d780-4568-9e01-543f67b26bee',
5555
});
5656

57+
console.log({ data, isLoading, isError });
58+
5759
const { data: topics } = useEntities(Topic, {
5860
mode: 'public',
5961
first: 10,
@@ -74,7 +76,15 @@ function RouteComponent() {
7476

7577
console.log({ topics });
7678

77-
console.log({ data, isLoading, isError });
79+
const { data: spaces } = useEntities(Space, {
80+
mode: 'public',
81+
spaces: 'all',
82+
include: {
83+
avatar: {},
84+
},
85+
});
86+
console.log('spaces', spaces);
87+
7888
return (
7989
<>
8090
<h1>Podcasts</h1>

apps/events/src/schema.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SystemIds } from '@graphprotocol/grc-20';
12
import { Entity, Id, Type } from '@graphprotocol/hypergraph';
23

34
export const User = Entity.Schema(
@@ -304,3 +305,19 @@ export const Episode = Entity.Schema(
304305
},
305306
},
306307
);
308+
309+
export const Space = Entity.Schema(
310+
{
311+
name: Type.String,
312+
avatar: Type.Relation(Image),
313+
},
314+
{
315+
types: [Id('362c1dbd-dc64-44bb-a3c4-652f38a642d7')],
316+
properties: {
317+
name: Id(SystemIds.NAME_PROPERTY),
318+
avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'),
319+
},
320+
},
321+
);
322+
323+
export type Space = Entity.Entity<typeof Space>;

packages/hypergraph-react/src/hooks/use-entities-public-infinite.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,24 @@ export const useEntitiesPublicInfinite = <S extends Schema.Schema.AnyNoContext>(
1010
type: S,
1111
params?: QueryPublicParams<S>,
1212
) => {
13-
const { enabled = true, filter, include, space: spaceFromParams, first = 2, offset = 0 } = params ?? {};
13+
const { enabled = true, filter, include, space: spaceFromParams, spaces, first = 2, offset = 0 } = params ?? {};
1414
const { space: spaceFromContext } = useHypergraphSpaceInternal();
1515
const space = spaceFromParams ?? spaceFromContext;
16+
const spaceSelectionKey = spaces ?? space;
1617
const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
1718
Option.getOrElse(() => []),
1819
);
1920

2021
const result = useInfiniteQueryTanstack({
21-
queryKey: ['hypergraph-public-entities', space, typeIds, include, filter, 'infinite'],
22+
queryKey: ['hypergraph-public-entities', spaceSelectionKey, typeIds, include, filter, 'infinite'],
2223
queryFn: async ({ pageParam }) => {
23-
return Entity.findManyPublic(type, { filter, include, space, first, offset: pageParam });
24+
return Entity.findManyPublic(type, {
25+
filter,
26+
include,
27+
...(spaces ? { spaces } : { space }),
28+
first,
29+
offset: pageParam,
30+
});
2431
},
2532
getNextPageParam: (_lastPage, pages) => {
2633
return offset + pages.length * first;

packages/hypergraph-react/src/hooks/use-entities.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import type { Entity } from '@graphprotocol/hypergraph';
22
import type * as Schema from 'effect/Schema';
33
import { useEntitiesPrivate } from '../internal/use-entities-private.js';
44
import { useEntitiesPublic } from '../internal/use-entities-public.js';
5+
import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js';
56

6-
type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
7+
type SpaceSelectionInputOptionInContext = {
8+
space?: never;
9+
spaces?: never;
10+
};
11+
12+
type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = (
13+
| Entity.SpaceSelectionInput
14+
| SpaceSelectionInputOptionInContext
15+
) & {
716
mode: 'public' | 'private';
817
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
918
// TODO: restrict multi-level nesting to the actual relation keys
1019
include?: Entity.EntityInclude<S> | undefined;
11-
space?: string | undefined;
1220
first?: number | undefined;
1321
offset?: number | undefined;
1422
orderBy?:
@@ -21,18 +29,21 @@ type UseEntitiesParams<S extends Schema.Schema.AnyNoContext> = {
2129
};
2230

2331
export function useEntities<const S extends Schema.Schema.AnyNoContext>(type: S, params: UseEntitiesParams<S>) {
24-
const { mode, filter, include, space, first, offset, orderBy, backlinksTotalCountsTypeId1 } = params;
32+
const { mode, filter, include, space, spaces, first, offset, orderBy, backlinksTotalCountsTypeId1 } = params;
33+
const { space: spaceFromContext } = useHypergraphSpaceInternal();
34+
const resolvedSpace = space ?? spaceFromContext;
35+
const publicSpaceParams = spaces ? { spaces } : { space: resolvedSpace };
2536
const publicResult = useEntitiesPublic(type, {
2637
enabled: mode === 'public',
2738
filter,
2839
include,
2940
first,
3041
offset,
31-
space,
3242
orderBy,
3343
backlinksTotalCountsTypeId1,
44+
...publicSpaceParams,
3445
});
35-
const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space });
46+
const localResult = useEntitiesPrivate(type, { enabled: mode === 'private', filter, include, space: resolvedSpace });
3647

3748
if (mode === 'public') {
3849
return {

packages/hypergraph-react/src/internal/types.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,4 @@ import type * as Schema from 'effect/Schema';
33

44
export type QueryPublicParams<S extends Schema.Schema.AnyNoContext> = {
55
enabled?: boolean | undefined;
6-
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
7-
// TODO: restrict multi-level nesting to the actual relation keys
8-
include?: Entity.EntityInclude<S> | undefined;
9-
space?: string | undefined;
10-
first?: number | undefined;
11-
offset?: number | undefined;
12-
orderBy?:
13-
| {
14-
property: keyof Schema.Schema.Type<S>;
15-
direction: 'asc' | 'desc';
16-
}
17-
| undefined;
18-
backlinksTotalCountsTypeId1?: string | undefined;
19-
};
6+
} & Entity.FindManyPublicParams<S>;

packages/hypergraph-react/src/internal/use-entities-public.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
1212
filter,
1313
include,
1414
space: spaceFromParams,
15+
spaces,
1516
first = 100,
1617
offset,
1718
orderBy,
1819
backlinksTotalCountsTypeId1,
1920
} = params ?? {};
2021
const { space: spaceFromContext } = useHypergraphSpaceInternal();
2122
const space = spaceFromParams ?? spaceFromContext;
23+
const spaceSelectionKey = spaces ?? space;
2224
const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
2325
Option.getOrElse(() => []),
2426
);
2527

2628
const result = useQueryTanstack({
2729
queryKey: [
2830
'hypergraph-public-entities',
29-
space,
31+
spaceSelectionKey,
3032
typeIds,
3133
include,
3234
filter,
@@ -39,7 +41,7 @@ export const useEntitiesPublic = <S extends Schema.Schema.AnyNoContext>(type: S,
3941
return Entity.findManyPublic(type, {
4042
filter,
4143
include,
42-
space,
44+
...(spaces ? { spaces } : { space }),
4345
first,
4446
offset,
4547
orderBy,

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

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { request } from 'graphql-request';
88
import type { RelationsListWithNodes } from '../utils/convert-relations.js';
99
import type { RelationTypeIdInfo } from '../utils/get-relation-type-ids.js';
1010
import { buildRelationsSelection } from '../utils/relation-query-helpers.js';
11+
import type { SpaceSelection } from './internal/space-selection.js';
12+
import { normalizeSpaceSelection } from './internal/space-selection.js';
13+
import type { SpaceSelectionInput } from './types.js';
1114

12-
export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
15+
export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = SpaceSelectionInput & {
1316
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
1417
// TODO: restrict multi-level nesting to the actual relation keys
1518
include?: Entity.EntityInclude<S> | undefined;
16-
space: string;
1719
first?: number | undefined;
1820
offset?: number | undefined;
1921
orderBy?:
@@ -25,34 +27,73 @@ export type FindManyPublicParams<S extends Schema.Schema.AnyNoContext> = {
2527
backlinksTotalCountsTypeId1?: string | undefined;
2628
};
2729

28-
const buildEntitiesQuery = (relationInfoLevel1: RelationTypeIdInfo[], useOrderBy: boolean) => {
29-
const level1Relations = buildRelationsSelection(relationInfoLevel1);
30+
const buildEntitiesQuery = (
31+
relationInfoLevel1: RelationTypeIdInfo[],
32+
useOrderBy: boolean,
33+
spaceSelection: SpaceSelection,
34+
) => {
35+
const level1Relations = buildRelationsSelection(relationInfoLevel1, spaceSelection.mode);
3036

3137
const queryName = useOrderBy ? 'entitiesOrderedByProperty' : 'entities';
32-
const orderByParams = useOrderBy ? '$propertyId: UUID!, $sortDirection: SortOrder!, ' : '';
38+
const variableDefinitions = [
39+
spaceSelection.mode === 'single'
40+
? '$spaceId: UUID!'
41+
: spaceSelection.mode === 'many'
42+
? '$spaceIds: [UUID!]!'
43+
: undefined,
44+
'$typeIds: [UUID!]!',
45+
useOrderBy ? '$propertyId: UUID!' : undefined,
46+
useOrderBy ? '$sortDirection: SortOrder!' : undefined,
47+
'$first: Int',
48+
'$filter: EntityFilter!',
49+
'$offset: Int',
50+
'$backlinksTotalCountsTypeId1: UUID',
51+
'$backlinksTotalCountsTypeId1Present: Boolean!',
52+
]
53+
.filter(Boolean)
54+
.join(', ');
55+
3356
const orderByArgs = useOrderBy ? 'propertyId: $propertyId\n sortDirection: $sortDirection\n ' : '';
57+
const entitySpaceFilter =
58+
spaceSelection.mode === 'single'
59+
? 'spaceIds: {in: [$spaceId]},'
60+
: spaceSelection.mode === 'many'
61+
? 'spaceIds: {in: $spaceIds},'
62+
: '';
63+
const valuesListFilter =
64+
spaceSelection.mode === 'single'
65+
? '(filter: { spaceId: { is: $spaceId } })'
66+
: spaceSelection.mode === 'many'
67+
? '(filter: { spaceId: { in: $spaceIds } })'
68+
: '';
69+
const backlinksSpaceFilter =
70+
spaceSelection.mode === 'single'
71+
? 'spaceId: {is: $spaceId}, '
72+
: spaceSelection.mode === 'many'
73+
? 'spaceId: {in: $spaceIds}, '
74+
: '';
3475

3576
return `
36-
query ${queryName}($spaceId: UUID!, $typeIds: [UUID!]!, ${orderByParams}$first: Int, $filter: EntityFilter!, $offset: Int, $backlinksTotalCountsTypeId1: UUID, $backlinksTotalCountsTypeId1Present: Boolean!) {
77+
query ${queryName}(${variableDefinitions}) {
3778
entities: ${queryName}(
3879
${orderByArgs}filter: { and: [{
3980
relations: {some: {typeId: {is: "8f151ba4-de20-4e3c-9cb4-99ddf96f48f1"}, toEntityId: {in: $typeIds}}},
40-
spaceIds: {in: [$spaceId]},
81+
${entitySpaceFilter}
4182
}, $filter]}
4283
first: $first
4384
offset: $offset
4485
) {
4586
id
4687
name
47-
valuesList(filter: {spaceId: {is: $spaceId}}) {
88+
valuesList${valuesListFilter} {
4889
propertyId
4990
string
5091
boolean
5192
number
5293
time
5394
point
5495
}
55-
backlinksTotalCountsTypeId1: backlinks(filter: { spaceId: {is: $spaceId}, fromEntity: { typeIds: { is: [$backlinksTotalCountsTypeId1] } }}) @include(if: $backlinksTotalCountsTypeId1Present) {
96+
backlinksTotalCountsTypeId1: backlinks(filter: { ${backlinksSpaceFilter}fromEntity: { typeIds: { is: [$backlinksTotalCountsTypeId1] } }}) @include(if: $backlinksTotalCountsTypeId1Present) {
5697
totalCount
5798
}
5899
${level1Relations}
@@ -150,7 +191,16 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
150191
type: S,
151192
params?: FindManyPublicParams<S>,
152193
) => {
153-
const { filter, include, space, first = 100, offset = 0, orderBy, backlinksTotalCountsTypeId1 } = params ?? {};
194+
const {
195+
filter,
196+
include,
197+
space,
198+
spaces,
199+
first = 100,
200+
offset = 0,
201+
orderBy,
202+
backlinksTotalCountsTypeId1,
203+
} = params ?? {};
154204

155205
// constructing the relation type ids for the query
156206
const relationTypeIds = Utils.getRelationTypeIds(type, include);
@@ -187,18 +237,26 @@ export const findManyPublic = async <S extends Schema.Schema.AnyNoContext>(
187237
}
188238

189239
// Build the query dynamically with aliases for each relation type ID
190-
const queryDocument = buildEntitiesQuery(relationTypeIds, Boolean(orderBy));
240+
const spaceSelection = normalizeSpaceSelection(space, spaces);
241+
242+
// Build the query dynamically with aliases for each relation type ID
243+
const queryDocument = buildEntitiesQuery(relationTypeIds, Boolean(orderBy), spaceSelection);
191244

192245
const filterParams = filter ? Utils.translateFilterToGraphql(filter, type) : {};
193246

194247
const queryVariables: Record<string, unknown> = {
195-
spaceId: space,
196248
typeIds,
197249
first,
198250
filter: filterParams,
199251
offset,
200252
};
201253

254+
if (spaceSelection.mode === 'single') {
255+
queryVariables.spaceId = spaceSelection.spaceId;
256+
} else if (spaceSelection.mode === 'many') {
257+
queryVariables.spaceIds = spaceSelection.spaceIds;
258+
}
259+
202260
if (orderByPropertyId && sortDirection) {
203261
queryVariables.propertyId = orderByPropertyId;
204262
queryVariables.sortDirection = sortDirection;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type FindOnePublicParams<S extends Schema.Schema.AnyNoContext> = {
2121
};
2222

2323
const buildEntityQuery = (relationInfoLevel1: RelationTypeIdInfo[]) => {
24-
const relationsSelection = buildRelationsSelection(relationInfoLevel1);
24+
const relationsSelection = buildRelationsSelection(relationInfoLevel1, 'single');
2525
const relationsSelectionBlock = relationsSelection ? `\n ${relationsSelection}\n` : '';
2626
return `
2727
query entity($id: UUID!, $spaceId: UUID!) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type SpaceSelection =
2+
| {
3+
mode: 'single';
4+
spaceId: string;
5+
}
6+
| {
7+
mode: 'many';
8+
spaceIds: readonly [string, ...string[]];
9+
}
10+
| {
11+
mode: 'all';
12+
};
13+
14+
export const normalizeSpaceSelection = (
15+
space: string | undefined,
16+
spaces: readonly [string, ...string[]] | 'all' | undefined,
17+
): SpaceSelection => {
18+
if (space && spaces) {
19+
throw new Error('Provide either "space" or "spaces", not both.');
20+
}
21+
22+
if (space) {
23+
return { mode: 'single', spaceId: space };
24+
}
25+
26+
if (spaces === 'all') {
27+
return { mode: 'all' };
28+
}
29+
30+
if (spaces && spaces.length > 0) {
31+
return { mode: 'many', spaceIds: spaces };
32+
}
33+
34+
throw new Error('Either "space" or non-empty "spaces" must be provided.');
35+
};

0 commit comments

Comments
 (0)