@apollo/federation-internals 2.1.0-alpha.1 → 2.1.0-alpha.4

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 (56) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +13 -3
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/definitions.d.ts +47 -8
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +137 -23
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/error.d.ts +1 -0
  10. package/dist/error.d.ts.map +1 -1
  11. package/dist/error.js +2 -0
  12. package/dist/error.js.map +1 -1
  13. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  14. package/dist/extractSubgraphsFromSupergraph.js +9 -0
  15. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  16. package/dist/federation.d.ts +5 -1
  17. package/dist/federation.d.ts.map +1 -1
  18. package/dist/federation.js +13 -5
  19. package/dist/federation.js.map +1 -1
  20. package/dist/federationSpec.d.ts +13 -9
  21. package/dist/federationSpec.d.ts.map +1 -1
  22. package/dist/federationSpec.js +27 -5
  23. package/dist/federationSpec.js.map +1 -1
  24. package/dist/inaccessibleSpec.js +1 -2
  25. package/dist/inaccessibleSpec.js.map +1 -1
  26. package/dist/operations.d.ts +48 -21
  27. package/dist/operations.d.ts.map +1 -1
  28. package/dist/operations.js +329 -48
  29. package/dist/operations.js.map +1 -1
  30. package/dist/print.d.ts +1 -1
  31. package/dist/print.d.ts.map +1 -1
  32. package/dist/print.js +1 -1
  33. package/dist/print.js.map +1 -1
  34. package/dist/schemaUpgrader.js +2 -2
  35. package/dist/schemaUpgrader.js.map +1 -1
  36. package/dist/utils.d.ts +9 -0
  37. package/dist/utils.d.ts.map +1 -1
  38. package/dist/utils.js +31 -1
  39. package/dist/utils.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/__tests__/definitions.test.ts +18 -0
  42. package/src/__tests__/operations.test.ts +217 -99
  43. package/src/__tests__/subgraphValidation.test.ts +2 -0
  44. package/src/buildSchema.ts +19 -5
  45. package/src/definitions.ts +217 -29
  46. package/src/error.ts +7 -0
  47. package/src/extractSubgraphsFromSupergraph.ts +20 -0
  48. package/src/federation.ts +16 -5
  49. package/src/federationSpec.ts +32 -5
  50. package/src/inaccessibleSpec.ts +2 -5
  51. package/src/operations.ts +520 -71
  52. package/src/print.ts +1 -1
  53. package/src/schemaUpgrader.ts +2 -2
  54. package/src/utils.ts +40 -0
  55. package/tsconfig.test.tsbuildinfo +1 -1
  56. package/tsconfig.tsbuildinfo +1 -1
@@ -15,9 +15,11 @@ import {
15
15
  TypeNode,
16
16
  VariableDefinitionNode,
17
17
  VariableNode,
18
- TypeSystemDefinitionNode,
19
18
  SchemaDefinitionNode,
20
- TypeDefinitionNode
19
+ TypeDefinitionNode,
20
+ DefinitionNode,
21
+ DirectiveDefinitionNode,
22
+ DirectiveNode,
21
23
  } from "graphql";
