@apollo/federation-internals 2.12.0-preview.3 → 2.12.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.
Files changed (44) hide show
  1. package/dist/argumentCompositionStrategies.d.ts +6 -0
  2. package/dist/argumentCompositionStrategies.d.ts.map +1 -1
  3. package/dist/argumentCompositionStrategies.js +77 -0
  4. package/dist/argumentCompositionStrategies.js.map +1 -1
  5. package/dist/buildSchema.d.ts.map +1 -1
  6. package/dist/buildSchema.js +42 -2
  7. package/dist/buildSchema.js.map +1 -1
  8. package/dist/directiveAndTypeSpecification.d.ts +8 -3
  9. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  10. package/dist/directiveAndTypeSpecification.js +2 -1
  11. package/dist/directiveAndTypeSpecification.js.map +1 -1
  12. package/dist/error.d.ts +2 -0
  13. package/dist/error.d.ts.map +1 -1
  14. package/dist/error.js +4 -0
  15. package/dist/error.js.map +1 -1
  16. package/dist/federation.d.ts.map +1 -1
  17. package/dist/federation.js +37 -8
  18. package/dist/federation.js.map +1 -1
  19. package/dist/specs/authenticatedSpec.d.ts +2 -0
  20. package/dist/specs/authenticatedSpec.d.ts.map +1 -1
  21. package/dist/specs/authenticatedSpec.js +3 -0
  22. package/dist/specs/authenticatedSpec.js.map +1 -1
  23. package/dist/specs/connectSpec.d.ts +0 -3
  24. package/dist/specs/connectSpec.d.ts.map +1 -1
  25. package/dist/specs/connectSpec.js +240 -64
  26. package/dist/specs/connectSpec.js.map +1 -1
  27. package/dist/specs/policySpec.d.ts +4 -0
  28. package/dist/specs/policySpec.d.ts.map +1 -1
  29. package/dist/specs/policySpec.js +4 -1
  30. package/dist/specs/policySpec.js.map +1 -1
  31. package/dist/specs/requiresScopesSpec.d.ts +4 -0
  32. package/dist/specs/requiresScopesSpec.d.ts.map +1 -1
  33. package/dist/specs/requiresScopesSpec.js +4 -1
  34. package/dist/specs/requiresScopesSpec.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/argumentCompositionStrategies.ts +114 -2
  37. package/src/buildSchema.ts +51 -0
  38. package/src/directiveAndTypeSpecification.ts +8 -2
  39. package/src/error.ts +14 -0
  40. package/src/federation.ts +45 -8
  41. package/src/specs/authenticatedSpec.ts +5 -0
  42. package/src/specs/connectSpec.ts +380 -126
  43. package/src/specs/policySpec.ts +6 -2
  44. package/src/specs/requiresScopesSpec.ts +6 -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,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
  }
@@ -56,6 +56,9 @@ import {
56
56
  } from "./definitions";
57
57
  import { ERRORS, errorCauses, withModifiedErrorNodes } from "./error";
58
58
  import { introspectionTypeNames } from "./introspection";
59
+ import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures";
60
+ import { connectIdentity } from "./specs/connectSpec";
61
+
59
62
 
