@apollo/federation-internals 2.4.1 → 2.4.2

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 (62) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/argumentCompositionStrategies.d.ts +34 -0
  3. package/dist/argumentCompositionStrategies.d.ts.map +1 -0
  4. package/dist/argumentCompositionStrategies.js +35 -0
  5. package/dist/argumentCompositionStrategies.js.map +1 -0
  6. package/dist/coreSpec.d.ts +12 -3
  7. package/dist/coreSpec.d.ts.map +1 -1
  8. package/dist/coreSpec.js +68 -17
  9. package/dist/coreSpec.js.map +1 -1
  10. package/dist/definitions.d.ts +1 -0
  11. package/dist/definitions.d.ts.map +1 -1
  12. package/dist/definitions.js +30 -27
  13. package/dist/definitions.js.map +1 -1
  14. package/dist/directiveAndTypeSpecification.d.ts +26 -7
  15. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  16. package/dist/directiveAndTypeSpecification.js +56 -4
  17. package/dist/directiveAndTypeSpecification.js.map +1 -1
  18. package/dist/federation.d.ts.map +1 -1
  19. package/dist/federation.js +24 -2
  20. package/dist/federation.js.map +1 -1
  21. package/dist/federationSpec.d.ts +2 -13
  22. package/dist/federationSpec.d.ts.map +1 -1
  23. package/dist/federationSpec.js +10 -60
  24. package/dist/federationSpec.js.map +1 -1
  25. package/dist/inaccessibleSpec.d.ts +0 -2
  26. package/dist/inaccessibleSpec.d.ts.map +1 -1
  27. package/dist/inaccessibleSpec.js +3 -6
  28. package/dist/inaccessibleSpec.js.map +1 -1
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/knownCoreFeatures.d.ts +1 -0
  34. package/dist/knownCoreFeatures.d.ts.map +1 -1
  35. package/dist/knownCoreFeatures.js +5 -1
  36. package/dist/knownCoreFeatures.js.map +1 -1
  37. package/dist/operations.d.ts.map +1 -1
  38. package/dist/operations.js +10 -2
  39. package/dist/operations.js.map +1 -1
  40. package/dist/print.d.ts +7 -1
  41. package/dist/print.d.ts.map +1 -1
  42. package/dist/print.js +33 -5
  43. package/dist/print.js.map +1 -1
  44. package/dist/tagSpec.d.ts +0 -2
  45. package/dist/tagSpec.d.ts.map +1 -1
  46. package/dist/tagSpec.js +4 -10
  47. package/dist/tagSpec.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/__tests__/directiveAndTypeSpecifications.test.ts +41 -0
  50. package/src/argumentCompositionStrategies.ts +39 -0
  51. package/src/coreSpec.ts +94 -34
  52. package/src/definitions.ts +35 -29
  53. package/src/directiveAndTypeSpecification.ts +101 -14
  54. package/src/federation.ts +33 -4
  55. package/src/federationSpec.ts +13 -73
  56. package/src/inaccessibleSpec.ts +4 -11
  57. package/src/index.ts +3 -0
  58. package/src/knownCoreFeatures.ts +9 -0
  59. package/src/operations.ts +14 -4
  60. package/src/print.ts +39 -4
  61. package/src/tagSpec.ts +4 -12
  62. package/tsconfig.tsbuildinfo +1 -1
@@ -21,10 +21,23 @@ import { ERRORS } from "./error";
21
21
  import { valueEquals, valueToString } from "./values";
22
22
  import { sameType } from "./types";
23
23
  import { arrayEquals, assert } from "./utils";
24
+ import { ArgumentCompositionStrategy } from "./argumentCompositionStrategies";
25
+ import { FeatureDefinition } from "./coreSpec";
24
26
 
25
27
  export type DirectiveSpecification = {
26
28
  name: string,
27
29
  checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => GraphQLError[],
30
+ composition?: DirectiveCompositionSpecification,
31
+ }
32
+
33
+ export type DirectiveCompositionSpecification = {
34
+ supergraphSpecification: () => FeatureDefinition,
35
+ argumentsMerger?: (schema: Schema) => ArgumentMerger | GraphQLError,
36
+ }
37
+
38
+ export type ArgumentMerger = {
39
+ merge: (argName: string, values: any[]) => any,
40
+ toString: () => string,
28
41
  }
