@apollo/federation-internals 2.2.2 → 2.3.0-alpha.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.
Files changed (50) hide show
  1. package/dist/definitions.d.ts +2 -0
  2. package/dist/definitions.d.ts.map +1 -1
  3. package/dist/definitions.js +14 -2
  4. package/dist/definitions.js.map +1 -1
  5. package/dist/error.d.ts +3 -1
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js +17 -12
  8. package/dist/error.js.map +1 -1
  9. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  10. package/dist/extractSubgraphsFromSupergraph.js +31 -5
  11. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  12. package/dist/federation.d.ts +14 -6
  13. package/dist/federation.d.ts.map +1 -1
  14. package/dist/federation.js +141 -62
  15. package/dist/federation.js.map +1 -1
  16. package/dist/federationSpec.d.ts +2 -1
  17. package/dist/federationSpec.d.ts.map +1 -1
  18. package/dist/federationSpec.js +9 -1
  19. package/dist/federationSpec.js.map +1 -1
  20. package/dist/joinSpec.d.ts +9 -1
  21. package/dist/joinSpec.d.ts.map +1 -1
  22. package/dist/joinSpec.js +24 -2
  23. package/dist/joinSpec.js.map +1 -1
  24. package/dist/operations.d.ts +12 -0
  25. package/dist/operations.d.ts.map +1 -1
  26. package/dist/operations.js +59 -5
  27. package/dist/operations.js.map +1 -1
  28. package/dist/schemaUpgrader.d.ts.map +1 -1
  29. package/dist/schemaUpgrader.js +9 -0
  30. package/dist/schemaUpgrader.js.map +1 -1
  31. package/dist/supergraphs.d.ts.map +1 -1
  32. package/dist/supergraphs.js +1 -0
  33. package/dist/supergraphs.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +0 -1
  36. package/src/__tests__/schemaUpgrader.test.ts +43 -0
  37. package/src/__tests__/subgraphValidation.test.ts +126 -57
  38. package/src/__tests__/testUtils.ts +28 -0
  39. package/src/__tests__/values.test.ts +1 -1
  40. package/src/definitions.ts +12 -0
  41. package/src/error.ts +34 -16
  42. package/src/extractSubgraphsFromSupergraph.ts +40 -9
  43. package/src/federation.ts +178 -73
  44. package/src/federationSpec.ts +10 -1
  45. package/src/joinSpec.ts +40 -11
  46. package/src/operations.ts +76 -8
  47. package/src/schemaUpgrader.ts +13 -0
  48. package/src/supergraphs.ts +1 -0
  49. package/tsconfig.test.tsbuildinfo +1 -1
  50. package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  sourceASTs,
29
29
  UnionType,
30
30
  } from "./definitions";
