@apollo/federation-internals 2.0.0-alpha.2 → 2.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +2 -18
- package/dist/debug.js.map +1 -1
- package/dist/definitions.d.ts +11 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +54 -0
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +87 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +143 -5
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +40 -3
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +4 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +192 -53
- package/dist/federation.js.map +1 -1
- package/dist/genErrorCodeDoc.d.ts +2 -0
- package/dist/genErrorCodeDoc.d.ts.map +1 -0
- package/dist/genErrorCodeDoc.js +55 -0
- package/dist/genErrorCodeDoc.js.map +1 -0
- package/dist/tagSpec.js +3 -1
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
- package/src/__tests__/subgraphValidation.test.ts +452 -0
- package/src/debug.ts +2 -19
- package/src/definitions.ts +98 -0
- package/src/error.ts +334 -7
- package/src/extractSubgraphsFromSupergraph.ts +49 -4
- package/src/federation.ts +229 -85
- package/src/genErrorCodeDoc.ts +69 -0
- package/src/tagSpec.ts +4 -4
- package/src/utils.ts +27 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { DocumentNode } from 'graphql';
|
|
2
|
+
import gql from 'graphql-tag';
|
|
3
|
+
import { errorCauses } from '..';
|
|
4
|
+
import { buildSubgraph } from "../federation"
|
|
5
|
+
|
|
6
|
+
// Builds the provided subgraph (using name 'S' for the subgraph) and, if the
|
|
7
|
+
// subgraph is invalid/has errors, return those errors as a list of [code, message].
|
|
8
|
+
// If the subgraph is valid, return undefined.
|
|
9
|
+
function buildForErrors(subgraphDefs: DocumentNode, subgraphName: string = 'S'): [string, string][] | undefined {
|
|
10
|
+
try {
|
|
11
|
+
buildSubgraph(subgraphName, subgraphDefs);
|
|
12
|
+
return undefined;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
const causes = errorCauses(e);
|
|
15
|
+
if (!causes) {
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
return causes.map((err) => [err.extensions.code, err.message]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('fieldset-based directives', () => {
|
|
23
|
+
it('rejects field defined with arguments in @key', () => {
|
|
24
|
+
const subgraph = gql`
|
|
25
|
+
type Query {
|
|
26
|
+
t: T
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type T @key(fields: "f") {
|
|
30
|
+
f(x: Int): Int
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
34
|
+
['KEY_FIELDS_HAS_ARGS', '[S] On type "T", for @key(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @key)']
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects field defined with arguments in @provides', () => {
|
|
39
|
+
const subgraph = gql`
|
|
40
|
+
type Query {
|
|
41
|
+
t: T @provides(fields: "f")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type T {
|
|
45
|
+
f(x: Int): Int @external
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
49
|
+
['PROVIDES_FIELDS_HAS_ARGS', '[S] On field "Query.t", for @provides(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @provides)']
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects field defined with arguments in @requires', () => {
|
|
54
|
+
const subgraph = gql`
|
|
55
|
+
type Query {
|
|
56
|
+
t: T
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type T {
|
|
60
|
+
f(x: Int): Int @external
|
|
61
|
+
g: Int @requires(fields: "f")
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
65
|
+
['REQUIRES_FIELDS_HAS_ARGS', '[S] On field "T.g", for @requires(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @requires)']
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects @provides on non-external fields', () => {
|
|
70
|
+
const subgraph = gql`
|
|
71
|
+
type Query {
|
|
72
|
+
t: T @provides(fields: "f")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type T {
|
|
76
|
+
f: Int
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
80
|
+
['PROVIDES_FIELDS_MISSING_EXTERNAL', '[S] On field "Query.t", for @provides(fields: "f"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)']
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects @requires on non-external fields', () => {
|
|
85
|
+
const subgraph = gql`
|
|
86
|
+
type Query {
|
|
87
|
+
t: T
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type T {
|
|
91
|
+
f: Int
|
|
92
|
+
g: Int @requires(fields: "f")
|
|
93
|
+
}
|
|
94
|
+
`
|
|
95
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
96
|
+
['REQUIRES_FIELDS_MISSING_EXTERNAL', '[S] On field "T.g", for @requires(fields: "f"): field "T.f" should not be part of a @requires since it is already provided by this subgraph (it is not marked @external)']
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects @key on interfaces', () => {
|
|
101
|
+
const subgraph = gql`
|
|
102
|
+
type Query {
|
|
103
|
+
t: T
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface T @key(fields: "f") {
|
|
107
|
+
f: Int
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
111
|
+
['KEY_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @key on interface "T": @key is not yet supported on interfaces'],
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('rejects @provides on interfaces', () => {
|
|
116
|
+
const subgraph = gql`
|
|
117
|
+
type Query {
|
|
118
|
+
t: T
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface T {
|
|
122
|
+
f: U @provides(fields: "g")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type U {
|
|
126
|
+
g: Int @external
|
|
127
|
+
}
|
|
128
|
+
`
|
|
129
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
130
|
+
['PROVIDES_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @provides on field "T.f" of parent type "T": @provides is not yet supported within interfaces'],
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('rejects @requires on interfaces', () => {
|
|
135
|
+
const subgraph = gql`
|
|
136
|
+
type Query {
|
|
137
|
+
t: T
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface T {
|
|
141
|
+
f: Int @external
|
|
142
|
+
g: Int @requires(fields: "f")
|
|
143
|
+
}
|
|
144
|
+
`
|
|
145
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
146
|
+
['REQUIRES_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @requires on field "T.g" of parent type "T": @requires is not yet supported within interfaces' ],
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('rejects unused @external', () => {
|
|
151
|
+
const subgraph = gql`
|
|
152
|
+
type Query {
|
|
153
|
+
t: T
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type T {
|
|
157
|
+
f: Int @external
|
|
158
|
+
}
|
|
159
|
+
`
|
|
160
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
161
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).'],
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('rejects @provides on non-object fields', () => {
|
|
166
|
+
const subgraph = gql`
|
|
167
|
+
type Query {
|
|
168
|
+
t: Int @provides(fields: "f")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type T {
|
|
172
|
+
f: Int
|
|
173
|
+
}
|
|
174
|
+
`
|
|
175
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
176
|
+
['PROVIDES_ON_NON_OBJECT_FIELD', '[S] Invalid @provides directive on field "Query.t": field has type "Int" which is not a Composite Type'],
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('rejects a non-string argument to @key', () => {
|
|
181
|
+
const subgraph = gql`
|
|
182
|
+
type Query {
|
|
183
|
+
t: T
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type T @key(fields: ["f"]) {
|
|
187
|
+
f: Int
|
|
188
|
+
}
|
|
189
|
+
`
|
|
190
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
191
|
+
['KEY_INVALID_FIELDS_TYPE', '[S] On type "T", for @key(fields: ["f"]): Invalid value for argument "fields": must be a string.'],
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('rejects a non-string argument to @provides', () => {
|
|
196
|
+
const subgraph = gql`
|
|
197
|
+
type Query {
|
|
198
|
+
t: T @provides(fields: ["f"])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type T {
|
|
202
|
+
f: Int @external
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
// Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on
|
|
206
|
+
// `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully
|
|
207
|
+
// not a big deal (having errors dependencies is not exactly unheard of).
|
|
208
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
209
|
+
['PROVIDES_INVALID_FIELDS_TYPE', '[S] On field "Query.t", for @provides(fields: ["f"]): Invalid value for argument "fields": must be a string.'],
|
|
210
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('rejects a non-string argument to @requires', () => {
|
|
215
|
+
const subgraph = gql`
|
|
216
|
+
type Query {
|
|
217
|
+
t: T
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type T {
|
|
221
|
+
f: Int @external
|
|
222
|
+
g: Int @requires(fields: ["f"])
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
// Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on
|
|
226
|
+
// `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully
|
|
227
|
+
// not a big deal (having errors dependencies is not exactly unheard of).
|
|
228
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
229
|
+
['REQUIRES_INVALID_FIELDS_TYPE', '[S] On field "T.g", for @requires(fields: ["f"]): Invalid value for argument "fields": must be a string.'],
|
|
230
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Special case of non-string argument, specialized because it hits a different
|
|
235
|
+
// code-path due to enum values being parsed as string and requiring special care.
|
|
236
|
+
it('rejects an enum-like argument to @key', () => {
|
|
237
|
+
const subgraph = gql`
|
|
238
|
+
type Query {
|
|
239
|
+
t: T
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
type T @key(fields: f) {
|
|
243
|
+
f: Int
|
|
244
|
+
}
|
|
245
|
+
`
|
|
246
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
247
|
+
['KEY_INVALID_FIELDS_TYPE', '[S] On type "T", for @key(fields: f): Invalid value for argument "fields": must be a string.'],
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Special case of non-string argument, specialized because it hits a different
|
|
252
|
+
// code-path due to enum values being parsed as string and requiring special care.
|
|
253
|
+
it('rejects an enum-lik argument to @provides', () => {
|
|
254
|
+
const subgraph = gql`
|
|
255
|
+
type Query {
|
|
256
|
+
t: T @provides(fields: f)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type T {
|
|
260
|
+
f: Int @external
|
|
261
|
+
}
|
|
262
|
+
`
|
|
263
|
+
// Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on
|
|
264
|
+
// `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully
|
|
265
|
+
// not a big deal (having errors dependencies is not exactly unheard of).
|
|
266
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
267
|
+
['PROVIDES_INVALID_FIELDS_TYPE', '[S] On field "Query.t", for @provides(fields: f): Invalid value for argument "fields": must be a string.'],
|
|
268
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
269
|
+
]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Special case of non-string argument, specialized because it hits a different
|
|
273
|
+
// code-path due to enum values being parsed as string and requiring special care.
|
|
274
|
+
it('rejects an enum-like argument to @requires', () => {
|
|
275
|
+
const subgraph = gql`
|
|
276
|
+
type Query {
|
|
277
|
+
t: T
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type T {
|
|
281
|
+
f: Int @external
|
|
282
|
+
g: Int @requires(fields: f)
|
|
283
|
+
}
|
|
284
|
+
`
|
|
285
|
+
// Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on
|
|
286
|
+
// `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully
|
|
287
|
+
// not a big deal (having errors dependencies is not exactly unheard of).
|
|
288
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
289
|
+
['REQUIRES_INVALID_FIELDS_TYPE', '[S] On field "T.g", for @requires(fields: f): Invalid value for argument "fields": must be a string.'],
|
|
290
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('rejects an invalid `fields` argument to @key', () => {
|
|
295
|
+
const subgraph = gql`
|
|
296
|
+
type Query {
|
|
297
|
+
t: T
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
type T @key(fields: ":f") {
|
|
301
|
+
f: Int
|
|
302
|
+
}
|
|
303
|
+
`
|
|
304
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
305
|
+
['KEY_INVALID_FIELDS', '[S] On type "T", for @key(fields: ":f"): Syntax Error: Expected Name, found ":".'],
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('rejects an invalid `fields` argument to @provides', () => {
|
|
310
|
+
const subgraph = gql`
|
|
311
|
+
type Query {
|
|
312
|
+
t: T @provides(fields: "{{f}}")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type T {
|
|
316
|
+
f: Int @external
|
|
317
|
+
}
|
|
318
|
+
`
|
|
319
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
320
|
+
['PROVIDES_INVALID_FIELDS', '[S] On field "Query.t", for @provides(fields: "{{f}}"): Syntax Error: Expected Name, found "{".'],
|
|
321
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('rejects an invalid `fields` argument to @requires', () => {
|
|
326
|
+
const subgraph = gql`
|
|
327
|
+
type Query {
|
|
328
|
+
t: T
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type T {
|
|
332
|
+
f: Int @external
|
|
333
|
+
g: Int @requires(fields: "f b")
|
|
334
|
+
}
|
|
335
|
+
`
|
|
336
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
337
|
+
['REQUIRES_INVALID_FIELDS', '[S] On field "T.g", for @requires(fields: "f b"): Cannot query field "b" on type "T" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).'],
|
|
338
|
+
['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ],
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('rejects @key on a list field', () => {
|
|
343
|
+
const subgraph = gql`
|
|
344
|
+
type Query {
|
|
345
|
+
t: T
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
type T @key(fields: "f") {
|
|
349
|
+
f: [Int]
|
|
350
|
+
}
|
|
351
|
+
`
|
|
352
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
353
|
+
['KEY_FIELDS_SELECT_INVALID_TYPE', '[S] On type "T", for @key(fields: "f"): field "T.f" is a List type which is not allowed in @key'],
|
|
354
|
+
]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('rejects @key on an interface field', () => {
|
|
358
|
+
const subgraph = gql`
|
|
359
|
+
type Query {
|
|
360
|
+
t: T
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
type T @key(fields: "f") {
|
|
364
|
+
f: I
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
interface I {
|
|
368
|
+
i: Int
|
|
369
|
+
}
|
|
370
|
+
`
|
|
371
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
372
|
+
['KEY_FIELDS_SELECT_INVALID_TYPE', '[S] On type "T", for @key(fields: "f"): field "T.f" is a Interface type which is not allowed in @key'],
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('rejects @key on an union field', () => {
|
|
377
|
+
const subgraph = gql`
|
|
378
|
+
type Query {
|
|
379
|
+
t: T
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
type T @key(fields: "f") {
|
|
383
|
+
f: U
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
union U = Query | T
|
|
387
|
+
`
|
|
388
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
389
|
+
['KEY_FIELDS_SELECT_INVALID_TYPE', '[S] On type "T", for @key(fields: "f"): field "T.f" is a Union type which is not allowed in @key'],
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('root types', () => {
|
|
395
|
+
it('rejects using Query as type name if not the query root', () => {
|
|
396
|
+
const subgraph = gql`
|
|
397
|
+
schema {
|
|
398
|
+
query: MyQuery
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
type MyQuery {
|
|
402
|
+
f: Int
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type Query {
|
|
406
|
+
g: Int
|
|
407
|
+
}
|
|
408
|
+
`
|
|
409
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
410
|
+
['ROOT_QUERY_USED', '[S] The schema has a type named "Query" but it is not set as the query root type ("MyQuery" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'],
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('rejects using Mutation as type name if not the mutation root', () => {
|
|
415
|
+
const subgraph = gql`
|
|
416
|
+
schema {
|
|
417
|
+
mutation: MyMutation
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
type MyMutation {
|
|
421
|
+
f: Int
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
type Mutation {
|
|
425
|
+
g: Int
|
|
426
|
+
}
|
|
427
|
+
`
|
|
428
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
429
|
+
['ROOT_MUTATION_USED', '[S] The schema has a type named "Mutation" but it is not set as the mutation root type ("MyMutation" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'],
|
|
430
|
+
]);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('rejects using Subscription as type name if not the subscription root', () => {
|
|
434
|
+
const subgraph = gql`
|
|
435
|
+
schema {
|
|
436
|
+
subscription: MySubscription
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
type MySubscription {
|
|
440
|
+
f: Int
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
type Subscription {
|
|
444
|
+
g: Int
|
|
445
|
+
}
|
|
446
|
+
`
|
|
447
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
448
|
+
['ROOT_SUBSCRIPTION_USED', '[S] The schema has a type named "Subscription" but it is not set as the subscription root type ("MySubscription" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'],
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
package/src/debug.ts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
// Simple debugging facility.
|
|
2
2
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
function stringIsBoolean(str?: string) : boolean | undefined {
|
|
6
|
-
if (!str) {
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
switch (str.toLocaleLowerCase()) {
|
|
10
|
-
case "true":
|
|
11
|
-
case "yes":
|
|
12
|
-
case "1":
|
|
13
|
-
return true;
|
|
14
|
-
case "false":
|
|
15
|
-
case "no":
|
|
16
|
-
case "0":
|
|
17
|
-
return false;
|
|
18
|
-
default:
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
4
|
+
import { validateStringContainsBoolean } from './utils';
|
|
22
5
|
|
|
23
6
|
function indentString(indentLevel: number) : string {
|
|
24
7
|
let str = "";
|
|
@@ -30,7 +13,7 @@ function indentString(indentLevel: number) : string {
|
|
|
30
13
|
|
|
31
14
|
function isEnabled(name: string): boolean {
|
|
32
15
|
const v = process.env.APOLLO_FEDERATION_DEBUG;
|
|
33
|
-
const bool =
|
|
16
|
+
const bool = validateStringContainsBoolean(v);
|
|
34
17
|
if (bool !== undefined) {
|
|
35
18
|
return bool;
|
|
36
19
|
}
|
package/src/definitions.ts
CHANGED
|
@@ -697,6 +697,26 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
|
|
|
697
697
|
return toReturn;
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
/**
|
|
701
|
+
* Removes this this definition _and_, recursively, any other elements that references this type and would be invalid
|
|
702
|
+
* after the removal.
|
|
703
|
+
*
|
|
704
|
+
* Note that contrarily to `remove()` (which this method essentially call recursively), this method leaves the schema
|
|
705
|
+
* valid (assuming it was valid beforehand) _unless_ all the schema ends up being removed through recursion (in which
|
|
706
|
+
* case this leaves an empty schema, and that is not technically valid).
|
|
707
|
+
*
|
|
708
|
+
* Also note that this method does _not_ necessarily remove all the elements that reference this type: for instance,
|
|
709
|
+
* if this type is an interface, objects implementing it will _not_ be removed, they will simply stop implementing
|
|
710
|
+
* the interface. In practice, this method mainly remove fields that were using the removed type (in either argument or
|
|
711
|
+
* return type), but it can also remove object/input object/interface if through such field removal some type ends up
|
|
712
|
+
* empty, and it can remove unions if through that removal process and union becomes empty.
|
|
713
|
+
*/
|
|
714
|
+
removeRecursive(): void {
|
|
715
|
+
this.remove().forEach(ref => this.removeReferenceRecursive(ref));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
protected abstract removeReferenceRecursive(ref: TReferencer): void;
|
|
719
|
+
|
|
700
720
|
referencers(): readonly TReferencer[] {
|
|
701
721
|
return setValues(this._referencers);
|
|
702
722
|
}
|
|
@@ -1530,6 +1550,10 @@ export class ScalarType extends BaseNamedType<OutputTypeReferencer | InputTypeRe
|
|
|
1530
1550
|
protected removeInnerElements(): void {
|
|
1531
1551
|
// No inner elements
|
|
1532
1552
|
}
|
|
1553
|
+
|
|
1554
|
+
protected removeReferenceRecursive(ref: OutputTypeReferencer | InputTypeReferencer): void {
|
|
1555
|
+
ref.remove();
|
|
1556
|
+
}
|
|
1533
1557
|
}
|
|
1534
1558
|
|
|
1535
1559
|
export class InterfaceImplementation<T extends ObjectType | InterfaceType> extends BaseExtensionMember<T> {
|
|
@@ -1753,6 +1777,20 @@ export class ObjectType extends FieldBasedType<ObjectType, ObjectTypeReferencer>
|
|
|
1753
1777
|
const schema = this.schema();
|
|
1754
1778
|
return schema.schemaDefinition.root('query')?.type === this;
|
|
1755
1779
|
}
|
|
1780
|
+
|
|
1781
|
+
protected removeReferenceRecursive(ref: ObjectTypeReferencer): void {
|
|
1782
|
+
// Note that the ref can also be a`SchemaDefinition`, but don't have anything to do then.
|
|
1783
|
+
switch (ref.kind) {
|
|
1784
|
+
case 'FieldDefinition':
|
|
1785
|
+
ref.removeRecursive();
|
|
1786
|
+
break;
|
|
1787
|
+
case 'UnionType':
|
|
1788
|
+
if (ref.membersCount() === 0) {
|
|
1789
|
+
ref.removeRecursive();
|
|
1790
|
+
}
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1756
1794
|
}
|
|
1757
1795
|
|
|
1758
1796
|
export class InterfaceType extends FieldBasedType<InterfaceType, InterfaceTypeReferencer> {
|
|
@@ -1771,6 +1809,14 @@ export class InterfaceType extends FieldBasedType<InterfaceType, InterfaceTypeRe
|
|
|
1771
1809
|
const typeName = typeof type === 'string' ? type : type.name;
|
|
1772
1810
|
return this.possibleRuntimeTypes().some(t => t.name == typeName);
|
|
1773
1811
|
}
|
|
1812
|
+
|
|
1813
|
+
protected removeReferenceRecursive(ref: InterfaceTypeReferencer): void {
|
|
1814
|
+
// Note that an interface can be referenced by an object/interface that implements it, but after remove(), said object/interface
|
|
1815
|
+
// will simply not implement "this" anymore and we have nothing more to do.
|
|
1816
|
+
if (ref.kind === 'FieldDefinition') {
|
|
1817
|
+
ref.removeRecursive();
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1774
1820
|
}
|
|
1775
1821
|
|
|
1776
1822
|
export class UnionMember extends BaseExtensionMember<UnionType> {
|
|
@@ -1895,6 +1941,10 @@ export class UnionType extends BaseNamedType<OutputTypeReferencer, UnionType> {
|
|
|
1895
1941
|
protected hasNonExtensionInnerElements(): boolean {
|
|
1896
1942
|
return this.members().some(m => m.ofExtension() === undefined);
|
|
1897
1943
|
}
|
|
1944
|
+
|
|
1945
|
+
protected removeReferenceRecursive(ref: OutputTypeReferencer): void {
|
|
1946
|
+
ref.removeRecursive();
|
|
1947
|
+
}
|
|
1898
1948
|
}
|
|
1899
1949
|
|
|
1900
1950
|
export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
|
|
@@ -1949,6 +1999,10 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
|
|
|
1949
1999
|
protected hasNonExtensionInnerElements(): boolean {
|
|
1950
2000
|
return this._values.some(v => v.ofExtension() === undefined);
|
|
1951
2001
|
}
|
|
2002
|
+
|
|
2003
|
+
protected removeReferenceRecursive(ref: OutputTypeReferencer): void {
|
|
2004
|
+
ref.removeRecursive();
|
|
2005
|
+
}
|
|
1952
2006
|
}
|
|
1953
2007
|
|
|
1954
2008
|
export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObjectType> {
|
|
@@ -2019,6 +2073,19 @@ export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObj
|
|
|
2019
2073
|
protected hasNonExtensionInnerElements(): boolean {
|
|
2020
2074
|
return this.fields().some(f => f.ofExtension() === undefined);
|
|
2021
2075
|
}
|
|
2076
|
+
|
|
2077
|
+
protected removeReferenceRecursive(ref: InputTypeReferencer): void {
|
|
2078
|
+
if (ref.kind === 'ArgumentDefinition') {
|
|
2079
|
+
// Not only do we want to remove the argument, but we want to remove its parent. Technically, only removing the argument would
|
|
2080
|
+
// leave the schema in a valid state so it would be an option, but this feel a bit too weird of a behaviour in practice for a
|
|
2081
|
+
// method calling `removeRecursive`. And in particular, it would mean that if the argument is a directive definition one,
|
|
2082
|
+
// we'd also have to update each of the directive application to remove the correspond argument. Removing the full directive
|
|
2083
|
+
// definition (and all its applications) feels a bit more predictable.
|
|
2084
|
+
ref.parent().removeRecursive();
|
|
2085
|
+
} else {
|
|
2086
|
+
ref.removeRecursive();
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2022
2089
|
}
|
|
2023
2090
|
|
|
2024
2091
|
class BaseWrapperType<T extends Type> {
|
|
@@ -2192,6 +2259,19 @@ export class FieldDefinition<TParent extends CompositeType> extends NamedSchemaE
|
|
|
2192
2259
|
return [];
|
|
2193
2260
|
}
|
|
2194
2261
|
|
|
2262
|
+
/**
|
|
2263
|
+
* Like `remove()`, but if this field was the last field of its parent type, the parent type is removed through its `removeRecursive` method.
|
|
2264
|
+
*/
|
|
2265
|
+
removeRecursive(): void {
|
|
2266
|
+
const parent = this._parent;
|
|
2267
|
+
this.remove();
|
|
2268
|
+
// Note that we exclude the union type here because it doesn't have the `fields()` method, but the only field unions can have is the __typename
|
|
2269
|
+
// one and it cannot be removed, so remove() above will actually throw in practice before reaching this.
|
|
2270
|
+
if (parent && !isUnionType(parent) && parent.fields().length === 0) {
|
|
2271
|
+
parent.removeRecursive();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2195
2275
|
toString(): string {
|
|
2196
2276
|
const args = this._args.size == 0
|
|
2197
2277
|
? ""
|
|
@@ -2251,6 +2331,17 @@ export class InputFieldDefinition extends NamedSchemaElementWithType<InputType,
|
|
|
2251
2331
|
return [];
|
|
2252
2332
|
}
|
|
2253
2333
|
|
|
2334
|
+
/**
|
|
2335
|
+
* Like `remove()`, but if this field was the last field of its parent type, the parent type is removed through its `removeRecursive` method.
|
|
2336
|
+
*/
|
|
2337
|
+
removeRecursive(): void {
|
|
2338
|
+
const parent = this._parent;
|
|
2339
|
+
this.remove();
|
|
2340
|
+
if (parent && parent.fields().length === 0) {
|
|
2341
|
+
parent.removeRecursive();
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2254
2345
|
toString(): string {
|
|
2255
2346
|
const defaultStr = this.defaultValue === undefined ? "" : ` = ${valueToString(this.defaultValue, this.type)}`;
|
|
2256
2347
|
return `${this.name}: ${this.type}${defaultStr}`;
|
|
@@ -2491,6 +2582,13 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
|
|
|
2491
2582
|
return toReturn;
|
|
2492
2583
|
}
|
|
2493
2584
|
|
|
2585
|
+
/**
|
|
2586
|
+
* Removes this this directive definition _and_ all its applications.
|
|
2587
|
+
*/
|
|
2588
|
+
removeRecursive(): void {
|
|
2589
|
+
this.remove().forEach(ref => ref.remove());
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2494
2592
|
toString(): string {
|
|
2495
2593
|
return `@${this.name}`;
|
|
2496
2594
|
}
|