@apollo/federation-internals 2.7.1 → 2.7.3-testing.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/operations.ts CHANGED
@@ -48,6 +48,8 @@ import {
48
48
  isObjectType,
49
49
  NamedType,
50
50
  isUnionType,
51
+ directivesToString,
52
+ directivesToDirectiveNodes,
51
53
  } from "./definitions";
52
54
  import { isInterfaceObjectType } from "./federation";
53
55
  import { ERRORS } from "./error";
@@ -877,7 +879,6 @@ function computeFragmentsToKeep(
877
879
  return toExpand.size === 0 ? fragments : fragments.filter((f) => !toExpand.has(f.name));
878
880
  }
879
881
 
880
- // TODO Operations can also have directives
881
882
  export class Operation {
882
883
  constructor(
883
884
  readonly schema: Schema,
@@ -885,7 +886,8 @@ export class Operation {
885
886
  readonly selectionSet: SelectionSet,
886
887
  readonly variableDefinitions: VariableDefinitions,
887
888
  readonly fragments?: NamedFragments,
888
- readonly name?: string) {
889
+ readonly name?: string,
890
+ readonly directives?: readonly Directive<any>[]) {
889
891
  }
890
892
 
891
893
  // Returns a copy of this operation with the provided updated selection set.
@@ -901,7 +903,8 @@ export class Operation {
901
903
  newSelectionSet,
902
904
  this.variableDefinitions,
903
905
  this.fragments,
904
- this.name
906
+ this.name,
907
+ this.directives
905
908
  );
906
909
  }
907
910
 
@@ -917,7 +920,8 @@ export class Operation {
917
920
  newSelectionSet,
918
921
  this.variableDefinitions,
919
922
  newFragments,
920
- this.name
923
+ this.name,
924
+ this.directives
921
925
  );
922
926
  }
923
927
 
@@ -973,6 +977,19 @@ export class Operation {
973
977
  return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
974
978
  }
975
979
 
980
+ generateQueryFragments(): Operation {
981
+ const [minimizedSelectionSet, fragments] = this.selectionSet.minimizeSelectionSet();
982
+ return new Operation(
983
+ this.schema,
984
+ this.rootKind,
985
+ minimizedSelectionSet,
986
+ this.variableDefinitions,
987
+ fragments,
988
+ this.name,
989
+ this.directives
990
+ );
991
+ }
992
+
976
993
  expandAllFragments(): Operation {
977
994
  // We clear up the fragments since we've expanded all.
978
995
  // Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
@@ -1041,7 +1058,7 @@ export class Operation {
1041
1058
  }
1042
1059
 
1043
1060
  toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
1044
- return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.fragments, this.name, expandFragments, prettyPrint);
1061
+ return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.fragments, this.name, this.directives, expandFragments, prettyPrint);
1045
1062
  }
1046
1063
  }
1047
1064
 
@@ -1207,6 +1224,14 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1207
1224
  const expandedSelectionSet = this.expandedSelectionSet();
1208
1225
  const selectionSet = expandedSelectionSet.normalize({ parentType: type });
1209
1226
 
1227
+ if (!isObjectType(this.typeCondition)) {
1228
+ // When the type condition of the fragment is not an object type, the `FieldsInSetCanMerge` rule is more
1229
+ // restrictive and any fields can create conflicts. Thus, we have to use the full validator in this case.
1230
+ // (see https://github.com/graphql/graphql-spec/issues/1085 for details.)
1231
+ const validator = FieldsConflictValidator.build(expandedSelectionSet);
1232
+ return { selectionSet, validator };
1233
+ }
1234
+
1210
1235
  // Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
1211
1236
  // so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
1212
1237
  // that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
@@ -1378,28 +1403,6 @@ export class NamedFragments {
1378
1403
  });
1379
1404
  }
1380
1405
 
