@apollo/federation-internals 2.12.0-preview.4 → 2.12.1

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 (35) hide show
  1. package/dist/argumentCompositionStrategies.d.ts +7 -0
  2. package/dist/argumentCompositionStrategies.d.ts.map +1 -1
  3. package/dist/argumentCompositionStrategies.js +100 -1
  4. package/dist/argumentCompositionStrategies.js.map +1 -1
  5. package/dist/definitions.d.ts +1 -0
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +5 -1
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/error.d.ts +2 -0
  10. package/dist/error.d.ts.map +1 -1
  11. package/dist/error.js +4 -0
  12. package/dist/error.js.map +1 -1
  13. package/dist/federation.d.ts.map +1 -1
  14. package/dist/federation.js +34 -5
  15. package/dist/federation.js.map +1 -1
  16. package/dist/specs/authenticatedSpec.d.ts +2 -0
  17. package/dist/specs/authenticatedSpec.d.ts.map +1 -1
  18. package/dist/specs/authenticatedSpec.js +3 -0
  19. package/dist/specs/authenticatedSpec.js.map +1 -1
  20. package/dist/specs/policySpec.d.ts +4 -0
  21. package/dist/specs/policySpec.d.ts.map +1 -1
  22. package/dist/specs/policySpec.js +4 -1
  23. package/dist/specs/policySpec.js.map +1 -1
  24. package/dist/specs/requiresScopesSpec.d.ts +4 -0
  25. package/dist/specs/requiresScopesSpec.d.ts.map +1 -1
  26. package/dist/specs/requiresScopesSpec.js +4 -1
  27. package/dist/specs/requiresScopesSpec.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/argumentCompositionStrategies.ts +148 -2
  30. package/src/definitions.ts +4 -0
  31. package/src/error.ts +14 -0
  32. package/src/federation.ts +46 -6
  33. package/src/specs/authenticatedSpec.ts +9 -0
  34. package/src/specs/policySpec.ts +10 -2
  35. package/src/specs/requiresScopesSpec.ts +10 -2
