@apollo/federation-internals 2.0.2-alpha.2 → 2.0.4

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 (50) hide show
  1. package/CHANGELOG.md +8 -29
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +76 -49
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts +1 -1
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +12 -5
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts +19 -3
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js +106 -9
  12. package/dist/definitions.js.map +1 -1
  13. package/dist/directiveAndTypeSpecification.js +1 -1
  14. package/dist/directiveAndTypeSpecification.js.map +1 -1
  15. package/dist/federation.d.ts +1 -0
  16. package/dist/federation.d.ts.map +1 -1
  17. package/dist/federation.js +20 -0
  18. package/dist/federation.js.map +1 -1
  19. package/dist/graphQLJSSchemaToAST.d.ts +8 -0
  20. package/dist/graphQLJSSchemaToAST.d.ts.map +1 -0
  21. package/dist/graphQLJSSchemaToAST.js +96 -0
  22. package/dist/graphQLJSSchemaToAST.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/operations.d.ts +2 -1
  28. package/dist/operations.d.ts.map +1 -1
  29. package/dist/operations.js +29 -18
  30. package/dist/operations.js.map +1 -1
  31. package/dist/print.d.ts +2 -1
  32. package/dist/print.d.ts.map +1 -1
  33. package/dist/print.js +23 -15
  34. package/dist/print.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/__tests__/definitions.test.ts +134 -1
  37. package/src/__tests__/graphQLJSSchemaToAST.test.ts +156 -0
  38. package/src/__tests__/schemaUpgrader.test.ts +0 -2
  39. package/src/__tests__/subgraphValidation.test.ts +58 -0
  40. package/src/buildSchema.ts +138 -47
  41. package/src/coreSpec.ts +41 -6
  42. package/src/definitions.ts +141 -15
  43. package/src/directiveAndTypeSpecification.ts +1 -1
  44. package/src/federation.ts +40 -0
  45. package/src/graphQLJSSchemaToAST.ts +138 -0
  46. package/src/index.ts +1 -0
  47. package/src/operations.ts +44 -18
  48. package/src/print.ts +30 -15
  49. package/tsconfig.test.tsbuildinfo +1 -1
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,156 @@
1
+ import {
2
+ buildClientSchema,
3
+ buildSchema,
4
+ GraphQLSchema,
5
+ introspectionFromSchema,
6
+ print
7
+ } from "graphql";
8
+ import { graphQLJSSchemaToAST } from "../graphQLJSSchemaToAST";
9
+ import './matchers';
10
+
11
+ function validateRoundtrip(schemaStr: string, expectedWithoutASTNodes: string | undefined = schemaStr) {
12
+ const schema = buildSchema(schemaStr);
13
+ expect(print(graphQLJSSchemaToAST(schema))).toMatchString(schemaStr);
14
+ if (expectedWithoutASTNodes) {
15
+ expect(print(graphQLJSSchemaToAST(withoutASTNodes(schema)))).toMatchString(expectedWithoutASTNodes);
16
+ }
17
+ }
18
+
19
+ function withoutASTNodes(schema: GraphQLSchema): GraphQLSchema {
20
+ return buildClientSchema(introspectionFromSchema(schema));
21
+ }
22
+
23
+ it('round-trip for all type definitions', () => {
24
+ const schema = `
25
+ type Query {
26
+ a: A
27
+ b: B
28
+ c: C
29
+ d(arg: D): Int
30
+ }
31
+
32
+ interface I {
33
+ x: Int
34
+ }
35
+
36
+ type A implements I {
37
+ x: Int
38
+ y: Int
39
+ }
40
+
41
+ union B = A | Query
42
+
43
+ enum C {
44
+ V1
45
+ V2
46
+ }
47
+
48
+ input D {
49
+ m: Int
50
+ n: Int = 3
51
+ }
52
+ `;
53
+
54
+ validateRoundtrip(schema);
55
+ });
56
+
57
+ it('round-trip with default arguments', () => {
58
+ const schemaFct = (v: string) => `
59
+ type Query {
60
+ f(arg: V = ${v}): Int
61
+ }
62
+
63
+ input V {
64
+ x: Int
65
+ y: Int = 3
66
+ }
67
+ `;
68
+
69
+ const schema = schemaFct('{x: 2}');
70
+ // We go through introspection to ensure the AST nodes are
71
+ // removed, but that also somehow expand default values (which is
72
+ // fine, we just have to account for it in our assertion).
73
+ const schemaWithDefaultExpanded = schemaFct('{x: 2, y: 3}');
74
+
75
+ validateRoundtrip(schema, schemaWithDefaultExpanded);
76
+ });
77
+
78
+ it('round-trip for directive definitions and applications', () => {
79
+ const directiveDefinitions = `directive @schemaDirective(v: Int!) on SCHEMA
80
+
81
+ directive @typeDirective repeatable on OBJECT
82
+
83
+ directive @fieldDirective(s: String, m: Int = 3) on FIELD_DEFINITION
84
+ `;
85
+
86
+ const schema = `
87
+ schema @schemaDirective(v: 3) {
88
+ query: Query
89
+ }
90
+
91
+ type Query @typeDirective @typeDirective {
92
+ f: Int @fieldDirective(s: "foo")
93
+ g: Int @deprecated
94
+ }
95
+
96
+ ${directiveDefinitions}
97
+ `;
98
+
99
+ // With the ast nodes removed, we lose custom directive applications
100
+ const noApplications = `
101
+ type Query {
102
+ f: Int
103
+ g: Int @deprecated
104
+ }
105
+
106
+ ${directiveDefinitions}
107
+ `;
108
+
109
+ validateRoundtrip(schema, noApplications);
110
+ });
111
+
112
+ it('round-trip with extensions', () => {
113
+ const common = `scalar federation_FieldSet
114
+
115
+ scalar link_Import
116
+
117
+ directive @link(url: String!, import: link_Import) on SCHEMA
118
+
119
+ directive @key(fields: federation_FieldSet) repeatable on OBJECT
120
+ `;
121
+
122
+ const schema = `
123
+ extend schema @link(url: "https://specs.apollo.dev", import: ["@key"])
124
+
125
+ type Query {
126
+ t: T
127
+ }
128
+
129
+ type T
130
+
131
+ extend type T @key(fields: "id") {
132
+ id: ID!
133
+ x: Int
134
+ }
135
+
136
+ ${common}
137
+ `;
138
+
139
+ // No AST means we lose both the directive applications, but also whether something is an
140
+ // extension or not.
141
+ const noAST = `
142
+ type Query {
143
+ t: T
144
+ }
145
+
146
+ type T {
147
+ id: ID!
148
+ x: Int
149
+ }
150
+
151
+ ${common}
152
+ `;
153
+
154
+ validateRoundtrip(schema, noAST);
155
+ });
156
+
@@ -92,7 +92,6 @@ test('upgrade complex schema', () => {
92
92
 
93
93
  expect(res.subgraphs?.get('s1')?.toString()).toMatchString(`
94
94
  schema
95
- @link(url: "https://specs.apollo.dev/link/v1.0")
96
95
  ${FEDERATION2_LINK_WTH_FULL_IMPORTS}
97
96
  {
98
97
  query: Query
@@ -149,7 +148,6 @@ test('update federation directive non-string arguments', () => {
149
148
 
150
149
  expect(res.subgraphs?.get('s')?.toString()).toMatchString(`
151
150
  schema
152
- @link(url: "https://specs.apollo.dev/link/v1.0")
153
151
  ${FEDERATION2_LINK_WTH_FULL_IMPORTS}
154
152
  {
155
153
  query: Query
@@ -682,6 +682,19 @@ describe('@core/@link handling', () => {
682
682
 
683
683
  directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
684
684
  `,
685
+ gql`
686
+ extend schema
687
+ @link(url: "https://specs.apollo.dev/federation/v2.0")
688
+
689
+ type T {
690
+ k: ID!
691
+ }
692
+
693
+ enum link__Purpose {
694
+ EXECUTION
695
+ SECURITY
696
+ }
697
+ `,
685
698
  ];
686
699
 
687
700
  // Note that we cannot use `validateFullSchema` as-is for those examples because the order or directive is going
@@ -862,6 +875,27 @@ describe('@core/@link handling', () => {
862
875
  ]]);
863
876
  });
864
877
 
878
+ it('errors on invalid definition for @link Purpose', () => {
879
+ const doc = gql`
880
+ extend schema
881
+ @link(url: "https://specs.apollo.dev/federation/v2.0")
882
+
883
+ type T {
884
+ k: ID!
885
+ }
886
+
887
+ enum link__Purpose {
888
+ EXECUTION
889
+ RANDOM
890
+ }
891
+ `;
892
+
893
+ expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[
894
+ 'TYPE_DEFINITION_INVALID',
895
+ '[S] Invalid definition for type "Purpose": expected values [EXECUTION, SECURITY] but found [EXECUTION, RANDOM].',
896
+ ]]);
897
+ });
898
+
865
899
  it('allows any (non-scalar) type in redefinition when expected type is a scalar', () => {
866
900
  const doc = gql`
867
901
  extend schema
@@ -878,6 +912,30 @@ describe('@core/@link handling', () => {
878
912
  // Just making sure this don't error out.
879
913
  buildAndValidate(doc);
880
914
  });
915
+
916
+ it('allows defining a repeatable directive as non-repeatable but validates usages', () => {
917
+ const doc = gql`
918
+ type T @key(fields: "k1") @key(fields: "k2") {
919
+ k1: ID!
920
+ k2: ID!
921
+ }
922
+
923
+ directive @key(fields: String!) on OBJECT
924
+ `;
925
+
926
+
927
+ // Test for fed2 (with @key being @link-ed)
928
+ expect(buildForErrors(doc)).toStrictEqual([[
929
+ 'INVALID_GRAPHQL',
930
+ '[S] The directive "@key" can only be used once at this location.',
931
+ ]]);
932
+
933
+ // Test for fed1
934
+ expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[
935
+ 'INVALID_GRAPHQL',
936
+ '[S] The directive "@key" can only be used once at this location.',
937
+ ]]);
938
+ });
881
939
  });
882
940
 
883
941
  describe('federation 1 schema', () => {
@@ -19,6 +19,10 @@ import {
19
19
  SchemaExtensionNode,
20
20
  parseType,
21
21
  Kind,
22
+ TypeDefinitionNode,
23
+ TypeExtensionNode,
24
+ EnumTypeExtensionNode,
25
+ EnumTypeDefinitionNode,
22
26
  } from "graphql";
23
27
  import { Maybe } from "graphql/jsutils/Maybe";
24
28
  import { valueFromASTUntyped } from "./values";
@@ -49,6 +53,7 @@ import {
49
53
  Extension,
50
54
  ErrGraphQLValidationFailed,
51
55
  errorCauses,
56
+ NamedSchemaElement,
52
57
  } from "./definitions";
53
58
 
54
59
  function buildValue(value?: ValueNode): any {
@@ -70,9 +75,52 @@ export function buildSchemaFromAST(
70
75
  ): Schema {
71
76
  const errors: GraphQLError[] = [];
72
77
  const schema = new Schema(options?.blueprint);
78
+
79
+ // Building schema has to proceed in a particular order due to 2 main constraints:
80
+ // 1. some elements can refer other elements even if the definition of those referenced elements appear later in the AST.
81
+ // And in fact, definitions can be cyclic (a type having field whose type is themselves for instance). Which we
82
+ // deal with by first adding empty definition for every type and directive name, because handling any of their content.
83
+ // 2. we accept "incomplete" schema due to `@link` (incomplete in the sense of the graphQL spec). Indeed, `@link` is all
84
+ // about importing definitions, but that mean that some element may be _reference_ in the AST without their _definition_
85
+ // being in the AST. So we need to ensure we "import" those definitions before we try to "build" references to them.
86
+
87
+
73
88
  // We do a first pass to add all empty types and directives definition. This ensure any reference on one of
74
89
  // those can be resolved in the 2nd pass, regardless of the order of the definitions in the AST.
75
- const { directiveDefinitions, schemaDefinitions, schemaExtensions } = buildNamedTypeAndDirectivesShallow(documentNode, schema);
90
+ const {
91
+ directiveDefinitions,
92
+ typeDefinitions,
93
+ typeExtensions,
94
+ schemaDefinitions,
95
+ schemaExtensions,
96
+ } = buildNamedTypeAndDirectivesShallow(documentNode, schema, errors);
97
+
98
+ // We then build the content of enum types, but excluding their directive _applications. The reason we do this
99
+ // is that:
100
+ // 1. we can (enum values are self-contained and cannot reference anything that may need to be imported first; this
101
+ // is also why we skip directive applications at that point, as those _may_ reference something that hasn't been imported yet)
102
+ // 2. this allows the code to handle better the case where the `link__Purpose` enum is provided in the AST despite the `@link`
103
+ // _definition_ not being provided. And the reason that is true is that as we later _add_ the `@link` definition, we
104
+ // will need to check if `link_Purpose` needs to be added or not, but when it is already present, we check it's definition
105
+ // is the expected, but that check will unexpected fail if we haven't finished "building" said type definition.
106
+ // Do note that we can only do that "early building" for scalar and enum types (and it happens that there is nothing to do
107
+ // for scalar because they are the only types whose "content" don't reference other types (and again, for definitions
108
+ // referencing other types, we need to import `@link`-ed definition first). Thankfully, the `@link` directive definition
109
+ // only rely on a scalar (`Import`) and an enum (`Purpose`) type (if that ever changes, we may have to something more here
110
+ // to be resilient to weirdly incomplete schema).
111
+ for (const typeNode of typeDefinitions) {
112
+ if (typeNode.kind === Kind.ENUM_TYPE_DEFINITION) {
113
+ buildEnumTypeValuesWithoutDirectiveApplications(typeNode, schema.type(typeNode.name.value) as EnumType);
114
+ }
115
+ }
116
+ for (const typeExtensionNode of typeExtensions) {
117
+ if (typeExtensionNode.kind === Kind.ENUM_TYPE_EXTENSION) {
118
+ const toExtend = schema.type(typeExtensionNode.name.value)!;
119
+ const extension = toExtend.newExtension();
120
+ extension.sourceAST = typeExtensionNode;
121
+ buildEnumTypeValuesWithoutDirectiveApplications(typeExtensionNode, schema.type(typeExtensionNode.name.value) as EnumType, extension);
122
+ }
123
+ }
76
124
 
77
125
  // We then deal with directive definition first. This is mainly for the sake of core schemas: the core schema
78
126
  // handling in `Schema` detects that the schema is a core one when it see the application of `@core(feature: ".../core/...")`
@@ -105,32 +153,14 @@ export function buildSchemaFromAST(
105
153
  buildDirectiveApplicationsInDirectiveDefinition(directiveDefinitionNode, schema.directive(directiveDefinitionNode.name.value)!, errors);
106
154
  }
107
155
 
108
- for (const definitionNode of documentNode.definitions) {
109
- switch (definitionNode.kind) {
110
- case 'OperationDefinition':
111
- case 'FragmentDefinition':
112
- errors.push(new GraphQLError("Invalid executable definition found while building schema", definitionNode));
113
- continue;
114
- case 'ScalarTypeDefinition':
115
- case 'ObjectTypeDefinition':
116
- case 'InterfaceTypeDefinition':
117
- case 'UnionTypeDefinition':
118
- case 'EnumTypeDefinition':
119
- case 'InputObjectTypeDefinition':
120
- buildNamedTypeInner(definitionNode, schema.type(definitionNode.name.value)!, schema.blueprint, errors);
121
- break;
122
- case 'ScalarTypeExtension':
123
- case 'ObjectTypeExtension':
124
- case 'InterfaceTypeExtension':
125
- case 'UnionTypeExtension':
126
- case 'EnumTypeExtension':
127
- case 'InputObjectTypeExtension':
128
- const toExtend = schema.type(definitionNode.name.value)!;
129
- const extension = toExtend.newExtension();
130
- extension.sourceAST = definitionNode;
131
- buildNamedTypeInner(definitionNode, toExtend, schema.blueprint, errors, extension);
132
- break;
133
- }
156
+ for (const typeNode of typeDefinitions) {
157
+ buildNamedTypeInner(typeNode, schema.type(typeNode.name.value)!, schema.blueprint, errors);
158
+ }
159
+ for (const typeExtensionNode of typeExtensions) {
160
+ const toExtend = schema.type(typeExtensionNode.name.value)!;
161
+ const extension = toExtend.newExtension();
162
+ extension.sourceAST = typeExtensionNode;
163
+ buildNamedTypeInner(typeExtensionNode, toExtend, schema.blueprint, errors, extension);
134
164
  }
135
165
 
136
166
  // Note: we could try calling `schema.validate()` regardless of errors building the schema and merge the resulting
@@ -149,18 +179,27 @@ export function buildSchemaFromAST(
149
179
  return schema;
150
180
  }
151
181
 
152
- function buildNamedTypeAndDirectivesShallow(documentNode: DocumentNode, schema: Schema): {
182
+ function buildNamedTypeAndDirectivesShallow(documentNode: DocumentNode, schema: Schema, errors: GraphQLError[]): {
153
183
  directiveDefinitions: DirectiveDefinitionNode[],
184
+ typeDefinitions: TypeDefinitionNode[],
185
+ typeExtensions: TypeExtensionNode[],
154
186
  schemaDefinitions: SchemaDefinitionNode[],
155
187
  schemaExtensions: SchemaExtensionNode[],
156
188
  } {
157
189
  const directiveDefinitions = [];
190
+ const typeDefinitions = [];
191
+ const typeExtensions = [];
158
192
  const schemaDefinitions = [];
159
193
  const schemaExtensions = [];
160
194
  for (const definitionNode of documentNode.definitions) {
161
195
  switch (definitionNode.kind) {
196
+ case 'OperationDefinition':
197
+ case 'FragmentDefinition':
198
+ errors.push(new GraphQLError("Invalid executable definition found while building schema", definitionNode));
199
+ continue;
162
200
  case 'SchemaDefinition':
163
201
  schemaDefinitions.push(definitionNode);
202
+ schema.schemaDefinition.preserveEmptyDefinition = true;
164
203
  break;
165
204
  case 'SchemaExtension':
166
205
  schemaExtensions.push(definitionNode);
@@ -171,17 +210,48 @@ function buildNamedTypeAndDirectivesShallow(documentNode: DocumentNode, schema:
171
210
  case 'UnionTypeDefinition':
172
211
  case 'EnumTypeDefinition':
173
212
  case 'InputObjectTypeDefinition':
213
+ typeDefinitions.push(definitionNode);
214
+ let type = schema.type(definitionNode.name.value);
215
+ // Note that the type may already exists due to an extension having been processed first, but we know we
216
+ // have seen 2 definitions (which is invalid) if the definition has `preserverEmptyDefnition` already set
217
+ // since it's only set for definitions, not extensions.
218
+ // Also note that we allow to redefine built-ins.
219
+ if (!type || type.isBuiltIn) {
220
+ type = schema.addType(newNamedType(withoutTrailingDefinition(definitionNode.kind), definitionNode.name.value));
221
+ } else if (type.preserveEmptyDefinition) {
222
+ // Note: we reuse the same error message than graphQL-js would output
223
+ throw new GraphQLError(`There can be only one type named "${definitionNode.name.value}"`);
224
+ }
225
+ // It's possible for the type definition to be empty, because it is valid graphQL to have:
226
+ // type Foo
227
+ //
228
+ // extend type Foo {
229
+ // bar: Int
230
+ // }
231
+ // and we need a way to distinguish between the case above, and the case where only an extension is provided.
232
+ // `preserveEmptyDefinition` serves that purpose.
233
+ // Note that we do this even if the type was already existing because an extension could have been processed
234
+ // first and have created the definition, but we still want to remember that the definition _does_ exists.
235
+ type.preserveEmptyDefinition = true;
236
+ break;
174
237
  case 'ScalarTypeExtension':
175
238
  case 'ObjectTypeExtension':
176
239
  case 'InterfaceTypeExtension':
177
240
  case 'UnionTypeExtension':
178
241
  case 'EnumTypeExtension':
179
242
  case 'InputObjectTypeExtension':
180
- // Note that because of extensions, this may be called multiple times for the same type.
181
- // But at the same time, we want to allow redefining built-in types, because some users do it.
243
+ typeExtensions.push(definitionNode);
182
244
  const existing = schema.type(definitionNode.name.value);
183
- if (!existing || existing.isBuiltIn) {
245
+ // In theory, graphQL does not let you have an extension without a corresponding definition. However,
246
+ // 1) this is validated later, so there is no real reason to do it here and
247
+ // 2) we actually accept it for federation subgraph (due to federation 1 mostly as it's not strictly needed
248
+ // for federation 22, but it is still supported to ease migration there too).
249
+ // So if the type exists, we simply create it. However, we don't set `preserveEmptyDefinition` since it
250
+ // is _not_ a definition.
251
+ if (!existing) {
184
252
  schema.addType(newNamedType(withoutTrailingDefinition(definitionNode.kind), definitionNode.name.value));
253
+ } else if (existing.isBuiltIn) {
254
+ throw new GraphQLError(`Cannot extend built-in type "${definitionNode.name.value}"`);
185
255
  }
186
256
  break;
187
257
  case 'DirectiveDefinition':
@@ -192,6 +262,8 @@ function buildNamedTypeAndDirectivesShallow(documentNode: DocumentNode, schema:
192
262
  }
193
263
  return {
194
264
  directiveDefinitions,
265
+ typeDefinitions,
266
+ typeExtensions,
195
267
  schemaDefinitions,
196
268
  schemaExtensions,
197
269
  }
@@ -293,6 +365,15 @@ function buildNamedTypeInner(
293
365
  extension?: Extension<any>,
294
366
  ) {
295
367
  switch (definitionNode.kind) {
368
+ case 'EnumTypeDefinition':
369
+ case 'EnumTypeExtension':
370
+ // We built enum values earlier in the `buildEnumTypeValuesWithoutDirectiveApplications`, but as the name
371
+ // of that method implies, we just need to finish building directive applications.
372
+ const enumType = type as EnumType;
373
+ for (const enumVal of definitionNode.values ?? []) {
374
+ buildAppliedDirectives(enumVal, enumType.value(enumVal.name.value)!, errors);
375
+ }
376
+ break;
296
377
  case 'ObjectTypeDefinition':
297
378
  case 'ObjectTypeExtension':
298
379
  case 'InterfaceTypeDefinition':
@@ -337,18 +418,6 @@ function buildNamedTypeInner(
337
418
  );
338
419
  }
339
420
  break;
340
- case 'EnumTypeDefinition':
341
- case 'EnumTypeExtension':
342
- const enumType = type as EnumType;
343
- for (const enumVal of definitionNode.values ?? []) {
344
- const v = enumType.addValue(enumVal.name.value);
345
- if (enumVal.description) {
346
- v.description = enumVal.description.value;
347
- }
348
- v.setOfExtension(extension);
349
- buildAppliedDirectives(enumVal, v, errors);
350
- }
351
- break;
352
421
  case 'InputObjectTypeDefinition':
353
422
  case 'InputObjectTypeExtension':
354
423
  const inputObjectType = type as InputObjectType;
@@ -360,10 +429,33 @@ function buildNamedTypeInner(
360
429
  break;
361
430
  }
362
431
  buildAppliedDirectives(definitionNode, type, errors, extension);
432
+ buildDescriptionAndSourceAST(definitionNode, type);
433
+ }
434
+
435
+ function buildEnumTypeValuesWithoutDirectiveApplications(
436
+ definitionNode: EnumTypeDefinitionNode | EnumTypeExtensionNode,
437
+ type: EnumType,
438
+ extension?: Extension<any>,
439
+ ) {
440
+ const enumType = type as EnumType;
441
+ for (const enumVal of definitionNode.values ?? []) {
442
+ const v = enumType.addValue(enumVal.name.value);
443
+ if (enumVal.description) {
444
+ v.description = enumVal.description.value;
445
+ }
446
+ v.setOfExtension(extension);
447
+ }
448
+ buildDescriptionAndSourceAST(definitionNode, type);
449
+ }
450
+
451
+ function buildDescriptionAndSourceAST<T extends NamedSchemaElement<T, Schema, unknown>>(
452
+ definitionNode: DefinitionNode & NodeWithDescription,
453
+ dest: T,
454
+ ) {
363
455
  if (definitionNode.description) {
364
- type.description = definitionNode.description.value;
456
+ dest.description = definitionNode.description.value;
365
457
  }
366
- type.sourceAST = definitionNode;
458
+ dest.sourceAST = definitionNode;
367
459
  }
368
460
 
369
461
  function buildFieldDefinitionInner(
@@ -458,8 +550,7 @@ function buildDirectiveDefinitionInnerWithoutDirectiveApplications(
458
550
  directive.repeatable = directiveNode.repeatable;
459
551
  const locations = directiveNode.locations.map(({ value }) => value as DirectiveLocation);
460
552
  directive.addLocations(...locations);
461
- directive.description = directiveNode.description?.value;
462
- directive.sourceAST = directiveNode;
553
+ buildDescriptionAndSourceAST(directiveNode, directive);
463
554
  }
464
555
 
465
556
  function buildDirectiveApplicationsInDirectiveDefinition(
package/src/coreSpec.ts CHANGED
@@ -3,7 +3,7 @@ import { URL } from "url";
3
3
  import { CoreFeature, Directive, DirectiveDefinition, EnumType, ErrGraphQLAPISchemaValidationFailed, ErrGraphQLValidationFailed, InputType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition, SchemaElement } from "./definitions";
4
4
  import { sameType } from "./types";
5
5
  import { err } from '@apollo/core-schema';
6
- import { assert } from './utils';
6
+ import { assert, firstOf } from './utils';
7
7
  import { ERRORS } from "./error";
8
8
  import { valueToString } from "./values";
9
9
  import { coreFeatureDefinitionIfKnown, registerKnownFeature } from "./knownCoreFeatures";
@@ -361,8 +361,8 @@ export class CoreSpecDefinition extends FeatureDefinition {
361
361
 
362
362
  // TODO: we may want to allow some `import` as argument to this method. When we do, we need to watch for imports of
363
363
  // `Purpose` and `Import` and add the types under their imported name.
364
- addToSchema(schema: Schema, as?: string): GraphQLError[] {
365
- const errors = this.addDefinitionsToSchema(schema, as);
364
+ addToSchema(schema: Schema, alias?: string): GraphQLError[] {
365
+ const errors = this.addDefinitionsToSchema(schema, alias);
366
366
  if (errors.length > 0) {
367
367
  return errors;
368
368
  }
@@ -370,10 +370,45 @@ export class CoreSpecDefinition extends FeatureDefinition {
370
370
  // Note: we don't use `applyFeatureToSchema` because it would complain the schema is not a core schema, which it isn't
371
371
  // until the next line.
372
372
  const args = { [this.urlArgName()]: this.toString() } as unknown as CoreOrLinkDirectiveArgs;
373
- if (as) {
374
- args.as = as;
373
+ if (alias) {
374
+ args.as = alias;
375
+ }
376
+
377
+ // This adds `@link(url: "https://specs.apollo.dev/link/v1.0")` to the "schema" definition. And we have
378
+ // a choice to add it either the main definition, or to an `extend schema`.
379
+ //
380
+ // In theory, always adding it to the main definition should be safe since even if some root operations
381
+ // can be defined in extensions, you shouldn't have an extension without a definition, and so we should
382
+ // never be in a case where _all_ root operations are defined in extensions (which would be a problem
383
+ // for printing the definition itsef since it's syntactically invalid to have a schema definition with
384
+ // no operations).
385
+ //
386
+ // In practice however, graphQL-js has historically accepted extensions without definition for schema,
387
+ // and we even abuse this a bit with federation out of convenience, so we could end up in the situation
388
+ // where if we put the directive on the definition, it cannot be printed properly due to the user having
389
+ // defined all its root operations in an extension.
390
+ //
391
+ // We could always add the directive to an extension, and that could kind of work but:
392
+ // 1. the core/link spec says that the link-to-link application should be the first `@link` of the
393
+ // schema, but if user put some `@link` on their schema definition but we always put the link-to-link
394
+ // on an extension, then we're kind of not respecting our own spec (in practice, our own code can
395
+ // actually handle this as it does not strongly rely on that "it should be the first" rule, but that
396
+ // would set a bad example).
397
+ // 2. earlier versions (pre-#1875) were always putting that directive on the definition, and we wanted
398
+ // to avoid suprising users by changing that for not reason.
399
+ //
400
+ // So instead, we put the directive on the schema definition unless some extensions exists but no
401
+ // definition does (that is, no non-extension elements are populated).
402
+ const schemaDef = schema.schemaDefinition;
403
+ // Side-note: this test must be done _before_ we call `applyDirective`, otherwise it would take it into
404
+ // account.
405
+ const hasDefinition = schemaDef.hasNonExtensionElements();
406
+ const directive = schemaDef.applyDirective(alias ?? this.url.name, args, true);
407
+ if (!hasDefinition && schemaDef.hasExtensionElements()) {
408
+ const extension = firstOf(schemaDef.extensions());
409
+ assert(extension, '`hasExtensionElements` should not have been `true`');
410
+ directive.setOfExtension(extension);
375
411
  }
376
- schema.schemaDefinition.applyDirective(as ?? this.url.name, args, true);
377
412
  return [];
378
413
  }
379
414