@apollo/federation-internals 2.0.0 → 2.0.2-alpha.1

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 (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +17 -6
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts +1 -0
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +3 -0
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/definitions.js +9 -5
  11. package/dist/definitions.js.map +1 -1
  12. package/dist/error.js +7 -7
  13. package/dist/error.js.map +1 -1
  14. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  15. package/dist/extractSubgraphsFromSupergraph.js +170 -152
  16. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  17. package/dist/federation.d.ts.map +1 -1
  18. package/dist/federation.js +15 -0
  19. package/dist/federation.js.map +1 -1
  20. package/dist/genErrorCodeDoc.js +12 -6
  21. package/dist/genErrorCodeDoc.js.map +1 -1
  22. package/dist/inaccessibleSpec.d.ts +2 -1
  23. package/dist/inaccessibleSpec.d.ts.map +1 -1
  24. package/dist/inaccessibleSpec.js +4 -1
  25. package/dist/inaccessibleSpec.js.map +1 -1
  26. package/dist/joinSpec.d.ts +2 -1
  27. package/dist/joinSpec.d.ts.map +1 -1
  28. package/dist/joinSpec.js +3 -0
  29. package/dist/joinSpec.js.map +1 -1
  30. package/dist/operations.d.ts +38 -4
  31. package/dist/operations.d.ts.map +1 -1
  32. package/dist/operations.js +107 -8
  33. package/dist/operations.js.map +1 -1
  34. package/dist/schemaUpgrader.d.ts +8 -1
  35. package/dist/schemaUpgrader.d.ts.map +1 -1
  36. package/dist/schemaUpgrader.js +40 -1
  37. package/dist/schemaUpgrader.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/__tests__/definitions.test.ts +66 -0
  40. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
  41. package/src/__tests__/operations.test.ts +119 -1
  42. package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
  43. package/src/__tests__/schemaUpgrader.test.ts +38 -0
  44. package/src/__tests__/subgraphValidation.test.ts +74 -0
  45. package/src/buildSchema.ts +32 -5
  46. package/src/coreSpec.ts +4 -0
  47. package/src/definitions.ts +15 -6
  48. package/src/error.ts +7 -7
  49. package/src/extractSubgraphsFromSupergraph.ts +229 -204
  50. package/src/federation.ts +50 -0
  51. package/src/genErrorCodeDoc.ts +13 -7
  52. package/src/inaccessibleSpec.ts +17 -3
  53. package/src/joinSpec.ts +5 -1
  54. package/src/operations.ts +179 -8
  55. package/src/schemaUpgrader.ts +45 -0
  56. package/tsconfig.test.tsbuildinfo +1 -1
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -590,6 +590,72 @@ test('default arguments for directives', () => {
590
590
  expect(d3.arguments(true)).toEqual({ inputObject: { number: 3 }});
591
591
  });
592
592
 
593
+ describe('clone', () => {
594
+ it('should allow directive application before definition', () => {
595
+ const schema = buildSchema(`
596
+ directive @foo(arg: String @wizz(arg: "foo")) on FIELD_DEFINITION
597
+ directive @wizz(arg: String @fuzz(arg: "wizz")) on ARGUMENT_DEFINITION
598
+ directive @fuzz(arg: String @buzz(arg: "fuzz")) on ARGUMENT_DEFINITION
599
+ directive @buzz(arg: String @baz(arg: "buzz")) on ARGUMENT_DEFINITION
600
+ directive @baz(arg: String @bar) on ARGUMENT_DEFINITION
601
+ directive @bar on ARGUMENT_DEFINITION
602
+
603
+ type Query {
604
+ foo: String! @foo(arg: "query")
605
+ }
606
+ `).clone();
607
+
608
+ expect(schema.elementByCoordinate("@foo")).toBeDefined();
609
+ expect(schema.elementByCoordinate("@wizz")).toBeDefined();
610
+ expect(schema.elementByCoordinate("@fuzz")).toBeDefined();
611
+ expect(schema.elementByCoordinate("@buzz")).toBeDefined();
612
+ expect(schema.elementByCoordinate("@baz")).toBeDefined();
613
+ expect(schema.elementByCoordinate("@bar")).toBeDefined();
614
+ });
615
+
616
+ // https://github.com/apollographql/federation/issues/1794
617
+ it('should allow using an imported federation diretive in another directive', () => {
618
+ const schema = buildSubgraph('foo', "", `
619
+ extend schema
620
+ @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
621
+
622
+ directive @foo(arg: String @tag(name: "tag")) on FIELD_DEFINITION
623
+
624
+ type Query {
625
+ hi: String! @foo
626
+ }
627
+ `).schema.clone();
628
+ expect(schema.elementByCoordinate("@foo")).toBeDefined();
629
+ expect(schema.elementByCoordinate("@tag")).toBeDefined();
630
+ });
631
+
632
+ it('should allow type use in directives', () => {
633
+ const schema = buildSchema(`
634
+ directive @foo(arg: Thing!) on FIELD_DEFINITION
635
+ scalar Thing
636
+
637
+ type Query {
638
+ foo: String! @foo(arg: "sunshine")
639
+ }
640
+ `).clone();
641
+
642
+ expect(schema.elementByCoordinate("@foo")).toBeDefined();
643
+ });
644
+
645
+ it('should allow recursive directive definitions', () => {
646
+ const schema = buildSchema(`
647
+ directive @foo(a: Int @bar) on ARGUMENT_DEFINITION
648
+ directive @bar(b: Int @foo) on ARGUMENT_DEFINITION
649
+
650
+ type Query {
651
+ getData(arg: String @foo): String!
652
+ }
653
+ `).clone();
654
+ expect(schema.elementByCoordinate("@foo")).toBeDefined();
655
+ expect(schema.elementByCoordinate("@bar")).toBeDefined();
656
+ });
657
+ });
658
+
593
659
  test('correctly convert to a graphQL-js schema', () => {
594
660
  const sdl = `
595
661
  schema {
@@ -597,3 +597,86 @@ test('throw meaningful error for invalid federation directive fieldSet', () => {
597
597
  + '[serviceB] On field "A.a", for @requires(fields: "b { x }"): Cannot query field "b" on type "A" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).'
598
598
  );
599
599
  })
600
+
601
+
602
+ test('throw meaningful error for type erased from supergraph due to extending an entity without a key', () => {
603
+ // Supergraph generated by fed1 composition from:
604
+ // ServiceA:
605
+ // type Query {
606
+ // t: T
607
+ // }
608
+ //
609
+ // type T @key(fields: "id") {
610
+ // id: ID!
611
+ // x: Int!
612
+ // }
613
+ //
614
+ // ServiceB
615
+ // extend type T {
616
+ // x: Int! @external
617
+ // }
618
+ //
619
+ // type Other {
620
+ // f: T @provides(fields: "x")
621
+ // }
622
+ //
623
+ // The issue of that schema is that `T` is referenced in `ServiceB`, but because it extends an entity type
624
+ // without a key and has noly external field, ther is no remaining traces of it's definition in `ServiceB`
625
+ // in the supergraph. As extract cannot make up the original definition out of thin air, it ends up erroring
626
+ // when extracting `Other.t` due to not knowing that type.
627
+ const supergraph = `
628
+ schema
629
+ @core(feature: "https://specs.apollo.dev/core/v0.2"),
630
+ @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION)
631
+ {
632
+ query: Query
633
+ }
634
+
635
+ directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA
636
+
637
+ directive @join__field(graph: join__Graph, provides: join__FieldSet, requires: join__FieldSet) on FIELD_DEFINITION
638
+
639
+ directive @join__graph(name: String!, url: String!) on ENUM_VALUE
640
+
641
+ directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT
642
+
643
+ directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT
644
+
645
+ type Other {
646
+ f: T @join__field(graph: SERVICEB, provides: "x")
647
+ }
648
+
649
+ type Query {
650
+ t: T @join__field(graph: SERVICEA)
651
+ }
652
+
653
+ type T
654
+ @join__owner(graph: SERVICEA)
655
+ @join__type(graph: SERVICEA, key: "id")
656
+ {
657
+ id: ID! @join__field(graph: SERVICEA)
658
+ x: Int! @join__field(graph: SERVICEA)
659
+ }
660
+
661
+ enum core__Purpose {
662
+ EXECUTION
663
+ SECURITY
664
+ }
665
+
666
+ scalar join__FieldSet
667
+
668
+ enum join__Graph {
669
+ SERVICEA @join__graph(name: "serviceA" url: "")
670
+ SERVICEB @join__graph(name: "serviceB" url: "")
671
+ }
672
+ `;
673
+
674
+ const schema = buildSupergraphSchema(supergraph)[0];
675
+ expect(() => extractSubgraphsFromSupergraph(schema)).toThrow(
676
+ 'Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n'
677
+ + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n'
678
+ + '\n'
679
+ + 'Details:\n'
680
+ + 'Error: Cannot find type "T" in subgraph "serviceB"'
681
+ );
682
+ })
@@ -2,7 +2,7 @@ import {
2
2
  Schema,
3
3
  } from '../../dist/definitions';
4
4
  import { buildSchema } from '../../dist/buildSchema';
5
- import { parseOperation } from '../../dist/operations';
5
+ import { Field, FieldSelection, parseOperation, SelectionSet } from '../../dist/operations';
6
6
  import './matchers';
7
7
 
8
8
  function parseSchema(schema: string): Schema {
@@ -141,3 +141,121 @@ test('fragments optimization of selection sets', () => {
141
141
  }
142
142
  `);
143
143
  });
