@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.
@@ -1,4 +1,4 @@
1
- import { InputType, NonNullType, Schema, isListType, isNonNullType } from "./definitions"
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: mergeNullableValues(
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 { COST_VERSIONS, CostDirectiveArguments, ListSizeDirectiveArguments, costIdentity } from "./specs/costSpec";
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 costFeature = schema.coreFeatures?.getByIdentity(costIdentity);
1824
- const costSpec = costFeature && COST_VERSIONS.find(costFeature.url.version);
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?.applications() ?? []) {
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?.applications() ?? []) {
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
  }
@@ -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.UNION,
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.UNION,
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
  }