-
Notifications
You must be signed in to change notification settings - Fork 242
/
graphPath.test.ts
216 lines (187 loc) · 9.24 KB
/
graphPath.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import {
Field,
FieldDefinition,
Schema,
assert,
buildSupergraphSchema,
} from "@apollo/federation-internals";
import {
GraphPath,
OpGraphPath,
SimultaneousPathsWithLazyIndirectPaths,
advanceSimultaneousPathsWithOperation,
createInitialOptions
} from "../graphPath";
import { QueryGraph, Vertex, buildFederatedQueryGraph } from "../querygraph";
import { emptyContext } from "../pathContext";
import { simpleValidationConditionResolver } from "../conditionsValidation";
function parseSupergraph(subgraphs: number, schema: string): { supergraph: Schema, api: Schema, queryGraph: QueryGraph } {
assert(subgraphs >= 1, 'Should have at least 1 subgraph');
const header = `
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
query: Query
}
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
scalar join__FieldSet
scalar link__Import
enum link__Purpose {
SECURITY
EXECUTION
}
enum join__Graph {
${[...Array(subgraphs).keys()].map((n) => `S${n+1} @join__graph(name: "S${n+1}", url: "https://S${n+1}")`).join('\n')}
}
`;
try {
const supergraph = buildSupergraphSchema(header + schema)[0];
return {
supergraph,
api: supergraph.toAPISchema(),
queryGraph: buildFederatedQueryGraph(supergraph, true),
};
} catch (e) {
throw new Error('Error parsing supergraph schema:\n' + e.toString());
}
}
function createOptions(supergraph: Schema, queryGraph: QueryGraph): SimultaneousPathsWithLazyIndirectPaths<Vertex>[] {
// We know we only use `Query` in the supergraph, so there is only that as root.
const root = queryGraph.roots()[0];
const initialPath: OpGraphPath<Vertex> = GraphPath.create(queryGraph, root);
return createInitialOptions(
initialPath,
emptyContext,
simpleValidationConditionResolver({ supergraph, queryGraph }),
[],
[],
);
}
function field(schema: Schema, coordinate: string): Field {
const def = schema.elementByCoordinate(coordinate) as FieldDefinition<any>;
return new Field(def);
}
describe("advanceSimultaneousPathsWithOperation", () => {
test("do not use key `x` to fetch `x`", () => {
const { supergraph, api, queryGraph } = parseSupergraph(3, `
type Query
@join__type(graph: S1)
{
t: T @join__field(graph: S1)
}
type T
@join__type(graph: S1)
@join__type(graph: S2, key: "otherId")
@join__type(graph: S2, key: "id")
@join__type(graph: S3, key: "id")
{
otherId: ID! @join__field(graph: S1) @join__field(graph: S2)
id: ID! @join__field(graph: S2) @join__field(graph: S3)
}
`);
// Picking the first initial, the one going to S1
const initial = createOptions(supergraph, queryGraph)[0];
// Then picking `t`, which should be just the one option of picking it in S1 at this point.
const allAfterT = advanceSimultaneousPathsWithOperation(supergraph, initial, field(api, "Query.t"));
assert(allAfterT, 'Should have advanced correctly');
expect(allAfterT).toHaveLength(1);
const afterT = allAfterT[0];
expect(afterT.toString()).toBe(`Query(S1) --[t]--> T(S1) (types: [T])`);
// Checking that, at this point, we technically have 2 options:
// 1. we can go to S2 using `otherId`.
// 2. we can go to S3 using `id`, assuming we first get `id` from S2 (using `otherId`).
const indirect = afterT.indirectOptions(afterT.context, 0);
expect(indirect.paths).toHaveLength(2);
expect(indirect.paths[0].toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ otherId } ⊢ key()]--> T(S2) (types: [T])`);
expect(indirect.paths[1].toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ id } ⊢ key()]--> T(S3) (types: [T])`);
const allForId = advanceSimultaneousPathsWithOperation(supergraph, afterT, field(api, "T.id"));
assert(allForId, 'Should have advanced correctly');
// Here, `id` is a direct path from both of our indirect paths. However, it makes no sense to use the 2nd
// indirect path above, since the condition to get to `S3` was `id`, and this means another indirect path
// is able to get to `id` more directly (the first one in this case).
// So ultimately, we should only keep the 1st option.
expect(allForId).toHaveLength(1);
const forId = allForId[0];
expect(forId.toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ otherId } ⊢ key()]--> T(S2) --[id]--> ID(S2)`);
});
test("do not use key containing `x` to fetch `x`", () => {
// Similar to the previous test, but the key used is not exactly the fetch field, it only contains
// it (but the optimisation should still work).
const { supergraph, api, queryGraph } = parseSupergraph(3, `
type Query
@join__type(graph: S1)
{
t: T @join__field(graph: S1)
}
type T
@join__type(graph: S1)
@join__type(graph: S2, key: "otherId")
@join__type(graph: S2, key: "id1 id2")
@join__type(graph: S3, key: "id1 id2")
{
otherId: ID! @join__field(graph: S1) @join__field(graph: S2)
id1: ID! @join__field(graph: S2) @join__field(graph: S3)
id2: ID! @join__field(graph: S2) @join__field(graph: S3)
}
`);
// Picking the first initial, the one going to S1
const initial = createOptions(supergraph, queryGraph)[0];
// Then picking `t`, which should be just the one option of picking it in S1 at this point.
const allAfterT = advanceSimultaneousPathsWithOperation(supergraph, initial, field(api, "Query.t"));
assert(allAfterT, 'Should have advanced correctly');
expect(allAfterT).toHaveLength(1);
const afterT = allAfterT[0];
expect(afterT.toString()).toBe(`Query(S1) --[t]--> T(S1) (types: [T])`);
// Checking that, at this point, we technically have 2 options:
// 1. we can go to S2 using `otherId`.
// 2. we can go to S3 using `id1 id2`, assuming we first get `id1 id2` from S2 (using `otherId`).
const indirect = afterT.indirectOptions(afterT.context, 0);
expect(indirect.paths).toHaveLength(2);
expect(indirect.paths[0].toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ otherId } ⊢ key()]--> T(S2) (types: [T])`);
expect(indirect.paths[1].toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ id1 id2 } ⊢ key()]--> T(S3) (types: [T])`);
const allForId = advanceSimultaneousPathsWithOperation(supergraph, afterT, field(api, "T.id1"));
assert(allForId, 'Should have advanced correctly');
// Here, `id1` is a direct path from both of our indirect paths. However, it makes no sense to use the 2nd
// indirect path above, since the condition to get to `S3` was `id1 id2`, which includes `id1`.
expect(allForId).toHaveLength(1);
const forId = allForId[0];
expect(forId.toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ otherId } ⊢ key()]--> T(S2) --[id1]--> ID(S2)`);
});
test("avoids indirect path that needs a key to the same subgraph to validate its condition", () => {
const { supergraph, api, queryGraph } = parseSupergraph(2, `
type Query
@join__type(graph: S1)
{
t: T @join__field(graph: S1)
}
type T
@join__type(graph: S1)
@join__type(graph: S2, key: "id1")
@join__type(graph: S2, key: "id2")
{
id1: ID! @join__field(graph: S2)
id2: ID! @join__field(graph: S1) @join__field(graph: S2)
}
`);
// Picking the first initial, the one going to S1
const initial = createOptions(supergraph, queryGraph)[0];
// Then picking `t`, which should be just the one option of picking it in S1 at this point.
const allAfterT = advanceSimultaneousPathsWithOperation(supergraph, initial, field(api, "Query.t"));
assert(allAfterT, 'Should have advanced correctly');
expect(allAfterT).toHaveLength(1);
const afterT = allAfterT[0];
expect(afterT.toString()).toBe(`Query(S1) --[t]--> T(S1) (types: [T])`);
// Technically, the `id1` key could be used to go to S2 by first getting `id1` from S2 using `id2`, but
// that's obviously unecessary to consider since we can just use `id2` to go to S2 in the first place.
const indirect = afterT.indirectOptions(afterT.context, 0);
expect(indirect.paths).toHaveLength(1);
expect(indirect.paths[0].toString()).toBe(`Query(S1) --[t]--> T(S1) --[{ id2 } ⊢ key()]--> T(S2) (types: [T])`);
});
});