Skip to content

Commit 64f4e00

Browse files
committed
add query config
1 parent 56f49c4 commit 64f4e00

4 files changed

Lines changed: 177 additions & 19 deletions

File tree

packages/hypergraph/src/entity/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ 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 = {
13+
_config?: RelationIncludeConfig;
14+
} & {
615
[key: string]: RelationIncludeBranch | boolean | undefined;
716
};
817

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

Lines changed: 15 additions & 5 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) {
@@ -53,6 +56,8 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
5356
listField,
5457
includeNodes,
5558
includeTotalCount,
59+
relationSpaces: includeBranch?._config?.relationSpaces,
60+
valueSpaces: includeBranch?._config?.valueSpaces,
5661
};
5762
const nestedRelations: RelationTypeIdInfo[] = [];
5863

@@ -78,8 +83,11 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
7883

7984
const nestedResult = SchemaAST.getAnnotation<string>(Constants.PropertyIdSymbol)(nestedProp.type);
8085
const nestedPropertyName = String(nestedProp.name);
81-
const nestedIncludeBranch = includeBranch?.[nestedPropertyName];
82-
const nestedIncludeNodes = isRelationIncludeBranch(nestedIncludeBranch);
86+
const nestedIncludeBranchCandidate = includeBranch?.[nestedPropertyName];
87+
const nestedIncludeBranch = isRelationIncludeBranch(nestedIncludeBranchCandidate)
88+
? nestedIncludeBranchCandidate
89+
: undefined;
90+
const nestedIncludeNodes = Boolean(nestedIncludeBranch);
8391
const nestedIncludeTotalCount = hasTotalCountFlag(
8492
includeBranch as Record<string, unknown> | undefined,
8593
nestedPropertyName,
@@ -96,6 +104,8 @@ export const getRelationTypeIds = <S extends Schema.Schema.AnyNoContext>(
96104
listField: nestedListField,
97105
includeNodes: nestedIncludeNodes,
98106
includeTotalCount: nestedIncludeTotalCount,
107+
relationSpaces: nestedIncludeBranch?._config?.relationSpaces,
108+
valueSpaces: nestedIncludeBranch?._config?.valueSpaces,
99109
};
100110
nestedRelations.push(nestedInfo);
101111
}

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,56 @@ 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 stringifyUuidList = (spaces: readonly string[]) => `[${spaces.map((space) => JSON.stringify(space)).join(', ')}]`;
6+
7+
const buildValuesListFilter = (
8+
spaceSelectionMode: SpaceSelectionMode,
9+
override?: RelationTypeIdInfo['valueSpaces'],
10+
) => {
11+
if (!override) {
12+
if (spaceSelectionMode === 'single') {
13+
return '(filter: {spaceId: {is: $spaceId}})';
14+
}
15+
if (spaceSelectionMode === 'many') {
16+
return '(filter: {spaceId: {in: $spaceIds}})';
17+
}
18+
return '';
19+
}
20+
21+
if (override === 'all') {
22+
return '';
823
}
9-
if (spaceSelectionMode === 'many') {
10-
return '(filter: {spaceId: {in: $spaceIds}})';
24+
25+
if (override.length === 0) {
26+
return '';
1127
}
12-
return '';
28+
29+
return `(filter: {spaceId: {in: ${stringifyUuidList(override)}}})`;
1330
};
1431

15-
const buildRelationSpaceFilter = (spaceSelectionMode: SpaceSelectionMode) => {
16-
if (spaceSelectionMode === 'single') {
17-
return 'spaceId: {is: $spaceId}, ';
32+
const buildRelationSpaceFilter = (
33+
spaceSelectionMode: SpaceSelectionMode,
34+
override?: RelationTypeIdInfo['relationSpaces'],
35+
) => {
36+
if (!override) {
37+
if (spaceSelectionMode === 'single') {
38+
return 'spaceId: {is: $spaceId}, ';
39+
}
40+
if (spaceSelectionMode === 'many') {
41+
return 'spaceId: {in: $spaceIds}, ';
42+
}
43+
return '';
1844
}
19-
if (spaceSelectionMode === 'many') {
20-
return 'spaceId: {in: $spaceIds}, ';
45+
46+
if (override === 'all') {
47+
return '';
2148
}
22-
return '';
49+
50+
if (override.length === 0) {
51+
return '';
52+
}
53+
54+
return `spaceId: {in: ${stringifyUuidList(override)}}, `;
2355
};
2456

2557
export const getRelationAlias = (typeId: string) => `relations_${typeId.replace(/-/g, '_')}`;
@@ -31,8 +63,8 @@ const buildRelationsListFragment = (info: RelationTypeIdInfo, level: 1 | 2, spac
3163
const connectionField = listField === 'backlinks' ? 'backlinks' : 'relations';
3264
const toEntityField = listField === 'backlinks' ? 'fromEntity' : 'toEntity';
3365
const toEntitySelectionHeader = toEntityField === 'toEntity' ? 'toEntity' : `toEntity: ${toEntityField}`;
34-
const valuesListFilter = buildValuesListFilter(spaceSelectionMode);
35-
const relationSpaceFilter = buildRelationSpaceFilter(spaceSelectionMode);
66+
const valuesListFilter = buildValuesListFilter(spaceSelectionMode, info.valueSpaces);
67+
const relationSpaceFilter = buildRelationSpaceFilter(spaceSelectionMode, info.relationSpaces);
3668

3769
if (!info.includeNodes && !info.includeTotalCount) {
3870
return '';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Id } from '@graphprotocol/grc-20';
2+
import { describe, expect, it } from 'vitest';
3+
import * as Entity from '../../src/entity/index.js';
4+
import * as Type from '../../src/type/type.js';
5+
import { getRelationTypeIds } from '../../src/utils/get-relation-type-ids.js';
6+
import { buildRelationsSelection } from '../../src/utils/relation-query-helpers.js';
7+
8+
const FRIENDS_RELATION_PROPERTY_ID = Id('f44ae32a-2f13-4d3f-875f-19d2338a32b8');
9+
const CHILDREN_RELATION_PROPERTY_ID = Id('8a6dcb99-9c7b-4ca9-9f7b-98f2f404b405');
10+
const PARENT_TYPES = [Id('842e8ae0-9904-40a8-9bfe-19e1f4400c5e')];
11+
const CHILD_TYPES = [Id('cd9a2ae2-831c-4fa2-b714-ad3aa254db7d')];
12+
const FRIEND_TYPES = [Id('35ac5c3e-4f31-466e-b3da-51fdfbb4b38e')];
13+
const NAME_PROPERTY_ID = Id('9f5e7ea4-51bb-4c9f-8739-7fa0aa695d02');
14+
15+
const Friend = Entity.Schema(
16+
{
17+
nickname: Type.String,
18+
},
19+
{
20+
types: FRIEND_TYPES,
21+
properties: {
22+
nickname: NAME_PROPERTY_ID,
23+
},
24+
},
25+
);
26+
27+
const Child = Entity.Schema(
28+
{
29+
name: Type.String,
30+
friends: Type.Relation(Friend),
31+
},
32+
{
33+
types: CHILD_TYPES,
34+
properties: {
35+
name: NAME_PROPERTY_ID,
36+
friends: FRIENDS_RELATION_PROPERTY_ID,
37+
},
38+
},
39+
);
40+
41+
const Parent = Entity.Schema(
42+
{
43+
title: Type.String,
44+
children: Type.Relation(Child),
45+
},
46+
{
47+
types: PARENT_TYPES,
48+
properties: {
49+
title: NAME_PROPERTY_ID,
50+
children: CHILDREN_RELATION_PROPERTY_ID,
51+
},
52+
},
53+
);
54+
55+
describe('relation include config overrides', () => {
56+
it('propagates spaces overrides to query fragments', () => {
57+
const include = {
58+
children: {
59+
_config: {
60+
relationSpaces: ['space-rel', 'space-rel-2'],
61+
valueSpaces: ['space-values'],
62+
},
63+
friends: {
64+
_config: {
65+
relationSpaces: 'all',
66+
},
67+
},
68+
},
69+
} satisfies Entity.EntityInclude<typeof Parent>;
70+
71+
const relationInfo = getRelationTypeIds(Parent, include);
72+
expect(relationInfo[0]).toMatchObject({
73+
relationSpaces: ['space-rel', 'space-rel-2'],
74+
valueSpaces: ['space-values'],
75+
});
76+
expect(relationInfo[0]?.children?.[0]).toMatchObject({
77+
relationSpaces: 'all',
78+
});
79+
80+
const selection = buildRelationsSelection(relationInfo, 'single');
81+
82+
expect(selection).toContain('spaceId: {in: ["space-rel", "space-rel-2"]}');
83+
expect(selection).toContain('valuesList(filter: {spaceId: {in: ["space-values"]}})');
84+
expect(selection.split('relations_f44ae32a_2f13_4d3f_875f_19d2338a32b8')[0]).not.toContain(
85+
'spaceId: {is: $spaceId}',
86+
);
87+
});
88+
89+
it('omits filters entirely when overrides use "all"', () => {
90+
const include = {
91+
children: {
92+
_config: {
93+
relationSpaces: 'all',
94+
valueSpaces: 'all',
95+
},
96+
},
97+
} satisfies Entity.EntityInclude<typeof Parent>;
98+
99+
const relationInfo = getRelationTypeIds(Parent, include);
100+
const selection = buildRelationsSelection(relationInfo, 'single');
101+
102+
expect(selection).not.toContain('spaceId: {is: $spaceId}');
103+
expect(selection).not.toContain('spaceId: {in: $spaceIds}');
104+
expect(selection).not.toContain('valuesList(filter: {spaceId: {is: $spaceId}})');
105+
expect(selection).not.toContain('valuesList(filter: {spaceId: {in: $spaceIds}})');
106+
});
107+
});

0 commit comments

Comments
 (0)