@apollo/federation-internals 2.0.0-alpha.2 → 2.0.0-alpha.6
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 +15 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +3 -3
- package/dist/buildSchema.js.map +1 -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 +18 -7
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +80 -26
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +88 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +145 -5
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +41 -4
- 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 +231 -58
- 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/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +1 -1
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +6 -5
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +15 -15
- package/dist/operations.js.map +1 -1
- package/dist/tagSpec.d.ts +2 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +10 -2
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -1
- package/dist/utils.js.map +1 -1
- package/dist/values.d.ts +2 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +27 -1
- package/dist/values.js.map +1 -1
- package/jest.config.js +5 -1
- package/package.json +3 -6
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +535 -0
- package/src/__tests__/subgraphValidation.test.ts +480 -0
- package/src/buildSchema.ts +7 -6
- package/src/debug.ts +2 -19
- package/src/definitions.ts +151 -40
- package/src/error.ts +340 -7
- package/src/extractSubgraphsFromSupergraph.ts +50 -5
- package/src/federation.ts +297 -92
- package/src/genErrorCodeDoc.ts +69 -0
- package/src/inaccessibleSpec.ts +7 -2
- package/src/joinSpec.ts +11 -5
- package/src/operations.ts +20 -18
- package/src/tagSpec.ts +11 -6
- package/src/utils.ts +49 -0
- package/src/values.ts +47 -5
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,480 @@
|
|
|
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 as string, 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
|
+
|
|
453
|
+
|
|
454
|
+
it('validates all implementations of interface field have same type if any has @external', () => {
|
|
455
|
+
const subgraph = gql`
|
|
456
|
+
type Query {
|
|
457
|
+
is: [I!]!
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
interface I {
|
|
461
|
+
f: Int
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
type T1 implements I {
|
|
465
|
+
f: Int
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
type T2 implements I {
|
|
469
|
+
f: Int!
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
type T3 implements I {
|
|
473
|
+
id: ID!
|
|
474
|
+
f: Int @external
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
expect(buildForErrors(subgraph)).toStrictEqual([
|
|
478
|
+
['INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH', '[S] Some of the runtime implementations of interface field "I.f" are marked @external or have a @require ("T3.f") so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but "T1.f" and "T3.f" have type "Int" while "T2.f" has type "Int!".'],
|
|
479
|
+
]);
|
|
480
|
+
})
|
package/src/buildSchema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DefinitionNode,
|
|
3
3
|
DirectiveDefinitionNode,
|
|
4
|
-
|
|
4
|
+
DirectiveLocation,
|
|
5
5
|
DirectiveNode,
|
|
6
6
|
DocumentNode,
|
|
7
7
|
FieldDefinitionNode,
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
StringValueNode,
|
|
19
19
|
ASTNode,
|
|
20
20
|
SchemaExtensionNode,
|
|
21
|
-
parseType
|
|
21
|
+
parseType,
|
|
22
|
+
Kind,
|
|
22
23
|
} from "graphql";
|
|
23
24
|
import { Maybe } from "graphql/jsutils/Maybe";
|
|
24
25
|
import {
|
|
@@ -332,11 +333,11 @@ export function builtTypeReference(encodedType: string, schema: Schema): Type {
|
|
|
332
333
|
|
|
333
334
|
function buildTypeReferenceFromAST(typeNode: TypeNode, schema: Schema): Type {
|
|
334
335
|
switch (typeNode.kind) {
|
|
335
|
-
case
|
|
336
|
+
case Kind.LIST_TYPE:
|
|
336
337
|
return new ListType(buildTypeReferenceFromAST(typeNode.type, schema));
|
|
337
|
-
case
|
|
338
|
+
case Kind.NON_NULL_TYPE:
|
|
338
339
|
const wrapped = buildTypeReferenceFromAST(typeNode.type, schema);
|
|
339
|
-
if (wrapped.kind ==
|
|
340
|
+
if (wrapped.kind == Kind.NON_NULL_TYPE) {
|
|
340
341
|
throw new GraphQLError(`Cannot apply the non-null operator (!) twice to the same type`, typeNode);
|
|
341
342
|
}
|
|
342
343
|
return new NonNullType(wrapped);
|
|
@@ -368,7 +369,7 @@ function buildDirectiveDefinitionInner(directiveNode: DirectiveDefinitionNode, d
|
|
|
368
369
|
buildArgumentDefinitionInner(inputValueDef, directive.addArgument(inputValueDef.name.value));
|
|
369
370
|
}
|
|
370
371
|
directive.repeatable = directiveNode.repeatable;
|
|
371
|
-
const locations = directiveNode.locations.map(({ value }) => value as
|
|
372
|
+
const locations = directiveNode.locations.map(({ value }) => value as DirectiveLocation);
|
|
372
373
|
directive.addLocations(...locations);
|
|
373
374
|
directive.description = directiveNode.description?.value;
|
|
374
375
|
directive.sourceAST = directiveNode;
|
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
|
}
|