60
63
  function buildValue(value?: ValueNode): any {
61
64
  return value ? valueFromASTUntyped(value) : undefined;
@@ -143,6 +146,48 @@ export function buildSchemaFromAST(
143
146
  buildSchemaDefinitionInner(schemaExtension, schema.schemaDefinition, errors, schema.schemaDefinition.newExtension());
144
147
  }
145
148
 
149
+ // The following block of code is a one-off to support input objects in the
150
+ // connect spec. It will be non-maintainable/bug-prone to do this again, and
151
+ // has various limitations/unsupported edge cases already.
152
+ //
153
+ // There's work to be done to support input objects more generally; please see
154
+ // https://github.com/apollographql/federation/pull/3311 for more information.
155
+ const connectFeature = schema.coreFeatures?.getByIdentity(connectIdentity);
156
+ const handledConnectTypeNames = new Set<string>();
157
+ if (connectFeature) {
158
+ const connectFeatureDefinition =
159
+ coreFeatureDefinitionIfKnown(connectFeature.url);
160
+ if (connectFeatureDefinition) {
161
+ const connectTypeNamesInSchema = new Set(
162
+ connectFeatureDefinition.typeSpecs()
163
+ .map(({ name }) => connectFeature.typeNameInSchema(name))
164
+ );
165
+ for (const typeNode of typeDefinitions) {
166
+ if (connectTypeNamesInSchema.has(typeNode.name.value)
167
+ && typeNode.kind === 'InputObjectTypeDefinition'
168
+ ) {
169
+ handledConnectTypeNames.add(typeNode.name.value)
170
+ } else {
171
+ continue;
172
+ }
173
+ buildNamedTypeInner(typeNode, schema.type(typeNode.name.value)!, schema.blueprint, errors);
174
+ }
175
+ for (const typeExtensionNode of typeExtensions) {
176
+ if (connectTypeNamesInSchema.has(typeExtensionNode.name.value)
177
+ && typeExtensionNode.kind === 'InputObjectTypeExtension'
178
+ ) {
179
+ handledConnectTypeNames.add(typeExtensionNode.name.value)
180
+ } else {
181
+ continue;
182
+ }
183
+ const toExtend = schema.type(typeExtensionNode.name.value)!;
184
+ const extension = toExtend.newExtension();
185
+ extension.sourceAST = typeExtensionNode;
186
+ buildNamedTypeInner(typeExtensionNode, toExtend, schema.blueprint, errors, extension);
187
+ }
188
+ }
189
+ }
190
+
146
191
  // The following is a no-op for "standard" schema, but for federation subgraphs, this is where we handle the auto-addition
147
192
  // of imported federation directive definitions. That is why we have avoid looking at directive applications within
148
193
  // directive definition earlier: if one of those application was of an imported federation directive, the definition
@@ -155,9 +200,15 @@ export function buildSchemaFromAST(
155
200
  }
156
201
 
157
202
  for (const typeNode of typeDefinitions) {
203
+ if (handledConnectTypeNames.has(typeNode.name.value)) {
204
+ continue;
205
+ }
158
206
  buildNamedTypeInner(typeNode, schema.type(typeNode.name.value)!, schema.blueprint, errors);
159
207
  }
160
208
  for (const typeExtensionNode of typeExtensions) {
209
+ if (handledConnectTypeNames.has(typeExtensionNode.name.value)) {
210
+ continue;
211
+ }
161
212
  const toExtend = schema.type(typeExtensionNode.name.value)!;
162
213
  const extension = toExtend.newExtension();
163
214
  extension.sourceAST = typeExtensionNode;
@@ -67,7 +67,13 @@ export type FieldSpecification = {
67
67
  args?: ResolvedArgumentSpecification[],
68
68
  }
69
69
 
70
- type ResolvedArgumentSpecification = {
70
+ export type ResolvedArgumentSpecification = {
71
+ name: string,
72
+ type: InputType,
73
+ defaultValue?: any,
74
+ }
75
+
76
+ export type InputFieldSpecification = {
71
77
  name: string,
72
78
  type: InputType,
73
79
  defaultValue?: any,
@@ -342,7 +348,7 @@ export function createEnumTypeSpecification({
342
348
  }
343
349
  }
344
350
 
345
- function ensureSameTypeKind(expected: NamedType['kind'], actual: NamedType): GraphQLError[] {
351
+ export function ensureSameTypeKind(expected: NamedType['kind'], actual: NamedType): GraphQLError[] {
346
352
  return expected === actual.kind
347
353
  ? []
348
354
  : [
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 object or their fields.',
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();
@@ -1828,18 +1828,16 @@ export class FederationBlueprint extends SchemaBlueprint {
1828
1828
  }
1829
1829
  }
1830
1830
 
1831
- const costFeature = schema.coreFeatures?.getByIdentity(costIdentity);
1832
- const costSpec = costFeature && COST_VERSIONS.find(costFeature.url.version);
1833
- const costDirective = costSpec?.costDirective(schema);
1834
- const listSizeDirective = costSpec?.listSizeDirective(schema);
1831
+ const costDirective = metadata.costDirective();
1832
+ const listSizeDirective = metadata.listSizeDirective();
1835
1833
 
1836
1834
  // Validate @cost
1837
- for (const application of costDirective?.applications() ?? []) {
1835
+ for (const application of costDirective.applications()) {
1838
1836
  validateCostNotAppliedToInterface(application, errorCollector);
1839
1837
  }
1840
1838
 
1841
1839
  // Validate @listSize
1842
- for (const application of listSizeDirective?.applications() ?? []) {
1840
+ for (const application of listSizeDirective.applications()) {
1843
1841
  const parent = application.parent;
1844
1842
  assert(parent instanceof FieldDefinition, "@listSize can only be applied to FIELD_DEFINITION");
1845
1843
  validateListSizeAppliedToList(application, parent, errorCollector);
@@ -1848,6 +1846,9 @@ export class FederationBlueprint extends SchemaBlueprint {
1848
1846
  validateSizedFieldsAreValidLists(application, parent, errorCollector);
1849
1847
  }
1850
1848
 
1849
+ // Validate @authenticated, @requireScopes and @policy
1850
+ validateNoAuthenticationOnInterfaces(metadata, errorCollector);
1851
+
1851
1852
  return errorCollector;
1852
1853
  }
1853
1854
 
@@ -2899,3 +2900,39 @@ function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet
2899
2900
  return undefined;
2900
2901
  });
2901
2902
  }
2903
+
2904
+ function validateNoAuthenticationOnInterfaces(metadata: FederationMetadata, errorCollector: GraphQLError[]) {
2905
+ const authenticatedDirective = metadata.authenticatedDirective();
2906
+ const requiresScopesDirective = metadata.requiresScopesDirective();
2907
+ const policyDirective = metadata.policyDirective();
2908
+ [authenticatedDirective, requiresScopesDirective, policyDirective].forEach((directive) => {
2909
+ for (const application of directive.applications()) {
2910
+ const element = application.parent;
2911
+ function isAppliedOnInterface(type: Type) {
2912
+ return isInterfaceType(type) || isInterfaceObjectType(baseType(type));
2913
+ }
2914
+ function isAppliedOnInterfaceField(elem: SchemaElement<any, any>) {
2915
+ return isFieldDefinition(elem) && isAppliedOnInterface(elem.parent);
2916
+ }
2917
+
2918
+ if (isAppliedOnInterface(element) || isAppliedOnInterfaceField(element)) {
2919
+ let kind = '';
2920
+ switch (element.kind) {
2921
+ case 'FieldDefinition':
2922
+ kind = 'field';
2923
+ break;
2924
+ case 'InterfaceType':
2925
+ kind = 'interface';
2926
+ break;
2927
+ case 'ObjectType':
2928
+ kind = 'interface object';
2929
+ break;
2930
+ }
2931
+ errorCollector.push(ERRORS.AUTHENTICATION_APPLIED_ON_INTERFACE.err(
2932
+ `Invalid use of @${directive.name} on ${kind} "${element.coordinate}": @${directive.name} cannot be applied on interfaces, interface objects or their fields`,
2933
+ {nodes: sourceASTs(application, element.parent)},
2934
+ ));
2935
+ }
2936
+ }
2937
+ });
2938
+ }
@@ -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
  }