1381
- // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
1382
- // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
1383
- // want to consider to ignore the fragment for that subgraph, and that is when:
1384
- // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
1385
- // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
1386
- // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
1387
- // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
1388
- // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
1389
- // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
1390
- // don't really apply to the subgraph, and that this will ensure this is the case.
1391
- private selectionSetIsWorthUsing(selectionSet: SelectionSet): boolean {
1392
- const selections = selectionSet.selections();
1393
- if (selections.length === 0) {
1394
- return false;
1395
- }
1396
- if (selections.length === 1) {
1397
- const s = selections[0];
1398
- return !(s.kind === 'FieldSelection' && s.element.isLeafField());
1399
- }
1400
- return true;
1401
- }
1402
-
1403
1406
  rebaseOn(schema: Schema): NamedFragments | undefined {
1404
1407
  return this.mapInDependencyOrder((fragment, newFragments) => {
1405
1408
  const rebasedType = schema.type(fragment.selectionSet.parentType.name);
@@ -1411,7 +1414,7 @@ export class NamedFragments {
1411
1414
  // Rebasing can leave some inefficiencies in some case (particularly when a spread has to be "expanded", see `FragmentSpreadSelection.rebaseOn`),
1412
1415
  // so we do a top-level normalization to keep things clean.
1413
1416
  rebasedSelection = rebasedSelection.normalize({ parentType: rebasedType });
1414
- return this.selectionSetIsWorthUsing(rebasedSelection)
1417
+ return rebasedSelection.isWorthUsing()
1415
1418
  ? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection)
1416
1419
  : undefined;
1417
1420
  });
@@ -1535,6 +1538,60 @@ export class SelectionSet {
1535
1538
  this._selections = mapValues(keyedSelections);
1536
1539
  }
1537
1540
 
1541
+ /**
1542
+ * Takes a selection set and extracts inline fragments into named fragments,
1543
+ * reusing generated named fragments when possible.
1544
+ */
1545
+ minimizeSelectionSet(
1546
+ namedFragments: NamedFragments = new NamedFragments(),
1547
+ seenSelections: Map<string, [SelectionSet, NamedFragmentDefinition][]> = new Map(),
1548
+ ): [SelectionSet, NamedFragments] {
1549
+ const minimizedSelectionSet = this.lazyMap((selection) => {
1550
+ if (selection.kind === 'FragmentSelection' && selection.element.typeCondition && selection.element.appliedDirectives.length === 0
1551
+ && selection.selectionSet && selection.selectionSet.isWorthUsing() ) {
1552
+ // No proper hash code, so we use a unique enough number that's cheap to
1553
+ // compute and handle collisions as necessary.
1554
+ const mockHashCode = `on${selection.element.typeCondition}` + selection.selectionSet.selections().length;
1555
+ const equivalentSelectionSetCandidates = seenSelections.get(mockHashCode);
1556
+ if (equivalentSelectionSetCandidates) {
1557
+ // See if any candidates have an equivalent selection set, i.e. {x y} and {y x}.
1558
+ const match = equivalentSelectionSetCandidates.find(([candidateSet]) => candidateSet.equals(selection.selectionSet!));
1559
+ if (match) {
1560
+ // If we found a match, we can reuse the fragment (but we still need
1561
+ // to create a new FragmentSpread since parent types may differ).
1562
+ return new FragmentSpreadSelection(this.parentType, namedFragments, match[1], []);
1563
+ }
1564
+ }
1565
+
1566
+ // No match, so we need to create a new fragment. First, we minimize the
1567
+ // selection set before creating the fragment with it.
1568
+ const [minimizedSelectionSet] = selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections);
1569
+ const fragmentDefinition = new NamedFragmentDefinition(
1570
+ this.parentType.schema(),
1571
+ `_generated_${mockHashCode}_${equivalentSelectionSetCandidates?.length ?? 0}`,
1572
+ selection.element.typeCondition
1573
+ ).setSelectionSet(minimizedSelectionSet);
1574
+
1575
+ // Create a new "hash code" bucket or add to the existing one.
1576
+ if (!equivalentSelectionSetCandidates) {
1577
+ seenSelections.set(mockHashCode, [[selection.selectionSet, fragmentDefinition]]);
1578
+ namedFragments.add(fragmentDefinition);
1579
+ } else {
1580
+ equivalentSelectionSetCandidates.push([selection.selectionSet, fragmentDefinition]);
1581
+ }
1582
+
1583
+ return new FragmentSpreadSelection(this.parentType, namedFragments, fragmentDefinition, []);
1584
+ } else if (selection.kind === 'FieldSelection') {
1585
+ if (selection.selectionSet) {
1586
+ selection = selection.withUpdatedSelectionSet(selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections)[0]);
1587
+ }
1588
+ }
1589
+ return selection;
1590
+ });
1591
+
1592
+ return [minimizedSelectionSet, namedFragments];
1593
+ }
1594
+
1538
1595
  selectionsInReverseOrder(): readonly Selection[] {
1539
1596
  const length = this._selections.length;
1540
1597
  const reversed = new Array<Selection>(length);
@@ -2033,6 +2090,7 @@ export class SelectionSet {
2033
2090
  variableDefinitions: VariableDefinitions,
2034
2091
  fragments: NamedFragments | undefined,
2035
2092
  operationName?: string,
2093
+ directives?: readonly Directive<any>[],
2036
2094
  expandFragments: boolean = false,
2037
2095
  prettyPrint: boolean = true
2038
2096
  ): string {
@@ -2046,7 +2104,8 @@ export class SelectionSet {
2046
2104
  const nameAndVariables = operationName
2047
2105
  ? " " + (operationName + (variableDefinitions.isEmpty() ? "" : variableDefinitions.toString()))
2048
2106
  : (variableDefinitions.isEmpty() ? "" : " " + variableDefinitions.toString());
2049
- return fragmentsDefinitions + rootKind + nameAndVariables + " " + this.toString(expandFragments, true, indent);
2107
+ const directives_str = directivesToString(directives);
2108
+ return fragmentsDefinitions + rootKind + nameAndVariables + directives_str + " " + this.toString(expandFragments, true, indent);
2050
2109
  }
2051
2110
 
2052
2111
  /**
@@ -2077,6 +2136,34 @@ export class SelectionSet {
2077
2136
  : selectionsToString;
2078
2137
  }
2079
2138
  }
2139
+
2140
+ // `isWorthUsing` method is used to determine whether we want to factor out
2141
+ // given selection set into a named fragment so it can be reused across the query.
2142
+ // Currently, it is used in these cases:
2143
+ // 1) to reuse existing named fragments in subgraph queries (when reuseQueryFragments is on)
2144
+ // 2) to factor selection sets into named fragments (when generateQueryFragments is on).
2145
+ //
2146
+ // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
2147
+ // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
2148
+ // want to consider to ignore the fragment for that subgraph, and that is when:
2149
+ // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
2150
+ // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
2151
+ // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
2152
+ // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
2153
+ // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
2154
+ // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
2155
+ // don't really apply to the subgraph, and that this will ensure this is the case.
2156
+ isWorthUsing(): boolean {
2157
+ const selections = this.selections();
2158
+ if (selections.length === 0) {
2159
+ return false;
2160
+ }
2161
+ if (selections.length === 1) {
2162
+ const s = selections[0];
2163
+ return !(s.kind === 'FieldSelection' && s.element.isLeafField());
2164
+ }
2165
+ return true;
2166
+ }
2080
2167
  }
2081
2168
 
2082
2169
  type PathBasedUpdate = { path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[] };
@@ -2801,7 +2888,7 @@ class FieldsConflictValidator {
2801
2888
  continue;
2802
2889
  }
2803
2890
 
2804
- // We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
2891
+ // We're basically checking [FieldsInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
2805
2892
  // but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
2806
2893
  // merge already.
2807
2894
  for (const [thisField, thisValidator] of thisFields.entries()) {
@@ -3490,7 +3577,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3490
3577
 
3491
3578
  key(): string {
3492
3579
  if (!this.computedKey) {
3493
- this.computedKey = '...' + this.namedFragment.name + (this.spreadDirectives.length === 0 ? '' : ' ' + this.spreadDirectives.join(' '));
3580
+ this.computedKey = '...' + this.namedFragment.name + directivesToString(this.spreadDirectives);
3494
3581
  }
3495
3582
  return this.computedKey;
3496
3583
  }
@@ -3516,18 +3603,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3516
3603
  }
3517
3604
 
3518
3605
  toSelectionNode(): FragmentSpreadNode {
3519
- const directiveNodes = this.spreadDirectives.length === 0
3520
- ? undefined
3521
- : this.spreadDirectives.map(directive => {
3522
- return {
3523
- kind: Kind.DIRECTIVE,
3524
- name: {
3525
- kind: Kind.NAME,
3526
- value: directive.name,
3527
- },
3528
- arguments: directive.argumentsToAST()
3529
- } as DirectiveNode;
3530
- });
3606
+ const directiveNodes = directivesToDirectiveNodes(this.spreadDirectives);
3531
3607
  return {
3532
3608
  kind: Kind.FRAGMENT_SPREAD,
3533
3609
  name: { kind: Kind.NAME, value: this.namedFragment.name },
@@ -3672,9 +3748,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3672
3748
  if (expandFragments) {
3673
3749
  return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(true, true, indent);
3674
3750
  } else {
3675
- const directives = this.spreadDirectives;
3676
- const directiveString = directives.length == 0 ? '' : ' ' + directives.join(' ');
3677
- return (indent ?? '') + '...' + this.namedFragment.name + directiveString;
3751
+ return (indent ?? '') + '...' + this.namedFragment.name + directivesToString(this.spreadDirectives);
3678
3752
  }
3679
3753
  }
3680
3754
  }
@@ -3760,6 +3834,7 @@ export function operationFromDocument(
3760
3834
  }
3761
3835
  ) : Operation {
3762
3836
  let operation: OperationDefinitionNode | undefined;
3837
+ let operation_directives: Directive<any>[] | undefined; // the directives on `operation`
3763
3838
  const operationName = options?.operationName;
3764
3839
  const fragments = new NamedFragments();
3765
3840
  // We do a first pass to collect the operation, and create all named fragment, but without their selection set yet.
@@ -3770,6 +3845,7 @@ export function operationFromDocument(
3770
3845
  validate(!operation || operationName, () => 'Must provide operation name if query contains multiple operations.');
3771
3846
  if (!operationName || (definition.name && definition.name.value === operationName)) {
3772
3847
  operation = definition;
3848
+ operation_directives = directivesOfNodes(schema, definition.directives);
3773
3849
  }
3774
3850
  break;
3775
3851
  case Kind.FRAGMENT_DEFINITION:
@@ -3803,18 +3879,20 @@ export function operationFromDocument(
3803
3879
  }
3804
3880
  });
3805
3881
  fragments.validate(variableDefinitions);
3806
- return operationFromAST({schema, operation, variableDefinitions, fragments, validateInput: options?.validate});
3882
+ return operationFromAST({schema, operation, operation_directives, variableDefinitions, fragments, validateInput: options?.validate});
3807
3883
  }
3808
3884
 
3809
3885
  function operationFromAST({
3810
3886
  schema,
3811
3887
  operation,
3888
+ operation_directives,
3812
3889
  variableDefinitions,
3813
3890
  fragments,
3814
3891
  validateInput,
3815
3892
  }:{
3816
3893
  schema: Schema,
3817
3894
  operation: OperationDefinitionNode,
3895
+ operation_directives?: Directive<any>[],
3818
3896
  variableDefinitions: VariableDefinitions,
3819
3897
  fragments: NamedFragments,
3820
3898
  validateInput?: boolean,
@@ -3834,7 +3912,8 @@ function operationFromAST({
3834
3912
  }),
3835
3913
  variableDefinitions,
3836
3914
  fragmentsIfAny,
3837
- operation.name?.value
3915
+ operation.name?.value,
3916
+ operation_directives
3838
3917
  );
3839
3918
  }
3840
3919
 
@@ -3889,6 +3968,7 @@ export function operationToDocument(operation: Operation): DocumentNode {
3889
3968
  name: operation.name ? { kind: Kind.NAME, value: operation.name } : undefined,
3890
3969
  selectionSet: operation.selectionSet.toSelectionSetNode(),
3891
3970
  variableDefinitions: operation.variableDefinitions.toVariableDefinitionNodes(),
3971
+ directives: directivesToDirectiveNodes(operation.directives),
3892
3972
  };
3893
3973
  const fragmentASTs: DefinitionNode[] = operation.fragments
3894
3974
  ? operation.fragments?.toFragmentDefinitionNodes()
@@ -0,0 +1,146 @@
1
+ import { DirectiveLocation, GraphQLError } from 'graphql';
2
+ import { CorePurpose, FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
3
+ import {
4
+ Schema,
5
+ NonNullType,
6
+ InputObjectType,
7
+ InputFieldDefinition,
8
+ ListType,
9
+ } from '../definitions';
10
+ import { registerKnownFeature } from '../knownCoreFeatures';
11
+ import { createDirectiveSpecification } from '../directiveAndTypeSpecification';
12
+
13
+ export const connectIdentity = 'https://specs.apollo.dev/connect';
14
+
15
+ const CONNECT = "connect";
16
+ const SOURCE = "source";
17
+ const URL_PATH_TEMPLATE = "URLPathTemplate";
18
+ const JSON_SELECTION = "JSONSelection";
19
+ const CONNECT_HTTP = "ConnectHTTP";
20
+ const SOURCE_HTTP = "SourceHTTP";
21
+ const HTTP_HEADER_MAPPING = "HTTPHeaderMapping";
22
+
23
+ export class ConnectSpecDefinition extends FeatureDefinition {
24
+ constructor(version: FeatureVersion, readonly minimumFederationVersion: FeatureVersion) {
25
+ super(new FeatureUrl(connectIdentity, 'connect', version), minimumFederationVersion);
26
+
27
+ this.registerDirective(createDirectiveSpecification({
28
+ name: CONNECT,
29
+ locations: [DirectiveLocation.FIELD_DEFINITION],
30
+ repeatable: true,
31
+ // We "compose" these directives using the `@join__directive` mechanism,
32
+ // so they do not need to be composed in the way passing `composes: true`
33
+ // here implies.
34
+ composes: false,
35
+ }));
36
+
37
+ this.registerDirective(createDirectiveSpecification({
38
+ name: SOURCE,
39
+ locations: [DirectiveLocation.SCHEMA],
40
+ repeatable: true,
41
+ composes: false,
42
+ }));
43
+
44
+ this.registerType({ name: URL_PATH_TEMPLATE, checkOrAdd: () => [] });
45
+ this.registerType({ name: JSON_SELECTION, checkOrAdd: () => [] });
46
+ this.registerType({ name: CONNECT_HTTP, checkOrAdd: () => [] });
47
+ this.registerType({ name: SOURCE_HTTP, checkOrAdd: () => [] });
48
+ this.registerType({ name: HTTP_HEADER_MAPPING, checkOrAdd: () => [] });
49
+ }
50
+
51
+ addElementsToSchema(schema: Schema): GraphQLError[] {
52
+ /* scalar URLPathTemplate */
53
+ const URLPathTemplate = this.addScalarType(schema, URL_PATH_TEMPLATE);
54
+
55
+ /* scalar JSONSelection */
56
+ const JSONSelection = this.addScalarType(schema, JSON_SELECTION);
57
+
58
+ /*
59
+ directive @connect(
60
+ source: String
61
+ http: ConnectHTTP
62
+ selection: JSONSelection!
63
+ ) repeatable on FIELD_DEFINITION
64
+ */
65
+ const connect = this.addDirective(schema, CONNECT).addLocations(DirectiveLocation.FIELD_DEFINITION);
66
+ connect.repeatable = true;
67
+
68
+ connect.addArgument(SOURCE, schema.stringType());
69
+
70
+ /*
71
+ input HTTPHeaderMapping {
72
+ name: String!
73
+ as: String
74
+ value: String
75
+ }
76
+ */
77
+ const HTTPHeaderMapping = schema.addType(new InputObjectType(this.typeNameInSchema(schema, HTTP_HEADER_MAPPING)!));
78
+ HTTPHeaderMapping.addField(new InputFieldDefinition('name')).type =
79
+ new NonNullType(schema.stringType());
80
+ HTTPHeaderMapping.addField(new InputFieldDefinition('as')).type =
81
+ schema.stringType();
82
+ HTTPHeaderMapping.addField(new InputFieldDefinition('value')).type =
83
+ schema.stringType();
84
+
85
+ /*
86
+ input ConnectHTTP {
87
+ GET: URLPathTemplate
88
+ POST: URLPathTemplate
89
+ PUT: URLPathTemplate
90
+ PATCH: URLPathTemplate
91
+ DELETE: URLPathTemplate
92
+ body: JSONSelection
93
+ headers: [HTTPHeaderMapping!]
94
+ }
95
+ */
96
+ const ConnectHTTP = schema.addType(new InputObjectType(this.typeNameInSchema(schema, CONNECT_HTTP)!));
97
+ ConnectHTTP.addField(new InputFieldDefinition('GET')).type = URLPathTemplate;
98
+ ConnectHTTP.addField(new InputFieldDefinition('POST')).type = URLPathTemplate;
99
+ ConnectHTTP.addField(new InputFieldDefinition('PUT')).type = URLPathTemplate;
100
+ ConnectHTTP.addField(new InputFieldDefinition('PATCH')).type = URLPathTemplate;
101
+ ConnectHTTP.addField(new InputFieldDefinition('DELETE')).type = URLPathTemplate;
102
+ ConnectHTTP.addField(new InputFieldDefinition('body')).type = JSONSelection;
103
+ ConnectHTTP.addField(new InputFieldDefinition('headers')).type =
104
+ new ListType(new NonNullType(HTTPHeaderMapping));
105
+ connect.addArgument('http', ConnectHTTP);
106
+
107
+ connect.addArgument('selection', JSONSelection);
108
+
109
+ /*
110
+ directive @source(
111
+ name: String!
112
+ http: ConnectHTTP
113
+ ) repeatable on SCHEMA
114
+ */
115
+ const source = this.addDirective(schema, SOURCE).addLocations(
116
+ DirectiveLocation.SCHEMA,
117
+ );
118
+ source.repeatable = true;
119
+ source.addArgument('name', new NonNullType(schema.stringType()));
120
+
121
+ /*
122
+ input SourceHTTP {
123
+ baseURL: String!
124
+ headers: [HTTPHeaderMapping!]
125
+ }
126
+ */
127
+ const SourceHTTP = schema.addType(new InputObjectType(this.typeNameInSchema(schema, SOURCE_HTTP)!));
128
+ SourceHTTP.addField(new InputFieldDefinition('baseURL')).type =
129
+ new NonNullType(schema.stringType());
130
+ SourceHTTP.addField(new InputFieldDefinition('headers')).type =
131
+ new ListType(new NonNullType(HTTPHeaderMapping));
132
+
133
+ source.addArgument('http', SourceHTTP);
134
+
135
+ return [];
136
+ }
137
+
138
+ get defaultCorePurpose(): CorePurpose {
139
+ return 'EXECUTION';
140
+ }
141
+ }
142
+
143
+ export const CONNECT_VERSIONS = new FeatureDefinitions<ConnectSpecDefinition>(connectIdentity)
144
+ .add(new ConnectSpecDefinition(new FeatureVersion(0, 1), new FeatureVersion(2, 8)));
145
+
146
+ registerKnownFeature(CONNECT_VERSIONS);
@@ -640,6 +640,28 @@ export class FeatureVersion {
640
640
  return new this(+match[1], +match[2])
641
641
  }
