@apollo/federation-internals 2.13.2 → 2.14.0
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/dist/definitions.d.ts +24 -2
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +428 -37
- package/dist/definitions.js.map +1 -1
- package/dist/federation.d.ts +2 -2
- package/dist/federation.js +2 -2
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +19 -7
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/specs/connectSpec.d.ts.map +1 -1
- package/dist/specs/connectSpec.js +2 -2
- package/dist/specs/connectSpec.js.map +1 -1
- package/dist/specs/coreSpec.d.ts +7 -1
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +19 -1
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/federationSpec.d.ts.map +1 -1
- package/dist/specs/federationSpec.js +2 -1
- package/dist/specs/federationSpec.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -3
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +758 -47
- package/src/federation.ts +2 -2
- package/src/schemaUpgrader.ts +27 -12
- package/src/specs/connectSpec.ts +3 -2
- package/src/specs/coreSpec.ts +44 -2
- package/src/specs/federationSpec.ts +2 -1
- package/src/utils.ts +6 -3
- package/dist/specs/sourceSpec.d.ts +0 -69
- package/dist/specs/sourceSpec.d.ts.map +0 -1
- package/dist/specs/sourceSpec.js +0 -345
- package/dist/specs/sourceSpec.js.map +0 -1
package/src/definitions.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
isCoreSpecDirectiveApplication,
|
|
33
33
|
removeAllCoreFeatures,
|
|
34
34
|
} from "./specs/coreSpec";
|
|
35
|
-
import { assert, mapValues, MapWithCachedArrays, removeArrayElement } from "./utils";
|
|
35
|
+
import { assert, assertUnreachable, mapValues, MapWithCachedArrays, removeArrayElement, SetMultiMap } from "./utils";
|
|
36
36
|
import {
|
|
37
37
|
withDefaultValues,
|
|
38
38
|
valueEquals,
|
|
@@ -43,7 +43,8 @@ import {
|
|
|
43
43
|
argumentsEquals,
|
|
44
44
|
collectVariablesInValue
|
|
45
45
|
} from "./values";
|
|
46
|
-
import {
|
|
46
|
+
import { tagIdentity } from "./specs/tagSpec";
|
|
47
|
+
import { inaccessibleIdentity, removeInaccessibleElements } from "./specs/inaccessibleSpec";
|
|
47
48
|
import { printDirectiveDefinition, printSchema } from './print';
|
|
48
49
|
import { sameType } from './types';
|
|
49
50
|
import { addIntrospectionFields, introspectionFieldNames, isIntrospectionName } from "./introspection";
|
|
@@ -1019,10 +1020,53 @@ export class CoreFeature {
|
|
|
1019
1020
|
}
|
|
1020
1021
|
}
|
|
1021
1022
|
|
|
1023
|
+
export type ImportConflictsByIdentity = Map<
|
|
1024
|
+
string,
|
|
1025
|
+
{ self: Set<string>, other: Set<string> }
|
|
1026
|
+
>;
|
|
1027
|
+
|
|
1022
1028
|
export class CoreFeatures {
|
|
1023
1029
|
readonly coreDefinition: CoreSpecDefinition;
|
|
1030
|
+
/**
|
|
1031
|
+
* For specs, a map from their name-in-schemas (a.k.a. aliases) to their
|
|
1032
|
+
* CoreFeatures.
|
|
1033
|
+
*/
|
|
1024
1034
|
private readonly byAlias: Map<string, CoreFeature> = new Map();
|
|
1025
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* For specs, a map from their identities to their CoreFeatures plus another
|
|
1037
|
+
* map from imported type/directive name-in-specs to name-in-schemas. Like
|
|
1038
|
+
* imports, we distinguish types from directives by using a leading "@".
|
|
1039
|
+
*/
|
|
1040
|
+
private readonly byIdentity: Map<string, [CoreFeature, Map<string, string>]>
|
|
1041
|
+
= new Map();
|
|
1042
|
+
/**
|
|
1043
|
+
* For imported types/directives, this is a map from their name-in-schemas to
|
|
1044
|
+
* their CoreFeatures plus name-in-specs. Like imports, we distinguish types
|
|
1045
|
+
* from directives by using a leading "@".
|
|
1046
|
+
*/
|
|
1047
|
+
private readonly byImportName: Map<string, [CoreFeature, string]>
|
|
1048
|
+
= new Map();
|
|
1049
|
+
/**
|
|
1050
|
+
* For composed elements, merge will generally keep the name-in-schemas of
|
|
1051
|
+
* spec elements in subgraphs as a way to minimize conflicts while keeping
|
|
1052
|
+
* element names predictable for user-defined downstream code. However, merge
|
|
1053
|
+
* will also sometimes change the spec of certain spec elements (e.g. of a
|
|
1054
|
+
* federation spec directive). The result of this is that sometimes elements
|
|
1055
|
+
* using a default name of one spec may be imported using another spec, so we
|
|
1056
|
+
* need to permit e.g. the cost spec to import "@cost" as "@federation__cost"
|
|
1057
|
+
* in the supergraph schema. This kind of thing is generally fine, provided
|
|
1058
|
+
* the old spec alias is no longer in use in the supergraph schema.
|
|
1059
|
+
*
|
|
1060
|
+
* So whenever an import occurs with a name-in-schema that uses a spec alias
|
|
1061
|
+
* prefix that isn't in the schema, we store an entry here from the yet-unused
|
|
1062
|
+
* spec alias to the name-in-schema. This lets us easily lookup those elements
|
|
1063
|
+
* in `this.byImportName` if that spec alias ends up getting used later and
|
|
1064
|
+
* we need to generate an error message. (You might think we only need to
|
|
1065
|
+
* remember one example for error messages, but because we can remove features
|
|
1066
|
+
* we need to remember all of them.)
|
|
1067
|
+
*/
|
|
1068
|
+
private readonly conflictsByAlias: SetMultiMap<string, string>
|
|
1069
|
+
= new SetMultiMap();
|
|
1026
1070
|
|
|
1027
1071
|
constructor(readonly coreItself: CoreFeature) {
|
|
1028
1072
|
this.add(coreItself);
|
|
@@ -1034,18 +1078,45 @@ export class CoreFeatures {
|
|
|
1034
1078
|
}
|
|
1035
1079
|
|
|
1036
1080
|
getByIdentity(identity: string): CoreFeature | undefined {
|
|
1037
|
-
return this.byIdentity.get(identity);
|
|
1081
|
+
return this.byIdentity.get(identity)?.[0];
|
|
1038
1082
|
}
|
|
1039
1083
|
|
|
1040
|
-
allFeatures():
|
|
1041
|
-
return this.byIdentity.values();
|
|
1084
|
+
allFeatures(): CoreFeature[] {
|
|
1085
|
+
return [...this.byIdentity.values()].map(([feature]) => feature);
|
|
1042
1086
|
}
|
|
1043
1087
|
|
|
1044
1088
|
private removeFeature(featureIdentity: string) {
|
|
1045
|
-
const
|
|
1046
|
-
if (
|
|
1089
|
+
const entry = this.byIdentity.get(featureIdentity);
|
|
1090
|
+
if (entry) {
|
|
1091
|
+
const [feature] = entry;
|
|
1047
1092
|
this.byIdentity.delete(featureIdentity);
|
|
1048
|
-
|
|
1093
|
+
const alias = feature.nameInSchema;
|
|
1094
|
+
this.byAlias.delete(alias);
|
|
1095
|
+
for (const { name: importInSpec, as } of feature.imports) {
|
|
1096
|
+
const importInSchema = as ?? importInSpec;
|
|
1097
|
+
const isDirective = importInSpec.charAt(0) === "@";
|
|
1098
|
+
const nameInSchema = isDirective
|
|
1099
|
+
? importInSchema.slice(1)
|
|
1100
|
+
: importInSchema;
|
|
1101
|
+
this.byImportName.delete(importInSchema);
|
|
1102
|
+
const split = CoreFeatures.splitPrefixedName(nameInSchema);
|
|
1103
|
+
if (!split) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const [splitAlias] = split;
|
|
1107
|
+
if (splitAlias === alias) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
let conflicts = this.conflictsByAlias.get(importInSchema);
|
|
1111
|
+
if (!conflicts) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
conflicts.delete(importInSchema);
|
|
1115
|
+
if (conflicts.size) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
this.conflictsByAlias.delete(importInSchema);
|
|
1119
|
+
}
|
|
1049
1120
|
}
|
|
1050
1121
|
}
|
|
1051
1122
|
|
|
@@ -1056,11 +1127,6 @@ export class CoreFeatures {
|
|
|
1056
1127
|
const typedDirective = directive as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>
|
|
1057
1128
|
const args = typedDirective.arguments();
|
|
1058
1129
|
const url = this.coreDefinition.extractFeatureUrl(args);
|
|
1059
|
-
const existing = this.byIdentity.get(url.identity);
|
|
1060
|
-
if (existing) {
|
|
1061
|
-
// TODO: we may want to lossen that limitation at some point. Including the same feature for 2 different major versions should be ok.
|
|
1062
|
-
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(`Duplicate inclusion of feature ${url.identity}`);
|
|
1063
|
-
}
|
|
1064
1130
|
const imports = extractCoreFeatureImports(url, typedDirective);
|
|
1065
1131
|
const feature = new CoreFeature(url, args.as ?? url.name, directive, imports, args.for);
|
|
1066
1132
|
this.add(feature);
|
|
@@ -1069,48 +1135,680 @@ export class CoreFeatures {
|
|
|
1069
1135
|
}
|
|
1070
1136
|
|
|
1071
1137
|
private add(feature: CoreFeature) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1138
|
+
const identity = feature.url.identity;
|
|
1139
|
+
// The identity can't already be mapped to another @link/CoreFeature. (Even
|
|
1140
|
+
// when they're different major versions, they're usually describing the
|
|
1141
|
+
// same capabilities but in incompatible ways, so we don't want to allow
|
|
1142
|
+
// the same schema to try to use multiple of them.)
|
|
1143
|
+
if (this.byIdentity.has(identity)) {
|
|
1144
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1145
|
+
`Cannot link feature "${identity}" since it has already been linked in the schema.`,
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1075
1148
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1149
|
+
const alias = feature.nameInSchema;
|
|
1150
|
+
// Normally we'd always forbid "__" in aliases. However, there are some
|
|
1151
|
+
// older supergraph schemas that link the "tag" and "inaccessible" specs to
|
|
1152
|
+
// the aliases "federation__tag" and "federation__inaccessible". This is
|
|
1153
|
+
// due to bugs in older versions of composition, but is technically fine
|
|
1154
|
+
// since these specs have no types and directives other than the default
|
|
1155
|
+
// directive, so they never prefix anything with "__". So we make a very
|
|
1156
|
+
// specific exception here for that case. We may remove this exception in
|
|
1157
|
+
// the future, once support has been dropped for those bugged composition
|
|
1158
|
+
// versions.
|
|
1159
|
+
if (
|
|
1160
|
+
!(identity === tagIdentity &&
|
|
1161
|
+
alias === 'federation__tag' &&
|
|
1162
|
+
feature.imports.length === 0) &&
|
|
1163
|
+
!(identity === inaccessibleIdentity &&
|
|
1164
|
+
alias === 'federation__inaccessible' &&
|
|
1165
|
+
feature.imports.length === 0)
|
|
1166
|
+
) {
|
|
1167
|
+
// Don't allow spec name-in-schemas/aliases to have "__" in them, as
|
|
1168
|
+
// namespace splitting splits on the earliest "__" (so a namespaced name
|
|
1169
|
+
// with an alias containing "__" would be erroneously split mid-alias).
|
|
1170
|
+
if (alias.indexOf('__') !== -1) {
|
|
1171
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1172
|
+
`Cannot link feature "${identity}" as "${alias}" since it contains "__". Please rename to a compliant name via "as".`,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// Don't allow spec name-in-schemas/aliases to end in "_", as namespace
|
|
1177
|
+
// splitting splits on the earliest "__" (so a namespaced name with an alias
|
|
1178
|
+
// ending with "_" would end up with "___", and be split before the ending
|
|
1179
|
+
// "_" instead of after).
|
|
1180
|
+
if (alias.charAt(alias.length - 1) === '_') {
|
|
1181
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1182
|
+
`Cannot link feature "${identity}" as "${alias}" since it ends in "_". Please rename to a compliant name via "as".`,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
// Ideally here, we wouldn't allow spec name-in-schemas/aliases to not be
|
|
1186
|
+
// valid GraphQL names. However, enough supergraph schemas use "." and "-"
|
|
1187
|
+
// after the first character that we can't impose that validation now. So
|
|
1188
|
+
// instead, we match using a slightly relaxed regex than allows "." and "-"
|
|
1189
|
+
// after the first character. For schemas that have "." or "-", they won't
|
|
1190
|
+
// be able to use namespaced names for their spec schema elements due to
|
|
1191
|
+
// GraphQL validation, but imports will still work.
|
|
1192
|
+
//
|
|
1193
|
+
// Note the error message below purposely says "not a valid GraphQL name"
|
|
1194
|
+
// because we want to encourage users to actually use GraphQL names and
|
|
1195
|
+
// avoid creating more exceptional cases.
|
|
1196
|
+
if (!aliasRegexp.test(alias)) {
|
|
1197
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1198
|
+
`Cannot link feature "${identity}" as "${alias}" since it is not a valid GraphQL name. Please rename to a compliant name via "as".`,
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
// Don't allow spec name-in-schemas/aliases to conflict with previous
|
|
1202
|
+
// imports using "__" with that alias.
|
|
1203
|
+
const conflicts = this.conflictsByAlias.get(alias);
|
|
1204
|
+
if (conflicts) {
|
|
1205
|
+
const importInSchema = conflicts?.values()?.next()?.value;
|
|
1206
|
+
assert(importInSchema !== undefined, `Unexpectedly empty conflicts set`);
|
|
1207
|
+
const entry = this.byImportName.get(importInSchema);
|
|
1208
|
+
assert(entry, `Unexpectedly cannot find feature for import`);
|
|
1209
|
+
const [conflictFeature, importInSpec] = entry;
|
|
1210
|
+
const conflictIdentity = conflictFeature.url.identity;
|
|
1211
|
+
this.checkTagInaccessibleConflict(conflictIdentity, identity);
|
|
1212
|
+
const importInErrorMessage = importInSchema !== importInSpec
|
|
1213
|
+
? `"${importInSpec}" as "${importInSchema}"`
|
|
1214
|
+
: `"${importInSpec}"`;
|
|
1215
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1216
|
+
`Cannot import ${importInErrorMessage} from feature "${conflictIdentity}" since it can be confused with a namespaced name from another linked feature "${identity}". Please rename the import or feature to avoid conflicts via "as".`,
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
// Don't allow spec name-in-schemas/aliases to have default directive names
|
|
1220
|
+
// that conflict with previous imports.
|
|
1221
|
+
const importInSchema = "@" + alias;
|
|
1222
|
+
const entry = this.byImportName.get(importInSchema);
|
|
1223
|
+
if (entry) {
|
|
1224
|
+
const [conflictFeature, importInSpec] = entry;
|
|
1225
|
+
const conflictIdentity = conflictFeature.url.identity;
|
|
1226
|
+
this.checkTagInaccessibleConflict(conflictIdentity, identity);
|
|
1227
|
+
const importInErrorMessage = importInSchema !== importInSpec
|
|
1228
|
+
? `"${importInSpec}" as "${importInSchema}"`
|
|
1229
|
+
: `"${importInSpec}"`;
|
|
1230
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1231
|
+
`Cannot import ${importInErrorMessage} from feature "${conflictIdentity}" since it can be confused with a namespaced name from another linked feature "${identity}". Please rename the import or feature to avoid conflicts via "as".`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
// The alias can't be already mapped to another @link/CoreFeature.
|
|
1235
|
+
const existingFeature = this.byAlias.get(alias);
|
|
1236
|
+
if (existingFeature !== undefined) {
|
|
1237
|
+
const existingIdentity = existingFeature.url.identity;
|
|
1238
|
+
this.checkTagInaccessibleConflict(existingIdentity, identity);
|
|
1239
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1240
|
+
`Cannot link feature ${identity} as "${alias}" since another feature "${existingIdentity}" already uses that alias. Please rename the feature to avoid conflicts via "as".`,
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const importsMap: Map<string, string> = new Map();
|
|
1245
|
+
for (const { name: importInSpec, as } of feature.imports) {
|
|
1246
|
+
const importInSchema = as ?? importInSpec;
|
|
1247
|
+
const importInErrorMessage = importInSchema !== importInSpec
|
|
1248
|
+
? `"${importInSpec}" as "${importInSchema}"`
|
|
1249
|
+
: `"${importInSpec}"`;
|
|
1250
|
+
const isDirective = importInSpec.charAt(0) === "@";
|
|
1251
|
+
const nameInSpec = isDirective
|
|
1252
|
+
? importInSpec.slice(1)
|
|
1253
|
+
: importInSpec;
|
|
1254
|
+
const nameInSchema = isDirective
|
|
1255
|
+
? importInSchema.slice(1)
|
|
1256
|
+
: importInSchema;
|
|
1257
|
+
|
|
1258
|
+
// Only allow mapping to a name with "__" if it's a no-op import or if
|
|
1259
|
+
// it uses a non-existent spec alias.
|
|
1260
|
+
const split = CoreFeatures.splitPrefixedName(nameInSchema);
|
|
1261
|
+
if (split) {
|
|
1262
|
+
const [splitAlias, splitNameInSpec] = split;
|
|
1263
|
+
if (splitAlias === alias) {
|
|
1264
|
+
if (splitNameInSpec !== nameInSpec) {
|
|
1265
|
+
const splitImportInSpec = isDirective
|
|
1266
|
+
? "@" + splitNameInSpec
|
|
1267
|
+
: splitNameInSpec;
|
|
1268
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1269
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it can be confused with the namespaced name for "${splitImportInSpec}". Please rename the import to avoid conflicts via "as".`,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
const conflictFeature = this.byAlias.get(splitAlias);
|
|
1274
|
+
if (conflictFeature) {
|
|
1275
|
+
const conflictIdentity = conflictFeature.url.identity;
|
|
1276
|
+
this.checkTagInaccessibleConflict(conflictIdentity, identity);
|
|
1277
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1278
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it can be confused with a namespaced name from another linked feature "${conflictIdentity}". Please rename the import or feature to avoid conflicts via "as".`,
|
|
1279
|
+
);
|
|
1280
|
+
} else {
|
|
1281
|
+
// As mentioned in the docs for `this.conflictsByAlias`, we have to
|
|
1282
|
+
// record the import in case a feature gets added with the spec
|
|
1283
|
+
// alias later.
|
|
1284
|
+
this.conflictsByAlias.add(splitAlias, importInSchema);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
// For default directives, only allow mapping to a spec alias if it's a
|
|
1289
|
+
// no-op import.
|
|
1290
|
+
if (isDirective) {
|
|
1291
|
+
if (nameInSchema === alias) {
|
|
1292
|
+
if (nameInSpec !== feature.url.name) {
|
|
1293
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1294
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it can be confused with the namespaced name for "@${feature.url.name}". Please rename the import to avoid conflicts via "as".`,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
const conflictFeature = this.byAlias.get(nameInSchema);
|
|
1299
|
+
if (conflictFeature) {
|
|
1300
|
+
const conflictIdentity = conflictFeature.url.identity;
|
|
1301
|
+
this.checkTagInaccessibleConflict(conflictIdentity, identity);
|
|
1302
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1303
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it can be confused with a namespaced name from another linked feature "${conflictIdentity}". Please rename the import or feature to avoid conflicts via "as".`,
|
|
1304
|
+
);
|
|
1098
1305
|
}
|
|
1099
1306
|
}
|
|
1100
1307
|
}
|
|
1308
|
+
// The name-in-spec can't be already mapped to a different name-in-schema.
|
|
1309
|
+
const existingImportInSchema = importsMap.get(importInSpec);
|
|
1310
|
+
if (existingImportInSchema === undefined) {
|
|
1311
|
+
importsMap.set(importInSpec, importInSchema);
|
|
1312
|
+
} else {
|
|
1313
|
+
if (existingImportInSchema !== importInSchema) {
|
|
1314
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1315
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it was previously imported as "${existingImportInSchema}". Please remove one of these imports.`,
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// The name-in-schema can't already be mapped to a different name-in-spec.
|
|
1320
|
+
const entry = this.byImportName.get(importInSchema);
|
|
1321
|
+
if (entry === undefined) {
|
|
1322
|
+
this.byImportName.set(importInSchema, [feature, importInSpec]);
|
|
1323
|
+
} else {
|
|
1324
|
+
const [existingFeature, existingImportInSpec] = entry;
|
|
1325
|
+
const existingIdentity = existingFeature.url.identity;
|
|
1326
|
+
if (existingIdentity !== identity) {
|
|
1327
|
+
this.checkTagInaccessibleConflict(existingIdentity, identity);
|
|
1328
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1329
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it was previously imported from feature "${existingIdentity}". Please rename the import to avoid conflicts via "as".`,
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (existingImportInSpec !== importInSpec) {
|
|
1333
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1334
|
+
`Cannot import ${importInErrorMessage} from feature "${identity}" since it was previously imported for "${existingImportInSpec}". Please rename the import to avoid conflicts via "as".`,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
this.byAlias.set(alias, feature);
|
|
1340
|
+
this.byIdentity.set(identity, [feature, importsMap]);
|
|
1341
|
+
}
|
|
1101
1342
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1343
|
+
/**
|
|
1344
|
+
* Returns whether the spec alias would pass the checks in `add()`, except
|
|
1345
|
+
* import conflicts are taken from the given map, which should be computed via
|
|
1346
|
+
* `computeAliasConflicts()`.
|
|
1347
|
+
*/
|
|
1348
|
+
isAliasValid(
|
|
1349
|
+
alias: string,
|
|
1350
|
+
identity: string,
|
|
1351
|
+
importConflictsByIdentity: ImportConflictsByIdentity,
|
|
1352
|
+
) {
|
|
1353
|
+
// Don't allow aliases to have "__" in them. Note that this method is only
|
|
1354
|
+
// used in merging to detect whether we need to rename the spec, so we don't
|
|
1355
|
+
// need the exception for "federation__tag" and "federation__inaccessible"
|
|
1356
|
+
// here.
|
|
1357
|
+
if (alias.indexOf('__') !== -1) {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
// Don't allow aliases to end in "_".
|
|
1361
|
+
if (alias.charAt(alias.length - 1) === '_') {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
// Don't allow aliases to not be valid GraphQL names. Note that unlike
|
|
1365
|
+
// `add()`, we consider "." and "-" to not be valid here, but since this
|
|
1366
|
+
// method is only used in merging to detect whether we need to rename the
|
|
1367
|
+
// spec, this has the effect of ensuring that supergraph schemas don't use
|
|
1368
|
+
// "." and "-" in their alias (which will help later if we want to fully
|
|
1369
|
+
// forbid "." and "-" in aliases).
|
|
1370
|
+
if (!nameRegexp.test(alias)) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
for (const [otherIdentity, importConflicts] of importConflictsByIdentity.entries()) {
|
|
1374
|
+
if (identity === otherIdentity) {
|
|
1375
|
+
// For import names namespaced using this alias, only allow them if
|
|
1376
|
+
// their no-op imports.
|
|
1377
|
+
if (importConflicts.self.has(alias)) {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1381
|
+
// Don't allow imports of other specs that are namespaced by this alias.
|
|
1382
|
+
if (importConflicts.other.has(alias)) {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
// The alias can't be already mapped to another @link/CoreFeature.
|
|
1388
|
+
if (this.byAlias.has(alias)) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* This is a method that helps us handle the case where:
|
|
1396
|
+
* 1. directives of some spec are being composed into the supergraph (due to
|
|
1397
|
+
* them being Apollo specs or via `@composeDirective`),
|
|
1398
|
+
* 2. those directives don't actually have any conflicts, and
|
|
1399
|
+
* 3. the spec itself has alias conflicts when linked with the spec's name.
|
|
1400
|
+
*
|
|
1401
|
+
* For the `@composeDirective` case at least, you might think we could just
|
|
1402
|
+
* use the `@link(as:)` rename from the subgraph, but when we established
|
|
1403
|
+
* `@composeDirective` we never mandated that the spec aliases be the same
|
|
1404
|
+
* (just their composed directive name-in-schemas). The reason we focused on
|
|
1405
|
+
* directives was that downstream consumers were expecting certain directive
|
|
1406
|
+
* names, so it makes sense to force agreement on a single name per directive
|
|
1407
|
+
* across subgraphs. As part of that, we explicitly generate imports for all
|
|
1408
|
+
* those directives, so the spec alias is never used for namespaced names via
|
|
1409
|
+
* "__", and consumers consequently don't deal or care about those aliases
|
|
1410
|
+
* much. We could make a breaking change to force alignment on a spec alias to
|
|
1411
|
+
* use in the supergraph, but alias agreement likely isn't valuable enough for
|
|
1412
|
+
* a breakage. More importantly, it also doesn't really solve the case for
|
|
1413
|
+
* conflicts linking Apollo specs.
|
|
1414
|
+
*
|
|
1415
|
+
* So instead, when we detect a conflict, we generate a unique alias. This
|
|
1416
|
+
* method does two things:
|
|
1417
|
+
* 1. Outputs data that can be used to efficiently detect import conflicts.
|
|
1418
|
+
* 2. Outputs a function that can be used to generate unique aliases.
|
|
1419
|
+
*
|
|
1420
|
+
* For unique alias computation, we compute a non-conflicting prefix by using
|
|
1421
|
+
* a trie to determine a GraphQL name that isn't a prefix of any existing
|
|
1422
|
+
* names (this prefix also doesn't use "_" outside the first character, so it
|
|
1423
|
+
* should be safe for aliases). We then add an incrementing index to it, to
|
|
1424
|
+
* account for core features that have the same spec name. Finally, we add
|
|
1425
|
+
* the spec name but without any non-letter characters.
|
|
1426
|
+
*/
|
|
1427
|
+
static computeAliasConflicts(
|
|
1428
|
+
specAliases: {
|
|
1429
|
+
url: FeatureUrl,
|
|
1430
|
+
alias: string,
|
|
1431
|
+
imports: CoreImport[],
|
|
1432
|
+
}[],
|
|
1433
|
+
elementNames: Set<string>,
|
|
1434
|
+
): {
|
|
1435
|
+
importConflictsByIdentity: ImportConflictsByIdentity,
|
|
1436
|
+
computeUniqueAlias: (specName: string) => string,
|
|
1437
|
+
} {
|
|
1438
|
+
// Generate `importConflictsByIdentity` and track names for the trie.
|
|
1439
|
+
const trieNames = elementNames;
|
|
1440
|
+
const importConflictsByIdentity: ImportConflictsByIdentity = new Map();
|
|
1441
|
+
for (const { url, alias, imports } of specAliases) {
|
|
1442
|
+
trieNames.add(alias);
|
|
1443
|
+
const self = new Set<string>();
|
|
1444
|
+
const other = new Set<string>();
|
|
1445
|
+
for (const { name: importInSpec, as } of imports) {
|
|
1446
|
+
const importInSchema = as ?? importInSpec;
|
|
1447
|
+
const isDirective = importInSpec.charAt(0) === "@";
|
|
1448
|
+
const nameInSpec = isDirective
|
|
1449
|
+
? importInSpec.slice(1)
|
|
1450
|
+
: importInSpec;
|
|
1451
|
+
const nameInSchema = isDirective
|
|
1452
|
+
? importInSchema.slice(1)
|
|
1453
|
+
: importInSchema;
|
|
1454
|
+
trieNames.add(nameInSchema);
|
|
1455
|
+
const split = CoreFeatures.splitPrefixedName(nameInSchema);
|
|
1456
|
+
if (split) {
|
|
1457
|
+
const [splitAlias, splitNameInSpec] = split;
|
|
1458
|
+
if (splitNameInSpec !== nameInSpec) {
|
|
1459
|
+
// Alias being `splitAlias` would generate a conflict due to the
|
|
1460
|
+
// import not being a no-op import for a namespaced name.
|
|
1461
|
+
self.add(splitAlias);
|
|
1462
|
+
}
|
|
1463
|
+
// Alias being `splitAlias` would generate a conflict due to this
|
|
1464
|
+
// import from some other identity being namespaced to it.
|
|
1465
|
+
other.add(splitAlias);
|
|
1466
|
+
}
|
|
1467
|
+
if (isDirective) {
|
|
1468
|
+
// Alias being 'nameInSchema' would generate a conflict due to the
|
|
1469
|
+
// import not being a no-op import for the default directive.
|
|
1470
|
+
if (nameInSpec !== url.name) {
|
|
1471
|
+
self.add(nameInSchema);
|
|
1472
|
+
}
|
|
1473
|
+
// Alias being `nameInSchema` would generate a conflict due to this
|
|
1474
|
+
// import from some other identity being the default directive for it.
|
|
1475
|
+
other.add(nameInSchema)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
importConflictsByIdentity.set(url.identity, { self, other });
|
|
1479
|
+
}
|
|
1480
|
+
// Create the closure for computing unique aliases via a trie.
|
|
1481
|
+
let prefix: string | null = null;
|
|
1482
|
+
let index: number = 0;
|
|
1483
|
+
let computeUniqueAlias = (specName: string): string => {
|
|
1484
|
+
if (prefix === null) {
|
|
1485
|
+
const aliasStart = [
|
|
1486
|
+
'_',
|
|
1487
|
+
'abcdefghijklmnopqrstuvwxyz',
|
|
1488
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
1489
|
+
].join('');
|
|
1490
|
+
const aliasContinue = [
|
|
1491
|
+
'abcdefghijklmnopqrstuvwxyz',
|
|
1492
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
1493
|
+
'0123456789',
|
|
1494
|
+
].join('');
|
|
1495
|
+
type TrieNode = {
|
|
1496
|
+
children: Map<string, TrieNode>;
|
|
1497
|
+
parent: TrieNode | null;
|
|
1498
|
+
char: string;
|
|
1109
1499
|
};
|
|
1500
|
+
const root: TrieNode = { children: new Map(), parent: null, char: '' };
|
|
1501
|
+
|
|
1502
|
+
// Populate the trie.
|
|
1503
|
+
for (const name of trieNames) {
|
|
1504
|
+
let node = root;
|
|
1505
|
+
for (const char of name) {
|
|
1506
|
+
let child = node.children.get(char);
|
|
1507
|
+
if (!child) {
|
|
1508
|
+
child = { children: new Map(), parent: node, char };
|
|
1509
|
+
node.children.set(char, child);
|
|
1510
|
+
}
|
|
1511
|
+
node = child;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Note that we never really remove elements from this queue, we just
|
|
1516
|
+
// advance the index pointing to the head of the queue. This is fine
|
|
1517
|
+
// since its size is bounded above by the number of nodes in trie.
|
|
1518
|
+
const queue: TrieNode[] = [root];
|
|
1519
|
+
let head = 0;
|
|
1520
|
+
while (prefix === null) {
|
|
1521
|
+
const possibleChars = head === 0 ? aliasStart : aliasContinue;
|
|
1522
|
+
const node = queue[head++];
|
|
1523
|
+
for (const char of possibleChars) {
|
|
1524
|
+
const child = node.children.get(char);
|
|
1525
|
+
if (child) {
|
|
1526
|
+
queue.push(child);
|
|
1527
|
+
} else {
|
|
1528
|
+
const chars = [char];
|
|
1529
|
+
for (let cur: TrieNode | null = node; cur?.parent; cur = cur.parent) {
|
|
1530
|
+
chars.push(cur.char);
|
|
1531
|
+
}
|
|
1532
|
+
prefix = chars.reverse().join('');
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const suffix = specName.replace(/[^a-zA-Z]/g, '');
|
|
1539
|
+
return `${prefix}${index++}${suffix}`;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return {
|
|
1543
|
+
importConflictsByIdentity,
|
|
1544
|
+
computeUniqueAlias,
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* There's a particular pattern in Fed 1 subgraphs, where they would try to
|
|
1550
|
+
* link the "tag" or "inaccessible" specs directly instead of importing the
|
|
1551
|
+
* directives from the "federation" spec, and this can cause a conflict. This
|
|
1552
|
+
* function gives a more helpful error message in that case.
|
|
1553
|
+
*
|
|
1554
|
+
* To elaborate, those are supergraph specs, not subgraph ones, and subgraph
|
|
1555
|
+
* code doesn't check for the supergraph spec (just the "federation" spec). It
|
|
1556
|
+
* may have worked before because the name we happened to import using the
|
|
1557
|
+
* "federation" spec was the same, but if they become unaligned in the future
|
|
1558
|
+
* (e.g. due to either our code or their schema using "as"), we'd suddenly
|
|
1559
|
+
* start silently ignoring those spec directive applications.
|
|
1560
|
+
*/
|
|
1561
|
+
private checkTagInaccessibleConflict(identity1: string, identity2: string) {
|
|
1562
|
+
// TODO: We can't import this from "./specs/federationSpec" because it
|
|
1563
|
+
// causes a circular import loop; we should fix that later.
|
|
1564
|
+
const federationIdentity = 'https://specs.apollo.dev/federation';
|
|
1565
|
+
const identities = new Set([identity1, identity2]);
|
|
1566
|
+
if (!identities.has(federationIdentity)) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const [directive, identity] = identities.has(tagIdentity)
|
|
1570
|
+
? ['tag', tagIdentity]
|
|
1571
|
+
: identities.has(inaccessibleIdentity)
|
|
1572
|
+
? ['inaccessible', inaccessibleIdentity]
|
|
1573
|
+
: [undefined, undefined];
|
|
1574
|
+
if (directive && identity) {
|
|
1575
|
+
throw ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1576
|
+
`Please import "@${directive}" from the feature "${federationIdentity}" instead of using "${identity}" to avoid potential unexpected behavior in the future.`,
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* If the given schema element belongs to a spec/feature, return that feature
|
|
1583
|
+
* along with the name-in-spec and whether it was imported. Note that if the
|
|
1584
|
+
* element uses a default name but its name-in-spec was imported already under
|
|
1585
|
+
* a different name (a.k.a. a shadowing import), this method will still
|
|
1586
|
+
* consider it to belong to that feature, but its name-in-spec will be null.
|
|
1587
|
+
*/
|
|
1588
|
+
sourceFeature(element: DirectiveDefinition | Directive | NamedType):
|
|
1589
|
+
| {
|
|
1590
|
+
feature: CoreFeature,
|
|
1591
|
+
nameInFeature: string | null,
|
|
1592
|
+
isImported: boolean,
|
|
1593
|
+
}
|
|
1594
|
+
| undefined
|
|
1595
|
+
{
|
|
1596
|
+
const isDirective =
|
|
1597
|
+
element instanceof DirectiveDefinition || element instanceof Directive;
|
|
1598
|
+
// Validations guarantee that import names don't collide with the default
|
|
1599
|
+
// names of different spec schema elements, so it doesn't technically matter
|
|
1600
|
+
// which order we check first. But we do have to some extra work for
|
|
1601
|
+
// shadowing imports if we don't check imports first, so we do that first.
|
|
1602
|
+
const importName = isDirective ? '@' + element.name : element.name;
|
|
1603
|
+
const entry = this.byImportName.get(importName);
|
|
1604
|
+
if (entry) {
|
|
1605
|
+
const [feature, importInSpec] = entry;
|
|
1606
|
+
return {
|
|
1607
|
+
feature,
|
|
1608
|
+
nameInFeature: isDirective ? importInSpec.slice(1) : importInSpec,
|
|
1609
|
+
isImported: true,
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
// If it's not an import, check whether it's a default name with no
|
|
1613
|
+
// shadowing imports.
|
|
1614
|
+
const defaultEntry = this.sourceDefaultName(isDirective, element.name);
|
|
1615
|
+
if (!defaultEntry) {
|
|
1616
|
+
return undefined;
|
|
1617
|
+
}
|
|
1618
|
+
const [feature, nameInSpec] = defaultEntry;
|
|
1619
|
+
const importInSpec = isDirective ? '@' + nameInSpec : nameInSpec;
|
|
1620
|
+
// Note that if the import name is the same as the element's name, it's not
|
|
1621
|
+
// a shadowing import, and we should return a non-null `nameInFeature`. But
|
|
1622
|
+
// if that were true, we would have found an entry in `this.byImportName`
|
|
1623
|
+
// above when checking for imports. So we don't need to handle that case
|
|
1624
|
+
// specially here.
|
|
1625
|
+
return {
|
|
1626
|
+
feature,
|
|
1627
|
+
nameInFeature: this.getImportName(feature, importInSpec) === undefined
|
|
1628
|
+
? nameInSpec
|
|
1629
|
+
: null,
|
|
1630
|
+
isImported: false,
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Assuming the core features are for the given schema, returns an error for
|
|
1636
|
+
* each schema element with a shadowing import. A "shadowing import" occurs
|
|
1637
|
+
* when an element would normally belong to a feature due to having a default
|
|
1638
|
+
* name for it, but the name-in-spec has been imported already under a
|
|
1639
|
+
* different name. Note that for backwards-compatibility reasons, we ignore
|
|
1640
|
+
* shadowed types if they're only used by other shadowed elements.
|
|
1641
|
+
*
|
|
1642
|
+
* We enforce this validation because downstream code almost always assumes
|
|
1643
|
+
* there's exactly one name for a spec element, and allowing multiple elements
|
|
1644
|
+
* with the same feature and name-in-spec will thus result in some of those
|
|
1645
|
+
* elements being erroneously ignored. (This is similar to the validation that
|
|
1646
|
+
* forbids importing the same name-in-spec with different name-in-schemas, but
|
|
1647
|
+
* that can't be easily done when adding a feature since it depends on what
|
|
1648
|
+
* elements are actually in the schema, and that doesn't get finalized until
|
|
1649
|
+
* later in the schema-building process.)
|
|
1650
|
+
*/
|
|
1651
|
+
validateNoShadowingImports(schema: Schema): GraphQLError[] {
|
|
1652
|
+
const errors: GraphQLError[] = [];
|
|
1653
|
+
for (const element of [...schema.allTypes(), ...schema.allDirectives()]) {
|
|
1654
|
+
const shadowingImport = this.getShadowingImport(element);
|
|
1655
|
+
if (!shadowingImport) {
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const isUsed = element instanceof DirectiveDefinition
|
|
1659
|
+
? element.applications().size !== 0
|
|
1660
|
+
: this.getReferencingRootElements(element)
|
|
1661
|
+
.some((referencer) => {
|
|
1662
|
+
return referencer.kind === 'SchemaDefinition'
|
|
1663
|
+
? true
|
|
1664
|
+
: !this.getShadowingImport(referencer)
|
|
1665
|
+
});
|
|
1666
|
+
if (!isUsed) {
|
|
1667
|
+
continue;
|
|
1110
1668
|
}
|
|
1669
|
+
const { feature, importInSpec, importInSchema } = shadowingImport;
|
|
1670
|
+
const importInErrorMessage = importInSchema !== importInSpec
|
|
1671
|
+
? `"${importInSpec}" as "${importInSchema}"`
|
|
1672
|
+
: `"${importInSpec}"`;
|
|
1673
|
+
errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
|
|
1674
|
+
`Cannot import ${importInErrorMessage} from feature "${feature.url.identity}" since there's a used definition for the namespaced name "${element.coordinate}". Please switch usages of the namespaced name to the import name and remove the definition.`,
|
|
1675
|
+
));
|
|
1676
|
+
}
|
|
1677
|
+
return errors;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private getShadowingImport(
|
|
1681
|
+
element: DirectiveDefinition | Directive | NamedType
|
|
1682
|
+
):
|
|
1683
|
+
| {
|
|
1684
|
+
feature: CoreFeature,
|
|
1685
|
+
importInSpec: string,
|
|
1686
|
+
importInSchema: string,
|
|
1687
|
+
}
|
|
1688
|
+
| undefined
|
|
1689
|
+
{
|
|
1690
|
+
const isDirective =
|
|
1691
|
+
element instanceof DirectiveDefinition || element instanceof Directive;
|
|
1692
|
+
const defaultEntry = this.sourceDefaultName(isDirective, element.name);
|
|
1693
|
+
if (!defaultEntry) {
|
|
1694
|
+
return undefined;
|
|
1695
|
+
}
|
|
1696
|
+
const importName = isDirective ? '@' + element.name : element.name;
|
|
1697
|
+
const [feature, nameInSpec] = defaultEntry;
|
|
1698
|
+
const importInSpec = isDirective ? '@' + nameInSpec : nameInSpec;
|
|
1699
|
+
const importInSchema = this.getImportName(feature, importInSpec);
|
|
1700
|
+
return importInSchema !== undefined && importInSchema !== importName
|
|
1701
|
+
? {
|
|
1702
|
+
feature,
|
|
1703
|
+
importInSpec,
|
|
1704
|
+
importInSchema,
|
|
1705
|
+
}
|
|
1706
|
+
: undefined;
|
|
1707
|
+
}
|
|
1111
1708
|
|
|
1709
|
+
/**
|
|
1710
|
+
* Returns the root schema elements (types/directives/schema definitions) that
|
|
1711
|
+
* contain references to the given type somewhere in their definition.
|
|
1712
|
+
*/
|
|
1713
|
+
getReferencingRootElements(
|
|
1714
|
+
element: NamedType,
|
|
1715
|
+
): (DirectiveDefinition | NamedType | SchemaDefinition)[] {
|
|
1716
|
+
const referencers: (DirectiveDefinition | NamedType | SchemaDefinition)[]
|
|
1717
|
+
= [];
|
|
1718
|
+
for (const referencer of element.referencers()) {
|
|
1719
|
+
switch (referencer.kind) {
|
|
1720
|
+
case 'ObjectType':
|
|
1721
|
+
referencers.push(referencer);
|
|
1722
|
+
break;
|
|
1723
|
+
case 'InterfaceType':
|
|
1724
|
+
referencers.push(referencer);
|
|
1725
|
+
break;
|
|
1726
|
+
case 'UnionType':
|
|
1727
|
+
referencers.push(referencer);
|
|
1728
|
+
break;
|
|
1729
|
+
case 'SchemaDefinition':
|
|
1730
|
+
referencers.push(referencer);
|
|
1731
|
+
break;
|
|
1732
|
+
case 'FieldDefinition':
|
|
1733
|
+
referencers.push(referencer.parent);
|
|
1734
|
+
break;
|
|
1735
|
+
case 'InputFieldDefinition':
|
|
1736
|
+
referencers.push(referencer.parent);
|
|
1737
|
+
break;
|
|
1738
|
+
case 'ArgumentDefinition':
|
|
1739
|
+
const parent: DirectiveDefinition | FieldDefinition<any>
|
|
1740
|
+
= referencer.parent;
|
|
1741
|
+
switch (parent.kind) {
|
|
1742
|
+
case 'DirectiveDefinition':
|
|
1743
|
+
referencers.push(parent);
|
|
1744
|
+
break;
|
|
1745
|
+
case 'FieldDefinition':
|
|
1746
|
+
referencers.push(parent.parent);
|
|
1747
|
+
break;
|
|
1748
|
+
default:
|
|
1749
|
+
assertUnreachable(parent);
|
|
1750
|
+
}
|
|
1751
|
+
break;
|
|
1752
|
+
default:
|
|
1753
|
+
assertUnreachable(referencer);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return referencers;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Returns the import name for the given feature and import-in-spec name. (By
|
|
1761
|
+
* "import", we mean the element name, prefixed with "@" if it's a directive.)
|
|
1762
|
+
*/
|
|
1763
|
+
private getImportName(
|
|
1764
|
+
feature: CoreFeature,
|
|
1765
|
+
importInSpec: string,
|
|
1766
|
+
): string | undefined {
|
|
1767
|
+
return this.byIdentity.get(feature.url.identity)?.[1]?.get(importInSpec)
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* If the give element name is a default name (i.e., it's prefixed with an
|
|
1772
|
+
* existing alias, or is a directive name for an existing alias), then return
|
|
1773
|
+
* the feature for that alias along with the name-in-spec for the element.
|
|
1774
|
+
*/
|
|
1775
|
+
private sourceDefaultName(
|
|
1776
|
+
isDirective: boolean,
|
|
1777
|
+
name: string,
|
|
1778
|
+
): [CoreFeature, string] | undefined {
|
|
1779
|
+
// Handle the alias-prefixed case first.
|
|
1780
|
+
const split = CoreFeatures.splitPrefixedName(name);
|
|
1781
|
+
if (split) {
|
|
1782
|
+
const [alias, nameInSpec] = split;
|
|
1783
|
+
const feature = this.byAlias.get(alias);
|
|
1784
|
+
// Note that we explicitly do not return `undefined` here if `feature`
|
|
1785
|
+
// isn't found, and instead fall-through to the default directive name
|
|
1786
|
+
// logic below. Normally that default directive name logic would also
|
|
1787
|
+
// return `undefined`, since validations above guarantee "__" isn't in
|
|
1788
|
+
// alias names. But as noted above, we make an exception for the "tag"
|
|
1789
|
+
// and "inaccessible" specs for backwards-compatibility reasons, so we
|
|
1790
|
+
// fall-through to allow those exceptions to be found in `this.byAlias`.
|
|
1791
|
+
if (feature) {
|
|
1792
|
+
return [feature, nameInSpec];
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// If not prefixed, then check whether it's the default directive name for
|
|
1796
|
+
// a spec.
|
|
1797
|
+
if (!isDirective) {
|
|
1112
1798
|
return undefined;
|
|
1113
1799
|
}
|
|
1800
|
+
const feature = this.byAlias.get(name);
|
|
1801
|
+
return feature ? [feature, feature.url.name] : undefined;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Splits alias-prefixed names into their spec alias and their name-in-spec.
|
|
1806
|
+
*/
|
|
1807
|
+
private static splitPrefixedName(name: string): [string, string] | undefined {
|
|
1808
|
+
const splitIndex = name.indexOf('__');
|
|
1809
|
+
return splitIndex !== -1
|
|
1810
|
+
? [name.slice(0, splitIndex), name.slice(splitIndex + 2)]
|
|
1811
|
+
: undefined;
|
|
1114
1812
|
}
|
|
1115
1813
|
}
|
|
1116
1814
|
|
|
@@ -1174,8 +1872,14 @@ export type StreamDirectiveArgs = {
|
|
|
1174
1872
|
if?: boolean,
|
|
1175
1873
|
}
|
|
1176
1874
|
|
|
1875
|
+
// A valid alias. Almost a valid GraphQL name, but we need to allow "." and "-"
|
|
1876
|
+
// after the first character for supergraph schema backwards compatibility.
|
|
1877
|
+
const aliasRegexp = /^[_A-Za-z][_0-9A-Za-z.-]*$/;
|
|
1177
1878
|
|
|
1178
|
-
// A
|
|
1879
|
+
// A valid GraphQL name.
|
|
1880
|
+
const nameRegexp = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
1881
|
+
|
|
1882
|
+
// A coordinate is up to 3 GraphQL names ([_A-Za-z][_0-9A-Za-z]*).
|
|
1179
1883
|
const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
|
|
1180
1884
|
|
|
1181
1885
|
export type SchemaConfig = {
|
|
@@ -1608,6 +2312,13 @@ export class Schema {
|
|
|
1608
2312
|
let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules()).map((e) => this.blueprint.onGraphQLJSValidationError(this, e));
|
|
1609
2313
|
errors = errors.concat(validateSchema(this));
|
|
1610
2314
|
|
|
2315
|
+
// Core feature validations around shadowing imports are @link-specific and
|
|
2316
|
+
// don't really depend on the rest of the schema being valid, so it's fine
|
|
2317
|
+
// to include them with standard GraphQL validation errors.
|
|
2318
|
+
errors = errors.concat(
|
|
2319
|
+
this.coreFeatures?.validateNoShadowingImports(this) ?? []
|
|
2320
|
+
)
|
|
2321
|
+
|
|
1611
2322
|
// We avoid adding federation-specific validations if the base schema is not proper graphQL as the later can easily trigger
|
|
1612
2323
|
// the former (for instance, someone mistyping the 'fields' argument name of a @key).
|
|
1613
2324
|
if (errors.length === 0) {
|