@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/src/federation.ts CHANGED
@@ -1964,9 +1964,9 @@ export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = fal
1964
1964
 
1965
1965
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
1966
1966
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
1967
- export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.13", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@context", "@fromContext", "@cost", "@listSize", "@cacheTag"])';
1967
+ export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.14", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@context", "@fromContext", "@cost", "@listSize", "@cacheTag"])';
1968
1968
  // This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests.
1969
- export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.13", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1969
+ export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.14", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1970
1970
 
1971
1971
  // This is the federation @link for tests that go through the SchemaUpgrader.
1972
1972
  export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
@@ -32,7 +32,7 @@ import {
32
32
  Subgraph,
33
33
  Subgraphs,
34
34
  } from "./federation";
35
- import { assert, firstOf, MultiMap } from "./utils";
35
+ import { assert, firstOf, MultiMap, SetMultiMap } from "./utils";
36
36
  import { valueEquals } from "./values";
37
37
  import { FEDERATION1_TYPES } from "./specs/federationSpec";
38
38
 
@@ -230,7 +230,8 @@ export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
230
230
 
231
231
  const subgraphs = new Subgraphs();
232
232
  let errors: GraphQLError[] = [];
233
- const subgraphsUsingInterfaceObject = [];
233
+ const fed2InterfaceObjectTypesToSubgraphs = new SetMultiMap<string, string>();
234
+ const fed1InterfaceKeyTypesToSubgraphs = new SetMultiMap<string, string>();
234
235
 
235
236
  // build a data structure to help us do computation only once
236
237
  const objectTypeMap = new Map<string, Map<string, [ObjectType | InterfaceType, FederationMetadata]>>();
@@ -256,8 +257,9 @@ export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
256
257
  for (const subgraph of inputs.values()) {
257
258
  if (subgraph.isFed2Subgraph()) {
258
259
  subgraphs.add(subgraph);
259
- if (subgraph.metadata().interfaceObjectDirective().applications().size > 0) {
260
- subgraphsUsingInterfaceObject.push(subgraph.name);
260
+ for (const application of subgraph.metadata().interfaceObjectDirective().applications()) {
261
+ const typeName = (application.parent as NamedType).name;
262
+ fed2InterfaceObjectTypesToSubgraphs.add(typeName, subgraph.name);
261
263
  }
262
264
  } else {
263
265
  const res = new SchemaUpgrader(subgraph, inputs.values(), objectTypeMap).upgrade();
@@ -266,16 +268,19 @@ export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
266
268
  } else {
267
269
  subgraphs.add(res.upgraded);
268
270
  changes.set(subgraph.name, res.changes);
271
+ for (const typeName of res.interfaceKeyTypes) {
272
+ fed1InterfaceKeyTypesToSubgraphs.add(typeName, subgraph.name);
273
+ }
269
274
  }
270
275
  }
271
276
  }
