@apollo/federation-internals 2.11.3 → 2.11.5-preview.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/argumentCompositionStrategies.d.ts +6 -0
- package/dist/argumentCompositionStrategies.d.ts.map +1 -1
- package/dist/argumentCompositionStrategies.js +77 -0
- package/dist/argumentCompositionStrategies.js.map +1 -1
- package/dist/error.d.ts +2 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +4 -0
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +37 -8
- package/dist/federation.js.map +1 -1
- package/dist/specs/authenticatedSpec.d.ts +2 -0
- package/dist/specs/authenticatedSpec.d.ts.map +1 -1
- package/dist/specs/authenticatedSpec.js +3 -0
- package/dist/specs/authenticatedSpec.js.map +1 -1
- package/dist/specs/policySpec.d.ts +4 -0
- package/dist/specs/policySpec.d.ts.map +1 -1
- package/dist/specs/policySpec.js +4 -1
- package/dist/specs/policySpec.js.map +1 -1
- package/dist/specs/requiresScopesSpec.d.ts +4 -0
- package/dist/specs/requiresScopesSpec.d.ts.map +1 -1
- package/dist/specs/requiresScopesSpec.js +4 -1
- package/dist/specs/requiresScopesSpec.js.map +1 -1
- package/package.json +1 -1
- package/src/argumentCompositionStrategies.ts +114 -2
- package/src/error.ts +14 -0
- package/src/federation.ts +45 -8
- package/src/specs/authenticatedSpec.ts +5 -0
- package/src/specs/policySpec.ts +6 -2
- package/src/specs/requiresScopesSpec.ts +6 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {InputType, NonNullType, Schema, isListType, isNonNullType} from "./definitions"
|
|
2
2
|
import { sameType } from "./types";
|
|
3
3
|
import { valueEquals } from "./values";
|
|
4
4
|
|
|
@@ -19,6 +19,14 @@ function supportFixedTypes(types: (schema: Schema) => InputType[]): TypeSupportV
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function supportAnyNonNullNestedArray(): TypeSupportValidator {
|
|
23
|
+
return (_, type) =>
|
|
24
|
+
isNonNullType(type) && isListType(type.ofType)
|
|
25
|
+
&& isNonNullType(type.ofType.ofType) && isListType(type.ofType.ofType.ofType)
|
|
26
|
+
? { valid: true }
|
|
27
|
+
: { valid: false, supportedMsg: 'non nullable nested list types of any type' }
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function supportAnyNonNullArray(): TypeSupportValidator {
|
|
23
31
|
return (_, type) => isNonNullType(type) && isListType(type.ofType)
|
|
24
32
|
? { valid: true }
|
|
@@ -54,6 +62,104 @@ function unionValues(values: any[]): any {
|
|
|
54
62
|
}, []);
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Performs conjunction of 2d arrays that represent conditions in Disjunctive Normal Form.
|
|
67
|
+
*
|
|
68
|
+
* * Each inner array is interpreted as the conjunction of the conditions in the array.
|
|
69
|
+
* * The top-level array is interpreted as the disjunction of the inner arrays
|
|
70
|
+
*
|
|
71
|
+
* Algorithm
|
|
72
|
+
* * filter out duplicate entries to limit the amount of necessary computations
|
|
73
|
+
* * calculate cartesian product of the arrays to find all possible combinations
|
|
74
|
+
* * simplify combinations by dropping duplicate conditions (i.e. p ^ p = p, p ^ q = q ^ p)
|
|
75
|
+
* * eliminate entries that are subsumed by others (i.e. (p ^ q) subsumes (p ^ q ^ r))
|
|
76
|
+
*/
|
|
77
|
+
function dnfConjunction<T>(values: T[][][]): T[][] {
|
|
78
|
+
// should never be the case
|
|
79
|
+
if (values.length == 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// we first filter out duplicate values from candidates
|
|
84
|
+
// this avoids exponential computation of exactly the same conditions
|
|
85
|
+
const filtered = filterNestedArrayDuplicates(values);
|
|
86
|
+
|
|
87
|
+
// initialize with first entry
|
|
88
|
+
let result: T[][] = filtered[0];
|
|
89
|
+
// perform cartesian product to find all possible entries
|
|
90
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
91
|
+
const current = filtered[i];
|
|
92
|
+
const accumulator: T[][] = [];
|
|
93
|
+
const seen = new Set<string>;
|
|
94
|
+
|
|
95
|
+
for (const accElement of result) {
|
|
96
|
+
for (const currentElement of current) {
|
|
97
|
+
// filter out elements that are already present in accElement
|
|
98
|
+
const filteredElement = currentElement.filter((e) => !accElement.includes(e));
|
|
99
|
+
const candidate = [...accElement, ...filteredElement].sort();
|
|
100
|
+
const key = JSON.stringify(candidate);
|
|
101
|
+
// only add entries which has not been seen yet
|
|
102
|
+
if (!seen.has(key)) {
|
|
103
|
+
seen.add(key);
|
|
104
|
+
accumulator.push(candidate);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Now we need to deduplicate the results. Given that
|
|
109
|
+
// - outer array implies OR requirements
|
|
110
|
+
// - inner array implies AND requirements
|
|
111
|
+
// We can filter out any inner arrays that fully contain other inner arrays, i.e.
|
|
112
|
+
// A OR B OR (A AND B) OR (A AND B AND C) => A OR B
|
|
113
|
+
result = deduplicateSubsumedValues(accumulator);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function filterNestedArrayDuplicates<T>(values: T[][][]): T[][][] {
|
|
119
|
+
const filtered: T[][][] = [];
|
|
120
|
+
const seen = new Set<string>;
|
|
121
|
+
values.forEach((value) => {
|
|
122
|
+
value.sort();
|
|
123
|
+
const key = JSON.stringify(value);
|
|
124
|
+
if (!seen.has(key)) {
|
|
125
|
+
seen.add(key);
|
|
126
|
+
filtered.push(value);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return filtered;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function deduplicateSubsumedValues<T>(values: T[][]): T[][] {
|
|
133
|
+
const result: T[][] = [];
|
|
134
|
+
// we first sort by length as the longer ones might be dropped
|
|
135
|
+
values.sort((first, second) => {
|
|
136
|
+
if (first.length < second.length) {
|
|
137
|
+
return -1;
|
|
138
|
+
} else if (first.length > second.length) {
|
|
139
|
+
return 1;
|
|
140
|
+
} else {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const candidate of values) {
|
|
146
|
+
const entry = new Set(candidate);
|
|
147
|
+
let redundant = false;
|
|
148
|
+
for (const r of result) {
|
|
149
|
+
if (r.every(e => entry.has(e))) {
|
|
150
|
+
// if `r` is a subset of a `candidate` then it means `candidate` is redundant
|
|
151
|
+
redundant = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!redundant) {
|
|
157
|
+
result.push(candidate);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
57
163
|
export const ARGUMENT_COMPOSITION_STRATEGIES = {
|
|
58
164
|
MAX: {
|
|
59
165
|
name: 'MAX',
|
|
@@ -95,7 +201,8 @@ export const ARGUMENT_COMPOSITION_STRATEGIES = {
|
|
|
95
201
|
schema.booleanType(),
|
|
96
202
|
new NonNullType(schema.booleanType())
|
|
97
203
|
]),
|
|
98
|
-
mergeValues:
|
|
204
|
+
mergeValues:
|
|
205
|
+
mergeNullableValues(
|
|
99
206
|
(values: boolean[]) => values.every((v) => v)
|
|
100
207
|
),
|
|
101
208
|
},
|
|
@@ -113,5 +220,10 @@ export const ARGUMENT_COMPOSITION_STRATEGIES = {
|
|
|
113
220
|
name: 'NULLABLE_UNION',
|
|
114
221
|
isTypeSupported: supportAnyArray(),
|
|
115
222
|
mergeValues: mergeNullableValues(unionValues),
|
|
223
|
+
},
|
|
224
|
+
DNF_CONJUNCTION: {
|
|
225
|
+
name: 'DNF_CONJUNCTION',
|
|
226
|
+
isTypeSupported: supportAnyNonNullNestedArray(),
|
|
227
|
+
mergeValues: dnfConjunction
|
|
116
228
|
}
|
|
117
229
|
}
|
package/src/error.ts
CHANGED
|
@@ -633,6 +633,18 @@ const MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED = makeCodeDefinition(
|
|
|
633
633
|
{ addedIn: '2.8.0' },
|
|
634
634
|
);
|
|
635
635
|
|
|
636
|
+
const AUTHENTICATION_APPLIED_ON_INTERFACE = makeCodeDefinition(
|
|
637
|
+
'AUTHENTICATION_APPLIED_ON_INTERFACE',
|
|
638
|
+
'The @authenticated, @requiresScopes and @policy directive cannot be applied on interface, interface fields and interface object',
|
|
639
|
+
{ addedIn: '2.9.4' },
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const MISSING_TRANSITIVE_AUTH_REQUIREMENTS = makeCodeDefinition(
|
|
643
|
+
'MISSING_TRANSITIVE_AUTH_REQUIREMENTS',
|
|
644
|
+
'Field missing transitive @authenticated, @requiresScopes and/or @policy auth requirements needed to access dependent data.',
|
|
645
|
+
{ addedIn: '2.9.4' },
|
|
646
|
+
)
|
|
647
|
+
|
|
636
648
|
export const ERROR_CATEGORIES = {
|
|
637
649
|
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
|
|
638
650
|
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
|
|
@@ -734,6 +746,8 @@ export const ERRORS = {
|
|
|
734
746
|
LIST_SIZE_INVALID_SIZED_FIELD,
|
|
735
747
|
LIST_SIZE_INVALID_SLICING_ARGUMENT,
|
|
736
748
|
MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED,
|
|
749
|
+
AUTHENTICATION_APPLIED_ON_INTERFACE,
|
|
750
|
+
MISSING_TRANSITIVE_AUTH_REQUIREMENTS,
|
|
737
751
|
};
|
|
738
752
|
|
|
739
753
|
const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
|
package/src/federation.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
isWrapperType,
|
|
38
38
|
possibleRuntimeTypes,
|
|
39
39
|
isIntType,
|
|
40
|
-
Type,
|
|
40
|
+
Type, isFieldDefinition,
|
|
41
41
|
} from "./definitions";
|
|
42
42
|
import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils";
|
|
43
43
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
@@ -97,7 +97,7 @@ import { createObjectTypeSpecification, createScalarTypeSpecification, createUni
|
|
|
97
97
|
import { didYouMean, suggestionList } from "./suggestions";
|
|
98
98
|
import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures";
|
|
99
99
|
import { joinIdentity } from "./specs/joinSpec";
|
|
100
|
-
import {
|
|
100
|
+
import { CostDirectiveArguments, ListSizeDirectiveArguments } from "./specs/costSpec";
|
|
101
101
|
|
|
102
102
|
const linkSpec = LINK_VERSIONS.latest();
|
|
103
103
|
const tagSpec = TAG_VERSIONS.latest();
|
|
@@ -1820,18 +1820,16 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
1820
1820
|
}
|
|
1821
1821
|
}
|
|
1822
1822
|
|
|
1823
|
-
const
|
|
1824
|
-
const
|
|
1825
|
-
const costDirective = costSpec?.costDirective(schema);
|
|
1826
|
-
const listSizeDirective = costSpec?.listSizeDirective(schema);
|
|
1823
|
+
const costDirective = metadata.costDirective();
|
|
1824
|
+
const listSizeDirective = metadata.listSizeDirective();
|
|
1827
1825
|
|
|
1828
1826
|
// Validate @cost
|
|
1829
|
-
for (const application of costDirective
|
|
1827
|
+
for (const application of costDirective.applications()) {
|
|
1830
1828
|
validateCostNotAppliedToInterface(application, errorCollector);
|
|
1831
1829
|
}
|
|
1832
1830
|
|
|
1833
1831
|
// Validate @listSize
|
|
1834
|
-
for (const application of listSizeDirective
|
|
1832
|
+
for (const application of listSizeDirective.applications()) {
|
|
1835
1833
|
const parent = application.parent;
|
|
1836
1834
|
assert(parent instanceof FieldDefinition, "@listSize can only be applied to FIELD_DEFINITION");
|
|
1837
1835
|
validateListSizeAppliedToList(application, parent, errorCollector);
|
|
@@ -1840,6 +1838,9 @@ export class FederationBlueprint extends SchemaBlueprint {
|
|
|
1840
1838
|
validateSizedFieldsAreValidLists(application, parent, errorCollector);
|
|
1841
1839
|
}
|
|
1842
1840
|
|
|
1841
|
+
// Validate @authenticated, @requireScopes and @policy usage on interfaces and interface objects
|
|
1842
|
+
validateNoAuthenticationOnInterfaces(metadata, errorCollector);
|
|
1843
|
+
|
|
1843
1844
|
return errorCollector;
|
|
1844
1845
|
}
|
|
1845
1846
|
|
|
@@ -2891,3 +2892,39 @@ function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet
|
|
|
2891
2892
|
return undefined;
|
|
2892
2893
|
});
|
|
2893
2894
|
}
|
|
2895
|
+
|
|
2896
|
+
function validateNoAuthenticationOnInterfaces(metadata: FederationMetadata, errorCollector: GraphQLError[]) {
|
|
2897
|
+
const authenticatedDirective = metadata.authenticatedDirective();
|
|
2898
|
+
const requiresScopesDirective = metadata.requiresScopesDirective();
|
|
2899
|
+
const policyDirective = metadata.policyDirective();
|
|
2900
|
+
[authenticatedDirective, requiresScopesDirective, policyDirective].forEach((directive) => {
|
|
2901
|
+
for (const application of directive.applications()) {
|
|
2902
|
+
const element = application.parent;
|
|
2903
|
+
function isAppliedOnInterface(type: Type) {
|
|
2904
|
+
return isInterfaceType(type) || isInterfaceObjectType(baseType(type));
|
|
2905
|
+
}
|
|
2906
|
+
function isAppliedOnInterfaceField(elem: SchemaElement<any, any>) {
|
|
2907
|
+
return isFieldDefinition(elem) && isInterfaceType(elem.parent);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
if (isAppliedOnInterface(element) || isAppliedOnInterfaceField(element)) {
|
|
2911
|
+
let kind = '';
|
|
2912
|
+
switch (element.kind) {
|
|
2913
|
+
case 'FieldDefinition':
|
|
2914
|
+
kind = 'field';
|
|
2915
|
+
break;
|
|
2916
|
+
case 'InterfaceType':
|
|
2917
|
+
kind = 'interface';
|
|
2918
|
+
break;
|
|
2919
|
+
case 'ObjectType':
|
|
2920
|
+
kind = 'interface object';
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
errorCollector.push(ERRORS.AUTHENTICATION_APPLIED_ON_INTERFACE.err(
|
|
2924
|
+
`Invalid use of @${directive.name} on ${kind} "${element.coordinate}": @${directive.name} cannot be applied on interfaces, interface fields and interface objects`,
|
|
2925
|
+
{nodes: sourceASTs(application, element.parent)},
|
|
2926
|
+
));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./coreSpec";
|
|
9
9
|
import { createDirectiveSpecification } from "../directiveAndTypeSpecification";
|
|
10
10
|
import { registerKnownFeature } from "../knownCoreFeatures";
|
|
11
|
+
import {DirectiveDefinition, Schema} from "../definitions";
|
|
11
12
|
|
|
12
13
|
export class AuthenticatedSpecDefinition extends FeatureDefinition {
|
|
13
14
|
public static readonly directiveName = "authenticated";
|
|
@@ -37,6 +38,10 @@ export class AuthenticatedSpecDefinition extends FeatureDefinition {
|
|
|
37
38
|
}));
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
authenticatedDirective(schema: Schema): DirectiveDefinition | undefined {
|
|
42
|
+
return this.directive(schema, AuthenticatedSpecDefinition.directiveName);
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
get defaultCorePurpose(): CorePurpose {
|
|
41
46
|
return 'SECURITY';
|
|
42
47
|
}
|
package/src/specs/policySpec.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
FeatureUrl,
|
|
7
7
|
FeatureVersion,
|
|
8
8
|
} from "./coreSpec";
|
|
9
|
-
import { ListType, NonNullType } from "../definitions";
|
|
9
|
+
import {DirectiveDefinition, ListType, NonNullType, Schema} from "../definitions";
|
|
10
10
|
import { createDirectiveSpecification, createScalarTypeSpecification } from "../directiveAndTypeSpecification";
|
|
11
11
|
import { registerKnownFeature } from "../knownCoreFeatures";
|
|
12
12
|
import { ARGUMENT_COMPOSITION_STRATEGIES } from "../argumentCompositionStrategies";
|
|
@@ -42,7 +42,7 @@ export class PolicySpecDefinition extends FeatureDefinition {
|
|
|
42
42
|
assert(PolicyType, () => `Expected "${policyName}" to be defined`);
|
|
43
43
|
return new NonNullType(new ListType(new NonNullType(new ListType(new NonNullType(PolicyType)))));
|
|
44
44
|
},
|
|
45
|
-
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.
|
|
45
|
+
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.DNF_CONJUNCTION,
|
|
46
46
|
}],
|
|
47
47
|
locations: [
|
|
48
48
|
DirectiveLocation.FIELD_DEFINITION,
|
|
@@ -56,6 +56,10 @@ export class PolicySpecDefinition extends FeatureDefinition {
|
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
policyDirective(schema: Schema): DirectiveDefinition<{policies: string[][]}> | undefined {
|
|
60
|
+
return this.directive(schema, PolicySpecDefinition.directiveName);
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
get defaultCorePurpose(): CorePurpose {
|
|
60
64
|
return 'SECURITY';
|
|
61
65
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
FeatureUrl,
|
|
7
7
|
FeatureVersion,
|
|
8
8
|
} from "./coreSpec";
|
|
9
|
-
import { ListType, NonNullType } from "../definitions";
|
|
9
|
+
import {DirectiveDefinition, ListType, NonNullType, Schema} from "../definitions";
|
|
10
10
|
import { createDirectiveSpecification, createScalarTypeSpecification } from "../directiveAndTypeSpecification";
|
|
11
11
|
import { registerKnownFeature } from "../knownCoreFeatures";
|
|
12
12
|
import { ARGUMENT_COMPOSITION_STRATEGIES } from "../argumentCompositionStrategies";
|
|
@@ -43,7 +43,7 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition {
|
|
|
43
43
|
assert(scopeType, () => `Expected "${scopeName}" to be defined`);
|
|
44
44
|
return new NonNullType(new ListType(new NonNullType(new ListType(new NonNullType(scopeType)))));
|
|
45
45
|
},
|
|
46
|
-
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.
|
|
46
|
+
compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.DNF_CONJUNCTION,
|
|
47
47
|
}],
|
|
48
48
|
locations: [
|
|
49
49
|
DirectiveLocation.FIELD_DEFINITION,
|
|
@@ -57,6 +57,10 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition {
|
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
requiresScopesDirective(schema: Schema): DirectiveDefinition<{scopes: string[][]}> | undefined {
|
|
61
|
+
return this.directive(schema, RequiresScopesSpecDefinition.directiveName);
|
|
62
|
+
}
|
|
63
|
+
|
|
60
64
|
get defaultCorePurpose(): CorePurpose {
|
|
61
65
|
return 'SECURITY';
|
|
62
66
|
}
|