Skip to content

Commit e04e477

Browse files
authored
Nik/query config (#569)
1 parent 56f49c4 commit e04e477

8 files changed

Lines changed: 299 additions & 24 deletions

File tree

.changeset/warm-oranges-check.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@graphprotocol/hypergraph": patch
3+
"@graphprotocol/hypergraph-react": patch
4+
---
5+
6+
Allow relation includes to override nested relation and value space filters by adding _config: { relationSpaces, valueSpaces } to any include branch; GraphQL fragments now honor those overrides when building queries.
7+
8+
```
9+
include: {
10+
friends: {
11+
_config: {
12+
relationSpaces: ['space-a', 'space-b'],
13+
valueSpaces: 'all',
14+
},
15+
},
16+
}
17+
```
18+

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

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

55
export const Route = createLazyFileRoute('/podcasts')({
66
component: RouteComponent,
@@ -22,6 +22,25 @@ function RouteComponent() {
2222
// }, 1000);
2323
// }, []);
2424

25+
const {
26+
data: person,
27+
invalidEntity: personInvalidEntity,
28+
invalidRelationEntities: personInvalidRelationEntities,
29+
} = useEntity(Person, {
30+
id: '9800a4e8-8437-4310-9af6-ac91644f7c26',
31+
mode: 'public',
32+
space: '95a4a1cc-bfcc-4038-b7a1-02c513d27700',
33+
include: {
34+
skills: {
35+
_config: {
36+
relationSpaces: ['95a4a1cc-bfcc-4038-b7a1-02c513d27700'],
37+
valueSpaces: ['021265e2-d839-47c3-8d03-0ee3dfb29ffc', '95a4a1cc-bfcc-4038-b7a1-02c513d27700'],
38+
},
39+
},
40+
},
41+
});
42+
console.log({ person, personInvalidEntity, personInvalidRelationEntities });
43+
2544
const {
2645
data: podcast,
2746
invalidEntity,

apps/events/src/schema.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SystemIds } from '@graphprotocol/grc-20';
1+
import { ContentIds, SystemIds } from '@graphprotocol/grc-20';
22
import { Entity, Id, Type } from '@graphprotocol/hypergraph';
33

44
export const User = Entity.Schema(
@@ -119,18 +119,32 @@ export const Project = Entity.Schema(
119119
},
120120
);
121121

122+
export const Skill = Entity.Schema(
123+
{
124+
name: Type.String,
125+
},
126+
{
127+
types: [ContentIds.SKILL_TYPE],
128+
properties: {
129+
name: SystemIds.NAME_PROPERTY,
130+
},
131+
},
132+
);
133+
122134
export const Person = Entity.Schema(
123135
{
124136
name: Type.String,
125137
description: Type.optional(Type.String),
126138
avatar: Type.Relation(Image),
139+
skills: Type.Relation(Skill),
127140
},
128141
{
129142
types: [Id('7ed45f2b-c48b-419e-8e46-64d5ff680b0d')],
130143
properties: {
131144
name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
132145
description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'),
133146
avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'),
147+
skills: Id(ContentIds.SKILLS_PROPERTY),
134148
},
135149
},
136150
);

docs/docs/query-public-data.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,36 @@ const { data, isPending, isError } = useEntities(Event, {
6262

6363
For deeper relations you can use the `include` parameter multiple levels deep. Currently two levels of relations are supported for public data.
6464

65+
#### Controlling include scopes with `_config`
66+
67+
Each branch within `include` can optionally carry a `_config` object that lets you override which spaces Hypergraph will inspect for the relation edges and the related entity values. When you omit `_config`, the query automatically reuses the `space`/`spaces` selection you passed to `useEntities`, `useEntity`, `Entity.findOnePublic`, `Entity.findManyPublic` and `Entity.searchManyPublic` helpers.
68+
69+
```ts
70+
const { data: project } = useEntity(Project, {
71+
id: '9f130661-8c3f-4db7-9bdc-3ce69631c5ef',
72+
mode: 'public',
73+
space: '3f32353d-3b27-4a13-b71a-746f06e1f7db',
74+
include: {
75+
contributors: {
76+
_config: {
77+
relationSpaces: ['3f32353d-3b27-4a13-b71a-746f06e1f7db', '95a4a1cc-bfcc-4038-b7a1-02c513d27700'],
78+
valueSpaces: 'all',
79+
},
80+
organizations: {
81+
_config: {
82+
valueSpaces: ['95a4a1cc-bfcc-4038-b7a1-02c513d27700'],
83+
},
84+
},
85+
},
86+
},
87+
});
88+
```
89+
90+
- `relationSpaces` controls which spaces are searched for the relation edges themselves (`relations`/`backlinks`). Pass an array to whitelist specific spaces, `'all'` to drop the filter entirely, or `[]` if you intentionally want the branch to match nothing.
91+
- `valueSpaces` applies the same override to the `valuesList` lookups for the related entities. This lets you fetch relation edges from one space while trusting the canonical values that live in another.
92+
93+
Each nested branch can have its own `_config` settings,so you can attach `_config` anywhere within the two supported include levels. Mix and match the settings per branch to stitch together data that spans multiple public spaces without issuing separate queries.
94+
6595
### Querying from a specific space
6696

6797
You can also query from a specific space by passing in the `space` parameter.

packages/hypergraph/src/entity/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ import type * as Schema from 'effect/Schema';
22

33
type SchemaKey<S extends Schema.Schema.AnyNoContext> = Extract<keyof Schema.Schema.Type<S>, string>;
44

5+
export type RelationSpacesOverride = 'all' | readonly string[];
6+
7+
export type RelationIncludeConfig = {
8+
relationSpaces?: RelationSpacesOverride;
9+
valueSpaces?: RelationSpacesOverride;
10+
};
11+
512
export type RelationIncludeBranch = {
6-
[key: string]: RelationIncludeBranch | boolean | undefined;
13+
_config?: RelationIncludeConfig;
14+
} & {
15+
[key: string]: RelationIncludeBranch | RelationIncludeConfig | boolean | undefined;
716
};
817

918
export type EntityInclude<S extends Schema.Schema.AnyNoContext> = Partial<

packages/hypergraph/src/utils/get-relation-type-ids.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Constants, Utils } from '@graphprotocol/hypergraph';
22
import * as Option from 'effect/Option';
33
import type * as Schema from 'effect/Schema';
44
import * as SchemaAST from 'effect/SchemaAST';
5-
import type { EntityInclude, RelationIncludeBranch } from '../entity/types.js';
5+
import type { EntityInclude, RelationIncludeBranch, RelationSpacesOverride } from '../entity/types.js';
66

77
export type RelationListField = 'relations' | 'backlinks';
88

@@ -12,6 +12,8 @@ export type RelationTypeIdInfo = {
1212
listField: RelationListField;
1313
includeNodes: boolean;
1414
includeTotalCount: boolean;
15+
relationSpaces?: RelationSpacesOverride;
16+
valueSpaces?: RelationSpacesOverride;
1517
children?: RelationTypeIdInfo[];
1618
};
1719

@@ -35,8 +37,9 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
3537
const result = SchemaAST.getAnnotation<string>(Constants.PropertyIdSymbol)(prop.type);
3638
if (Option.isSome(result)) {
3739
const propertyName = String(prop.name);
38-
const includeBranch = include?.[propertyName as keyof EntityInclude<S>] as RelationIncludeBranch | undefined;
39-
const includeNodes = isRelationIncludeBranch(includeBranch);
40+
const includeBranchCandidate = include?.[propertyName as keyof EntityInclude<S>];
41+
const includeBranch = isRelationIncludeBranch(includeBranchCandidate) ? includeBranchCandidate : undefined;
42+
const includeNodes = Boolean(includeBranch);
4043
const includeTotalCount = hasTotalCountFlag(include as Record<string, unknown> | undefined, propertyName);
4144

4245
if (!includeNodes && !includeTotalCount) {
@@ -47,13 +50,24 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
4750
Option.getOrElse(() => false),
4851
);
4952
const listField: RelationListField = isBacklink ? 'backlinks' : 'relations';
50-
const level1Info: RelationTypeIdInfo = {
53+
const relationSpaces = includeBranch?._config?.relationSpaces;
54+
const valueSpaces = includeBranch?._config?.valueSpaces;
55+
56+
const level1InfoBase: RelationTypeIdInfo = {
5157
typeId: result.value,
5258
propertyName,
5359
listField,
5460
includeNodes,
5561
includeTotalCount,
5662
};
63+
const level1Info: RelationTypeIdInfo =
64+
relationSpaces === undefined && valueSpaces === undefined
65+
? level1InfoBase
66+
: {
67+
...level1InfoBase,
68+
...(relationSpaces !== undefined ? { relationSpaces } : {}),
69+
...(valueSpaces !== undefined ? { valueSpaces } : {}),
70+
};
5771
const nestedRelations: RelationTypeIdInfo[] = [];
5872

5973
if (!SchemaAST.isTupleType(prop.type)) {
@@ -78,8 +92,11 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
7892

7993
const nestedResult = SchemaAST.getAnnotation<string>(Constants.PropertyIdSymbol)(nestedProp.type);
8094
const nestedPropertyName = String(nestedProp.name);
81-
const nestedIncludeBranch = includeBranch?.[nestedPropertyName];
82-
const nestedIncludeNodes = isRelationIncludeBranch(nestedIncludeBranch);
95+
const nestedIncludeBranchCandidate = includeBranch?.[nestedPropertyName];
96+
const nestedIncludeBranch = isRelationIncludeBranch(nestedIncludeBranchCandidate)
97+
? nestedIncludeBranchCandidate
98+
: undefined;
99+
const nestedIncludeNodes = Boolean(nestedIncludeBranch);
83100
const nestedIncludeTotalCount = hasTotalCountFlag(
84101
includeBranch as Record<string, unknown> | undefined,
85102
nestedPropertyName,
@@ -90,13 +107,23 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
90107
nestedProp.type,
91108
).pipe(Option.getOrElse(() => false));
92109
const nestedListField: RelationListField = nestedIsBacklink ? 'backlinks' : 'relations';
93-
const nestedInfo: RelationTypeIdInfo = {
110+
const nestedRelationSpaces = nestedIncludeBranch?._config?.relationSpaces;
111+
const nestedValueSpaces = nestedIncludeBranch?._config?.valueSpaces;
112+
const nestedInfoBase: RelationTypeIdInfo = {
94113
typeId: nestedResult.value,
95114
propertyName: nestedPropertyName,
96115
listField: nestedListField,
97116
includeNodes: nestedIncludeNodes,
98117
includeTotalCount: nestedIncludeTotalCount,
99118
};
119+
const nestedInfo: RelationTypeIdInfo =
120+
nestedRelationSpaces === undefined && nestedValueSpaces === undefined
121+
? nestedInfoBase
122+
: {
123+
...nestedInfoBase,
124+
...(nestedRelationSpaces !== undefined ? { relationSpaces: nestedRelationSpaces } : {}),
125+
...(nestedValueSpaces !== undefined ? { valueSpaces: nestedValueSpaces } : {}),
126+
};
100127
nestedRelations.push(nestedInfo);
101128
}
102129
}

packages/hypergraph/src/utils/relation-query-helpers.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,58 @@ import type { RelationTypeIdInfo } from './get-relation-type-ids.js';
22

33
type SpaceSelectionMode = 'single' | 'many' | 'all';
44

5-
const buildValuesListFilter = (spaceSelectionMode: SpaceSelectionMode) => {
6-
if (spaceSelectionMode === 'single') {
7-
return '(filter: {spaceId: {is: $spaceId}})';
5+
const formatGraphQLStringArray = (values: readonly string[]) =>
6+
`[${values.map((value) => JSON.stringify(value)).join(', ')}]`;
7+
8+
const buildValuesListFilter = (
9+
spaceSelectionMode: SpaceSelectionMode,
10+
override?: RelationTypeIdInfo['valueSpaces'],
11+
) => {
12+
if (!override) {
13+
if (spaceSelectionMode === 'single') {
14+
return '(filter: {spaceId: {is: $spaceId}})';
15+
}
16+
if (spaceSelectionMode === 'many') {
17+
return '(filter: {spaceId: {in: $spaceIds}})';
18+
}
19+
return '';
820
}
9-
if (spaceSelectionMode === 'many') {
10-
return '(filter: {spaceId: {in: $spaceIds}})';
21+
22+
if (override === 'all') {
23+
return '';
24+
}
25+
26+
if (override.length === 0) {
27+
// Explicit empty overrides should produce a match-nothing filter.
28+
return '(filter: {spaceId: {in: []}})';
1129
}
12-
return '';
30+
31+
return `(filter: {spaceId: {in: ${formatGraphQLStringArray(override)}}})`;
1332
};
1433

15-
const buildRelationSpaceFilter = (spaceSelectionMode: SpaceSelectionMode) => {
16-
if (spaceSelectionMode === 'single') {
17-
return 'spaceId: {is: $spaceId}, ';
34+
const buildRelationSpaceFilter = (
35+
spaceSelectionMode: SpaceSelectionMode,
36+
override?: RelationTypeIdInfo['relationSpaces'],
37+
) => {
38+
if (!override) {
39+
if (spaceSelectionMode === 'single') {
40+
return 'spaceId: {is: $spaceId}, ';
41+
}
42+
if (spaceSelectionMode === 'many') {
43+
return 'spaceId: {in: $spaceIds}, ';
44+
}
45+
return '';
46+
}
47+
48+
if (override === 'all') {
49+
return '';
1850
}
19-
if (spaceSelectionMode === 'many') {
20-
return 'spaceId: {in: $spaceIds}, ';
51+
52+
if (override.length === 0) {
53+
return 'spaceId: {in: []}, ';
2154
}
22-
return '';
55+
56+
return `spaceId: {in: ${formatGraphQLStringArray(override)}}, `;
2357
};
2458

2559
export const getRelationAlias = (typeId: string) => `relations_${typeId.replace(/-/g, '_')}`;
@@ -31,8 +65,8 @@ const buildRelationsListFragment = (info: RelationTypeIdInfo, level: 1 | 2, spac
3165
const connectionField = listField === 'backlinks' ? 'backlinks' : 'relations';
3266
const toEntityField = listField === 'backlinks' ? 'fromEntity' : 'toEntity';
3367
const toEntitySelectionHeader = toEntityField === 'toEntity' ? 'toEntity' : `toEntity: ${toEntityField}`;
34-
const valuesListFilter = buildValuesListFilter(spaceSelectionMode);
35-
const relationSpaceFilter = buildRelationSpaceFilter(spaceSelectionMode);
68+
const valuesListFilter = buildValuesListFilter(spaceSelectionMode, info.valueSpaces);
69+
const relationSpaceFilter = buildRelationSpaceFilter(spaceSelectionMode, info.relationSpaces);
3670

3771
if (!info.includeNodes && !info.includeTotalCount) {
3872
return '';

0 commit comments

Comments
 (0)