22
24
  import {
23
25
  CoreImport,
@@ -32,7 +34,7 @@ import {
32
34
  import { assert, mapValues, MapWithCachedArrays, setValues } from "./utils";
33
35
  import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode, argumentsEquals } from "./values";
34
36
  import { removeInaccessibleElements } from "./inaccessibleSpec";
35
- import { printSchema } from './print';
37
+ import { printDirectiveDefinition, printSchema } from './print';
36
38
  import { sameType } from './types';
37
39
  import { addIntrospectionFields, introspectionFieldNames, isIntrospectionName } from "./introspection";
38
40
  import { err } from '@apollo/core-schema';
@@ -291,6 +293,32 @@ export const executableDirectiveLocations: DirectiveLocation[] = [
291
293
  DirectiveLocation.VARIABLE_DEFINITION,
292
294
  ];
293
295
 
296
+ const executableDirectiveLocationsSet = new Set(executableDirectiveLocations);
297
+
298
+ export function isExecutableDirectiveLocation(loc: DirectiveLocation): boolean {
299
+ return executableDirectiveLocationsSet.has(loc);
300
+ }
301
+
302
+ export const typeSystemDirectiveLocations: DirectiveLocation[] = [
303
+ DirectiveLocation.SCHEMA,
304
+ DirectiveLocation.SCALAR,
305
+ DirectiveLocation.OBJECT,
306
+ DirectiveLocation.FIELD_DEFINITION,
307
+ DirectiveLocation.ARGUMENT_DEFINITION,
308
+ DirectiveLocation.INTERFACE,
309
+ DirectiveLocation.UNION,
310
+ DirectiveLocation.ENUM,
311
+ DirectiveLocation.ENUM_VALUE,
312
+ DirectiveLocation.INPUT_OBJECT,
313
+ DirectiveLocation.INPUT_FIELD_DEFINITION,
314
+ ];
315
+
316
+ const typeSystemDirectiveLocationsSet = new Set(typeSystemDirectiveLocations);
317
+
318
+ export function isTypeSystemDirectiveLocation(loc: DirectiveLocation): boolean {
319
+ return typeSystemDirectiveLocationsSet.has(loc);
320
+ }
321
+
294
322
  /**
295
323
  * Converts a type to an AST of a "reference" to that type, one corresponding to the type `toString()` (and thus never a type definition).
296
324
  *
@@ -495,11 +523,37 @@ export class Extension<TElement extends ExtendableElement> {
495
523
  }
496
524
  }
497
525
 
526
+ type UnappliedDirective = {
527
+ nameOrDef: DirectiveDefinition<Record<string, any>> | string,
528
+ args: Record<string, any>,
529
+ extension?: Extension<any>,
530
+ directive: DirectiveNode,
531
+ };
532
+
498
533
  // TODO: ideally, we should hide the ctor of this class as we rely in places on the fact the no-one external defines new implementations.
499
534
  export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>, TParent extends SchemaElement<any, any> | Schema> extends Element<TParent> {
500
535
  protected readonly _appliedDirectives: Directive<TOwnType>[] = [];
536
+ protected _unappliedDirectives: UnappliedDirective[] = [];
501
537
  description?: string;
502
538
 
539
+ addUnappliedDirective({ nameOrDef, args, extension, directive }: UnappliedDirective) {
540
+ this._unappliedDirectives.push({
541
+ nameOrDef,
542
+ args: args ?? {},
543
+ extension,
544
+ directive,
545
+ });
546
+ }
547
+
548
+ processUnappliedDirectives() {
549
+ for (const { nameOrDef, args, extension, directive } of this._unappliedDirectives) {
550
+ const d = this.applyDirective(nameOrDef, args);
551
+ d.setOfExtension(extension);
552
+ d.sourceAST = directive;
553
+ }
554
+ this._unappliedDirectives = [];
555
+ }
556
+
503
557
  get appliedDirectives(): readonly Directive<TOwnType>[] {
504
558
  return this._appliedDirectives;
505
559
  }
@@ -697,7 +751,7 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
697
751
  }
698
752
 
699
753
  hasNonExtensionElements(): boolean {
700
- return this.preserveEmptyDefinition
754
+ return this.preserveEmptyDefinition
701
755
  || this._appliedDirectives.some(d => d.ofExtension() === undefined)
702
756
  || this.hasNonExtensionInnerElements();
703
757
  }
@@ -916,6 +970,10 @@ export class SchemaBlueprint {
916
970
  onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
917
971
  return error;
918
972
  }
973
+
974
+ applyDirectivesAfterParsing() {
975
+ return false;
976
+ }
919
977
  }
920
978
 
921
979
  export const defaultSchemaBlueprint = new SchemaBlueprint();
@@ -1009,24 +1067,37 @@ export class CoreFeatures {
1009
1067
  this.byIdentity.set(feature.url.identity, feature);
1010
1068
  }
1011
1069
 
1012
- sourceFeature(element: DirectiveDefinition | Directive | NamedType): CoreFeature | undefined {
1070
+ sourceFeature(element: DirectiveDefinition | Directive | NamedType): { feature: CoreFeature, nameInFeature: string, isImported: boolean } | undefined {
1013
1071
  const isDirective = element instanceof DirectiveDefinition || element instanceof Directive;
1014
1072
  const splitted = element.name.split('__');
1015
1073
  if (splitted.length > 1) {
1016
- return this.byAlias.get(splitted[0]);
1074
+ const feature = this.byAlias.get(splitted[0]);
1075
+ return feature ? {
1076
+ feature,
1077
+ nameInFeature: splitted[1],
1078
+ isImported: false,
1079
+ } : undefined;
1017
1080
  } else {
1018
1081
  const directFeature = this.byAlias.get(element.name);
1019
1082
  if (directFeature && isDirective) {
1020
- return directFeature;
1083
+ return {
1084
+ feature: directFeature,
1085
+ nameInFeature: directFeature.imports.find(imp => imp.as === `@${element.name}`)?.name.slice(1) ?? element.name,
1086
+ isImported: true,
1087
+ };
1021
1088
  }
1022
1089
 
1023
1090
  // Let's see if it's an import. If not, it's not associated to a declared feature.
1024
1091
  const importName = isDirective ? '@' + element.name : element.name;
1025
1092
  const allFeatures = [this.coreItself, ...this.byIdentity.values()];
1026
1093
  for (const feature of allFeatures) {
1027
- for (const { as } of feature.imports) {
1028
- if (as === importName) {
1029
- return feature;
1094
+ for (const { as, name } of feature.imports) {
1095
+ if ((as ?? name) === importName) {
1096
+ return {
1097
+ feature,
1098
+ nameInFeature: name.slice(1),
1099
+ isImported: true,
1100
+ };
1030
1101
  }
1031
1102
  }
1032
1103
  }
@@ -1059,8 +1130,50 @@ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[]
1059
1130
  locations: [DirectiveLocation.SCALAR],
1060
1131
  argumentFct: (schema) => ({ args: [{ name: 'url', type: new NonNullType(schema.stringType()) }], errors: [] })
1061
1132
  }),
1133
+ // TODO: currently inconditionally adding @defer as the list of built-in. It's probably fine, but double check if we want to not do so when @defer-support is
1134
+ // not enabled or something (it would probably be hard to handle it at that point anyway but well...).
1135
+ createDirectiveSpecification({
1136
+ name: 'defer',
1137
+ locations: [DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT],
1138
+ argumentFct: (schema) => ({
1139
+ args: [
1140
+ { name: 'label', type: schema.stringType() },
1141
+ { name: 'if', type: schema.booleanType() },
1142
+ ],
1143
+ errors: [],
1144
+ })
1145
+ }),
1146
+ // Adding @stream too so that it's know and we don't error out if it is queries. It feels like it would be weird to do so for @stream but not
1147
+ // @defer when both are defined in the same spec. That said, that does *not* mean we currently _implement_ @stream, we don't, and so putting
1148
+ // it in a query will be a no-op at the moment (which technically is valid according to the spec so ...).
1149
+ createDirectiveSpecification({
1150
+ name: 'stream',
1151
+ locations: [DirectiveLocation.FIELD],
1152
+ argumentFct: (schema) => ({
1153
+ args: [
1154
+ { name: 'label', type: schema.stringType() },
1155
+ { name: 'initialCount', type: schema.intType(), defaultValue: 0 },
1156
+ { name: 'if', type: schema.booleanType() },
1157
+ ],
1158
+ errors: [],
1159
+ })
1160
+ }),
1062
1161
  ];
1063
1162
 
1163
+ export type DeferDirectiveArgs = {
1164
+ // TODO: we currently do not support variables for the defer label. Passing a label in a variable
1165
+ // feels like a weird use case in the first place, but we should probably fix this nonetheless (or
1166
+ // if we decide to have it be a known limitations, we should at least reject it cleanly).
1167
+ label?: string,
1168
+ if?: boolean | Variable,
1169
+ }
1170
+
1171
+ export type StreamDirectiveArgs = {
1172
+ label?: string,
1173
+ initialCount: number,
1174
+ if?: boolean,
1175
+ }
1176
+
1064
1177
 
1065
1178
  // A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
1066
1179
  const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
@@ -1162,7 +1275,7 @@ export class Schema {
1162
1275
  return this.apiSchema;
1163
1276
  }
1164
1277
 
1165
- private emptyASTDefinitionsForExtensionsWithoutDefinition(): TypeSystemDefinitionNode[] {
1278
+ private emptyASTDefinitionsForExtensionsWithoutDefinition(): DefinitionNode[] {
1166
1279
  const nodes = [];
1167
1280
  if (this.schemaDefinition.hasExtensionElements() && !this.schemaDefinition.hasNonExtensionElements()) {
1168
1281
  const node: SchemaDefinitionNode = { kind: Kind.SCHEMA_DEFINITION, operationTypes: [] };
@@ -1180,7 +1293,9 @@ export class Schema {
1180
1293
  return nodes;
1181
1294
  }
1182
1295
 
1183
- toGraphQLJSSchema(): GraphQLSchema {
1296
+ toGraphQLJSSchema(config?: { includeDefer?: boolean }): GraphQLSchema {
1297
+ const includeDefer = config?.includeDefer ?? false;
1298
+
1184
1299
  let ast = this.toAST();
1185
1300
 
1186
1301
  // Note that AST generated by `this.toAST()` may not be fully graphQL valid because, in federation subgraphs, we accept
@@ -1188,6 +1303,9 @@ export class Schema {
1188
1303
  // we need to "fix" that problem. For that, we add empty definitions for every element that has extensions without
1189
1304
  // definitions (which is also what `fed1` was effectively doing).
1190
1305
  const additionalNodes = this.emptyASTDefinitionsForExtensionsWithoutDefinition();
1306
+ if (includeDefer) {
1307
+ additionalNodes.push(this.deferDirective().toAST());
1308
+ }
1191
1309
  if (additionalNodes.length > 0) {
1192
1310
  ast = {
1193
1311
  kind: Kind.DOCUMENT,
@@ -1464,28 +1582,35 @@ export class Schema {
1464
1582
  }
1465
1583
 
1466
1584
  private getBuiltInDirective<TApplicationArgs extends {[key: string]: any}>(
1467
- schema: Schema,
1468
1585
  name: string
1469
1586
  ): DirectiveDefinition<TApplicationArgs> {
1470
- const directive = schema.directive(name);
1587
+ const directive = this.directive(name);
1471
1588
  assert(directive, `The provided schema has not be built with the ${name} directive built-in`);
1472
1589
  return directive as DirectiveDefinition<TApplicationArgs>;
1473
1590
  }
1474
1591
 
1475
- includeDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
1476
- return this.getBuiltInDirective(schema, 'include');
1592
+ includeDirective(): DirectiveDefinition<{if: boolean}> {
1593
+ return this.getBuiltInDirective('include');
1594
+ }
1595
+
1596
+ skipDirective(): DirectiveDefinition<{if: boolean}> {
1597
+ return this.getBuiltInDirective('skip');
1477
1598
  }
1478
1599
 
1479
- skipDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
1480
- return this.getBuiltInDirective(schema, 'skip');
1600
+ deprecatedDirective(): DirectiveDefinition<{reason?: string}> {
1601
+ return this.getBuiltInDirective('deprecated');
1481
1602
  }
1482
1603
 
1483
- deprecatedDirective(schema: Schema): DirectiveDefinition<{reason?: string}> {
1484
- return this.getBuiltInDirective(schema, 'deprecated');
1604
+ specifiedByDirective(): DirectiveDefinition<{url: string}> {
1605
+ return this.getBuiltInDirective('specifiedBy');
1485
1606
  }
1486
1607
 
1487
- specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
1488
- return this.getBuiltInDirective(schema, 'specifiedBy');
1608
+ deferDirective(): DirectiveDefinition<DeferDirectiveArgs> {
1609
+ return this.getBuiltInDirective('defer');
1610
+ }
1611
+
1612
+ streamDirective(): DirectiveDefinition<StreamDirectiveArgs> {
1613
+ return this.getBuiltInDirective('stream');
1489
1614
  }
1490
1615
 
1491
1616
  /**
@@ -1653,7 +1778,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1653
1778
  }
1654
1779
 
1655
1780
  hasNonExtensionElements(): boolean {
1656
- return this.preserveEmptyDefinition
1781
+ return this.preserveEmptyDefinition
1657
1782
  || this._appliedDirectives.some((d) => d.ofExtension() === undefined)
1658
1783
  || this.roots().some((r) => r.ofExtension() === undefined);
1659
1784
  }
@@ -2776,6 +2901,9 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
2776
2901
  return this.addLocations(...Object.values(DirectiveLocation));
2777
2902
  }
2778
2903
 
2904
+ /**
2905
+ * Adds the subset of type system locations that correspond to type definitions.
2906
+ */
2779
2907
  addAllTypeLocations(): DirectiveDefinition {
2780
2908
  return this.addLocations(
2781
2909
  DirectiveLocation.SCALAR,
@@ -2802,6 +2930,14 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
2802
2930
  return this;
2803
2931
  }
2804
2932
 
2933
+ hasExecutableLocations(): boolean {
2934
+ return this.locations.some((loc) => isExecutableDirectiveLocation(loc));
2935
+ }
2936
+
2937
+ hasTypeSystemLocations(): boolean {
2938
+ return this.locations.some((loc) => isTypeSystemDirectiveLocation(loc));
2939
+ }
2940
+
2805
2941
  applications(): readonly Directive<SchemaElement<any, any>, TApplicationArgs>[] {
2806
2942
  return setValues(this._referencers);
2807
2943
  }
@@ -2858,6 +2994,11 @@ export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} =
2858
2994
  this.remove().forEach(ref => ref.remove());
2859
2995
  }