144
+
145
+ describe('selection set freezing', () => {
146
+ const schema = parseSchema(`
147
+ type Query {
148
+ t: T
149
+ }
150
+
151
+ type T {
152
+ a: Int
153
+ b: Int
154
+ }
155
+ `);
156
+
157
+ const tField = schema.schemaDefinition.rootType('query')!.field('t')!;
158
+
159
+ it('throws if one tries to modify a frozen selection set', () => {
160
+ // Note that we use parseOperation to help us build selection/selection sets because it's more readable/convenient
161
+ // thant to build the object "programmatically".
162
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
163
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
164
+
165
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
166
+
167
+ // Control: check we can add to the selection set while not yet frozen
168
+ expect(s.isFrozen()).toBeFalsy();
169
+ expect(() => s.mergeIn(s1)).not.toThrow();
170
+
171
+ s.freeze();
172
+ expect(s.isFrozen()).toBeTruthy();
173
+ expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
174
+ });
175
+
176
+ it('it does not clone non-frozen selections when adding to another one', () => {
177
+ // This test is a bit debatable because what it tests is not so much a behaviour
178
+ // we *need* absolutely to preserve, but rather test how things happens to
179
+ // behave currently and illustrate the current contrast between frozen and
180
+ // non-frozen selection set.
181
+ // That is, this test show what happens if the test
182
+ // "it automaticaly clones frozen selections when adding to another one"
183
+ // is done without freezing.
184
+
185
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
186
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
187
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
188
+
189
+ s.mergeIn(s1);
190
+ s.mergeIn(s2);
191
+
192
+ expect(s.toString()).toBe('{ t { a b } }');
193
+
194
+ // This next assertion is where differs from the case where `s1` is frozen. Namely,
195
+ // when we do `s.mergeIn(s1)`, then `s` directly references `s1` without cloning
196
+ // and thus the next modification (`s.mergeIn(s2)`) ends up modifying both `s` and `s1`.
197
+ // Note that we don't mean by this test that the fact that `s.mergeIn(s1)` does
198
+ // not clone `s1` is a behaviour one should *rely* on, but it currently done for
199
+ // efficiencies sake: query planning does a lot of selection set building through
200
+ // `SelectionSet::mergeIn` and `SelectionSet::add` and we often pass to those method
201
+ // newly constructed selections as input, so cloning them would wast CPU and early
202
+ // query planning benchmarking showed that this could add up on the more expansive
203
+ // plan computations. This is why freezing exists: it allows us to save cloning
204
+ // in general, but to protect those selection set we know should be immutable
205
+ // so they do get cloned in such situation.
206
+ expect(s1.toString()).toBe('{ t { a b } }');
207
+ expect(s2.toString()).toBe('{ t { b } }');
208
+ });
209
+
210
+ it('it automaticaly clones frozen field selections when merging to another one', () => {
211
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
212
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
213
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
214
+
215
+ s.mergeIn(s1);
216
+ s.mergeIn(s2);
217
+
218
+ // We check S is what we expect...
219
+ expect(s.toString()).toBe('{ t { a b } }');
220
+
221
+ // ... but more importantly for this test, that s1/s2 were not modified.
222
+ expect(s1.toString()).toBe('{ t { a } }');
223
+ expect(s2.toString()).toBe('{ t { b } }');
224
+ });
225
+
226
+ it('it automaticaly clones frozen fragment selections when merging to another one', () => {
227
+ // Note: needlessly complex queries, but we're just ensuring the cloning story works when fragments are involved
228
+ const s1 = parseOperation(schema, `{ ... on Query { t { ... on T { a } } } }`).selectionSet.freeze();
229
+ const s2 = parseOperation(schema, `{ ... on Query { t { ... on T { b } } } }`).selectionSet.freeze();
230
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
231
+
232
+ s.mergeIn(s1);
233
+ s.mergeIn(s2);
234
+
235
+ expect(s.toString()).toBe('{ ... on Query { t { ... on T { a b } } } }');
236
+
237
+ expect(s1.toString()).toBe('{ ... on Query { t { ... on T { a } } } }');
238
+ expect(s2.toString()).toBe('{ ... on Query { t { ... on T { b } } } }');
239
+ });
240
+
241
+ it('it automaticaly clones frozen field selections when adding to another one', () => {
242
+ const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
243
+ const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
244
+ const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
245
+ const tSelection = new FieldSelection(new Field(tField));
246
+ s.add(tSelection);
247
+
248
+ // Note that this test both checks the auto-cloning for the `add` method, but
249
+ // also shows that freezing dose apply deeply (since we freeze the whole `s1`/`s2`
250
+ // but only add some sub-selection of it).
251
+ tSelection.selectionSet!.add(s1.selections()[0].selectionSet!.selections()[0]);
252
+ tSelection.selectionSet!.add(s2.selections()[0].selectionSet!.selections()[0]);
253
+
254
+ // We check S is what we expect...
255
+ expect(s.toString()).toBe('{ t { a b } }');
256
+
257
+ // ... but more importantly for this test, that s1/s2 were not modified.
258
+ expect(s1.toString()).toBe('{ t { a } }');
259
+ expect(s2.toString()).toBe('{ t { b } }');
260
+ });
261
+ });
@@ -178,7 +178,7 @@ describe("removeInaccessibleElements", () => {
178
178
  `);
179
179
  });
180
180
 
181
- it(`handles renames of @inaccessible`, () => {
181
+ it(`handles renames of @inaccessible via core "as"`, () => {
182
182
  const schema = buildSchema(`
183
183
  directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
184
184
 
@@ -212,6 +212,42 @@ describe("removeInaccessibleElements", () => {
212
212
  expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined();
213
213
  });
214
214
 
215
+ it(`handles renames of @inaccessible via import "as"`, () => {
216
+ const schema = buildSchema(`
217
+ directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
218
+
219
+ enum link__Purpose {
220
+ EXECUTION
221
+ SECURITY
222
+ }
223
+
224
+ scalar link__Import
225
+
226
+ directive @foo on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
227
+
228
+ schema
229
+ @link(url: "https://specs.apollo.dev/link/v1.0")
230
+ @link(url: "https://specs.apollo.dev/inaccessible/v0.2", import: [{name: "@inaccessible", as: "@foo"}])
231
+ {
232
+ query: Query
233
+ }
234
+
235
+ type Query {
236
+ someField: Bar @inaccessible
237
+ privateField: String @foo
238
+ }
239
+
240
+ scalar Bar
241
+
242
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
243
+ `);
244
+
245
+ removeInaccessibleElements(schema);
246
+ schema.validate();
247
+ expect(schema.elementByCoordinate("Query.someField")).toBeDefined();
248
+ expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined();
249
+ });
250
+
215
251
  it(`fails for @inaccessible built-ins`, () => {
216
252
  const schema = buildSchema(`
217
253
  ${INACCESSIBLE_V02_HEADER}
@@ -2387,4 +2423,51 @@ describe("removeInaccessibleElements", () => {
2387
2423
  ]
2388
2424
  `);
2389
2425
  });
2426
+
2427
+ it(`fails to remove @inaccessible non-core feature elements referenced by core feature elements`, () => {
2428
+ const schema = buildSchema(`
2429
+ directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
2430
+
2431
+ enum core__Purpose {
2432
+ EXECUTION
2433
+ SECURITY
2434
+ }
2435
+
2436
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
2437
+
2438
+ schema
2439
+ @core(feature: "https://specs.apollo.dev/core/v0.2")
2440
+ @core(feature: "https://specs.apollo.dev/inaccessible/v0.2")
2441
+ @core(feature: "http://localhost/foo/v1.0")
2442
+ {
2443
+ query: Query
2444
+ }
2445
+
2446
+ type Query {
2447
+ someField: String
2448
+ }
2449
+
2450
+ # Inaccessible input object field
2451
+ input InputObject {
2452
+ someField: String
2453
+ privateField: String @inaccessible
2454
+ }
2455
+
2456
+ # Inaccessible input object field can't be referenced by default value of
2457
+ # directive argument of a core feature element
2458
+ directive @foo__referencer(
2459
+ someArg: InputObject = { privateField: "" }
2460
+ ) on FIELD
2461
+ `);
2462
+
2463
+ const errorMessages = expectErrors(1, () => {
2464
+ removeInaccessibleElements(schema);
2465
+ });
2466
+
2467
+ expect(errorMessages).toMatchInlineSnapshot(`
2468
+ Array [
2469
+ "Input field \\"InputObject.privateField\\" is @inaccessible but is used in the default value of \\"@foo__referencer(someArg:)\\", which is in the API schema.",
2470
+ ]
2471
+ `);
2472
+ });
2390
2473
  });
