@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/dist/definitions.d.ts +4 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +29 -17
- package/dist/definitions.js.map +1 -1
- package/dist/federation.d.ts +2 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +21 -12
- package/dist/federation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +10 -3
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +70 -37
- package/dist/operations.js.map +1 -1
- package/dist/specs/connectSpec.d.ts +12 -0
- package/dist/specs/connectSpec.d.ts.map +1 -0
- package/dist/specs/connectSpec.js +82 -0
- package/dist/specs/connectSpec.js.map +1 -0
- package/dist/specs/coreSpec.d.ts +1 -0
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +9 -0
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/sourceSpec.d.ts.map +1 -1
- package/dist/specs/sourceSpec.js +2 -11
- package/dist/specs/sourceSpec.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +39 -17
- package/src/federation.ts +31 -13
- package/src/index.ts +1 -0
- package/src/operations.ts +128 -48
- package/src/specs/connectSpec.ts +146 -0
- package/src/specs/coreSpec.ts +22 -0
- package/src/specs/sourceSpec.ts +2 -21
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
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/specs/coreSpec.ts
CHANGED
|
@@ -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
|
*
|
package/src/specs/sourceSpec.ts
CHANGED
|
@@ -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
|
|
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
|
|
187
|
+
} = this.getSourceDirectives(schema);
|
|
207
188
|
|
|
208
189
|
if (!(sourceAPI || sourceType || sourceField)) {
|
|
209
190
|
// If none of the @source* directives are present, nothing needs
|