2860
2996
 
2997
+ toAST(): DirectiveDefinitionNode {
2998
+ const doc = parse(printDirectiveDefinition(this));
2999
+ return doc.definitions[0] as DirectiveDefinitionNode;
3000
+ }
3001
+
2861
3002
  toString(): string {
2862
3003
  return `@${this.name}`;
2863
3004
  }
@@ -3054,7 +3195,7 @@ export function sameDirectiveApplications(applications1: Directive<any, any>[],
3054
3195
  /**
3055
3196
  * Checks whether a given array of directive applications (`maybeSubset`) is a sub-set of another array of directive applications (`applications`).
3056
3197
  *
3057
- * Sub-set here means that all of the applications in `maybeSubset` appears in `applications`.
3198
+ * Sub-set here means that all of the applications in `maybeSubset` appears in `applications`.
3058
3199
  */
3059
3200
  export function isDirectiveApplicationsSubset(applications: Directive<any, any>[], maybeSubset: Directive<any, any>[]): boolean {
3060
3201
  if (maybeSubset.length > applications.length) {
@@ -3306,6 +3447,34 @@ function *directivesToCopy(source: Schema, dest: Schema): Generator<DirectiveDef
3306
3447
  yield* source.directives();
3307
3448
  }
3308
3449
 
3450
+ /**
3451
+ * Creates, in the provided schema, a directive definition equivalent to the provided one.
3452
+ *
3453
+ * Note that this method assumes that:
3454
+ * - the provided schema does not already have a directive with the name of the definition to copy.
3455
+ * - if the copied definition has arguments, then the provided schema has existing types with
3456
+ * names matching any type used in copied definition.
3457
+ */
3458
+ export function copyDirectiveDefinitionToSchema({
3459
+ definition,
3460
+ schema,
3461
+ copyDirectiveApplicationsInArguments = true,
3462
+ locationFilter,
3463
+ }: {
3464
+ definition: DirectiveDefinition,
3465
+ schema: Schema,
3466
+ copyDirectiveApplicationsInArguments: boolean,
3467
+ locationFilter?: (loc: DirectiveLocation) => boolean,
3468
+ }
3469
+ ) {
3470
+ copyDirectiveDefinitionInner(
3471
+ definition,
3472
+ schema.addDirectiveDefinition(definition.name),
3473
+ copyDirectiveApplicationsInArguments,
3474
+ locationFilter,
3475
+ );
3476
+ }
3477
+
3309
3478
  function copy(source: Schema, dest: Schema) {
3310
3479
  // We shallow copy types first so any future reference to any of them can be dereferenced.
3311
3480
  for (const type of typesToCopy(source, dest)) {
@@ -3458,22 +3627,41 @@ function copyWrapperTypeOrTypeRef(source: Type | undefined, destParent: Schema):
3458
3627
  }
3459
3628
  }
3460
3629
 
3461
- function copyArgumentDefinitionInner<P extends FieldDefinition<any> | DirectiveDefinition>(source: ArgumentDefinition<P>, dest: ArgumentDefinition<P>) {
3630
+ function copyArgumentDefinitionInner<P extends FieldDefinition<any> | DirectiveDefinition>(
3631
+ source: ArgumentDefinition<P>,
3632
+ dest: ArgumentDefinition<P>,
3633
+ copyDirectiveApplications: boolean = true,
3634
+ ) {
3462
3635
  const type = copyWrapperTypeOrTypeRef(source.type, dest.schema()) as InputType;
3463
3636
  dest.type = type;
3464
3637
  dest.defaultValue = source.defaultValue;
3465
- copyAppliedDirectives(source, dest);
3638
+ if (copyDirectiveApplications) {
3639
+ copyAppliedDirectives(source, dest);
3640
+ }
3466
3641
  dest.description = source.description;
3467
3642
  dest.sourceAST = source.sourceAST;
3468
3643
  }
3469
3644
 
3470
- function copyDirectiveDefinitionInner(source: DirectiveDefinition, dest: DirectiveDefinition) {
3645
+ function copyDirectiveDefinitionInner(
3646
+ source: DirectiveDefinition,
3647
+ dest: DirectiveDefinition,
3648
+ copyDirectiveApplicationsInArguments: boolean = true,
3649
+ locationFilter?: (loc: DirectiveLocation) => boolean,
3650
+ ) {
3651
+ let locations = source.locations;
3652
+ if (locationFilter) {
3653
+ locations = locations.filter((loc) => locationFilter(loc));
3654
+ }
3655
+ if (locations.length === 0) {
3656
+ return;
3657
+ }
3658
+
3471
3659
  for (const arg of source.arguments()) {
3472
3660
  const type = copyWrapperTypeOrTypeRef(arg.type, dest.schema());
3473
- copyArgumentDefinitionInner(arg, dest.addArgument(arg.name, type as InputType));
3661
+ copyArgumentDefinitionInner(arg, dest.addArgument(arg.name, type as InputType), copyDirectiveApplicationsInArguments);
3474
3662
  }
3475
3663
  dest.repeatable = source.repeatable;
3476
- dest.addLocations(...source.locations);
3664
+ dest.addLocations(...locations);
3477
3665
  dest.sourceAST = source.sourceAST;
3478
3666
  dest.description = source.description;
3479
3667
  }
package/src/error.ts CHANGED
@@ -459,6 +459,12 @@ const DOWNSTREAM_SERVICE_ERROR = makeCodeDefinition(
459
459
  { addedIn: FED1_CODE },
460
460
  );
461
461
 
462
+ const DIRECTIVE_COMPOSITION_ERROR = makeCodeDefinition(
463
+ 'DIRECTIVE_COMPOSITION_ERROR',
464
+ 'Error when composing custom directives.',
465
+ { addedIn: '2.1.0' },
466
+ );
467
+
462
468
  export const ERROR_CATEGORIES = {
463
469
  DIRECTIVE_FIELDS_MISSING_EXTERNAL,
464
470
  DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
@@ -540,6 +546,7 @@ export const ERRORS = {
540
546
  KEY_HAS_DIRECTIVE_IN_FIELDS_ARGS,
541
547
  PROVIDES_HAS_DIRECTIVE_IN_FIELDS_ARGS,
542
548
  REQUIRES_HAS_DIRECTIVE_IN_FIELDS_ARGS,
549
+ DIRECTIVE_COMPOSITION_ERROR,
543
550
  };
544
551
 
545
552
  const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  baseType,
3
3
  CompositeType,
4
+ copyDirectiveDefinitionToSchema,
4
5
  Directive,
5
6
  FieldDefinition,
6
7
  InputFieldDefinition,
7
8
  InputObjectType,
8
9
  InterfaceType,
10
+ isExecutableDirectiveLocation,
9
11
  isEnumType,
10
12
  isInterfaceType,
11
13
  isObjectType,
@@ -234,6 +236,7 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
234
236
  }
235
237
  }
236
238
 
239
+ const allExecutableDirectives = supergraph.directives().filter((def) => def.hasExecutableLocations());
237
240
  for (const subgraph of subgraphs) {
238
241
  if (isFed1) {
239
242
  // The join spec in fed1 was not including external fields. Let's make sure we had them or we'll get validation
@@ -266,6 +269,23 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
266
269
  break;
267
270
  }
268
271
  }
272
+
273
+ // Lastly, we add all the "executable" directives from the supergraph to each subgraphs, as those may be part
274
+ // of a query and end up in any subgraph fetches. We do this "last" to make sure that if one of the directive
275
+ // use a type for an argument, that argument exists.
276
+ // Note that we don't bother with non-executable directives at the moment since we've don't extract their
277
+ // applications. It might become something we need later, but we don't so far.
278
+ for (const definition of allExecutableDirectives) {
279
+ // Note that we skip any potentially applied directives in the argument of the copied definition, because as said
280
+ // in the comment above, we haven't copied type-system directives. And so far, we really don't care about those
281
+ // applications.
282
+ copyDirectiveDefinitionToSchema({
283
+ definition,
284
+ schema: subgraph.schema,
285
+ copyDirectiveApplicationsInArguments: false,
286
+ locationFilter: (loc) => isExecutableDirectiveLocation(loc),
287
+ });
288
+ }
269
289
  }
270
290
 
271
291
  // TODO: Not sure that code is needed anymore (any field necessary to validate an interface will have been marked
package/src/federation.ts CHANGED
@@ -76,6 +76,7 @@ import {
76
76
  extendsDirectiveSpec,
77
77
  shareableDirectiveSpec,
78
78
  overrideDirectiveSpec,
79
+ composeDirectiveSpec,
79
80
  FEDERATION2_SPEC_DIRECTIVES,
80
81
  ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
81
82
  FEDERATION2_ONLY_SPEC_DIRECTIVES,
@@ -599,6 +600,10 @@ export class FederationMetadata {
599
600
  return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
600
601
  }
601
602
 
603
+ composeDirective(): DirectiveDefinition<{name: string}> {
604
+ return this.getFederationDirective(composeDirectiveSpec.name);
605
+ }
606
+
602
607
  inaccessibleDirective(): DirectiveDefinition<{}> {
603
608
  return this.getFederationDirective(
604
609
  inaccessibleSpec.inaccessibleDirectiveSpec.name
@@ -615,7 +620,7 @@ export class FederationMetadata {
615
620
  this.extendsDirective(),
616
621
  ];
617
622
  return this.isFed2Schema()
618
- ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective())
623
+ ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective(), this.composeDirective())
619
624
  : baseDirectives;
620
625
  }
621
626
 
@@ -698,7 +703,9 @@ export class FederationBlueprint extends SchemaBlueprint {
698
703
  }
699
704
 
700
705
  onDirectiveDefinitionAndSchemaParsed(schema: Schema): GraphQLError[] {
701
- return completeSubgraphSchema(schema);
706
+ const errors = completeSubgraphSchema(schema);
707
+ schema.schemaDefinition.processUnappliedDirectives();
708
+ return errors;
702
709
  }
703
710
 
704
711
  onInvalidation(schema: Schema) {
@@ -873,6 +880,10 @@ export class FederationBlueprint extends SchemaBlueprint {
873
880
  }
874
881
  return error;
875
882
  }
883
+
884
+ applyDirectivesAfterParsing() {
885
+ return true;
886
+ }
876
887
  }
877
888
 
878
889
  function findUnusedNamedForLinkDirective(schema: Schema): string | undefined {
@@ -927,7 +938,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
927
938
 
928
939
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
929
940
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
930
- export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override"])';
941
+ export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.1", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
931
942
 
932
943
  export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
933
944
  const fed2LinkExtension: SchemaExtensionNode = {
@@ -1422,7 +1433,7 @@ export class Subgraph {
1422
1433
  }
1423
1434
 
1424
1435
  const core = this.schema.coreFeatures;
1425
- return !core || core.sourceFeature(d)?.url.identity !== linkIdentity;
1436
+ return !core || core.sourceFeature(d)?.feature.url.identity !== linkIdentity;
1426
1437
  }
1427
1438
 
1428
1439
  private isPrintedType(t: NamedType): boolean {
@@ -1437,7 +1448,7 @@ export class Subgraph {
1437
1448
  }
1438
1449
 
1439
1450
  const core = this.schema.coreFeatures;
1440
- return !core || core.sourceFeature(t)?.url.identity !== linkIdentity;
1451
+ return !core || core.sourceFeature(t)?.feature.url.identity !== linkIdentity;
1441
1452
  }
1442
1453
 
1443
1454
  private isPrintedDirectiveApplication(d: Directive): boolean {
@@ -8,6 +8,7 @@ import {
8
8
  ArgumentSpecification,
9
9
  createDirectiveSpecification,
10
10
  createScalarTypeSpecification,
11
+ DirectiveSpecification,
11
12
  } from "./directiveAndTypeSpecification";
12
13
  import { DirectiveLocation, GraphQLError } from "graphql";
13
14
  import { assert } from "./utils";
@@ -79,6 +80,16 @@ export const overrideDirectiveSpec = createDirectiveSpecification({
79
80
  }),
80
81
  });
81
82
 
83
+ export const composeDirectiveSpec = createDirectiveSpecification({
84
+ name: 'composeDirective',
85
+ locations: [DirectiveLocation.SCHEMA],
86
+ repeatable: true,
87
+ argumentFct: (schema) => ({
88
+ args: [{ name: 'name', type: schema.stringType() }],
89
+ errors: [],
90
+ }),
91
+ })
92
+
82
93
  function fieldsArgument(schema: Schema): ArgumentSpecification {
83
94
  return { name: 'fields', type: fieldSetType(schema) };
84
95
  }
@@ -95,15 +106,24 @@ export const FEDERATION2_ONLY_SPEC_DIRECTIVES = [
95
106
  overrideDirectiveSpec,
96
107
  ];
97
108
 
98
- // Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
99
- export const FEDERATION2_SPEC_DIRECTIVES = [
109
+ export const FEDERATION2_1_ONLY_SPEC_DIRECTIVES = [
110
+ composeDirectiveSpec,
111
+ ];
112
+
113
+ const PRE_FEDERATION2_SPEC_DIRECTIVES = [
100
114
  keyDirectiveSpec,
101
115
  requiresDirectiveSpec,
102
116
  providesDirectiveSpec,
103
117
  externalDirectiveSpec,
104
118
  TAG_VERSIONS.latest().tagDirectiveSpec,
105
119
  extendsDirectiveSpec, // TODO: should we stop supporting that?
120
+ ];
121
+
122
+ // Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
123
+ export const FEDERATION2_SPEC_DIRECTIVES = [
124
+ ...PRE_FEDERATION2_SPEC_DIRECTIVES,
106
125
  ...FEDERATION2_ONLY_SPEC_DIRECTIVES,
126
+ ...FEDERATION2_1_ONLY_SPEC_DIRECTIVES,
107
127
  ];
108
128
 
109
129
  // Note that this is meant to contain _all_ federation directive names ever supported, regardless of which version.
@@ -120,6 +140,12 @@ export class FederationSpecDefinition extends FeatureDefinition {
120
140
  super(new FeatureUrl(federationIdentity, 'federation', version));
121
141
  }
122
142
 
143
+ private allFedDirectives(): DirectiveSpecification[] {
144
+ return PRE_FEDERATION2_SPEC_DIRECTIVES
145
+ .concat(FEDERATION2_ONLY_SPEC_DIRECTIVES)
146
+ .concat(this.url.version >= (new FeatureVersion(2, 1)) ? FEDERATION2_1_ONLY_SPEC_DIRECTIVES : []);
147
+ }
148
+
123
149
  addElementsToSchema(schema: Schema): GraphQLError[] {
124
150
  const feature = this.featureInSchema(schema);
125
151
  assert(feature, 'The federation specification should have been added to the schema before this is called');
@@ -127,20 +153,21 @@ export class FederationSpecDefinition extends FeatureDefinition {
127
153
  let errors: GraphQLError[] = [];
128
154
  errors = errors.concat(this.addTypeSpec(schema, fieldSetTypeSpec));
129
155
 
130
- for (const directive of FEDERATION2_SPEC_DIRECTIVES) {
156
+ for (const directive of this.allFedDirectives()) {
131
157
  errors = errors.concat(this.addDirectiveSpec(schema, directive));
132
158
  }
133
159
  return errors;
134
160
  }
135
161
 
136
162
  allElementNames(): string[] {
137
- return FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`).concat([
163
+ return this.allFedDirectives().map((spec) => `@${spec.name}`).concat([
138
164
  fieldSetTypeSpec.name,
139
165
  ])
140
166
  }
141
167
  }
142
168
 
143
169
  export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
144
- .add(new FederationSpecDefinition(new FeatureVersion(2, 0)));
170
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
171
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 1)));
145
172
 
146
173
  registerKnownFeature(FEDERATION_VERSIONS);
@@ -6,7 +6,6 @@ import {
6
6
  EnumType,
7
7
  EnumValue,
8
8
  ErrGraphQLAPISchemaValidationFailed,
9
- executableDirectiveLocations,
10
9
  FieldDefinition,
11
10
  InputFieldDefinition,
12
11
  InputObjectType,
@@ -17,6 +16,7 @@ import {
17
16
  isListType,
18
17
  isNonNullType,
19
18
  isScalarType,
19
+ isTypeSystemDirectiveLocation,
20
20
  isVariable,
21
21
  NamedType,
22
22
  ObjectType,
@@ -682,11 +682,8 @@ function validateInaccessibleElements(
682
682
  }
683
683
  }
684
684
 
685
- const executableDirectiveLocationSet = new Set(executableDirectiveLocations);
686
685
  for (const directive of schema.allDirectives()) {
687
- const typeSystemLocations = directive.locations.filter((loc) =>
688
- !executableDirectiveLocationSet.has(loc)
689
- );
686
+ const typeSystemLocations = directive.locations.filter((loc) => isTypeSystemDirectiveLocation(loc));
690
687
  if (hasBuiltInName(directive)) {
691
688
  // Built-in directives (and their descendants) aren't allowed to be
692
689
  // @inaccessible, regardless of shadowing.