@apollo/federation-internals 2.13.3 → 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.
@@ -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 { removeInaccessibleElements } from "./specs/inaccessibleSpec";
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
- private readonly byIdentity: Map<string, CoreFeature> = new Map();
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(): IterableIterator<CoreFeature> {
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 feature = this.byIdentity.get(featureIdentity);
1046
- if (feature) {
1089
+ const entry = this.byIdentity.get(featureIdentity);
1090
+ if (entry) {
1091
+ const [feature] = entry;
1047
1092
  this.byIdentity.delete(featureIdentity);
1048
- this.byAlias.delete(feature.nameInSchema);
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
- this.byAlias.set(feature.nameInSchema, feature);
1073
- this.byIdentity.set(feature.url.identity, feature);
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
- sourceFeature(element: DirectiveDefinition | Directive | NamedType): { feature: CoreFeature, nameInFeature: string, isImported: boolean } | undefined {
1077
- const isDirective = element instanceof DirectiveDefinition || element instanceof Directive;
1078
- const splitted = element.name.split('__');
1079
- if (splitted.length > 1) {
1080
- const feature = this.byAlias.get(splitted[0]);
1081
- return feature ? {
1082
- feature,
1083
- nameInFeature: splitted.slice(1).join('__'),
1084
- isImported: false,
1085
- } : undefined;
1086
- } else {
1087
- // Let's first see if it's an import, as this would take precedence over directive implicitely named like their feature.
1088
- const importName = isDirective ? '@' + element.name : element.name;
1089
- const allFeatures = [this.coreItself, ...this.byIdentity.values()];
1090
- for (const feature of allFeatures) {
1091
- for (const { as, name } of feature.imports) {
1092
- if ((as ?? name) === importName) {
1093
- return {
1094
- feature,
1095
- nameInFeature: isDirective ? name.slice(1) : name,
1096
- isImported: true,
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
- // Otherwise, this may be the special directive having the same name as its feature.
1103
- const directFeature = this.byAlias.get(element.name);
1104
- if (directFeature && isDirective) {
1105
- return {
1106
- feature: directFeature,
1107
- nameInFeature: element.name,
1108
- isImported: false,
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 coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
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) {