29
42
 
30
43
  export type TypeSpecification = {
@@ -34,43 +47,117 @@ export type TypeSpecification = {
34
47
 
35
48
  export type ArgumentSpecification = {
36
49
  name: string,
37
- type: InputType,
50
+ type: (schema: Schema, nameInSchema?: string) => InputType | GraphQLError[],
38
51
  defaultValue?: any,
39
52
  }
40
53
 
54
+ export type DirectiveArgumentSpecification = ArgumentSpecification & {
55
+ compositionStrategy?: ArgumentCompositionStrategy,
56
+ }
57
+
41
58
  export type FieldSpecification = {
42
59
  name: string,
43
60
  type: OutputType,
44
- args?: ArgumentSpecification[],
45
- };
61
+ args?: ResolvedArgumentSpecification[],
62
+ }
63
+
64
+ type ResolvedArgumentSpecification = {
65
+ name: string,
66
+ type: InputType,
67
+ defaultValue?: any,
68
+ }
46
69
 
47
70
  export function createDirectiveSpecification({
48
71
  name,
49
72
  locations,
50
73
  repeatable = false,
51
- argumentFct = undefined,
74
+ args = [],
75
+ composes = false,
76
+ supergraphSpecification = undefined,
52
77
  }: {
53
78
  name: string,
54
79
  locations: DirectiveLocation[],
55
80
  repeatable?: boolean,
56
- argumentFct?: (schema: Schema, nameInSchema?: string) => { args: ArgumentSpecification[], errors: GraphQLError[] },
81
+ args?: DirectiveArgumentSpecification[],
82
+ composes?: boolean,
83
+ supergraphSpecification?: () => FeatureDefinition,
57
84
  }): DirectiveSpecification {
85
+ let composition: DirectiveCompositionSpecification | undefined = undefined;
86
+ if (composes) {
87
+ assert(supergraphSpecification, `Should provide a @link specification to use in supergraph for directive @${name} if it composes`);
88
+ const argStrategies = new Map(args.filter((arg) => arg.compositionStrategy).map((arg) => [arg.name, arg.compositionStrategy!]));
89
+ let argumentsMerger: ((schema: Schema) => ArgumentMerger | GraphQLError) | undefined = undefined;
90
+ if (argStrategies.size > 0) {
91
+ assert(!repeatable, () => `Invalid directive specification for @${name}: @${name} is repeatable and should not define composition strategy for its arguments`);
92
+ assert(argStrategies.size === args.length, () => `Invalid directive specification for @${name}: not all arguments define a composition strategy`);
93
+ argumentsMerger = (schema) => {
94
+ // Validate that the arguments have compatible types with the declared strategies (a bit unfortunate that we can't do this until
95
+ // we have a schema but well, not a huge deal either).
96
+ for (const { name: argName, type } of args) {
97
+ const strategy = argStrategies.get(argName);
98
+ // Note that we've built `argStrategies` from the declared args and checked that all argument had a strategy, so it would be
99
+ // a bug in the code if we didn't get a strategy (not an issue in the directive declaration).
100
+ assert(strategy, () => `Should have a strategy for ${argName}`);
101
+ const argType = type(schema);
102
+ // By the time we call this, the directive should have been added to the schema and so getting the type should not raise errors.
103
+ assert(!Array.isArray(argType), () => `Should have gotten error getting type for @${name}(${argName}:), but got ${argType}`)
104
+ const strategyTypes = strategy.supportedTypes(schema);
105
+ if (!strategyTypes.some((t) => sameType(t, argType))) {
106
+ return new GraphQLError(
107
+ `Invalid composition strategy ${strategy.name} for argument @${name}(${argName}:) of type ${argType}; `
108
+ + `${strategy.name} only supports type(s) ${strategyTypes.join(', ')}`
109
+ );
110
+ }
111
+ };
112
+ return {
113
+ merge: (argName, values) => {
114
+ const strategy = argStrategies.get(argName);
115
+ assert(strategy, () => `Should have a strategy for ${argName}`);
116
+ return strategy.mergeValues(values);
117
+ },
118
+ toString: () => {
119
+ if (argStrategies.size === 0) {
120
+ return "<none>";
121
+ }
122
+ return '{ ' + [...argStrategies.entries()].map(([arg, strategy]) => `"${arg}": ${strategy.name}`).join(', ') + ' }';
123
+ }
124
+ };
125
+ }
126
+ }
127
+ composition = {
128
+ supergraphSpecification,
129
+ argumentsMerger,
130
+ };
131
+ }
132
+
58
133
  return {
59
134
  name,
135
+ composition,
60
136
  checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => {
61
137
  const actualName = nameInSchema ?? name;
62
- const {args, errors} = argumentFct ? argumentFct(schema, actualName) : { args: [], errors: []};
138
+ const { resolvedArgs, errors } = args.reduce<{ resolvedArgs: (ResolvedArgumentSpecification & { compositionStrategy?: ArgumentCompositionStrategy })[], errors: GraphQLError[] }>(
139
+ ({ resolvedArgs, errors }, arg) => {
140
+ const typeOrErrors = arg.type(schema, actualName);
141
+ if (Array.isArray(typeOrErrors)) {
142
+ errors.push(...typeOrErrors);
143
+ } else {
144
+ resolvedArgs.push({ ...arg, type: typeOrErrors });
145
+ }
146
+ return { resolvedArgs, errors };
147
+ },
148
+ { resolvedArgs: [], errors: [] }
149
+ );
63
150
  if (errors.length > 0) {
64
151
  return errors;
65
152
  }
66
153
  const existing = schema.directive(actualName);
67
154
  if (existing) {
68
- return ensureSameDirectiveStructure({name: actualName, locations, repeatable, args}, existing);
155
+ return ensureSameDirectiveStructure({ name: actualName, locations, repeatable, args: resolvedArgs }, existing);
69
156
  } else {
70
157
  const directive = schema.addDirectiveDefinition(new DirectiveDefinition(actualName, asBuiltIn));
71
158
  directive.repeatable = repeatable;
72
159
  directive.addLocations(...locations);
73
- for (const { name, type, defaultValue } of args) {
160
+ for (const { name, type, defaultValue } of resolvedArgs) {
74
161
  directive.addArgument(name, type, defaultValue);
75
162
  }
76
163
  return [];
@@ -95,7 +182,7 @@ export function createScalarTypeSpecification({ name }: { name: string }): TypeS
95
182
  }
96
183
  }
97
184
 
98
- export function createObjectTypeSpecification({
185
+ export function createObjectTypeSpecification({
99
186
  name,
100
187
  fieldsFct,
101
188
  }: {
@@ -156,7 +243,7 @@ export function createObjectTypeSpecification({
156
243
  }
157
244
  }
158
245
 
159
- export function createUnionTypeSpecification({
246
+ export function createUnionTypeSpecification({
160
247
  name,
161
248
  membersFct,
162
249
  }: {
@@ -210,7 +297,7 @@ export function createEnumTypeSpecification({
210
297
  values,
211
298
  }: {
212
299
  name: string,
213
- values: { name: string, description?: string}[],
300
+ values: { name: string, description?: string }[],
214
301
  }): TypeSpecification {
215
302
  return {
216
303
  name,
@@ -234,7 +321,7 @@ export function createEnumTypeSpecification({
234
321
  return errors;
235
322
  } else {
236
323
  const type = schema.addType(new EnumType(actualName, asBuiltIn));
237
- for (const {name, description} of values) {
324
+ for (const { name, description } of values) {
238
325
  type.addValue(name).description = description;
239
326
  }
240
327
  return [];
@@ -259,7 +346,7 @@ function ensureSameDirectiveStructure(
259
346
  name: string,
260
347
  locations: DirectiveLocation[],
261
348
  repeatable: boolean,
262
- args: ArgumentSpecification[]
349
+ args: ResolvedArgumentSpecification[]
263
350
  },
264
351
  actual: DirectiveDefinition<any>,
265
352
  ): GraphQLError[] {
@@ -285,7 +372,7 @@ function ensureSameDirectiveStructure(
285
372
  function ensureSameArguments(
286
373
  expected: {
287
374
  name: string,
288
- args?: ArgumentSpecification[]
375
+ args?: ResolvedArgumentSpecification[]
289
376
  },
290
377
  actual: { argument(name: string): ArgumentDefinition<any> | undefined, arguments(): readonly ArgumentDefinition<any>[] },
291
378
  what: string,
package/src/federation.ts CHANGED
@@ -80,6 +80,8 @@ import {
80
80
  import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
81
81
  import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
82
82
  import { didYouMean, suggestionList } from "./suggestions";
83
+ import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures";
84
+ import { joinIdentity } from "./joinSpec";
83
85
 
84
86
  const linkSpec = LINK_VERSIONS.latest();
85
87
  const tagSpec = TAG_VERSIONS.latest();
@@ -110,7 +112,6 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
110
112
  const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
111
113
 
112
114
  const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES: string[] = Object.values(FederationDirectiveName);
113
-
114
115
  function validateFieldSetSelections({
115
116
  directiveName,
116
117
  selectionSet,
@@ -1342,12 +1343,14 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1342
1343
  }
1343
1344
  }
1344
1345
 
1345
- return FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
1346
+ const errors = FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
1346
1347
  .concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
1347
1348
  .flat();
1349
+
1350
+ return errors.length === 0 ? expandKnownFeatures(schema) : errors;
1348
1351
  }
1349
1352
 
1350
- function completeFed2SubgraphSchema(schema: Schema) {
1353
+ function completeFed2SubgraphSchema(schema: Schema): GraphQLError[] {
1351
1354
  const coreFeatures = schema.coreFeatures;
1352
1355
  assert(coreFeatures, 'This method should not have been called on a non-core schema');
1353
1356
 
@@ -1362,7 +1365,33 @@ function completeFed2SubgraphSchema(schema: Schema) {
1362
1365
  )];
1363
1366
  }
1364
1367
 
1365
- return spec.addElementsToSchema(schema);
1368
+ const errors = spec.addElementsToSchema(schema);
1369
+ return errors.length === 0 ? expandKnownFeatures(schema) : errors;
1370
+ }
1371
+
1372
+ function expandKnownFeatures(schema: Schema): GraphQLError[] {
1373
+ const coreFeatures = schema.coreFeatures;
1374
+ if (!coreFeatures) {
1375
+ return [];
1376
+ }
1377
+
1378
+ let errors: GraphQLError[] = [];
1379
+ for (const feature of coreFeatures.allFeatures()) {
1380
+ // We should already have dealt with the core/link spec and federation at this point. Also, we shouldn't have the `join` spec in subgraphs,
1381
+ // but some tests play with the idea and currently the join spec is implemented in a way that is not idempotent (it doesn't use
1382
+ // `DirectiveSpecification.checkAndAdd`; we should clean it up at some point, but not exactly urgent).
1383
+ if (feature === coreFeatures.coreItself || feature.url.identity === federationIdentity || feature.url.identity === joinIdentity) {
1384
+ continue;
1385
+ }
1386
+
1387
+ const spec = coreFeatureDefinitionIfKnown(feature.url);
1388
+ if (!spec) {
1389
+ continue;
1390
+ }
1391
+
1392
+ errors = errors.concat(spec.addElementsToSchema(schema));
1393
+ }
1394
+ return errors;
1366
1395
  }
1367
1396
 
1368
1397
  export function parseFieldSetArgument({
@@ -8,11 +8,9 @@ import {
8
8
  ArgumentSpecification,
9
9
  createDirectiveSpecification,
10
10
  createScalarTypeSpecification,
11
- DirectiveSpecification,
12
- TypeSpecification,
13
11
  } from "./directiveAndTypeSpecification";
14
- import { DirectiveLocation, GraphQLError } from "graphql";
15
- import { assert, MapWithCachedArrays } from "./utils";
12
+ import { DirectiveLocation } from "graphql";
13
+ import { assert } from "./utils";
16
14
  import { TAG_VERSIONS } from "./tagSpec";
17
15
  import { federationMetadata } from "./federation";
18
16
  import { registerKnownFeature } from "./knownCoreFeatures";
@@ -40,17 +38,16 @@ export enum FederationDirectiveName {
40
38
 
41
39
  const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
42
40
 
41
+ const fieldsArgument: ArgumentSpecification = { name: 'fields', type: (schema) => fieldSetType(schema) };
42
+
43
43
  const keyDirectiveSpec = createDirectiveSpecification({
44
44
  name: FederationDirectiveName.KEY,
45
45
  locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
46
46
  repeatable: true,
47
- argumentFct: (schema) => ({
48
- args: [
49
- fieldsArgument(schema),
50
- { name: 'resolvable', type: schema.booleanType(), defaultValue: true },
51
- ],
52
- errors: [],
53
- }),
47
+ args: [
48
+ fieldsArgument,
49
+ { name: 'resolvable', type: (schema) => schema.booleanType(), defaultValue: true },
50
+ ]
54
51
  });
55
52
 
56
53
  const extendsDirectiveSpec = createDirectiveSpecification({
@@ -61,28 +58,19 @@ const extendsDirectiveSpec = createDirectiveSpecification({
61
58
  const externalDirectiveSpec = createDirectiveSpecification({
62
59
  name: FederationDirectiveName.EXTERNAL,
63
60
  locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
64
- argumentFct: (schema) => ({
65
- args: [{ name: 'reason', type: schema.stringType() }],
66
- errors: [],
67
- }),
61
+ args: [{ name: 'reason', type: (schema) => schema.stringType() }],
68
62
  });
69
63
 
70
64
  const requiresDirectiveSpec = createDirectiveSpecification({
71
65
  name: FederationDirectiveName.REQUIRES,
72
66
  locations: [DirectiveLocation.FIELD_DEFINITION],
73
- argumentFct: (schema) => ({
74
- args: [fieldsArgument(schema)],
75
- errors: [],
76
- }),
67
+ args: [fieldsArgument],
77
68
  });
78
69
 
79
70
  const providesDirectiveSpec = createDirectiveSpecification({
80
71
  name: FederationDirectiveName.PROVIDES,
81
72
  locations: [DirectiveLocation.FIELD_DEFINITION],
82
- argumentFct: (schema) => ({
83
- args: [fieldsArgument(schema)],
84
- errors: [],
85
- }),
73
+ args: [fieldsArgument],
86
74
  });
87
75
 
88
76
  const legacyFederationTypes = [
@@ -103,9 +91,6 @@ const legacyFederationDirectives = [
103
91
  export const FEDERATION1_TYPES = legacyFederationTypes;
104
92
  export const FEDERATION1_DIRECTIVES = legacyFederationDirectives;
105
93
 
106
- function fieldsArgument(schema: Schema): ArgumentSpecification {
107
- return { name: 'fields', type: fieldSetType(schema) };
108
- }
109
94
 
110
95
  function fieldSetType(schema: Schema): InputType {
111
96
  const metadata = federationMetadata(schema);
@@ -114,9 +99,6 @@ function fieldSetType(schema: Schema): InputType {
114
99
  }
115
100
 
116
101
  export class FederationSpecDefinition extends FeatureDefinition {
117
- private readonly _directiveSpecs = new MapWithCachedArrays<string, DirectiveSpecification>();
118
- private readonly _typeSpecs = new MapWithCachedArrays<string, TypeSpecification>();
119
-
120
102
  constructor(version: FeatureVersion) {
121
103
  super(new FeatureUrl(federationIdentity, 'federation', version));
122
104
 
@@ -139,10 +121,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
139
121
  this.registerDirective(createDirectiveSpecification({
140
122
  name: FederationDirectiveName.OVERRIDE,
141
123
  locations: [DirectiveLocation.FIELD_DEFINITION],
142
- argumentFct: (schema) => ({
143
- args: [{ name: 'from', type: new NonNullType(schema.stringType()) }],
144
- errors: [],
145
- }),
124
+ args: [{ name: 'from', type: (schema) => new NonNullType(schema.stringType()) }],
146
125
  }));
147
126
 
148
127
  if (version >= (new FeatureVersion(2, 1))) {
@@ -150,10 +129,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
150
129
  name: FederationDirectiveName.COMPOSE_DIRECTIVE,
151
130
  locations: [DirectiveLocation.SCHEMA],
152
131
  repeatable: true,
153
- argumentFct: (schema) => ({
154
- args: [{ name: 'name', type: schema.stringType() }],
155
- errors: [],
156
- }),
132
+ args: [{ name: 'name', type: (schema) => schema.stringType() }],
157
133
  }));
158
134
  }
159
135
 
@@ -167,42 +143,6 @@ export class FederationSpecDefinition extends FeatureDefinition {
167
143
  );
168
144
  }
169
145
  }
170
-
171
- private registerDirective(spec: DirectiveSpecification) {
172
- this._directiveSpecs.set(spec.name, spec);
173
- }
174
-
175
- private registerType(spec: TypeSpecification) {
176
- this._typeSpecs.set(spec.name, spec);
177
- }
178
-
179
- directiveSpecs(): readonly DirectiveSpecification[] {
180
- return this._directiveSpecs.values();
181
- }
182
-
183
- typeSpecs(): readonly TypeSpecification[] {
184
- return this._typeSpecs.values();
185
- }
186
-
187
- addElementsToSchema(schema: Schema): GraphQLError[] {
188
- const feature = this.featureInSchema(schema);
189
- assert(feature, 'The federation specification should have been added to the schema before this is called');
190
-
191
- let errors: GraphQLError[] = [];
192
- for (const type of this.typeSpecs()) {
193
- errors = errors.concat(this.addTypeSpec(schema, type));
194
- }
195
-
196
- for (const directive of this.directiveSpecs()) {
197
- errors = errors.concat(this.addDirectiveSpec(schema, directive));
198
- }
199
- return errors;
200
- }
201
-
202
- allElementNames(): string[] {
203
- return this.directiveSpecs().map((spec) => `@${spec.name}`)
204
- .concat(this.typeSpecs().map((spec) => spec.name));
205
- }
206
146
  }
207
147
 
208
148
  export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
@@ -62,17 +62,16 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
62
62
  this.inaccessibleDirectiveSpec = createDirectiveSpecification({
63
63
  name: 'inaccessible',
64
64
  locations: this.inaccessibleLocations,
65
+ composes: true,
66
+ supergraphSpecification: () => INACCESSIBLE_VERSIONS.latest(),
65
67
  });
68
+ this.registerDirective(this.inaccessibleDirectiveSpec);
66
69
  }
67
70
 
68
71
  isV01() {
69
72
  return this.version.equals(new FeatureVersion(0, 1));
70
73
  }
71
74
 
72
- addElementsToSchema(schema: Schema): GraphQLError[] {
73
- return this.addDirectiveSpec(schema, this.inaccessibleDirectiveSpec);
74
- }
75
-
76
75
  inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> | undefined {
77
76
  return this.directive(schema, 'inaccessible');
78
77
  }
@@ -89,10 +88,6 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
89
88
  return undefined;
90
89
  }
91
90
 
92
- allElementNames(): string[] {
93
- return ['@inaccessible'];
94
- }
95
-
96
91
  get defaultCorePurpose(): CorePurpose | undefined {
97
92
  return 'SECURITY';
98
93
  }
@@ -891,9 +886,7 @@ function getInputType(element: SchemaElementWithDefaultValue): InputType {
891
886
  // similar to the "Values of Correct Type" validation in the GraphQL spec.
892
887
  // However, there are two noteable differences:
893
888
  // 1. Variable references are not allowed.
894
- // 2. Scalar values are not required to be coercible (due to machine-specific
895
- // differences in input coercion rules).
896
- //
889
+ // 2. Scalar values are not required to be coercible (due to machine-specific differences in input coercion rules).
897
890
  // As it turns out, building a Schema object validates this (and a bit more)
898
891
  // already, so in the interests of not duplicating validations/keeping the logic
899
892
  // centralized, this code assumes the input values it receives satisfy the above
package/src/index.ts CHANGED
@@ -18,3 +18,6 @@ export * from './error';
18
18
  export * from './schemaUpgrader';
19
19
  export * from './suggestions';
20
20
  export * from './graphQLJSSchemaToAST';
21
+ export * from './directiveAndTypeSpecification';
22
+ export { coreFeatureDefinitionIfKnown } from './knownCoreFeatures';
23
+ export * from './argumentCompositionStrategies';
@@ -11,3 +11,12 @@ export function registerKnownFeature(definitions: FeatureDefinitions) {
11
11
  export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition | undefined {
12
12
  return registeredFeatures.get(url.identity)?.find(url.version);
13
13
  }
14
+
15
+ /**
16
+ * Removes a feature from the set of known features.
17
+ *
18
+ * This exists purely for testing purposes. There is no reason to unregistered features otherwise.
19
+ */
20
+ export function unregisterKnownFeatures(definitions: FeatureDefinitions) {
21
+ registeredFeatures.delete(definitions.identity);
22
+ }
package/src/operations.ts CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  } from "./definitions";
50
50
  import { isInterfaceObjectType } from "./federation";
51
51
  import { ERRORS } from "./error";
52
- import { sameType } from "./types";
52
+ import { isSubtype, sameType } from "./types";
53
53
  import { assert, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
54
54
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
55
55
  import { v1 as uuidv1 } from 'uuid';
@@ -678,12 +678,12 @@ function isUselessFollowupElement(first: OperationElement, followup: OperationEl
678
678
  : first.typeCondition;
679
679
 
680
680
  // The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
681
- // is already that of the first element.
681
+ // is already that of the first element (or a supertype).
682
682
  return !!typeOfFirst
683
683
  && followup.kind === 'FragmentElement'
684
684
  && !!followup.typeCondition
685
685
  && (followup.appliedDirectives.length === 0 || isDirectiveApplicationsSubset(conditionals, followup.appliedDirectives))
686
- && sameType(typeOfFirst, followup.typeCondition);
686
+ && isSubtype(followup.typeCondition, typeOfFirst);
687
687
  }
688
688
 
689
689
  export type RootOperationPath = {
@@ -1598,9 +1598,19 @@ function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, s
1598
1598
  }
1599
1599
  }
1600
1600
 
1601
+ function maybeRebaseOnSchema(toRebase: CompositeType, schema: Schema): CompositeType {
1602
+ if (toRebase.schema() === schema) {
1603
+ return toRebase;
1604
+ }
1605
+
1606
+ const rebased = schema.type(toRebase.name);
1607
+ assert(rebased && isCompositeType(rebased), () => `Expected ${toRebase} to exists and be composite in the rebased schema, but got ${rebased?.kind}`);
1608
+ return rebased;
1609
+ }
1610
+
1601
1611
  function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
1602
1612
  return fragment.element.appliedDirectives.length === 0
1603
- && (!fragment.element.typeCondition || sameType(parentType, fragment.element.typeCondition));
1613
+ && (!fragment.element.typeCondition || isSubtype(maybeRebaseOnSchema(fragment.element.typeCondition, parentType.schema()), parentType));
1604
1614
  }
1605
1615
 
1606
1616
  function withUnecessaryFragmentsRemoved(
package/src/print.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  Directive,
4
4
  DirectiveDefinition,
5
5
  EnumType,
6
+ EnumValue,
6
7
  ExtendableElement,
7
8
  Extension,
8
9
  FieldDefinition,
@@ -18,6 +19,7 @@ import {
18
19
  SchemaDefinition,
19
20
  SchemaElement,
20
21
  SchemaRootKind,
22
+ UnionMember,
21
23
  UnionType
22
24
  } from "./definitions";
23
25
  import { assert } from "./utils";
@@ -28,6 +30,11 @@ export type PrintOptions = {
28
30
  definitionsOrder: ('schema' | 'types' | 'directives')[],
29
31
  rootTypesOrder: SchemaRootKind[],
30
32
  typeCompareFn?: (t1: NamedType, t2: NamedType) => number;
33
+ implementedInterfaceCompareFn?: (t1: InterfaceImplementation<any>, t2: InterfaceImplementation<any>) => number;
34
+ fieldCompareFn?: (t1: FieldDefinition<any>, t2: FieldDefinition<any>) => number;
35
+ unionMemberCompareFn?: (t1: UnionMember, t2: UnionMember) => number;
36
+ enumValueCompareFn?: (t1: EnumValue, t2: EnumValue) => number;
37
+ inputObjectFieldCompareFn?: (t1: InputFieldDefinition, t2: InputFieldDefinition) => number;
31
38
  directiveCompareFn?: (d1: DirectiveDefinition, d2: DirectiveDefinition) => number;
32
39
  mergeTypesAndExtensions: boolean;
33
40
  showAllBuiltIns: boolean;
@@ -51,6 +58,19 @@ export const defaultPrintOptions: PrintOptions = {
51
58
  }
52
59
 
53
60
  export function orderPrintedDefinitions(options: PrintOptions): PrintOptions {
61
+ return {
62
+ ...options,
63
+ typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
64
+ implementedInterfaceCompareFn: (t1, t2) => t1.interface.name.localeCompare(t2.interface.name),
65
+ fieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
66
+ unionMemberCompareFn: (t1, t2) => t1.type.name.localeCompare(t2.type.name),
67
+ enumValueCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
68
+ inputObjectFieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
69
+ directiveCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
70
+ };
71
+ }
72
+
73
+ export function shallowOrderPrintedDefinitions(options: PrintOptions): PrintOptions {
54
74
  return {
55
75
  ...options,
56
76
  typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
@@ -255,7 +275,7 @@ function printImplementedInterfaces(implementations: readonly InterfaceImplement
255
275
 
256
276
  function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType | InterfaceType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
257
277
  const directives = appliedDirectives(type, options, extension);
258
- const interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
278
+ let interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
259
279
  let fields = forExtension<FieldDefinition<any>>(type.fields(), extension);
260
280
  if (options.fieldFilter) {
261
281
  fields = fields.filter(options.fieldFilter);
@@ -263,6 +283,12 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
263
283
  if (!directives.length && !interfaces.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
264
284
  return undefined;
265
285
  }
286
+ if (options.implementedInterfaceCompareFn) {
287
+ interfaces = interfaces.concat().sort(options.implementedInterfaceCompareFn);
288
+ }
289
+ if (options.fieldCompareFn) {
290
+ fields = fields.concat().sort(options.fieldCompareFn);
291
+ }
266
292
  return printDescription(type, options, extension)
267
293
  + printIsExtension(extension)
268
294
  + kind + ' ' + type
@@ -274,10 +300,13 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
274
300
 
275
301
  function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
276
302
  const directives = appliedDirectives(type, options, extension);
277
- const members = forExtension(type.members(), extension);
303
+ let members = forExtension(type.members(), extension);
278
304
  if (!directives.length && !members.length && (extension || !type.preserveEmptyDefinition)) {
279
305
  return undefined;
280
306
  }
307
+ if (options.unionMemberCompareFn) {
308
+ members = members.concat().sort(options.unionMemberCompareFn);
309
+ }
281
310
  const possibleTypes = members.length ? ' = ' + members.map(m => m.type).join(' | ') : '';
282
311
  return printDescription(type, options, extension)
283
312
  + printIsExtension(extension)
@@ -288,10 +317,13 @@ function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions,
288
317
 
289
318
  function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
290
319
  const directives = appliedDirectives(type, options, extension);
291
- const values = forExtension(type.values, extension);
320
+ let values = forExtension(type.values, extension);
292
321
  if (!directives.length && !values.length && (extension || !type.preserveEmptyDefinition)) {
293
322
  return undefined;
294
323
  }
324
+ if (options.enumValueCompareFn) {
325
+ values = values.concat().sort(options.enumValueCompareFn);
326
+ }
295
327
  const vals = values.map((v, i) =>
296
328
  printDescription(v, options, extension, options.indentString, !i)
297
329
  + options.indentString
@@ -307,10 +339,13 @@ function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, e
307
339
 
308
340
  function printInputDefinitionOrExtension(type: InputObjectType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
309
341
  const directives = appliedDirectives(type, options, extension);
310
- const fields = forExtension(type.fields(), extension);
342
+ let fields = forExtension(type.fields(), extension);
311
343
  if (!directives.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
312
344
  return undefined;
313
345
  }
346
+ if (options.inputObjectFieldCompareFn) {
347
+ fields = fields.concat().sort(options.inputObjectFieldCompareFn);
348
+ }
314
349
  return printDescription(type, options, extension)
315
350
  + printIsExtension(extension)
316
351
  + 'input ' + type