272
- if (errors.length === 0 && subgraphsUsingInterfaceObject.length > 0) {
273
- const fed1Subgraphs = inputs.values().filter((s) => !s.isFed2Subgraph()).map((s) => s.name);
274
- // Note that we exit this method early if everything is a fed2 schema, so we know at least one of them wasn't.
275
- errors = [ ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(
276
- 'The @interfaceObject directive can only be used if all subgraphs have federation 2 subgraph schema (schema with a `@link` to "https://specs.apollo.dev/federation" version 2.0 or newer): '
277
- + `@interfaceObject is used in ${printSubgraphNames(subgraphsUsingInterfaceObject)} but ${printSubgraphNames(fed1Subgraphs)} ${fed1Subgraphs.length > 1 ? 'are not' : 'is not a'} federation 2 subgraph schema.`,
278
- )];
277
+ for (const [typeName, interfaceObjectSubgraphs] of fed2InterfaceObjectTypesToSubgraphs) {
278
+ const interfaceKeySubgraphs = fed1InterfaceKeyTypesToSubgraphs.get(typeName);
279
+ if (interfaceKeySubgraphs) {
280
+ errors.push(ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(
281
+ `The @interfaceObject directive is used on type "${typeName}" in ${printSubgraphNames([...interfaceObjectSubgraphs])}, which requires other subgraphs to resolve its type name via an interface @key. However, @key on an interface in a federation 1 subgraph does not mean it can fulfill the __typename-resolution requirement that @interfaceObject depends on. For ${printSubgraphNames([...interfaceKeySubgraphs])}, either upgrade them to federation 2 subgraphs or remove @key from the type.`,
282
+ ));
283
+ }
279
284
  }
280
285
 
281
286
  return errors.length === 0 ? { subgraphs, changes } : { errors };
@@ -324,6 +329,7 @@ class SchemaUpgrader {
324
329
  private readonly subgraph: Subgraph;
325
330
  private readonly metadata: FederationMetadata;
326
331
  private readonly errors: GraphQLError[] = [];
332
+ private readonly interfaceKeyTypes: Set<string> = new Set();
327
333
 
328
334
  constructor(private readonly originalSubgraph: Subgraph, private readonly allSubgraphs: readonly Subgraph[], private readonly objectTypeMap: Map<string, Map<string, [ObjectType | InterfaceType, FederationMetadata]>>) {
329
335
  // 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
@@ -413,7 +419,14 @@ class SchemaUpgrader {
413
419
  }
414
420
  }
415
421
 
416
- upgrade(): { upgraded: Subgraph, changes: UpgradeChanges, errors?: never } | { errors: GraphQLError[] } {
422
+ upgrade(): { upgraded: Subgraph, changes: UpgradeChanges, interfaceKeyTypes: Set<string>, errors?: never } | { errors: GraphQLError[] } {
423
+ // If there's already errors from constructor, we may not have inserted all
424
+ // directive definitions we need for upgrading (which may cause the methods
425
+ // below to throw). To avoid this, we return early in that case.
426
+ if (this.errors.length > 0) {
427
+ return { errors: this.errors };
428
+ }
429
+
417
430
  this.preUpgradeValidations();
418
431
 
419
432
  this.fixFederationDirectivesArguments();
@@ -452,6 +465,7 @@ class SchemaUpgrader {
452
465
  return {
453
466
  upgraded: this.subgraph,
454
467
  changes: this.changes,
468
+ interfaceKeyTypes: this.interfaceKeyTypes,
455
469
  };
456
470
  } catch (e) {
457
471
  const errors = errorCauses(e);
@@ -679,6 +693,7 @@ class SchemaUpgrader {
679
693
  for (const type of this.schema.interfaceTypes()) {
680
694
  for (const application of type.appliedDirectivesOf(this.metadata.keyDirective())) {
681
695
  this.addChange(new KeyOnInterfaceRemoval(type.name));
696
+ this.interfaceKeyTypes.add(type.name);
682
697
  application.remove();
683
698
  }
684
699
  for (const field of type.fields()) {
@@ -249,7 +249,7 @@ export class ConnectSpecDefinition extends FeatureDefinition {
249
249
  directive @connect(
250
250
  source: String
251
251
  id: String
252
- http: ConnectHTTP!
252
+ http: ConnectHTTP
253
253
  batch: ConnectBatch
254
254
  errors: ConnectorErrors
255
255
  selection: JSONSelection!
@@ -277,7 +277,7 @@ export class ConnectSpecDefinition extends FeatureDefinition {
277
277
  type: (schema, feature) => {
278
278
  const connectHttpType =
279
279
  lookupFeatureTypeInSchema<InputObjectType>(CONNECT_HTTP, 'InputObjectType', schema, feature);
280
- return new NonNullType(connectHttpType);
280
+ return connectHttpType;
281
281
  }
282
282
  },
283
283
  {
@@ -389,6 +389,7 @@ export const CONNECT_VERSIONS = new FeatureDefinitions<ConnectSpecDefinition>(
389
389
  new FeatureVersion(0, 4),
390
390
  new FeatureVersion(2, 13),
391
391
  ),
392
+ { preview: true },
392
393
  );
393
394
 
394
395
  registerKnownFeature(CONNECT_VERSIONS);
@@ -209,6 +209,9 @@ export type CoreImport = {
209
209
  as?: string,
210
210
  };
211
211
 
212
+ // The RegExp for a GraphQL name, plus an optional leading "@".
213
+ const importRegExp = /^@?[_A-Za-z][_0-9A-Za-z]*$/;
214
+
212
215
  export function extractCoreFeatureImports(url: FeatureUrl, directive: Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>): CoreImport[] {
213
216
  // Note: up to this point, we've kind of cheated with typing and force-casted the arguments to `CoreOrLinkDirectiveArgs`, and while this
214
217
  // graphQL type validations ensure this is "mostly" true, the `import' arg is an exception becuse it uses the `link__Import` scalar,
@@ -249,6 +252,13 @@ export function extractCoreFeatureImports(url: FeatureUrl, directive: Directive<
249
252
  ));
250
253
  continue importArgLoop;
251
254
  }
255
+ if (!importRegExp.test(value)) {
256
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
257
+ `Invalid value for the "name" field for sub-value ${valueToString(elt)} of @link(import:) argument: must use a GraphQL name.`,
258
+ { nodes: directive.sourceAST },
259
+ ));
260
+ continue importArgLoop;
261
+ }
252
262
  name = value;
253
263
  break;
254
264
  case 'as':
@@ -259,6 +269,13 @@ export function extractCoreFeatureImports(url: FeatureUrl, directive: Directive<
259
269
  ));
260
270
  continue importArgLoop;
261
271
  }
272
+ if (!importRegExp.test(value)) {
273
+ errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
274
+ `Invalid value for the "as" field for sub-value ${valueToString(elt)} of @link(import:) argument: must use a GraphQL name.`,
275
+ { nodes: directive.sourceAST },
276
+ ));
277
+ continue importArgLoop;
278
+ }
262
279
  break;
263
280
  default:
264
281
  errors.push(ERRORS.INVALID_LINK_DIRECTIVE_USAGE.err(
@@ -578,15 +595,25 @@ export class CoreSpecDefinition extends FeatureDefinition {
578
595
  }
579
596
  }
580
597
 
598
+ /**
599
+ * Options for {@link FeatureDefinitions.add}. Currently only used for the
600
+ * connect spec, where v0.4 is a preview version that should not be
601
+ * automatically selected by {@link FeatureDefinitions.latestNonPreview}.
602
+ */
603
+ interface FeatureDefinitionAddOptions {
604
+ preview?: boolean;
605
+ }
606
+
581
607
  export class FeatureDefinitions<T extends FeatureDefinition = FeatureDefinition> {
582
608
  // The list of definition corresponding to the known version of the particular feature this object handles,
583
609
  // sorted by _decreased_ versions.
584
610
  private readonly _definitions: T[] = [];
611
+ private readonly _previewVersions = new Set<string>();
585
612
 
586
613
  constructor(readonly identity: string) {
587
614
  }
588
615
 
589
- add(definition: T): FeatureDefinitions<T> {
616
+ add(definition: T, options?: FeatureDefinitionAddOptions): FeatureDefinitions<T> {
590
617
  if (definition.identity !== this.identity) {
591
618
  throw buildError(`Cannot add definition for ${definition} to the versions of definitions for ${this.identity}`);
592
619
  }
@@ -594,8 +621,11 @@ export class FeatureDefinitions<T extends FeatureDefinition = FeatureDefinition>
594
621
  return this;
595
622
  }
596
623
  this._definitions.push(definition);
597
- // We sort by decreased versions sa it feels somewhat natural anyway to have more recent versions first.
624
+ // We sort by decreased versions as it feels somewhat natural anyway to have more recent versions first.
598
625
  this._definitions.sort((def1, def2) => -def1.version.compareTo(def2.version));
626
+ if (options?.preview) {
627
+ this._previewVersions.add(definition.version.toString());
628
+ }
599
629
  return this;
600
630
  }
601
631
 
@@ -615,6 +645,18 @@ export class FeatureDefinitions<T extends FeatureDefinition = FeatureDefinition>
615
645
  return this._definitions[0];
616
646
  }
617
647
 
648
+ /**
649
+ * Like {@link latest}, but skips versions marked as preview. Used by
650
+ * `addJoinDirectiveDirectives` in the composition merger to avoid
651
+ * stamping a preview spec version (e.g. connect/v0.4) into the
652
+ * supergraph `@link` when no subgraph explicitly uses it. Falls back
653
+ * to {@link latest} if all versions are preview.
654
+ */
655
+ latestNonPreview(): T {
656
+ assert(this._definitions.length > 0, 'Trying to get latest when no definitions exist');
657
+ return this._definitions.find(def => !this._previewVersions.has(def.version.toString())) ?? this._definitions[0];
658
+ }
659
+
618
660
  getMinimumRequiredVersion(fedVersion: FeatureVersion): T {
619
661
  // this._definitions is already sorted with the most recent first
620
662
  // get the first definition that is compatible with the federation version
@@ -212,6 +212,7 @@ export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefiniti
212
212
  .add(new FederationSpecDefinition(new FeatureVersion(2, 10)))
213
213
  .add(new FederationSpecDefinition(new FeatureVersion(2, 11)))
214
214
  .add(new FederationSpecDefinition(new FeatureVersion(2, 12)))
215
- .add(new FederationSpecDefinition(new FeatureVersion(2, 13)));
215
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 13)))
216
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 14)));
216
217
 
