@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/debug.d.ts.map +1 -1
  3. package/dist/debug.js +2 -18
  4. package/dist/debug.js.map +1 -1
  5. package/dist/definitions.d.ts +11 -0
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +54 -0
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/error.d.ts +87 -3
  10. package/dist/error.d.ts.map +1 -1
  11. package/dist/error.js +143 -5
  12. package/dist/error.js.map +1 -1
  13. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  14. package/dist/extractSubgraphsFromSupergraph.js +40 -3
  15. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  16. package/dist/federation.d.ts +4 -1
  17. package/dist/federation.d.ts.map +1 -1
  18. package/dist/federation.js +192 -53
  19. package/dist/federation.js.map +1 -1
  20. package/dist/genErrorCodeDoc.d.ts +2 -0
  21. package/dist/genErrorCodeDoc.d.ts.map +1 -0
  22. package/dist/genErrorCodeDoc.js +55 -0
  23. package/dist/genErrorCodeDoc.js.map +1 -0
  24. package/dist/tagSpec.js +3 -1
  25. package/dist/tagSpec.js.map +1 -1
  26. package/dist/utils.d.ts +1 -0
  27. package/dist/utils.d.ts.map +1 -1
  28. package/dist/utils.js +19 -1
  29. package/dist/utils.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
  32. package/src/__tests__/subgraphValidation.test.ts +452 -0
  33. package/src/debug.ts +2 -19
  34. package/src/definitions.ts +98 -0
  35. package/src/error.ts +334 -7
  36. package/src/extractSubgraphsFromSupergraph.ts +49 -4
  37. package/src/federation.ts +229 -85
  38. package/src/genErrorCodeDoc.ts +69 -0
  39. package/src/tagSpec.ts +4 -4
  40. package/src/utils.ts +27 -0
  41. package/tsconfig.test.tsbuildinfo +1 -1
  42. 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 = stringIsBoolean(v);
16
+ const bool = validateStringContainsBoolean(v);
34
17
  if (bool !== undefined) {
35
18
  return bool;
36
19
  }
@@ -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
  }