31
- import { assert, joinStrings, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
31
+ import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
32
32
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
33
33
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
34
34
  import {
@@ -40,8 +40,11 @@ import {
40
40
  PossibleTypeExtensionsRule,
41
41
  print as printAST,
42
42
  Source,
43
- SchemaExtensionNode,
44
43
  GraphQLErrorOptions,
44
+ SchemaDefinitionNode,
45
+ OperationTypeNode,
46
+ OperationTypeDefinitionNode,
47
+ ConstDirectiveNode,
45
48
  } from "graphql";
46
49
  import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
47
50
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
@@ -302,6 +305,7 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
302
305
  isOnParentType = false,
303
306
  allowOnNonExternalLeafFields = false,
304
307
  allowFieldsWithArguments = false,
308
+ allowOnInterface = false,
305
309
  onFields,
306
310
  }: {
307
311
  definition: DirectiveDefinition<{fields: any}>,
@@ -311,13 +315,14 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>({
311
315
  isOnParentType?: boolean,
312
316
  allowOnNonExternalLeafFields?: boolean,
313
317
  allowFieldsWithArguments?: boolean,
318
+ allowOnInterface?: boolean,
314
319
  onFields?: (field: FieldDefinition<any>) => void,
315
320
  }): void {
316
321
  for (const application of definition.applications()) {
317
322
  const elt = application.parent as TParent;
318
323
  const type = targetTypeExtractor(elt);
319
324
  const parentType = isOnParentType ? type : (elt.parent as NamedType);
320
- if (isInterfaceType(parentType)) {
325
+ if (isInterfaceType(parentType) && !allowOnInterface) {
321
326
  const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
322
327
  errorCollector.push(code.err(
323
328
  isOnParentType
@@ -438,43 +443,64 @@ function validateNoExternalOnInterfaceFields(metadata: FederationMetadata, error
438
443
  }
439
444
  }
440
445
 
441
- /**
442
- * Register errors when, for an interface field, some of the implementations of that field are @external
443
- * _and_ not all of those field implementation have the same type (which otherwise allowed because field
444
- * implementation types can be a subtype of the interface field they implement).
445
- * This is done because if that is the case, federation may later generate invalid query plans (see details
446
- * on https://github.com/apollographql/federation/issues/1257).
447
- * This "limitation" will be removed when we stop generating invalid query plans for it.
448
- */
449
- function validateInterfaceRuntimeImplementationFieldsTypes(
450
- itf: InterfaceType,
451
- metadata: FederationMetadata,
452
- errorCollector: GraphQLError[],
453
- ): void {
454
- const requiresDirective = federationMetadata(itf.schema())?.requiresDirective();
455
- assert(requiresDirective, 'Schema should be a federation subgraph, but @requires directive not found');
456
- const runtimeTypes = itf.possibleRuntimeTypes();
457
- for (const field of itf.fields()) {
458
- const withExternalOrRequires: FieldDefinition<ObjectType>[] = [];
459
- const typeToImplems: MultiMap<string, FieldDefinition<ObjectType>> = new MultiMap();
460
- const nodes: ASTNode[] = [];
461
- for (const type of runtimeTypes) {
462
- const implemField = type.field(field.name);
463
- if (!implemField) continue;
464
- if (implemField.sourceAST) {
465
- nodes.push(implemField.sourceAST);
446
+ function validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
447
+ for (const itfType of metadata.schema.interfaceTypes()) {
448
+ const implementations = itfType.possibleRuntimeTypes();
449
+ for (const keyApplication of itfType.appliedDirectivesOf(metadata.keyDirective())) {
450
+ // Note that we will always have validated all @key fields at this point, so not bothering with extra validation
451
+ const fields = parseFieldSetArgument({parentType: itfType, directive: keyApplication, validate: false});
452
+ const isResolvable = !(keyApplication.arguments().resolvable === false);
453
+ const implementationsWithKeyButNotResolvable = new Array<ObjectType>();
454
+ const implementationsMissingKey = new Array<ObjectType>();
455
+ for (const type of implementations) {
456
+ const matchingApp = type.appliedDirectivesOf(metadata.keyDirective()).find((app) => {
457
+ const appFields = parseFieldSetArgument({parentType: type, directive: app, validate: false});
458
+ return fields.equals(appFields);
459
+ });
460
+ if (matchingApp) {
461
+ if (isResolvable && matchingApp.arguments().resolvable === false) {
462
+ implementationsWithKeyButNotResolvable.push(type);
463
+ }
464
+ } else {
465
+ implementationsMissingKey.push(type);
466
+ }
466
467
  }
467
- if (metadata.isFieldExternal(implemField) || implemField.hasAppliedDirective(requiresDirective)) {
468
- withExternalOrRequires.push(implemField);
468
+
469
+ if (implementationsMissingKey.length > 0) {
470
+ const typesString = printHumanReadableList(
471
+ implementationsMissingKey.map((i) => `"${i.coordinate}"`),
472
+ {
473
+ prefix: 'type',
474
+ prefixPlural: 'types',
475
+ }
476
+ );
477
+ errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(
478
+ `Key ${keyApplication} on interface type "${itfType.coordinate}" is missing on implementation ${typesString}.`,
479
+ { nodes: sourceASTs(...implementationsMissingKey) },
480
+ ));
481
+ } else if (implementationsWithKeyButNotResolvable.length > 0) {
482
+ const typesString = printHumanReadableList(
483
+ implementationsWithKeyButNotResolvable.map((i) => `"${i.coordinate}"`),
484
+ {
485
+ prefix: 'type',
486
+ prefixPlural: 'types',
487
+ }
488
+ );
489
+ errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err(
490
+ `Key ${keyApplication} on interface type "${itfType.coordinate}" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in ${typesString}.`,
491
+ { nodes: sourceASTs(...implementationsWithKeyButNotResolvable) },
492
+ ));
469
493
  }
470
- const returnType = implemField.type!;
471
- typeToImplems.add(returnType.toString(), implemField);
472
- }
473
- if (withExternalOrRequires.length > 0 && typeToImplems.size > 1) {
474
- const typeToImplemsArray = [...typeToImplems.entries()];
475
- errorCollector.push(ERRORS.INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH.err(
476
- `Some of the runtime implementations of interface field "${field.coordinate}" are marked @external or have a @require (${withExternalOrRequires.map(printFieldCoordinate)}) so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but ${formatFieldsToReturnType(typeToImplemsArray[0])} while ${joinStrings(typeToImplemsArray.slice(1).map(formatFieldsToReturnType), ' and ')}.`,
477
- { nodes },
494
+ }
495
+ }
496
+ }
497
+
498
+ function validateInterfaceObjectsAreOnEntities(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
499
+ for (const application of metadata.interfaceObjectDirective().applications()) {
500
+ if (!isEntityType(application.parent)) {
501
+ errorCollector.push(ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err(
502
+ `The @interfaceObject directive can only be applied to entity types but type "${application.parent.coordinate}" has no @key in this subgraph.`,
503
+ { nodes: application.parent.sourceAST }
478
504
  ));
479
505
  }
480
506
  }
@@ -521,13 +547,6 @@ function validateShareableNotRepeatedOnSameDeclaration(
521
547
  }
522
548
  }
523
549
 
524
-
525
- const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`;
526
-
527
- function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) {
528
- return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`;
529
- }
530
-
531
550
  export class FederationMetadata {
532
551
  private _externalTester?: ExternalTester;
533
552
  private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
@@ -606,6 +625,11 @@ export class FederationMetadata {
606
625
  return this.sharingPredicate()(field);
607
626
  }
608
627
 
628
+ isInterfaceObjectType(type: NamedType): type is ObjectType {
629
+ return isObjectType(type)
630
+ && hasAppliedDirective(type, this.interfaceObjectDirective());
631
+ }
632
+
609
633
  federationDirectiveNameInSchema(name: string): string {
610
634
  if (this.isFed2Schema()) {
611
635
  const coreFeatures = this.schema.coreFeatures;
@@ -660,6 +684,15 @@ export class FederationMetadata {
660
684
  return this.schema.directive(this.federationDirectiveNameInSchema(name)) as DirectiveDefinition<TApplicationArgs> | undefined;
661
685
  }
662
686
 
687
+ private getPost20FederationDirective<TApplicationArgs extends {[key: string]: any}>(
688
+ name: FederationDirectiveName
689
+ ): Post20FederationDirectiveDefinition<TApplicationArgs> {
690
+ return this.getFederationDirective<TApplicationArgs>(name) ?? {
691
+ name,
692
+ applications: () => new Array<Directive<any, TApplicationArgs>>(),
693
+ };
694
+ }
695
+
663
696
  keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
664
697
  return this.getLegacyFederationDirective(FederationDirectiveName.KEY);
665
698
  }
@@ -692,17 +725,18 @@ export class FederationMetadata {
692
725
  return this.getLegacyFederationDirective(FederationDirectiveName.TAG);
693
726
  }
694
727
 
695
- composeDirective(): DirectiveDefinition<{name: string}> | FederationDirectiveNotDefinedInSchema<{name: string}> {
696
- return this.getFederationDirective<{name: string}>(FederationDirectiveName.COMPOSE_DIRECTIVE) ?? {
697
- name: FederationDirectiveName.COMPOSE_DIRECTIVE,
698
- applications: () => new Array<Directive<any, {name: string}>>(),
699
- };
728
+ composeDirective(): Post20FederationDirectiveDefinition<{name: string}> {
729
+ return this.getPost20FederationDirective(FederationDirectiveName.COMPOSE_DIRECTIVE);
700
730
  }
701
731
 
702
732
  inaccessibleDirective(): DirectiveDefinition<{}> {
703
733
  return this.getLegacyFederationDirective(FederationDirectiveName.INACCESSIBLE);
704
734
  }
705
735
 
736
+ interfaceObjectDirective(): Post20FederationDirectiveDefinition<{}> {
737
+ return this.getPost20FederationDirective(FederationDirectiveName.INTERFACE_OBJECT);
738
+ }
739
+
706
740
  allFederationDirectives(): DirectiveDefinition[] {
707
741
  const baseDirectives: DirectiveDefinition[] = [
708
742
  this.keyDirective(),
@@ -723,6 +757,11 @@ export class FederationMetadata {
723
757
  if (isFederationDirectiveDefinedInSchema(composeDirective)) {
724
758
  baseDirectives.push(composeDirective);
725
759
  }
760
+ const interfaceObjectDirective = this.interfaceObjectDirective();
761
+ if (isFederationDirectiveDefinedInSchema(interfaceObjectDirective)) {
762
+ baseDirectives.push(interfaceObjectDirective);
763
+ }
764
+
726
765
  return baseDirectives;
727
766
  }
728
767
 
@@ -762,12 +801,20 @@ export type FederationDirectiveNotDefinedInSchema<TApplicationArgs extends {[key
762
801
  applications: () => readonly Directive<any, TApplicationArgs>[],
763
802
  }
764
803
 
804
+ export type Post20FederationDirectiveDefinition<TApplicationArgs extends {[key: string]: any}> =
805
+ DirectiveDefinition<TApplicationArgs>
806
+ | FederationDirectiveNotDefinedInSchema<TApplicationArgs>;
807
+
765
808
  export function isFederationDirectiveDefinedInSchema<TApplicationArgs extends {[key: string]: any}>(
766
- definition: DirectiveDefinition<TApplicationArgs> | FederationDirectiveNotDefinedInSchema<TApplicationArgs>
809
+ definition: Post20FederationDirectiveDefinition<TApplicationArgs>
767
810
  ): definition is DirectiveDefinition<TApplicationArgs> {
768
811
  return definition instanceof DirectiveDefinition;
769
812
  }
770
813
 
814
+ export function hasAppliedDirective(type: NamedType, definition: Post20FederationDirectiveDefinition<any>): boolean {
815
+ return isFederationDirectiveDefinedInSchema(definition) && type.hasAppliedDirective(definition);
816
+ }
817
+
771
818
  export class FederationBlueprint extends SchemaBlueprint {
772
819
  constructor(private readonly withRootTypeRenaming: boolean) {
773
820
  super();
@@ -872,6 +919,7 @@ export class FederationBlueprint extends SchemaBlueprint {
872
919
  metadata,
873
920
  isOnParentType: true,
874
921
  allowOnNonExternalLeafFields: true,
922
+ allowOnInterface: metadata.federationFeature()!.url.version.compareTo(new FeatureVersion(2, 3)) >= 0,
875
923
  onFields: field => {
876
924
  const type = baseType(field.type!);
877
925
  if (isUnionType(type) || isInterfaceType(type)) {
@@ -925,6 +973,8 @@ export class FederationBlueprint extends SchemaBlueprint {
925
973
 
926
974
  validateNoExternalOnInterfaceFields(metadata, errorCollector);
927
975
  validateAllExternalFieldsUsed(metadata, errorCollector);
976
+ validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector);
977
+ validateInterfaceObjectsAreOnEntities(metadata, errorCollector);
928
978
 
929
979
  // If tag is redefined by the user, make sure the definition is compatible with what we expect
930
980
  const tagDirective = metadata.tagDirective();
@@ -935,10 +985,6 @@ export class FederationBlueprint extends SchemaBlueprint {
935
985
  }
936
986
  }
937
987
 
938
- for (const itf of schema.interfaceTypes()) {
939
- validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
940
- }
941
-
942
988
  // While @shareable is "repeatable", this is only so one can use it on both a main
943
989
  // type definition _and_ possible other type extensions. But putting 2 @shareable
944
990
  // on the same type definition or field is both useless, and suggest some miscomprehension,
@@ -1070,15 +1116,22 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
1070
1116
 
1071
1117
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
1072
1118
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
1073
- export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.2", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
1074
-
1075
- export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
1076
- const fed2LinkExtension: SchemaExtensionNode = {
1077
- kind: Kind.SCHEMA_EXTENSION,
1078
- directives: [{
1079
- kind: Kind.DIRECTIVE,
1080
- name: { kind: Kind.NAME, value: linkDirectiveDefaultName },
1081
- arguments: [{
1119
+ export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1120
+
1121
+ /**
1122
+ * Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec),
1123
+ * returns an equivalent document that `@link` to the last known federation spec.
1124
+ *
1125
+ * @param document - the document to "augment".
1126
+ * @param options.addAsSchemaExtension - defines whethere the added `@link` is added as a schema extension (`extend schema`) or
1127
+ * added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually.
1128
+ */
1129
+ export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension: boolean }): DocumentNode {
1130
+ const directiveToAdd: ConstDirectiveNode = ({
1131
+ kind: Kind.DIRECTIVE,
1132
+ name: { kind: Kind.NAME, value: linkDirectiveDefaultName },
1133
+ arguments: [
1134
+ {
1082
1135
  kind: Kind.ARGUMENT,
1083
1136
  name: { kind: Kind.NAME, value: 'url' },
1084
1137
  value: { kind: Kind.STRING, value: federationSpec.url.toString() }
@@ -1087,13 +1140,54 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
1087
1140
  kind: Kind.ARGUMENT,
1088
1141
  name: { kind: Kind.NAME, value: 'import' },
1089
1142
  value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1090
- }]
1091
- }]
1092
- };
1093
- return {
1094
- kind: Kind.DOCUMENT,
1095
- loc: document.loc,
1096
- definitions: document.definitions.concat(fed2LinkExtension)
1143
+ }
1144
+ ]
1145
+ });
1146
+ if (options?.addAsSchemaExtension ?? true) {
1147
+ return {
1148
+ kind: Kind.DOCUMENT,
1149
+ loc: document.loc,
1150
+ definitions: document.definitions.concat({
1151
+ kind: Kind.SCHEMA_EXTENSION,
1152
+ directives: [directiveToAdd]
1153
+ }),
1154
+ }
1155
+ }
1156
+
1157
+ // We can't add a new schema definition if it already exists. If it doesn't we need to know if there is a mutation type or
1158
+ // not.
1159
+ const existingSchemaDefinition = document.definitions.find((d): d is SchemaDefinitionNode => d.kind == Kind.SCHEMA_DEFINITION);
1160
+ if (existingSchemaDefinition) {
1161
+ return {
1162
+ kind: Kind.DOCUMENT,
1163
+ loc: document.loc,
1164
+ definitions: document.definitions.filter((d) => d !== existingSchemaDefinition).concat([{
1165
+ ...existingSchemaDefinition,
1166
+ directives: [directiveToAdd].concat(existingSchemaDefinition.directives ?? []),
1167
+ }]),
1168
+ }
1169
+ } else {
1170
+ const hasMutation = document.definitions.some((d) => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === 'Mutation');
1171
+ const makeOpType = (opType: OperationTypeNode, name: string): OperationTypeDefinitionNode => ({
1172
+ kind: Kind.OPERATION_TYPE_DEFINITION,
1173
+ operation: opType,
1174
+ type: {
1175
+ kind: Kind.NAMED_TYPE,
1176
+ name: {
1177
+ kind: Kind.NAME,
1178
+ value: name,
1179
+ }
1180
+ },
1181
+ });
1182
+ return {
1183
+ kind: Kind.DOCUMENT,
1184
+ loc: document.loc,
1185
+ definitions: document.definitions.concat({
1186
+ kind: Kind.SCHEMA_DEFINITION,
1187
+ directives: [directiveToAdd],
1188
+ operationTypes: [ makeOpType(OperationTypeNode.QUERY, 'Query') ].concat(hasMutation ? makeOpType(OperationTypeNode.MUTATION, 'Mutation') : []),
1189
+ }),
1190
+ }
1097
1191
  }
1098
1192
  }
1099
1193
 
@@ -1123,13 +1217,21 @@ export function isFederationField(field: FieldDefinition<CompositeType>): boolea
1123
1217
  }
1124
1218
 
1125
1219
  export function isEntityType(type: NamedType): boolean {
1126
- if (type.kind !== "ObjectType") {
1220
+ if (!isObjectType(type) && !isInterfaceType(type)) {
1127
1221
  return false;
1128
1222
  }
1129
1223
  const metadata = federationMetadata(type.schema());
1130
1224
  return !!metadata && type.hasAppliedDirective(metadata.keyDirective());
1131
1225
  }
1132
1226
 
1227
+ export function isInterfaceObjectType(type: NamedType): boolean {
1228
+ if (!isObjectType(type)) {
1229
+ return false;
1230
+ }
1231
+ const metadata = federationMetadata(type.schema());
1232
+ return !!metadata && metadata.isInterfaceObjectType(type);
1233
+ }
1234
+
1133
1235
  export function buildSubgraph(
1134
1236
  name: string,
1135
1237
  url: string,
@@ -1191,7 +1293,6 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
1191
1293
  }
1192
1294
 
1193
1295
  function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1194
-
1195
1296
  // We special case @key, @requires and @provides because we've seen existing user schema where those
1196
1297
  // have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience,
1197
1298
  // if we detect one of those case, we just remove the definition and let the code afteward add the
@@ -1482,6 +1583,10 @@ export const serviceTypeSpec = createObjectTypeSpecification({
1482
1583
  export const entityTypeSpec = createUnionTypeSpecification({
1483
1584
  name: '_Entity',
1484
1585
  membersFct: (schema) => {
1586
+ // Please note that `_Entity` cannot use "interface entities" since interface types cannot be in unions.
1587
+ // It is ok in practice because _Entity is only use as return type for `_entities`, and even when interfaces
1588
+ // are involve, the result of an `_entities` call will always be an object type anyway, and since we force
1589
+ // all implementations of an interface entity to be entity themselves in a subgraph, we're fine.
1485
1590
  return schema.objectTypes().filter(isEntityType).map((t) => t.name);
1486
1591
  },
1487
1592
  });
@@ -35,6 +35,7 @@ export enum FederationDirectiveName {
35
35
  TAG = 'tag',
36
36
  INACCESSIBLE = 'inaccessible',
37
37
  COMPOSE_DIRECTIVE = 'composeDirective',
38
+ INTERFACE_OBJECT = 'interfaceObject',
38
39
  }
39
40
 
40
41
  const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
@@ -153,6 +154,13 @@ export class FederationSpecDefinition extends FeatureDefinition {
153
154
  }),
154
155
  }));
155
156
  }
157
+
158
+ if (version >= (new FeatureVersion(2, 3))) {
159
+ this.registerDirective(createDirectiveSpecification({
160
+ name: FederationDirectiveName.INTERFACE_OBJECT,
161
+ locations: [DirectiveLocation.OBJECT],
162
+ }));
163
+ }
156
164
  }
157
165
 
158
166
  private registerDirective(spec: DirectiveSpecification) {
@@ -195,6 +203,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
195
203
  export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
196
204
  .add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
197
205
  .add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
198
- .add(new FederationSpecDefinition(new FeatureVersion(2, 2)));
206
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
207
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 3)));
199
208
 
200
209
  registerKnownFeature(FEDERATION_VERSIONS);
package/src/joinSpec.ts CHANGED
@@ -64,11 +64,21 @@ export class JoinSpecDefinition extends FeatureDefinition {
64
64
  if (!this.isV01()) {
65
65
  joinType.addArgument('extension', new NonNullType(schema.booleanType()), false);
66
66
  joinType.addArgument('resolvable', new NonNullType(schema.booleanType()), true);
67
+
68
+ if (this.version >= (new FeatureVersion(0, 3))) {
69
+ joinType.addArgument('isInterfaceObject', new NonNullType(schema.booleanType()), false);
70
+ }
67
71
  }
68
72
 
69
73
  const joinField = this.addDirective(schema, 'field').addLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION);
70
74
  joinField.repeatable = true;
71
- joinField.addArgument('graph', new NonNullType(graphEnum));
75
+ // The `graph` argument used to be non-nullable, but @interfaceObject makes us add some field in
76
+ // the supergraph that don't "directly" come from any subgraph (they indirectly are inherited from
77
+ // an `@interfaceObject` type), and to indicate that, we use a `@join__field(graph: null)` annotation.
78
+ const graphArgType = this.version >= (new FeatureVersion(0, 3))
79
+ ? graphEnum
80
+ : new NonNullType(graphEnum);
81
+ joinField.addArgument('graph', graphArgType);
72
82
  joinField.addArgument('requires', joinFieldSet);
73
83
  joinField.addArgument('provides', joinFieldSet);
74
84
  if (!this.isV01()) {
@@ -87,6 +97,17 @@ export class JoinSpecDefinition extends FeatureDefinition {
87
97
  joinImplements.addArgument('interface', new NonNullType(schema.stringType()));
88
98
  }
89
99
 
100
+ if (this.version >= (new FeatureVersion(0, 3))) {
101
+ const joinUnionMember = this.addDirective(schema, 'unionMember').addLocations(DirectiveLocation.UNION);
102
+ joinUnionMember.repeatable = true;
103
+ joinUnionMember.addArgument('graph', new NonNullType(graphEnum));
104
+ joinUnionMember.addArgument('member', new NonNullType(schema.stringType()));
105
+
106
+ const joinEnumValue = this.addDirective(schema, 'enumValue').addLocations(DirectiveLocation.ENUM_VALUE);
107
+ joinEnumValue.repeatable = true;
108
+ joinEnumValue.addArgument('graph', new NonNullType(graphEnum));
109
+ }
110
+
90
111
  if (this.isV01()) {
91
112
  const joinOwner = this.addDirective(schema, 'owner').addLocations(DirectiveLocation.OBJECT);
92
113
  joinOwner.addArgument('graph', new NonNullType(graphEnum));
@@ -153,7 +174,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
153
174
  return this.directive(schema, 'graph')!;
154
175
  }
155
176
 
156
- typeDirective(schema: Schema): DirectiveDefinition<{graph: string, key?: string, extension?: boolean, resolvable?: boolean}> {
177
+ typeDirective(schema: Schema): DirectiveDefinition<{graph: string, key?: string, extension?: boolean, resolvable?: boolean, isInterfaceObject?: boolean}> {
157
178
  return this.directive(schema, 'type')!;
158
179
  }
159
180
 
@@ -162,7 +183,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
162
183
  }
163
184
 
164
185
  fieldDirective(schema: Schema): DirectiveDefinition<{
165
- graph: string,
186
+ graph?: string,
166
187
  requires?: string,
167
188
  provides?: string,
168
189
  override?: string,
@@ -173,6 +194,14 @@ export class JoinSpecDefinition extends FeatureDefinition {
173
194
  return this.directive(schema, 'field')!;
174
195
  }
175
196
 
197
+ unionMemberDirective(schema: Schema): DirectiveDefinition<{graph: string, member: string}> | undefined {
198
+ return this.directive(schema, 'unionMember');
199
+ }
200
+
201
+ enumValueDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
202
+ return this.directive(schema, 'enumValue');
203
+ }
204
+
176
205
  ownerDirective(schema: Schema): DirectiveDefinition<{graph: string}> | undefined {
177
206
  return this.directive(schema, 'owner');
178
207
  }
@@ -182,15 +211,15 @@ export class JoinSpecDefinition extends FeatureDefinition {
182
211
  }
183
212
  }
184
213
 
185
- // Note: This declare a no-yet-agreed-upon join spec v0.2, that:
186
- // 1. allows a repeatable join__field (join-spec#15).
187
- // 2. allows the 'key' argument of join__type to be optional (join-spec#13)
188
- // 3. relax conditions on join__type in general so as to not relate to the notion of owner (join-spec#16).
189
- // 4. has join__implements (join-spec#13)
190
- // The changes from join-spec#17 and join-spec#18 are not yet implemented, but probably should be or we may have bugs
191
- // due to the query planner having an invalid understanding of the subgraph services API.
214
+ // The versions are as follows:
215
+ // - 0.1: this is the version used by federation 1 composition. Federation 2 is still able to read supergraphs
216
+ // using that verison for backward compatibility, but never writes this spec version is not expressive enough
217
+ // for federation 2 in general.
218
+ // - 0.2: this is the original version released with federation 2.
219
+ // - 0.3: adds the `isInterfaceObject` argument to `@join__type`, and make the `graph` in `@join__field` skippable.
192
220
  export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
193
221
  .add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
194
- .add(new JoinSpecDefinition(new FeatureVersion(0, 2)));
222
+ .add(new JoinSpecDefinition(new FeatureVersion(0, 2)))
223
+ .add(new JoinSpecDefinition(new FeatureVersion(0, 3)));
195
224
 
196
225
  registerKnownFeature(JOIN_VERSIONS);