@@ -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,138 @@ 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 2D array is interpreted as follows
69
+ * * Inner array is interpreted as the conjunction (an AND) of the conditions in the array.
70
+ * * Outer array is interpreted as the disjunction (an OR) of the inner arrays.
71
+ *
72
+ * Algorithm
73
+ * * filter out duplicate entries to limit the amount of necessary computations
74
+ * * calculate cartesian product of the arrays to find all possible combinations
75
+ * * simplify combinations by dropping duplicate conditions (i.e. p ^ p = p, p ^ q = q ^ p)
76
+ * * eliminate entries that are subsumed by others (i.e. (p ^ q) subsumes (p ^ q ^ r))
77
+ */
78
+ export function dnfConjunction<T>(values: T[][][]): T[][] {
79
+ // should never be the case
80
+ if (values.length == 0) {
81
+ return [];
82
+ }
83
+
84
+ // Copy the 2D arrays, as we'll be modifying them below (due to sorting).
85
+ for (let i = 0; i < values.length; i++) {
86
+ // See the doc string for `convertEmptyToTrue()` to understand why this is
87
+ // necessary.
88
+ values[i] = convertEmptyToTrue(dnfCopy(values[i]));
89
+ }
90
+
91
+ // we first filter out duplicate values from candidates
92
+ // this avoids exponential computation of exactly the same conditions
93
+ const filtered = filterNestedArrayDuplicates(values);
94
+
95
+ // initialize with first entry
96
+ let result: T[][] = filtered[0];
97
+ // perform cartesian product to find all possible entries
98
+ for (let i = 1; i < filtered.length; i++) {
99
+ const current = filtered[i];
100
+ const accumulator: T[][] = [];
101
+ const seen = new Set<string>;
102
+
103
+ for (const accElement of result) {
104
+ for (const currentElement of current) {
105
+ // filter out elements that are already present in accElement
106
+ const filteredElement = currentElement.filter((e) => !accElement.includes(e));
107
+ const candidate = [...accElement, ...filteredElement].sort();
108
+ const key = JSON.stringify(candidate);
109
+ // only add entries which has not been seen yet
110
+ if (!seen.has(key)) {
111
+ seen.add(key);
112
+ accumulator.push(candidate);
113
+ }
114
+ }
115
+ }
116
+ // Now we need to deduplicate the results. Given that
117
+ // - outer array implies OR requirements
118
+ // - inner array implies AND requirements
119
+ // We can filter out any inner arrays that fully contain other inner arrays, i.e.
120
+ // A OR B OR (A AND B) OR (A AND B AND C) => A OR B
121
+ result = deduplicateSubsumedValues(accumulator);
122
+ }
123
+ return result;
124
+ }
125
+
126
+ function filterNestedArrayDuplicates<T>(values: T[][][]): T[][][] {
127
+ const filtered: T[][][] = [];
128
+ const seen = new Set<string>;
129
+ values.forEach((value) => {
130
+ value.forEach((inner) => {
131
+ inner.sort();
132
+ })
133
+ value.sort((a, b) => {
134
+ const left = JSON.stringify(a);
135
+ const right = JSON.stringify(b);
136
+ return left > right ? 1 : left < right ? -1 : 0;
137
+ });
138
+ const key = JSON.stringify(value);
139
+ if (!seen.has(key)) {
140
+ seen.add(key);
141
+ filtered.push(value);
142
+ }
143
+ });
144
+ return filtered;
145
+ }
146
+
147
+ function deduplicateSubsumedValues<T>(values: T[][]): T[][] {
148
+ const result: T[][] = [];
149
+ // we first sort by length as the longer ones might be dropped
150
+ values.sort((first, second) => {
151
+ if (first.length < second.length) {
152
+ return -1;
153
+ } else if (first.length > second.length) {
154
+ return 1;
155
+ } else {
156
+ return 0;
157
+ }
158
+ });
159
+
160
+ for (const candidate of values) {
161
+ const entry = new Set(candidate);
162
+ let redundant = false;
163
+ for (const r of result) {
164
+ if (r.every(e => entry.has(e))) {
165
+ // if `r` is a subset of a `candidate` then it means `candidate` is redundant
166
+ redundant = true;
167
+ break;
168
+ }
169
+ }
170
+
171
+ if (!redundant) {
172
+ result.push(candidate);
173
+ }
174
+ }
175
+ return result;
176
+ }
177
+
178
+ function dnfCopy<T>(value: T[][]): T[][] {
179
+ const newValue = new Array(value.length);
180
+ for (let i = 0; i < value.length; i++) {
181
+ newValue[i] = value[i].slice();
182
+ }
183
+ return newValue;
184
+ }
185
+
186
+ /**
187
+ * Normally for DNF, you'd consider [] to be always false and [[]] to be always
188
+ * true, and code that uses some()/every() needs no special-casing to work with
189
+ * these definitions. However, router special-cases [] to also mean true, and so
190
+ * if we're about to do any evaluation on DNFs, we need to do these conversions
191
+ * beforehand.
192
+ */
193
+ export function convertEmptyToTrue<T>(value: T[][]): T[][] {
194
+ return value.length === 0 ? [[]] : value;
195
+ }
196
+
57
197
  export const ARGUMENT_COMPOSITION_STRATEGIES = {
58
198
  MAX: {
59
199
  name: 'MAX',
@@ -95,7 +235,8 @@ export const ARGUMENT_COMPOSITION_STRATEGIES = {
95
235
  schema.booleanType(),
96
236
  new NonNullType(schema.booleanType())
97
237
  ]),
98
- mergeValues: mergeNullableValues(
238
+ mergeValues:
239
+ mergeNullableValues(
99
240
  (values: boolean[]) => values.every((v) => v)
100
241
  ),
101
242
  },
@@ -113,5 +254,10 @@ export const ARGUMENT_COMPOSITION_STRATEGIES = {
113
254
  name: 'NULLABLE_UNION',
114
255
  isTypeSupported: supportAnyArray(),
115
256
  mergeValues: mergeNullableValues(unionValues),
257
+ },
258
+ DNF_CONJUNCTION: {
259
+ name: 'DNF_CONJUNCTION',
260
+ isTypeSupported: supportAnyNonNullNestedArray(),
261
+ mergeValues: dnfConjunction
116
262
  }
117
263
  }