217
218
  registerKnownFeature(FEDERATION_VERSIONS);
package/src/utils.ts CHANGED
@@ -375,15 +375,18 @@ export function printHumanReadableList(
375
375
 
376
376
  const { lastIdx } = names.reduce(
377
377
  ({ lastIdx, length }, name) => {
378
- if (length + name.length > cutoff) {
378
+ const newLength = length + name.length;
379
+ if (newLength > cutoff) {
380
+ // there is no short-circuit logic in reduce
381
+ // if we already exceeded the cutoff length we need to pass the length that exceeded cutoff
379
382
  return {
380
383
  lastIdx,
381
- length,
384
+ length: newLength,
382
385
  };
383
386
  }
384
387
  return {
385
388
  lastIdx: lastIdx + 1,
386
- length: length + name.length,
389
+ length: newLength,
387
390
  };
388
391
  },
389
392
  { lastIdx: 0, length: 0}
@@ -1,69 +0,0 @@
1
- import { GraphQLError } from 'graphql';
2
- import { FeatureDefinition, FeatureDefinitions, FeatureVersion } from "./coreSpec";
3
- import { Schema, DirectiveDefinition } from '../definitions';
4
- export declare const sourceIdentity = "https://specs.apollo.dev/source";
5
- export declare class SourceSpecDefinition extends FeatureDefinition {
6
- readonly minimumFederationVersion: FeatureVersion;
7
- constructor(version: FeatureVersion, minimumFederationVersion: FeatureVersion);
8
- addElementsToSchema(schema: Schema): GraphQLError[];
9
- allElementNames(): string[];
10
- sourceAPIDirective(schema: Schema): DirectiveDefinition<SourceAPIDirectiveArgs>;
11
- sourceTypeDirective(schema: Schema): DirectiveDefinition<SourceTypeDirectiveArgs>;
12
- sourceFieldDirective(schema: Schema): DirectiveDefinition<SourceFieldDirectiveArgs>;
13
- private getSourceDirectives;
14
- validateSubgraphSchema(schema: Schema): GraphQLError[];
15
- private validateSourceAPI;
16
- private validateSourceType;
17
- private validateSourceField;
18
- }
19
- export type SourceAPIDirectiveArgs = {
20
- name: string;
21
- http?: HTTPSourceAPI;
22
- };
23
- export type HTTPSourceAPI = {
24
- baseURL: string;
25
- headers?: HTTPHeaderMapping[];
26
- };
27
- export type HTTPHeaderMapping = {
28
- name: string;
29
- as?: string;
30
- value?: string;
31
- };
32
- export type SourceTypeDirectiveArgs = {
33
- api: string;
34
- http?: HTTPSourceType;
35
- selection: JSONSelection;
36
- keyTypeMap?: KeyTypeMap;
37
- };
38
- export type HTTPSourceType = {
39
- GET?: URLPathTemplate;
40
- POST?: URLPathTemplate;
41
- headers?: HTTPHeaderMapping[];
42
- body?: JSONSelection;
43
- };
44
- type URLPathTemplate = string;
45
- type JSONSelection = string;
46
- type KeyTypeMap = {
47
- key: string;
48
- typeMap: {
49
- [__typename: string]: string;
50
- };
51
- };
52
- export type SourceFieldDirectiveArgs = {
53
- api: string;
54
- http?: HTTPSourceField;
55
- selection?: JSONSelection;
56
- keyTypeMap?: KeyTypeMap;
57
- };
58
- export type HTTPSourceField = {
59
- GET?: URLPathTemplate;
60
- POST?: URLPathTemplate;
61
- PUT?: URLPathTemplate;
62
- PATCH?: URLPathTemplate;
63
- DELETE?: URLPathTemplate;
64
- body?: JSONSelection;
65
- headers?: HTTPHeaderMapping[];
66
- };
67
- export declare const SOURCE_VERSIONS: FeatureDefinitions<SourceSpecDefinition>;
68
- export {};
69
- //# sourceMappingURL=sourceSpec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sourceSpec.d.ts","sourceRoot":"","sources":["../../src/specs/sourceSpec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAQ,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAc,cAAc,EAAqB,MAAM,YAAY,CAAC;AAClH,OAAO,EACL,MAAM,EAKN,mBAAmB,EAEpB,MAAM,gBAAgB,CAAC;AAKxB,eAAO,MAAM,cAAc,oCAAoC,CAAC;AAEhE,qBAAa,oBAAqB,SAAQ,iBAAiB;IACpB,QAAQ,CAAC,wBAAwB,EAAE,cAAc;gBAA1E,OAAO,EAAE,cAAc,EAAW,wBAAwB,EAAE,cAAc;IA4BtF,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE;IA2EnD,eAAe,IAAI,MAAM,EAAE;IAgB3B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAIjC,mBAAmB,CAAC,MAAM,EAAE,MAAM;IAIlC,oBAAoB,CAAC,MAAM,EAAE,MAAM;IAInC,OAAO,CAAC,mBAAmB;IA+BlB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE;IA+B/D,OAAO,CAAC,iBAAiB;IAkEzB,OAAO,CAAC,kBAAkB;IAiG1B,OAAO,CAAC,mBAAmB;CAiH5B;AA2DD,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC9B,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,KAAK,eAAe,GAAG,MAAM,CAAC;AAC9B,KAAK,aAAa,GAAG,MAAM,CAAC;AAE5B,KAAK,UAAU,GAAG;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QACP,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;KAC9B,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAEF,eAAO,MAAM,eAAe,0CACwD,CAAC"}