@@ -1,4 +1,5 @@
1
1
  import { FEDERATION2_LINK_WTH_FULL_IMPORTS } from '..';
2
+ import { ObjectType } from '../definitions';
2
3
  import { buildSubgraph, Subgraphs } from '../federation';
3
4
  import { UpgradeChangeID, UpgradeResult, upgradeSubgraphsIfNecessary } from '../schemaUpgrader';
4
5
  import './matchers';
@@ -167,3 +168,40 @@ test('update federation directive non-string arguments', () => {
167
168
  }
168
169
  `);
169
170
  })
171
+
172
+
173
+ test('remove tag on external field if found on definition', () => {
174
+ const s1 = `
175
+ type Query {
176
+ a: A @provides(fields: "y")
177
+ }
178
+
179
+ type A @key(fields: "id") {
180
+ id: String
181
+ x: Int
182
+ y: Int @external @tag(name: "a tag")
183
+ }
184
+ `;
185
+
186
+ const s2 = `
187
+ type A @key(fields: "id") {
188
+ id: String
189
+ y: Int @tag(name: "a tag")
190
+ }
191
+ `;
192
+
193
+ const subgraphs = new Subgraphs();
194
+ subgraphs.add(buildSubgraph('s1', 'http://s1', s1));
195
+ subgraphs.add(buildSubgraph('s2', 'http://s2', s2));
196
+ const res = upgradeSubgraphsIfNecessary(subgraphs);
197
+ expect(res.errors).toBeUndefined();
198
+
199
+ expect(changeMessages(res, 's1', 'REMOVED_TAG_ON_EXTERNAL')).toStrictEqual([
200
+ 'Removed @tag(name: "a tag") application on @external "A.y" as the @tag application is on another definition',
201
+ ]);
202
+
203
+ const typeAInS1 = res.subgraphs?.get('s1')?.schema.type("A") as ObjectType;
204
+ const typeAInS2 = res.subgraphs?.get('s2')?.schema.type("A") as ObjectType;
205
+ expect(typeAInS1.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([]);
206
+ expect(typeAInS2.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([ '@tag(name: "a tag")' ]);
207
+ })
@@ -879,3 +879,77 @@ describe('@core/@link handling', () => {
879
879
  buildAndValidate(doc);
880
880
  });
881
881
  });
882
+
883
+ describe('federation 1 schema', () => {
884
+ it('accepts federation directive definitions without arguments', () => {
885
+ const doc = gql`
886
+ type Query {
887
+ a: Int
888
+ }
889
+
890
+ directive @key on OBJECT | INTERFACE
891
+ directive @requires on FIELD_DEFINITION
892
+ `;
893
+
894
+ buildAndValidate(doc);
895
+ });
896
+
897
+ it('accepts federation directive definitions with nullable arguments', () => {
898
+ const doc = gql`
899
+ type Query {
900
+ a: Int
901
+ }
902
+
903
+ type T @key(fields: "id") {
904
+ id: ID! @requires(fields: "x")
905
+ x: Int @external
906
+ }
907
+
908
+ # Tests with the _FieldSet argument non-nullable
909
+ scalar _FieldSet
910
+ directive @key(fields: _FieldSet) on OBJECT | INTERFACE
911
+
912
+ # Tests with the argument as String and non-nullable
913
+ directive @requires(fields: String) on FIELD_DEFINITION
914
+ `;
915
+
916
+ buildAndValidate(doc);
917
+ });
918
+
919
+ it('accepts federation directive definitions with "FieldSet" type instead of "_FieldSet"', () => {
920
+ const doc = gql`
921
+ type Query {
922
+ a: Int
923
+ }
924
+
925
+ type T @key(fields: "id") {
926
+ id: ID!
927
+ }
928
+
929
+ scalar FieldSet
930
+ directive @key(fields: FieldSet) on OBJECT | INTERFACE
931
+ `;
932
+
933
+ buildAndValidate(doc);
934
+ });
935
+
936
+ it('rejects federation directive definition with unknown arguments', () => {
937
+ const doc = gql`
938
+ type Query {
939
+ a: Int
940
+ }
941
+
942
+ type T @key(fields: "id", unknown: 42) {
943
+ id: ID!
944
+ }
945
+
946
+ scalar _FieldSet
947
+ directive @key(fields: _FieldSet!, unknown: Int) on OBJECT | INTERFACE
948
+ `;
949
+
950
+ expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[
951
+ 'DIRECTIVE_DEFINITION_INVALID',
952
+ '[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown"'
953
+ ]]);
954
+ });
955
+ })
@@ -79,8 +79,13 @@ export function buildSchemaFromAST(
79
79
  // to the schema element. But that detection necessitates that the corresponding directive definition has been fully
80
80
  // populated (and at this point, we don't really know the name of the `@core` directive since it can be renamed, so
81
81
  // we just handle all directives).
82
+ // Note that one subtlety is that we skip, for now, directive _applications_ within those directive definitions (we can
83
+ // have such applications on the arguments). The reason is again core schema related: we haven't yet properly detected
84
+ // if the schema if a core-schema yet, and for federation subgraphs, we haven't yet "imported" federation definitions.
85
+ // So if one of those directive application was relying on that "importing", it would fail at this point. Which is why
86
+ // directive application is delayed to later in that method.
82
87
  for (const directiveDefinitionNode of directiveDefinitions) {
83
- buildDirectiveDefinitionInner(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
88
+ buildDirectiveDefinitionInnerWithoutDirectiveApplications(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
84
89
  }
85
90
  for (const schemaDefinition of schemaDefinitions) {
86
91
  buildSchemaDefinitionInner(schemaDefinition, schema.schemaDefinition, errors);
@@ -89,8 +94,17 @@ export function buildSchemaFromAST(
89
94
  buildSchemaDefinitionInner(schemaExtension, schema.schemaDefinition, errors, schema.schemaDefinition.newExtension());
90
95
  }
91
96
 
97
+ // The following is a no-op for "standard" schema, but for federation subgraphs, this is where we handle the auto-addition
98
+ // of imported federation directive definitions. That is why we have avoid looking at directive applications within
99
+ // directive definition earlier: if one of those application was of an imported federation directive, the definition
100
+ // wouldn't be presence before this point and we'd have triggered an error. After this, we can handle any directive
101
+ // application safely.
92
102
  errors.push(...schema.blueprint.onDirectiveDefinitionAndSchemaParsed(schema));
93
103
 
104
+ for (const directiveDefinitionNode of directiveDefinitions) {
105
+ buildDirectiveApplicationsInDirectiveDefinition(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
106
+ }
107
+
94
108
  for (const definitionNode of documentNode.definitions) {
95
109
  switch (definitionNode.kind) {
96
110
  case 'OperationDefinition':
@@ -360,7 +374,7 @@ function buildFieldDefinitionInner(
360
374
  const type = buildTypeReferenceFromAST(fieldNode.type, field.schema());
361
375
  field.type = validateOutputType(type, field.coordinate, fieldNode, errors);
362
376
  for (const inputValueDef of fieldNode.arguments ?? []) {
363
- buildArgumentDefinitionInner(inputValueDef, field.addArgument(inputValueDef.name.value), errors);
377
+ buildArgumentDefinitionInner(inputValueDef, field.addArgument(inputValueDef.name.value), errors, true);
364
378
  }
365
379
  buildAppliedDirectives(fieldNode, field, errors);
366
380
  field.description = fieldNode.description?.value;
@@ -408,11 +422,14 @@ function buildArgumentDefinitionInner(
408
422
  inputNode: InputValueDefinitionNode,
409
423
  arg: ArgumentDefinition<any>,
410
424
  errors: GraphQLError[],
425
+ includeDirectiveApplication: boolean,
411
426
  ) {
412
427
  const type = buildTypeReferenceFromAST(inputNode.type, arg.schema());
413
428
  arg.type = validateInputType(type, arg.coordinate, inputNode, errors);
414
429
  arg.defaultValue = buildValue(inputNode.defaultValue);
415
- buildAppliedDirectives(inputNode, arg, errors);
430
+ if (includeDirectiveApplication) {
431
+ buildAppliedDirectives(inputNode, arg, errors);
432
+ }
416
433
  arg.description = inputNode.description?.value;
417
434
  arg.sourceAST = inputNode;
418
435
  }
@@ -430,13 +447,13 @@ function buildInputFieldDefinitionInner(
430
447
  field.sourceAST = fieldNode;
431
448
  }
432
449
 
433
- function buildDirectiveDefinitionInner(
450
+ function buildDirectiveDefinitionInnerWithoutDirectiveApplications(
434
451
  directiveNode: DirectiveDefinitionNode,
435
452
  directive: DirectiveDefinition,
436
453
  errors: GraphQLError[],
437
454
  ) {
438
455
  for (const inputValueDef of directiveNode.arguments ?? []) {
439
- buildArgumentDefinitionInner(inputValueDef, directive.addArgument(inputValueDef.name.value), errors);
456
+ buildArgumentDefinitionInner(inputValueDef, directive.addArgument(inputValueDef.name.value), errors, false);
440
457
  }
441
458
  directive.repeatable = directiveNode.repeatable;
442
459
  const locations = directiveNode.locations.map(({ value }) => value as DirectiveLocation);
@@ -444,3 +461,13 @@ function buildDirectiveDefinitionInner(
444
461
  directive.description = directiveNode.description?.value;
445
462
  directive.sourceAST = directiveNode;
446
463
  }
464
+
465
+ function buildDirectiveApplicationsInDirectiveDefinition(
466
+ directiveNode: DirectiveDefinitionNode,
467
+ directive: DirectiveDefinition,
468
+ errors: GraphQLError[],
469
+ ) {
470
+ for (const inputValueDef of directiveNode.arguments ?? []) {
471
+ buildAppliedDirectives(inputValueDef, directive.argument(inputValueDef.name.value)!, errors);
472
+ }
473
+ }
package/src/coreSpec.ts CHANGED
@@ -131,6 +131,10 @@ export abstract class FeatureDefinition {
131
131
  return features.getByIdentity(this.identity);
132
132
  }
133
133
 
134
+ get defaultCorePurpose(): CorePurpose | undefined {
135
+ return undefined;
136
+ }
137
+
134
138
  toString(): string {
135
139
  return `${this.identity}/${this.version}`
136
140
  }
@@ -946,11 +946,13 @@ export class CoreFeature {
946
946
  }
947
947
 
948
948
  directiveNameInSchema(name: string): string {
949
- if (name === this.url.name) {
950
- return this.nameInSchema;
951
- }
952
949
  const elementImport = this.imports.find((i) => i.name.charAt(0) === '@' && i.name.slice(1) === name);
953
- return elementImport ? (elementImport.as?.slice(1) ?? name) : this.nameInSchema + '__' + name;
950
+ return elementImport
951
+ ? (elementImport.as?.slice(1) ?? name)
952
+ : (name === this.url.name
953
+ ? this.nameInSchema
954
+ : this.nameInSchema + '__' + name
955
+ );
954
956
  }
955
957
 
956
958
  typeNameInSchema(name: string): string {
@@ -2762,7 +2764,7 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
2762
2764
  assert(false, `Directive definition ${this} can't reference other types (it's arguments can); shouldn't be asked to remove reference to ${type}`);
