@apollo/federation-internals 2.0.0-alpha.6 → 2.0.0-preview.10
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 +43 -3
- package/dist/buildSchema.d.ts +7 -3
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +94 -61
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +39 -9
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +232 -42
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +71 -51
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +326 -231
- package/dist/definitions.js.map +1 -1
- package/dist/directiveAndTypeSpecification.d.ts +48 -0
- package/dist/directiveAndTypeSpecification.d.ts.map +1 -0
- package/dist/directiveAndTypeSpecification.js +253 -0
- package/dist/directiveAndTypeSpecification.js.map +1 -0
- package/dist/error.d.ts +21 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +63 -3
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +42 -97
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +102 -46
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +762 -234
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +23 -0
- package/dist/federationSpec.d.ts.map +1 -0
- package/dist/federationSpec.js +117 -0
- package/dist/federationSpec.js.map +1 -0
- package/dist/inaccessibleSpec.d.ts +5 -1
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +31 -3
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts.map +1 -1
- package/dist/introspection.js +8 -3
- package/dist/introspection.js.map +1 -1
- package/dist/joinSpec.d.ts +6 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +22 -0
- package/dist/joinSpec.js.map +1 -1
- package/dist/knownCoreFeatures.d.ts +4 -0
- package/dist/knownCoreFeatures.d.ts.map +1 -0
- package/dist/knownCoreFeatures.js +16 -0
- package/dist/knownCoreFeatures.js.map +1 -0
- package/dist/operations.d.ts +9 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +27 -5
- package/dist/operations.js.map +1 -1
- package/dist/precompute.d.ts +3 -0
- package/dist/precompute.d.ts.map +1 -0
- package/dist/precompute.js +51 -0
- package/dist/precompute.js.map +1 -0
- package/dist/print.d.ts +11 -9
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +32 -22
- package/dist/print.js.map +1 -1
- package/dist/schemaUpgrader.d.ts +108 -0
- package/dist/schemaUpgrader.d.ts.map +1 -0
- package/dist/schemaUpgrader.js +497 -0
- package/dist/schemaUpgrader.js.map +1 -0
- package/dist/suggestions.d.ts +1 -1
- package/dist/suggestions.d.ts.map +1 -1
- package/dist/suggestions.js.map +1 -1
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +3 -3
- package/dist/supergraphs.js.map +1 -1
- package/dist/tagSpec.d.ts +7 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +36 -16
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -1
- package/dist/utils.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +19 -11
- package/dist/validate.js.map +1 -1
- package/dist/validation/KnownTypeNamesInFederationRule.d.ts.map +1 -1
- package/dist/validation/KnownTypeNamesInFederationRule.js +1 -2
- package/dist/validation/KnownTypeNamesInFederationRule.js.map +1 -1
- package/dist/values.d.ts +1 -0
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +3 -2
- package/dist/values.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/coreSpec.test.ts +100 -0
- package/src/__tests__/definitions.test.ts +98 -17
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +64 -0
- package/src/__tests__/federation.test.ts +31 -0
- package/src/__tests__/operations.test.ts +2 -3
- package/src/__tests__/removeInaccessibleElements.test.ts +59 -6
- package/src/__tests__/schemaUpgrader.test.ts +169 -0
- package/src/__tests__/subgraphValidation.test.ts +422 -21
- package/src/__tests__/values.test.ts +2 -4
- package/src/buildSchema.ts +154 -84
- package/src/coreSpec.ts +294 -55
- package/src/definitions.ts +415 -275
- package/src/directiveAndTypeSpecification.ts +353 -0
- package/src/error.ts +143 -5
- package/src/extractSubgraphsFromSupergraph.ts +56 -122
- package/src/federation.ts +991 -302
- package/src/federationSpec.ts +146 -0
- package/src/inaccessibleSpec.ts +39 -11
- package/src/index.ts +4 -0
- package/src/introspection.ts +8 -3
- package/src/joinSpec.ts +35 -4
- package/src/knownCoreFeatures.ts +13 -0
- package/src/operations.ts +37 -7
- package/src/precompute.ts +65 -0
- package/src/print.ts +63 -48
- package/src/schemaUpgrader.ts +653 -0
- package/src/suggestions.ts +1 -1
- package/src/supergraphs.ts +4 -3
- package/src/tagSpec.ts +50 -18
- package/src/utils.ts +63 -0
- package/src/validate.ts +27 -16
- package/src/validation/KnownTypeNamesInFederationRule.ts +1 -7
- package/src/values.ts +7 -3
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTNode,
|
|
3
|
+
GraphQLError,
|
|
4
|
+
Kind,
|
|
5
|
+
print as printAST,
|
|
6
|
+
} from "graphql";
|
|
7
|
+
import { ERRORS } from "./error";
|
|
8
|
+
import {
|
|
9
|
+
baseType,
|
|
10
|
+
Directive,
|
|
11
|
+
errorCauses,
|
|
12
|
+
Extension,
|
|
13
|
+
FieldDefinition,
|
|
14
|
+
isCompositeType,
|
|
15
|
+
isInterfaceType,
|
|
16
|
+
isObjectType,
|
|
17
|
+
NamedSchemaElement,
|
|
18
|
+
NamedType,
|
|
19
|
+
ObjectType,
|
|
20
|
+
Schema,
|
|
21
|
+
SchemaElement,
|
|
22
|
+
} from "./definitions";
|
|
23
|
+
import {
|
|
24
|
+
addSubgraphToError,
|
|
25
|
+
collectTargetFields,
|
|
26
|
+
federationMetadata,
|
|
27
|
+
FederationMetadata,
|
|
28
|
+
printSubgraphNames,
|
|
29
|
+
removeInactiveProvidesAndRequires,
|
|
30
|
+
setSchemaAsFed2Subgraph,
|
|
31
|
+
Subgraph,
|
|
32
|
+
Subgraphs,
|
|
33
|
+
} from "./federation";
|
|
34
|
+
import { assert, firstOf, MultiMap } from "./utils";
|
|
35
|
+
import { FEDERATION_SPEC_TYPES } from "./federationSpec";
|
|
36
|
+
|
|
37
|
+
export type UpgradeResult = UpgradeSuccess | UpgradeFailure;
|
|
38
|
+
|
|
39
|
+
type UpgradeChanges = MultiMap<UpgradeChangeID, UpgradeChange>;
|
|
40
|
+
|
|
41
|
+
export type UpgradeSuccess = {
|
|
42
|
+
subgraphs: Subgraphs,
|
|
43
|
+
changes: Map<string, UpgradeChanges>,
|
|
44
|
+
errors?: never,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type UpgradeFailure = {
|
|
48
|
+
subgraphs?: never,
|
|
49
|
+
changes?: never,
|
|
50
|
+
errors: GraphQLError[],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type UpgradeChangeID = UpgradeChange['id'];
|
|
54
|
+
|
|
55
|
+
export type UpgradeChange =
|
|
56
|
+
ExternalOnTypeExtensionRemoval
|
|
57
|
+
| TypeExtensionRemoval
|
|
58
|
+
| UnusedExternalRemoval
|
|
59
|
+
| TypeWithOnlyUnusedExternalRemoval
|
|
60
|
+
| ExternalOnInterfaceRemoval
|
|
61
|
+
| InactiveProvidesOrRequiresRemoval
|
|
62
|
+
| InactiveProvidesOrRequiresFieldsRemoval
|
|
63
|
+
| ShareableFieldAddition
|
|
64
|
+
| ShareableTypeAddition
|
|
65
|
+
| KeyOnInterfaceRemoval
|
|
66
|
+
| ProvidesOrRequiresOnInterfaceFieldRemoval
|
|
67
|
+
| ProvidesOnNonCompositeRemoval
|
|
68
|
+
| FieldsArgumentCoercionToString
|
|
69
|
+
;
|
|
70
|
+
|
|
71
|
+
export class ExternalOnTypeExtensionRemoval {
|
|
72
|
+
readonly id = 'EXTERNAL_ON_TYPE_EXTENSION_REMOVAL' as const;
|
|
73
|
+
|
|
74
|
+
constructor(readonly field: string) {}
|
|
75
|
+
|
|
76
|
+
toString() {
|
|
77
|
+
return `Removed @external from field "${this.field}" as it is a key of an extension type`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class TypeExtensionRemoval {
|
|
82
|
+
readonly id = 'TYPE_EXTENSION_REMOVAL' as const;
|
|
83
|
+
|
|
84
|
+
constructor(readonly type: string) {}
|
|
85
|
+
|
|
86
|
+
toString() {
|
|
87
|
+
return `Switched type "${this.type}" from an extension to a definition`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class ExternalOnInterfaceRemoval {
|
|
92
|
+
readonly id = 'EXTERNAL_ON_INTERFACE_REMOVAL' as const;
|
|
93
|
+
|
|
94
|
+
constructor(readonly field: string) {}
|
|
95
|
+
|
|
96
|
+
toString() {
|
|
97
|
+
return `Removed @external directive on interface type field "${this.field}": @external is nonsensical on interface fields`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class UnusedExternalRemoval {
|
|
102
|
+
readonly id = 'UNUSED_EXTERNAL_REMOVAL' as const;
|
|
103
|
+
|
|
104
|
+
constructor(readonly field: string) {}
|
|
105
|
+
|
|
106
|
+
toString() {
|
|
107
|
+
return `Removed @external field "${this.field}" as it was not used in any @key, @provides or @requires`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class TypeWithOnlyUnusedExternalRemoval {
|
|
112
|
+
readonly id = 'TYPE_WITH_ONLY_UNUSED_EXTERNAL_REMOVAL' as const;
|
|
113
|
+
|
|
114
|
+
constructor(readonly type: string) {}
|
|
115
|
+
|
|
116
|
+
toString() {
|
|
117
|
+
return `Removed type ${this.type} that is not referenced in the schema and only declares unused @external fields`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class InactiveProvidesOrRequiresRemoval {
|
|
122
|
+
readonly id = 'INACTIVE_PROVIDES_OR_REQUIRES_REMOVAL' as const;
|
|
123
|
+
|
|
124
|
+
constructor(readonly parent: string, readonly removed: string) {}
|
|
125
|
+
|
|
126
|
+
toString() {
|
|
127
|
+
return `Removed directive ${this.removed} on "${this.parent}": none of the fields were truly @external`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class InactiveProvidesOrRequiresFieldsRemoval {
|
|
132
|
+
readonly id = 'INACTIVE_PROVIDES_OR_REQUIRES_FIELDS_REMOVAL' as const;
|
|
133
|
+
|
|
134
|
+
constructor(readonly parent: string, readonly original: string, readonly updated: string) {}
|
|
135
|
+
|
|
136
|
+
toString() {
|
|
137
|
+
return `Updated directive ${this.original} on "${this.parent}" to ${this.updated}: removed fields that were not truly @external`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class ShareableFieldAddition {
|
|
142
|
+
readonly id = 'SHAREABLE_FIELD_ADDITION' as const;
|
|
143
|
+
|
|
144
|
+
constructor(readonly field: string, readonly declaringSubgraphs: string[]) {}
|
|
145
|
+
|
|
146
|
+
toString() {
|
|
147
|
+
return `Added @shareable to field "${this.field}": it is also resolved by ${printSubgraphNames(this.declaringSubgraphs)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class ShareableTypeAddition {
|
|
152
|
+
readonly id = 'SHAREABLE_TYPE_ADDITION' as const;
|
|
153
|
+
|
|
154
|
+
constructor(readonly type: string, readonly declaringSubgraphs: string[]) {}
|
|
155
|
+
|
|
156
|
+
toString() {
|
|
157
|
+
return `Added @shareable to type "${this.type}": it is a "value type" and is also declared in ${printSubgraphNames(this.declaringSubgraphs)}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class KeyOnInterfaceRemoval {
|
|
162
|
+
readonly id = 'KEY_ON_INTERFACE_REMOVAL' as const;
|
|
163
|
+
|
|
164
|
+
constructor(readonly type: string) {}
|
|
165
|
+
|
|
166
|
+
toString() {
|
|
167
|
+
return `Removed @key on interface "${this.type}": while allowed by federation 0.x, @key on interfaces were completely ignored/had no effect`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class ProvidesOrRequiresOnInterfaceFieldRemoval {
|
|
172
|
+
readonly id = 'PROVIDES_OR_REQUIRES_ON_INTERFACE_FIELD_REMOVAL' as const;
|
|
173
|
+
|
|
174
|
+
constructor(readonly field: string, readonly directive: string) {}
|
|
175
|
+
|
|
176
|
+
toString() {
|
|
177
|
+
return `Removed @${this.directive} on interface field "${this.field}": while allowed by federation 0.x, @${this.directive} on interface fields were completely ignored/had no effect`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class ProvidesOnNonCompositeRemoval {
|
|
182
|
+
readonly id = 'PROVIDES_ON_NON_COMPOSITE_REMOVAL' as const;
|
|
183
|
+
|
|
184
|
+
constructor(readonly field: string, readonly type: string) {}
|
|
185
|
+
|
|
186
|
+
toString() {
|
|
187
|
+
return `Removed @provides directive on field "${this.field}" as it is of non-composite type "${this.type}": while not rejected by federation 0.x, such @provide is nonsensical and was ignored`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export class FieldsArgumentCoercionToString {
|
|
192
|
+
readonly id = 'FIELDS_ARGUMENT_COERCION_TO_STRING' as const;
|
|
193
|
+
|
|
194
|
+
constructor(readonly element: string, readonly directive: string, readonly before: string, readonly after: string) {}
|
|
195
|
+
|
|
196
|
+
toString() {
|
|
197
|
+
return `Coerced "fields" argument for directive @${this.directive} for "${this.element}" into a string: coerced from ${this.before} to ${this.after}`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
|
|
202
|
+
const changes: Map<string, UpgradeChanges> = new Map();
|
|
203
|
+
if (inputs.values().every((s) => s.isFed2Subgraph())) {
|
|
204
|
+
return { subgraphs: inputs, changes };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const subgraphs = new Subgraphs();
|
|
208
|
+
let errors: GraphQLError[] = [];
|
|
209
|
+
for (const subgraph of inputs.values()) {
|
|
210
|
+
if (subgraph.isFed2Subgraph()) {
|
|
211
|
+
subgraphs.add(subgraph);
|
|
212
|
+
} else {
|
|
213
|
+
const otherSubgraphs = inputs.values().filter((s) => s.name !== subgraph.name);
|
|
214
|
+
const res = new SchemaUpgrader(subgraph, otherSubgraphs).upgrade();
|
|
215
|
+
if (res.errors) {
|
|
216
|
+
errors = errors.concat(res.errors);
|
|
217
|
+
} else {
|
|
218
|
+
subgraphs.add(res.upgraded);
|
|
219
|
+
changes.set(subgraph.name, res.changes);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return errors.length === 0 ? { subgraphs, changes } : { errors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Wether the type represents a type extension in the sense of federation 1.
|
|
228
|
+
* That is, type extension are a thing in GraphQL, but federation 1 overloads the notion for entities. This method
|
|
229
|
+
* return true if the type is used in the federation 1 sense of an extension.
|
|
230
|
+
* And we recognize federation 1 type extensions as type extension that:
|
|
231
|
+
* 1. are on object type or interface type (note that federation 1 don't really handle interface type extension properly but it "accepts" them
|
|
232
|
+
* so we do it here too).
|
|
233
|
+
* 2. do not have a definition for the same type in the same subgraph (this is a GraphQL extension otherwise).
|
|
234
|
+
*
|
|
235
|
+
* Not that type extensions in federation 1 generally have a @key but in really the code consider something a type extension even without
|
|
236
|
+
* it (which I'd argue is a unintended bug of fed1 since this leads to various problems) so we don't check for the presence of @key here.
|
|
237
|
+
*/
|
|
238
|
+
function isFederationTypeExtension(type: NamedType): boolean {
|
|
239
|
+
const metadata = federationMetadata(type.schema());
|
|
240
|
+
assert(metadata, 'Should be a subgraph schema');
|
|
241
|
+
const hasExtend = type.hasAppliedDirective(metadata.extendsDirective());
|
|
242
|
+
return (type.hasExtensionElements() || hasExtend)
|
|
243
|
+
&& (isObjectType(type) || isInterfaceType(type))
|
|
244
|
+
&& (hasExtend || !type.hasNonExtensionElements());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether the type is a root type but is declared has (only) an extension, which federation 1 actually accepts.
|
|
249
|
+
*/
|
|
250
|
+
function isRootTypeExtension(type: NamedType): boolean {
|
|
251
|
+
const metadata = federationMetadata(type.schema());
|
|
252
|
+
assert(metadata, 'Should be a subgraph schema');
|
|
253
|
+
return isObjectType(type)
|
|
254
|
+
&& type.isRootType()
|
|
255
|
+
&& (type.hasAppliedDirective(metadata.extendsDirective()) || (type.hasExtensionElements() && !type.hasNonExtensionElements()));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolvesField(subgraph: Subgraph, field: FieldDefinition<ObjectType>): boolean {
|
|
259
|
+
const metadata = subgraph.metadata();
|
|
260
|
+
const t = subgraph.schema.type(field.parent.name);
|
|
261
|
+
if (!t || !isObjectType(t)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const f = t.field(field.name);
|
|
265
|
+
return !!f && (!metadata.isFieldExternal(f) || metadata.isFieldPartiallyExternal(f));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
class SchemaUpgrader {
|
|
269
|
+
private readonly changes = new MultiMap<UpgradeChangeID, UpgradeChange>();
|
|
270
|
+
private readonly schema: Schema;
|
|
271
|
+
private readonly subgraph: Subgraph;
|
|
272
|
+
private readonly metadata: FederationMetadata;
|
|
273
|
+
private readonly errors: GraphQLError[] = [];
|
|
274
|
+
|
|
275
|
+
constructor(private readonly originalSubgraph: Subgraph, private readonly otherSubgraphs: Subgraph[]) {
|
|
276
|
+
// Note that as we clone the original schema, the 'sourceAST' values in the elements of the new schema will be those of the original schema
|
|
277
|
+
// and those won't be updated as we modify the schema to make it fed2-enabled. This is _important_ for us here as this is what ensures that
|
|
278
|
+
// later merge errors "AST" nodes ends up pointing to the original schema, the one that make sense to the user.
|
|
279
|
+
this.schema = originalSubgraph.schema.clone();
|
|
280
|
+
this.renameFederationTypes();
|
|
281
|
+
setSchemaAsFed2Subgraph(this.schema);
|
|
282
|
+
this.subgraph = new Subgraph(originalSubgraph.name, originalSubgraph.url, this.schema);
|
|
283
|
+
this.metadata = this.subgraph.metadata();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private addError(e: GraphQLError): void {
|
|
287
|
+
this.errors.push(addSubgraphToError(e, this.subgraph.name, ERRORS.INVALID_GRAPHQL));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private renameFederationTypes() {
|
|
291
|
+
// When we set the upgraded schema as a fed2 schema, we only "import" the federation directives, but not the federation types. This
|
|
292
|
+
// means that those types will be called `_Entity`, `_Any`, ... in the fed1 original schema, but they should be called `federation__Entity`,
|
|
293
|
+
// `federation__Any`, ... in the new upgraded schema.
|
|
294
|
+
// But note that even "importing" those types would not completely work because fed2 essentially drops the `_` at the beginning of those
|
|
295
|
+
// type names (relying on the core schema prefixing instead) and so some special translation needs to happen.
|
|
296
|
+
for (const typeSpec of FEDERATION_SPEC_TYPES) {
|
|
297
|
+
const typeNameInOriginal = this.originalSubgraph.metadata().federationTypeNameInSchema(typeSpec.name);
|
|
298
|
+
const type = this.schema.type(typeNameInOriginal);
|
|
299
|
+
if (type) {
|
|
300
|
+
type.rename(`federation__${typeSpec.name}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private external(elt: FieldDefinition<any>): Directive<any, {}> | undefined {
|
|
306
|
+
const applications = elt.appliedDirectivesOf(this.metadata.externalDirective());
|
|
307
|
+
return applications.length === 0 ? undefined : applications[0];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private addChange(change: UpgradeChange) {
|
|
311
|
+
this.changes.add(change.id, change);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private checkForExtensionWithNoBase(type: NamedType): void {
|
|
315
|
+
// The checks that if the type is a "federation 1" type extension, then another subgraph has a proper definition
|
|
316
|
+
// for that type.
|
|
317
|
+
if (isRootTypeExtension(type) || !isFederationTypeExtension(type)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const extensionAST = firstOf<Extension<any>>(type.extensions().values())?.sourceAST;
|
|
322
|
+
for (const subgraph of this.otherSubgraphs) {
|
|
323
|
+
const otherType = subgraph.schema.type(type.name);
|
|
324
|
+
if (otherType && otherType.hasNonExtensionElements()) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// We look at all the other subgraphs and didn't found a (non-extension) definition of that type
|
|
330
|
+
this.addError(ERRORS.EXTENSION_WITH_NO_BASE.err({
|
|
331
|
+
message: `Type "${type}" is an extension type, but there is no type definition for "${type}" in any subgraph.`,
|
|
332
|
+
nodes: extensionAST,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private preUpgradeValidations(): void {
|
|
337
|
+
for (const type of this.schema.types()) {
|
|
338
|
+
this.checkForExtensionWithNoBase(type);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
upgrade(): { upgraded: Subgraph, changes: UpgradeChanges, errors?: never } | { errors: GraphQLError[] } {
|
|
343
|
+
this.preUpgradeValidations();
|
|
344
|
+
|
|
345
|
+
this.fixFederationDirectivesArguments();
|
|
346
|
+
|
|
347
|
+
this.removeExternalOnInterface();
|
|
348
|
+
|
|
349
|
+
// Note that we remove all external on type extensions first, so we don't have to care about it later in @key, @provides and @requires.
|
|
350
|
+
this.removeExternalOnTypeExtensions();
|
|
351
|
+
|
|
352
|
+
this.fixInactiveProvidesAndRequires();
|
|
353
|
+
|
|
354
|
+
this.removeTypeExtensions();
|
|
355
|
+
|
|
356
|
+
this.removeDirectivesOnInterface();
|
|
357
|
+
|
|
358
|
+
// Note that this rule rely on being after `removeDirectivesOnInterface` in practice (in that it doesn't check interfaces).
|
|
359
|
+
this.removeProvidesOnNonComposite();
|
|
360
|
+
|
|
361
|
+
// Note that this should come _after_ all the other changes that may remove/update federation directives, since those may create unused
|
|
362
|
+
// externals. Which is why this is toward the end.
|
|
363
|
+
this.removeUnusedExternals();
|
|
364
|
+
|
|
365
|
+
this.addShareable();
|
|
366
|
+
|
|
367
|
+
// If we had errors during the upgrade, we throw them before trying to validate the resulting subgraph, because any invalidity in the
|
|
368
|
+
// migrated subgraph may well due to those migration errors and confuse users.
|
|
369
|
+
if (this.errors.length > 0) {
|
|
370
|
+
return { errors: this.errors };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
this.subgraph.validate();
|
|
375
|
+
return {
|
|
376
|
+
upgraded: this.subgraph,
|
|
377
|
+
changes: this.changes,
|
|
378
|
+
};
|
|
379
|
+
} catch (e) {
|
|
380
|
+
const errors = errorCauses(e);
|
|
381
|
+
if (!errors) {
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
// Do note that it's genuinely possible to return errors here, because federation validations (validating @key, @provides, ...) is mostly
|
|
385
|
+
// not done on the input schema and will only be triggered now, on the upgraded schema. Importantly, the errors returned here shouldn't
|
|
386
|
+
// be due to the upgrade process, but either due to the fed1 schema being invalid in the first place, or due to validation of fed2 that
|
|
387
|
+
// cannot be dealt with by the upgrade process (like, for instance, the fact that fed1 doesn't always reject fields mentioned in a @key
|
|
388
|
+
// that are not defined in the subgraph, but fed2 consistently do).
|
|
389
|
+
return { errors };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private fixFederationDirectivesArguments() {
|
|
394
|
+
for (const directive of [this.metadata.keyDirective(), this.metadata.requiresDirective(), this.metadata.providesDirective()]) {
|
|
395
|
+
for (const application of directive.applications()) {
|
|
396
|
+
const fields = application.arguments().fields;
|
|
397
|
+
if (typeof fields !== 'string') {
|
|
398
|
+
// The one case we have seen in practice is user passing an array of string, so we handle that. If it's something else,
|
|
399
|
+
// it's probably just completely invalid, so we ignore the application and let validation complain later.
|
|
400
|
+
if (Array.isArray(fields) && fields.every((f) => typeof f === 'string')) {
|
|
401
|
+
this.replaceFederationDirectiveApplication(application, application.toString(), fields.join(' '), directive.sourceAST);
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// While validating if the field is a string will work in most cases, this will not catch the case where the field argument was
|
|
407
|
+
// unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually
|
|
408
|
+
// have in practice), use that to check that the argument was truly a string.
|
|
409
|
+
const nodes = application.sourceAST;
|
|
410
|
+
if (nodes && nodes.kind === 'Directive') {
|
|
411
|
+
for (const argNode of nodes.arguments ?? []) {
|
|
412
|
+
if (argNode.name.value === 'fields') {
|
|
413
|
+
if (argNode.value.kind === Kind.ENUM) {
|
|
414
|
+
// Note that we we mostly want here is replacing the sourceAST because that is what is later used by validation
|
|
415
|
+
// to detect the problem.
|
|
416
|
+
this.replaceFederationDirectiveApplication(application, printAST(nodes), fields, {
|
|
417
|
+
...nodes,
|
|
418
|
+
arguments: [{
|
|
419
|
+
...argNode,
|
|
420
|
+
value: {
|
|
421
|
+
kind: Kind.STRING,
|
|
422
|
+
value: fields
|
|
423
|
+
}
|
|
424
|
+
}]
|
|
425
|
+
})
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private removeExternalOnInterface() {
|
|
436
|
+
for (const itf of this.schema.interfaceTypes()) {
|
|
437
|
+
for (const field of itf.fields()) {
|
|
438
|
+
const external = this.external(field);
|
|
439
|
+
if (external) {
|
|
440
|
+
this.addChange(new ExternalOnInterfaceRemoval(field.coordinate));
|
|
441
|
+
external.remove();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private replaceFederationDirectiveApplication(
|
|
448
|
+
application: Directive<SchemaElement<any, any>, {fields: any}>,
|
|
449
|
+
before: string,
|
|
450
|
+
fields: string,
|
|
451
|
+
updatedSourceAST: ASTNode | undefined,
|
|
452
|
+
) {
|
|
453
|
+
const directive = application.definition!;
|
|
454
|
+
// Note that in practice, federation directives can only be on either a type or a field, both of which are named.
|
|
455
|
+
const parent = application.parent as NamedSchemaElement<any, any, any>;
|
|
456
|
+
application.remove();
|
|
457
|
+
const newDirective = parent.applyDirective(directive, {fields});
|
|
458
|
+
newDirective.sourceAST = updatedSourceAST;
|
|
459
|
+
this.addChange(new FieldsArgumentCoercionToString(parent.coordinate, directive.name, before, newDirective.toString()));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private fixInactiveProvidesAndRequires() {
|
|
463
|
+
removeInactiveProvidesAndRequires(
|
|
464
|
+
this.schema,
|
|
465
|
+
(field, original, updated) => {
|
|
466
|
+
if (updated) {
|
|
467
|
+
this.addChange(new InactiveProvidesOrRequiresFieldsRemoval(field.coordinate, original.toString(), updated.toString()));
|
|
468
|
+
} else {
|
|
469
|
+
this.addChange(new InactiveProvidesOrRequiresRemoval(field.coordinate, original.toString()));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private removeExternalOnTypeExtensions() {
|
|
476
|
+
for (const type of this.schema.types()) {
|
|
477
|
+
if (!isCompositeType(type)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (!isFederationTypeExtension(type) && !isRootTypeExtension(type)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const keyApplications = type.appliedDirectivesOf(this.metadata.keyDirective());
|
|
485
|
+
if (keyApplications.length > 0) {
|
|
486
|
+
// If the type extension has keys, then fed1 will essentially consider the key fields not external ...
|
|
487
|
+
for (const keyApplication of type.appliedDirectivesOf(this.metadata.keyDirective())) {
|
|
488
|
+
collectTargetFields({
|
|
489
|
+
parentType: type,
|
|
490
|
+
directive: keyApplication,
|
|
491
|
+
includeInterfaceFieldsImplementations: false,
|
|
492
|
+
validate: false,
|
|
493
|
+
}).forEach((field) => {
|
|
494
|
+
// We only consider "top-level" fields, the one of the type on which the key is, because that's what fed1 does.
|
|
495
|
+
if (field.parent !== type) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const external = this.external(field);
|
|
499
|
+
if (external) {
|
|
500
|
+
this.addChange(new ExternalOnTypeExtensionRemoval(field.coordinate));
|
|
501
|
+
external.remove();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
// ... but if the extension does _not_ have a key, then if the extension has a field that is
|
|
507
|
+
// part of the _1st_ key on the subgraph owning the type, then this field is not considered
|
|
508
|
+
// external (yes, it's pretty damn random, and it's even worst in that even if the extension
|
|
509
|
+
// does _not_ have the "field of the _1st_ key on the subraph owning the type", then the
|
|
510
|
+
// query planner will still request it to the subgraph, generating an invalid query; but
|
|
511
|
+
// we ignore that here). Note however that because other subgraphs may have already been
|
|
512
|
+
// upgraded, we don't know which is the "type owner", so instead we look up at the first
|
|
513
|
+
// key of every other subgraph. It's not 100% what fed1 does, but we're in very-strange
|
|
514
|
+
// case territory in the first place, so this is probably good enough (that is, there is
|
|
515
|
+
// customer schema for which what we do here matter but not that I know of for which it's
|
|
516
|
+
// not good enough).
|
|
517
|
+
for (const other of this.otherSubgraphs) {
|
|
518
|
+
const typeInOther = other.schema.type(type.name);
|
|
519
|
+
if (!typeInOther) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
assert(isCompositeType(typeInOther), () => `Type ${type} is of kind ${type.kind} in ${this.subgraph.name} but ${typeInOther.kind} in ${other.name}`);
|
|
523
|
+
const keysInOther = typeInOther.appliedDirectivesOf(other.metadata().keyDirective());
|
|
524
|
+
if (keysInOther.length === 0) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
collectTargetFields({
|
|
528
|
+
parentType: typeInOther,
|
|
529
|
+
directive: keysInOther[0],
|
|
530
|
+
includeInterfaceFieldsImplementations: false,
|
|
531
|
+
validate: false,
|
|
532
|
+
}).forEach((field) => {
|
|
533
|
+
if (field.parent !== typeInOther) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Remark that we're iterating on the fields of _another_ subgraph that the one we're upgrading.
|
|
537
|
+
// We only consider "top-level" fields, the one of the type on which the key is, because that's what fed1 does.
|
|
538
|
+
const ownField = type.field(field.name);
|
|
539
|
+
if (!ownField) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const external = this.external(ownField);
|
|
543
|
+
if (external) {
|
|
544
|
+
this.addChange(new ExternalOnTypeExtensionRemoval(ownField.coordinate));
|
|
545
|
+
external.remove();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private removeTypeExtensions() {
|
|
554
|
+
for (const type of this.schema.types()) {
|
|
555
|
+
if (!isFederationTypeExtension(type) && !isRootTypeExtension(type)) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this.addChange(new TypeExtensionRemoval(type.coordinate));
|
|
560
|
+
type.removeExtensions();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private removeUnusedExternals() {
|
|
565
|
+
for (const type of this.schema.types()) {
|
|
566
|
+
if (!isObjectType(type) && !isInterfaceType(type)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
for (const field of type.fields()) {
|
|
570
|
+
if (this.metadata.isFieldExternal(field) && !this.metadata.isFieldUsed(field)) {
|
|
571
|
+
this.addChange(new UnusedExternalRemoval(field.coordinate));
|
|
572
|
+
field.remove();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!type.hasFields()) {
|
|
576
|
+
if (type.isReferenced()) {
|
|
577
|
+
this.addError(ERRORS.TYPE_WITH_ONLY_UNUSED_EXTERNAL.err({
|
|
578
|
+
message: `Type ${type} contains only external fields and all those fields are all unused (they do not appear in any @key, @provides or @requires).`,
|
|
579
|
+
nodes: type.sourceAST
|
|
580
|
+
}));
|
|
581
|
+
} else {
|
|
582
|
+
// The type only had unused externals, but it is also unreferenced in the subgraph. Unclear why
|
|
583
|
+
// it was there in the first place, but we can remove it and move on.
|
|
584
|
+
this.addChange(new TypeWithOnlyUnusedExternalRemoval(type.name));
|
|
585
|
+
type.remove();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private removeDirectivesOnInterface() {
|
|
592
|
+
for (const type of this.schema.interfaceTypes()) {
|
|
593
|
+
for (const application of type.appliedDirectivesOf(this.metadata.keyDirective())) {
|
|
594
|
+
this.addChange(new KeyOnInterfaceRemoval(type.name));
|
|
595
|
+
application.remove();
|
|
596
|
+
}
|
|
597
|
+
for (const field of type.fields()) {
|
|
598
|
+
for (const directive of [this.metadata.providesDirective(), this.metadata.requiresDirective()]) {
|
|
599
|
+
for (const application of field.appliedDirectivesOf(directive)) {
|
|
600
|
+
this.addChange(new ProvidesOrRequiresOnInterfaceFieldRemoval(field.coordinate, directive.name));
|
|
601
|
+
application.remove();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private removeProvidesOnNonComposite() {
|
|
609
|
+
for (const type of this.schema.objectTypes()) {
|
|
610
|
+
for (const field of type.fields()) {
|
|
611
|
+
if (isCompositeType(baseType(field.type!))) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
for (const application of field.appliedDirectivesOf(this.metadata.providesDirective())) {
|
|
615
|
+
this.addChange(new ProvidesOnNonCompositeRemoval(field.coordinate, field.type!.toString()));
|
|
616
|
+
application.remove();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private addShareable() {
|
|
623
|
+
const originalMetadata = this.originalSubgraph.metadata();
|
|
624
|
+
const keyDirective = this.metadata.keyDirective();
|
|
625
|
+
const shareableDirective = this.metadata.shareableDirective();
|
|
626
|
+
// We add shareable:
|
|
627
|
+
// - to every "value type" (in the fed1 sense of non-root type and non-entity) if it is used in any other subgraphs
|
|
628
|
+
// - to any (non-external) field of an entity/root-type that is not a key field and if another subgraphs resolve it (fully or partially through @provides)
|
|
629
|
+
for (const type of this.schema.objectTypes()) {
|
|
630
|
+
if (type.hasAppliedDirective(keyDirective) || type.isRootType()) {
|
|
631
|
+
for (const field of type.fields()) {
|
|
632
|
+
// To know if the field is a "key" field which doesn't need shareable, we rely on whether the field is shareable in the original
|
|
633
|
+
// schema (the fed1 version), because as fed1 schema will have no @shareable, the key fields will effectively be the only field
|
|
634
|
+
// considered shareable.
|
|
635
|
+
if (originalMetadata.isFieldShareable(field)) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const otherResolvingSubgraphs = this.otherSubgraphs.filter((s) => resolvesField(s, field));
|
|
639
|
+
if (otherResolvingSubgraphs.length > 0) {
|
|
640
|
+
field.applyDirective(shareableDirective);
|
|
641
|
+
this.addChange(new ShareableFieldAddition(field.coordinate, otherResolvingSubgraphs.map((s) => s.name)));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
const otherDeclaringSubgraphs = this.otherSubgraphs.filter((s) => s.schema.type(type.name));
|
|
646
|
+
if (otherDeclaringSubgraphs.length > 0) {
|
|
647
|
+
type.applyDirective(shareableDirective);
|
|
648
|
+
this.addChange(new ShareableTypeAddition(type.coordinate, otherDeclaringSubgraphs.map((s) => s.name)));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
package/src/suggestions.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { mapKeys } from './utils';
|
|
|
5
5
|
* Given an invalid input string and a list of valid options, returns a filtered
|
|
6
6
|
* list of valid options sorted based on their similarity with the input.
|
|
7
7
|
*/
|
|
8
|
-
export function suggestionList(input: string, options: string[]): string[] {
|
|
8
|
+
export function suggestionList(input: string, options: readonly string[]): string[] {
|
|
9
9
|
const optionsByDistance = new Map<string, number>();
|
|
10
10
|
|
|
11
11
|
const threshold = Math.floor(input.length * 0.4) + 1;
|
package/src/supergraphs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ASTNode, DocumentNode, GraphQLError } from "graphql";
|
|
2
2
|
import { err } from '@apollo/core-schema';
|
|
3
3
|
import { ErrCoreCheckFailed, FeatureUrl, FeatureVersion } from "./coreSpec";
|
|
4
|
-
import { CoreFeature, CoreFeatures,
|
|
4
|
+
import { CoreFeature, CoreFeatures, Schema } from "./definitions";
|
|
5
5
|
import { joinIdentity, JoinSpecDefinition, JOIN_VERSIONS } from "./joinSpec";
|
|
6
6
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
7
7
|
import { extractSubgraphsNamesAndUrlsFromSupergraph } from "./extractSubgraphsFromSupergraph";
|
|
@@ -12,6 +12,7 @@ const SUPPORTED_FEATURES = new Set([
|
|
|
12
12
|
'https://specs.apollo.dev/join/v0.1',
|
|
13
13
|
'https://specs.apollo.dev/join/v0.2',
|
|
14
14
|
'https://specs.apollo.dev/tag/v0.1',
|
|
15
|
+
'https://specs.apollo.dev/tag/v0.2',
|
|
15
16
|
'https://specs.apollo.dev/inaccessible/v0.1',
|
|
16
17
|
]);
|
|
17
18
|
|
|
@@ -38,8 +39,8 @@ const coreVersionZeroDotOneUrl = FeatureUrl.parse('https://specs.apollo.dev/core
|
|
|
38
39
|
export function buildSupergraphSchema(supergraphSdl: string | DocumentNode): [Schema, {name: string, url: string}[]] {
|
|
39
40
|
// We delay validation because `checkFeatureSupport` gives slightly more useful errors if, say, 'for' is used with core v0.1.
|
|
40
41
|
const schema = typeof supergraphSdl === 'string'
|
|
41
|
-
? buildSchema(supergraphSdl,
|
|
42
|
-
: buildSchemaFromAST(supergraphSdl,
|
|
42
|
+
? buildSchema(supergraphSdl, { validate: false })
|
|
43
|
+
: buildSchemaFromAST(supergraphSdl, { validate: false });
|
|
43
44
|
|
|
44
45
|
const [coreFeatures] = validateSupergraph(schema);
|
|
45
46
|
checkFeatureSupport(coreFeatures);
|