642
642
 
643
+ /**
644
+ * Find the maximum version in a collection of versions, returning undefined in the case
645
+ * that the collection is empty.
646
+ *
647
+ * # Example
648
+ * ```
649
+ * expect(FeatureVersion.max([new FeatureVersion(1, 0), new FeatureVersion(2, 0)])).toBe(new FeatureVersion(2, 0))
650
+ * expect(FeatureVersion.max([])).toBe(undefined)
651
+ * ```
652
+ */
653
+ public static max(versions: Iterable<FeatureVersion>): FeatureVersion | undefined {
654
+ let max: FeatureVersion | undefined;
655
+
656
+ for (const version of versions) {
657
+ if (!max || version > max) {
658
+ max = version;
659
+ }
660
+ }
661
+
662
+ return max;
663
+ }
664
+
643
665
  /**
644
666
  * Return true if and only if this FeatureVersion satisfies the `required` version
645
667
  *
@@ -147,15 +147,13 @@ export class SourceSpecDefinition extends FeatureDefinition {
147
147
  return this.directive<SourceFieldDirectiveArgs>(schema, 'sourceField')!;
148
148
  }
149
149
 
150
- private getSourceDirectives(schema: Schema, errors: GraphQLError[]) {
150
+ private getSourceDirectives(schema: Schema) {
151
151
  const result: {
152
152
  sourceAPI?: DirectiveDefinition<SourceAPIDirectiveArgs>;
153
153
  sourceType?: DirectiveDefinition<SourceTypeDirectiveArgs>;
154
154
  sourceField?: DirectiveDefinition<SourceFieldDirectiveArgs>;
155
155
  } = {};
156
156
 
157
- let federationVersion: FeatureVersion | undefined;
158
-
159
157
  schema.schemaDefinition.appliedDirectivesOf<LinkDirectiveArgs>('link')
160
158
  .forEach(linkDirective => {
161
159
  const { url, import: imports } = linkDirective.arguments();
@@ -175,25 +173,8 @@ export class SourceSpecDefinition extends FeatureDefinition {
175
173
  }
176
174
  });
177
175
  }
178
- if (featureUrl && featureUrl.name === 'federation') {
179
- federationVersion = featureUrl.version;
180
- }
181
176
  });
182
177
 
183
- if (result.sourceAPI || result.sourceType || result.sourceField) {
184
- // Since this subgraph uses at least one of the @source{API,Type,Field}
185
- // directives, it must also use v2.7 or later of federation.
186
- if (!federationVersion || federationVersion.lt(this.minimumFederationVersion)) {
187
- errors.push(ERRORS.SOURCE_FEDERATION_VERSION_REQUIRED.err(
188
- `Schemas that @link to ${
189
- sourceIdentity
190
- } must also @link to federation version ${
191
- this.minimumFederationVersion
192
- } or later (found ${federationVersion})`,
193
- ));
194
- }
195
- }
196
-
197
178
  return result;
198
179
  }
199
180
 
@@ -203,7 +184,7 @@ export class SourceSpecDefinition extends FeatureDefinition {
203
184
  sourceAPI,
204
185
  sourceType,
205
186
  sourceField,
206
- } = this.getSourceDirectives(schema, errors);
187
+ } = this.getSourceDirectives(schema);
207
188
 
208
189
  if (!(sourceAPI || sourceType || sourceField)) {
209
190
  // If none of the @source* directives are present, nothing needs