@apollo/federation-internals 2.0.2 → 2.0.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 (45) hide show
  1. package/CHANGELOG.md +4 -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/definitions.d.ts +19 -3
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +106 -9
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/directiveAndTypeSpecification.js +1 -1
  10. package/dist/directiveAndTypeSpecification.js.map +1 -1
  11. package/dist/federation.d.ts +1 -0
  12. package/dist/federation.d.ts.map +1 -1
  13. package/dist/federation.js +20 -0
  14. package/dist/federation.js.map +1 -1
  15. package/dist/graphQLJSSchemaToAST.d.ts +8 -0
  16. package/dist/graphQLJSSchemaToAST.d.ts.map +1 -0
  17. package/dist/graphQLJSSchemaToAST.js +96 -0
  18. package/dist/graphQLJSSchemaToAST.js.map +1 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/operations.d.ts +2 -1
  24. package/dist/operations.d.ts.map +1 -1
  25. package/dist/operations.js +29 -18
  26. package/dist/operations.js.map +1 -1
  27. package/dist/print.d.ts +2 -1
  28. package/dist/print.d.ts.map +1 -1
  29. package/dist/print.js +23 -15
  30. package/dist/print.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/definitions.test.ts +117 -1
  33. package/src/__tests__/graphQLJSSchemaToAST.test.ts +156 -0
  34. package/src/__tests__/schemaUpgrader.test.ts +0 -2
  35. package/src/__tests__/subgraphValidation.test.ts +58 -0
  36. package/src/buildSchema.ts +138 -47
  37. package/src/definitions.ts +141 -15
  38. package/src/directiveAndTypeSpecification.ts +1 -1
  39. package/src/federation.ts +40 -0
  40. package/src/graphQLJSSchemaToAST.ts +138 -0
  41. package/src/index.ts +1 -0
  42. package/src/operations.ts +44 -18
  43. package/src/print.ts +30 -15
  44. package/tsconfig.test.tsbuildinfo +1 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -10,7 +10,7 @@ import {
10
10
  InputObjectType,
11
11
  } from '../../dist/definitions';
12
12
  import {
13
- printSchema as printGraphQLjsSchema
13
+ printSchema as printGraphQLjsSchema,
14
14
  } from 'graphql';
15
15
  import { defaultPrintOptions, printSchema } from '../../dist/print';
16
16
  import { buildSchema } from '../../dist/buildSchema';
@@ -571,6 +571,122 @@ test('handling of extensions', () => {
571
571
  `);
572
572
  });
573
573
 
574
+ describe('type extension where definition is empty', () => {
575
+ it('works for object types', () => {
576
+ const sdl = `
577
+ type Query {
578
+ foo: Foo
579
+ }
580
+
581
+ type Foo
582
+
583
+ extend type Foo {
584
+ baz: String
585
+ }
586
+ `;
587
+
588
+ const schema = buildSchema(sdl);
589
+ expect(printSchema(schema)).toMatchString(sdl);
590
+ expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy();
591
+ expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy();
592
+ });
593
+
594
+ it('works for union', () => {
595
+ const sdl = `
596
+ type Query {
597
+ foo: Foo
598
+ }
599
+
600
+ union Foo
601
+
602
+ extend union Foo = Bar
603
+
604
+ type Bar {
605
+ x: Int
606
+ }
607
+ `;
608
+
609
+ const schema = buildSchema(sdl);
610
+ expect(printSchema(schema)).toMatchString(sdl);
611
+ expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy();
612
+ expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy();
613
+ });
614
+
615
+ it('works for enum', () => {
616
+ const sdl = `
617
+ type Query {
618
+ foo: Foo
619
+ }
620
+
621
+ enum Foo
622
+
623
+ extend enum Foo {
624
+ Bar
625
+ }
626
+ `;
627
+
628
+ const schema = buildSchema(sdl);
629
+ expect(printSchema(schema)).toMatchString(sdl);
630
+ expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy();
631
+ expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy();
632
+ });
633
+
634
+ it('works for input object types', () => {
635
+ const sdl = `
636
+ type Query {
637
+ foo: Int
638
+ }
639
+
640
+ input Foo
641
+
642
+ extend input Foo {
643
+ bar: Int
644
+ }
645
+ `;
646
+
647
+ const schema = buildSchema(sdl);
648
+ expect(printSchema(schema)).toMatchString(sdl);
649
+ expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy();
650
+ expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy();
651
+ });
652
+
653
+ it('works for scalar type', () => {
654
+ const sdl = `
655
+ type Query {
656
+ foo: Int
657
+ }
658
+
659
+ scalar Foo
660
+
661
+ extend scalar Foo
662
+ @specifiedBy(url: "something")
663
+ `;
664
+
665
+ const schema = buildSchema(sdl);
666
+ expect(printSchema(schema)).toMatchString(sdl);
667
+ expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy();
668
+ expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy();
669
+ });
670
+ })
671
+
672
+ test('reject type defined multiple times', () => {
673
+ const sdl = `
674
+ type Query {
675
+ foo: Foo
676
+ }
677
+
678
+ type Foo {
679
+ bar: String
680
+ }
681
+
682
+ type Foo {
683
+ baz: String
684
+ }
685
+ `;
686
+
687
+ expect(() => buildSchema(sdl).validate()).toThrow('There can be only one type named "Foo"');
688
+ });
689
+
574
690
  test('default arguments for directives', () => {
575
691
  const sdl = `
576
692
  directive @Example(inputObject: ExampleInputObject! = {}) on FIELD_DEFINITION
@@ -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(