2763
2765
  }
2764
2766
 
2765
- /**
2767
+ /**
2766
2768
  * Removes this directive definition from its parent schema.
2767
2769
  *
2768
2770
  * After calling this method, this directive definition will be "detached": it will have no parent, schema, or
@@ -3213,9 +3215,16 @@ function copy(source: Schema, dest: Schema) {
3213
3215
  for (const type of typesToCopy(source, dest)) {
3214
3216
  dest.addType(newNamedType(type.kind, type.name));
3215
3217
  }
3218
+ // Directives can use other directives in their arguments. So, like types, we first shallow copy
3219
+ // directives so future references to any of them can be dereferenced. We'll copy the actual
3220
+ // definition later after all directives are defined.
3216
3221
  for (const directive of directivesToCopy(source, dest)) {
3217
- copyDirectiveDefinitionInner(directive, dest.addDirectiveDefinition(directive.name));
3222
+ dest.addDirectiveDefinition(directive.name);
3218
3223
  }
3224
+ for (const directive of directivesToCopy(source, dest)) {
3225
+ copyDirectiveDefinitionInner(directive, dest.directive(directive.name)!);
3226
+ }
3227
+
3219
3228
  copySchemaDefinitionInner(source.schemaDefinition, dest.schemaDefinition);
3220
3229
  for (const type of typesToCopy(source, dest)) {
3221
3230
  copyNamedTypeInner(type, dest.type(type.name)!);