@apollo/federation-internals 2.0.2-alpha.1 → 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.
- package/CHANGELOG.md +6 -27
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +76 -49
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts +19 -3
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +110 -10
- package/dist/definitions.js.map +1 -1
- package/dist/directiveAndTypeSpecification.js +1 -1
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/federation.d.ts +1 -0
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +20 -0
- package/dist/federation.js.map +1 -1
- package/dist/graphQLJSSchemaToAST.d.ts +8 -0
- package/dist/graphQLJSSchemaToAST.d.ts.map +1 -0
- package/dist/graphQLJSSchemaToAST.js +96 -0
- package/dist/graphQLJSSchemaToAST.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +2 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +29 -18
- package/dist/operations.js.map +1 -1
- package/dist/print.d.ts +2 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +23 -15
- package/dist/print.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/definitions.test.ts +138 -1
- package/src/__tests__/graphQLJSSchemaToAST.test.ts +156 -0
- package/src/__tests__/schemaUpgrader.test.ts +0 -2
- package/src/__tests__/subgraphValidation.test.ts +58 -0
- package/src/buildSchema.ts +138 -47
- package/src/definitions.ts +146 -16
- package/src/directiveAndTypeSpecification.ts +1 -1
- package/src/federation.ts +40 -0
- package/src/graphQLJSSchemaToAST.ts +138 -0
- package/src/index.ts +1 -0
- package/src/operations.ts +44 -18
- package/src/print.ts +30 -15
- package/tsconfig.test.tsbuildinfo +1 -1
- 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', () => {
|
package/src/buildSchema.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
+
dest.description = definitionNode.description.value;
|
|
365
457
|
}
|
|
366
|
-
|
|
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
|
|
462
|
-
directive.sourceAST = directiveNode;
|
|
553
|
+
buildDescriptionAndSourceAST(directiveNode, directive);
|
|
463
554
|
}
|
|
464
555
|
|
|
465
556
|
function buildDirectiveApplicationsInDirectiveDefinition(
|