@@ -3825,3 +3825,7 @@ function copyDirectiveDefinitionInner(
3825
3825
  export function isFieldDefinition(elem: SchemaElement<any, any>): elem is FieldDefinition<any> {
3826
3826
  return elem instanceof FieldDefinition;
3827
3827
  }
3828
+
3829
+ export function isElementNamedType(elem: SchemaElement<any, any>): elem is NamedType {
3830
+ return elem instanceof BaseNamedType;
3831
+ }
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 AUTH_REQUIREMENTS_APPLIED_ON_INTERFACE = makeCodeDefinition(
637
+ 'AUTH_REQUIREMENTS_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
+ AUTH_REQUIREMENTS_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, isElementNamedType,
41
41
  } from "./definitions";
42
42
  import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils";
43
43
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
@@ -1071,7 +1071,7 @@ function validateListSizeAppliedToList(
1071
1071
  ) {
1072
1072
  const { sizedFields = [] } = application.arguments();
1073
1073
  // @listSize must be applied to a list https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-List-Size-Target
1074
- if (!sizedFields.length && parent.type && !isListType(parent.type)) {
1074
+ if (!sizedFields.length && parent.type && !isListType(parent.type) && !isNonNullListType(parent.type)) {
1075
1075
  errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err(
1076
1076
  `"${parent.coordinate}" is not a list`,
1077
1077
  { nodes: sourceASTs(application, parent) },
@@ -1141,8 +1141,9 @@ function validateSizedFieldsAreValidLists(
1141
1141
  ) {
1142
1142
  const { sizedFields = [] } = application.arguments();
1143
1143
  // Validate sizedFields https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Sized-Fields-Target
1144
- if (sizedFields.length) {
1145
- if (!parent.type || !isCompositeType(parent.type)) {
1144
+ if (sizedFields.length && parent.type) {
1145
+ const baseParentType = baseType(parent.type);
1146
+ if (!isCompositeType(baseParentType)) {
1146
1147
  // The output type must have fields
1147
1148
  errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
1148
1149
  `Sized fields cannot be used because "${parent.type}" is not a composite type`,
@@ -1150,11 +1151,11 @@ function validateSizedFieldsAreValidLists(
1150
1151
  ));
1151
1152
  } else {
1152
1153
  for (const sizedFieldName of sizedFields) {
1153
- const sizedField = parent.type.field(sizedFieldName);
1154
+ const sizedField = baseParentType.field(sizedFieldName);
1154
1155
  if (!sizedField) {
1155
1156
  // Sized fields must be present on the output type
1156
1157
  errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err(
1157
- `Sized field "${sizedFieldName}" is not a field on type "${parent.type.coordinate}"`,
1158
+ `Sized field "${sizedFieldName}" is not a field on type "${baseParentType.coordinate}"`,
1158
1159
  { nodes: sourceASTs(application, parent) }
1159
1160
  ));
1160
1161
  } else if (!sizedField.type || !(isListType(sizedField.type) || isNonNullListType(sizedField.type))) {
@@ -1846,6 +1847,9 @@ export class FederationBlueprint extends SchemaBlueprint {
1846
1847
  validateSizedFieldsAreValidLists(application, parent, errorCollector);
1847
1848
  }
1848
1849
 
1850
+ // Validate @authenticated, @requireScopes and @policy usage on interfaces and interface objects
1851
+ validateNoAuthenticationOnInterfaces(metadata, errorCollector);
1852
+
1849
1853
  return errorCollector;
1850
1854
  }
1851
1855
 
@@ -2897,3 +2901,39 @@ function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet
2897
2901
  return undefined;
2898
2902
  });
2899
2903
  }
2904
+
2905
+ function validateNoAuthenticationOnInterfaces(metadata: FederationMetadata, errorCollector: GraphQLError[]) {
2906
+ const authenticatedDirective = metadata.authenticatedDirective();
2907
+ const requiresScopesDirective = metadata.requiresScopesDirective();
2908
+ const policyDirective = metadata.policyDirective();
2909
+ [authenticatedDirective, requiresScopesDirective, policyDirective].forEach((directive) => {
2910
+ for (const application of directive.applications()) {
2911
+ const element: SchemaElement<any, any> = application.parent;
2912
+ if (
2913
+ // Is it applied on interface or interface object types?
2914
+ (isElementNamedType(element) &&
2915
+ (isInterfaceType(element) || isInterfaceObjectType(element))
2916
+ ) ||
2917
+ // Is it applied on interface fields?
2918
+ (isFieldDefinition(element) && isInterfaceType(element.parent))
2919
+ ) {
2920
+ let kind = '';
2921
+ switch (element.kind) {
2922
+ case 'FieldDefinition':
2923
+ kind = 'field';
2924
+ break;
2925
+ case 'InterfaceType':
2926
+ kind = 'interface';
2927
+ break;
2928
+ case 'ObjectType':
2929
+ kind = 'interface object';
2930
+ break;
2931
+ }
2932
+ errorCollector.push(ERRORS.AUTH_REQUIREMENTS_APPLIED_ON_INTERFACE.err(
2933
+ `Invalid use of @${directive.name} on ${kind} "${element.coordinate}": @${directive.name} cannot be applied on interfaces, interface fields and interface objects`,
2934
+ {nodes: sourceASTs(application, element.parent)},
2935
+ ));
2936
+ }
2937
+ }
2938
+ });
2939
+ }
@@ -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";
@@ -23,6 +24,10 @@ export class AuthenticatedSpecDefinition extends FeatureDefinition {
23
24
  minimumFederationVersion,
24
25
  );
25
26
 
27
+ // WARNING: we cannot declare staticArgumentTransform() as access control merge logic needs to propagate
28
+ // requirements upwards/downwards between types and interfaces. We hijack the merge process by providing
29
+ // implementations/interfaces as "additional sources". This means that we cannot apply staticArgumentTransform()
30
+ // as subgraph index index will be wrong/undefined.
26
31
  this.registerDirective(createDirectiveSpecification({
27
32
  name: AuthenticatedSpecDefinition.directiveName,
28
33
  locations: [
@@ -37,6 +42,10 @@ export class AuthenticatedSpecDefinition extends FeatureDefinition {
37
42
  }));
38
43
  }
39
44
 
45
+ authenticatedDirective(schema: Schema): DirectiveDefinition | undefined {
46
+ return this.directive(schema, AuthenticatedSpecDefinition.directiveName);
47
+ }
48
+
40
49
  get defaultCorePurpose(): CorePurpose {
41
50
  return 'SECURITY';
42
51
  }
@@ -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";
@@ -31,6 +31,10 @@ export class PolicySpecDefinition extends FeatureDefinition {
31
31
 
32
32
  this.registerType(createScalarTypeSpecification({ name: PolicyTypeName.POLICY }));
33
33
 
34
+ // WARNING: we cannot declare staticArgumentTransform() as access control merge logic needs to propagate
35
+ // requirements upwards/downwards between types and interfaces. We hijack the merge process by providing
36
+ // implementations/interfaces as "additional sources". This means that we cannot apply staticArgumentTransform()
37
+ // as subgraph index index will be wrong/undefined.
34
38
  this.registerDirective(createDirectiveSpecification({
35
39
  name: PolicySpecDefinition.directiveName,
36
40
  args: [{
@@ -42,7 +46,7 @@ export class PolicySpecDefinition extends FeatureDefinition {
42
46
  assert(PolicyType, () => `Expected "${policyName}" to be defined`);
43
47
  return new NonNullType(new ListType(new NonNullType(new ListType(new NonNullType(PolicyType)))));
44
48
  },
45
- compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.UNION,
49
+ compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.DNF_CONJUNCTION,
46
50
  }],
47
51
  locations: [
48
52
  DirectiveLocation.FIELD_DEFINITION,
@@ -56,6 +60,10 @@ export class PolicySpecDefinition extends FeatureDefinition {
56
60
  }));
57
61
  }
58
62
 
63
+ policyDirective(schema: Schema): DirectiveDefinition<{policies: string[][]}> | undefined {
64
+ return this.directive(schema, PolicySpecDefinition.directiveName);
65
+ }
66
+
59
67
  get defaultCorePurpose(): CorePurpose {
60
68
  return 'SECURITY';
61
69
  }
@@ -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";
@@ -32,6 +32,10 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition {
32
32
 
33
33
  this.registerType(createScalarTypeSpecification({ name: RequiresScopesTypeName.SCOPE }));
34
34
 
35
+ // WARNING: we cannot declare staticArgumentTransform() as access control merge logic needs to propagate
36
+ // requirements upwards/downwards between types and interfaces. We hijack the merge process by providing
37
+ // implementations/interfaces as "additional sources". This means that we cannot apply staticArgumentTransform()
38
+ // as subgraph index index will be wrong/undefined.
35
39
  this.registerDirective(createDirectiveSpecification({
36
40
  name: RequiresScopesSpecDefinition.directiveName,
37
41
  args: [{
@@ -43,7 +47,7 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition {
43
47
  assert(scopeType, () => `Expected "${scopeName}" to be defined`);
44
48
  return new NonNullType(new ListType(new NonNullType(new ListType(new NonNullType(scopeType)))));
45
49
  },
46
- compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.UNION,
50
+ compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.DNF_CONJUNCTION,
47
51
  }],
48
52
  locations: [
49
53
  DirectiveLocation.FIELD_DEFINITION,
@@ -57,6 +61,10 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition {
57
61
  }));
58
62
  }
59
63
 
64
+ requiresScopesDirective(schema: Schema): DirectiveDefinition<{scopes: string[][]}> | undefined {
65
+ return this.directive(schema, RequiresScopesSpecDefinition.directiveName);
66
+ }
67
+
60
68
  get defaultCorePurpose(): CorePurpose {
61
69
  return 'SECURITY';
62
70
  }