@apollo/federation-internals 2.0.2 → 2.0.3

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 (45) hide show
  1. package/CHANGELOG.md +4 -29
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +76 -49
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/definitions.d.ts +19 -3
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +106 -9
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/directiveAndTypeSpecification.js +1 -1
  10. package/dist/directiveAndTypeSpecification.js.map +1 -1
  11. package/dist/federation.d.ts +1 -0
  12. package/dist/federation.d.ts.map +1 -1
  13. package/dist/federation.js +20 -0
  14. package/dist/federation.js.map +1 -1
  15. package/dist/graphQLJSSchemaToAST.d.ts +8 -0
  16. package/dist/graphQLJSSchemaToAST.d.ts.map +1 -0
  17. package/dist/graphQLJSSchemaToAST.js +96 -0
  18. package/dist/graphQLJSSchemaToAST.js.map +1 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/operations.d.ts +2 -1
  24. package/dist/operations.d.ts.map +1 -1
  25. package/dist/operations.js +29 -18
  26. package/dist/operations.js.map +1 -1
  27. package/dist/print.d.ts +2 -1
  28. package/dist/print.d.ts.map +1 -1
  29. package/dist/print.js +23 -15
  30. package/dist/print.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/definitions.test.ts +117 -1
  33. package/src/__tests__/graphQLJSSchemaToAST.test.ts +156 -0
  34. package/src/__tests__/schemaUpgrader.test.ts +0 -2
  35. package/src/__tests__/subgraphValidation.test.ts +58 -0
  36. package/src/buildSchema.ts +138 -47
  37. package/src/definitions.ts +141 -15
  38. package/src/directiveAndTypeSpecification.ts +1 -1
  39. package/src/federation.ts +40 -0
  40. package/src/graphQLJSSchemaToAST.ts +138 -0
  41. package/src/index.ts +1 -0
  42. package/src/operations.ts +44 -18
  43. package/src/print.ts +30 -15
  44. package/tsconfig.test.tsbuildinfo +1 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -14,7 +14,10 @@ import {
14
14
  parse,
15
15
  TypeNode,
16
16
  VariableDefinitionNode,
17
- VariableNode
17
+ VariableNode,
18
+ TypeSystemDefinitionNode,
19
+ SchemaDefinitionNode,
20
+ TypeDefinitionNode
18
21
  } from "graphql";
19
22
  import {
20
23
  CoreImport,
@@ -27,9 +30,9 @@ import {
27
30
  removeAllCoreFeatures,
28
31
  } from "./coreSpec";
29
32
  import { assert, mapValues, MapWithCachedArrays, setValues } from "./utils";
30
- import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode } from "./values";
33
+ import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode, argumentsEquals } from "./values";
31
34
  import { removeInaccessibleElements } from "./inaccessibleSpec";
32
- import { defaultPrintOptions, printSchema } from './print';
35
+ import { printSchema } from './print';
33
36
  import { sameType } from './types';
34
37
  import { addIntrospectionFields, introspectionFieldNames, isIntrospectionName } from "./introspection";
35
38
  import { err } from '@apollo/core-schema';
@@ -273,6 +276,10 @@ export function runtimeTypesIntersects(t1: CompositeType, t2: CompositeType): bo
273
276
  return false;
274
277
  }
275
278
 
279
+ export function isConditionalDirective(directive: Directive<any, any> | DirectiveDefinition<any>): boolean {
280
+ return ['include', 'skip'].includes(directive.name);
281
+ }
282
+
276
283
  export const executableDirectiveLocations: DirectiveLocation[] = [
277
284
  DirectiveLocation.QUERY,
278
285
  DirectiveLocation.MUTATION,
@@ -634,6 +641,7 @@ export abstract class NamedSchemaElement<TOwnType extends NamedSchemaElement<TOw
634
641
  abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSchemaElement<TOwnType, Schema, TReferencer>> extends NamedSchemaElement<TOwnType, Schema, TReferencer> {
635
642
  protected readonly _referencers: Set<TReferencer> = new Set();
636
643
  protected readonly _extensions: Set<Extension<TOwnType>> = new Set();
644
+ public preserveEmptyDefinition: boolean = false;
637
645
 
638
646
  constructor(name: string, readonly isBuiltIn: boolean = false) {
639
647
  super(name);
@@ -699,7 +707,9 @@ abstract class BaseNamedType<TReferencer, TOwnType extends NamedType & NamedSche
699
707
  }
700
708
 
701
709
  hasNonExtensionElements(): boolean {
702
- return this._appliedDirectives.some(d => d.ofExtension() === undefined) || this.hasNonExtensionInnerElements();
710
+ return this.preserveEmptyDefinition
711
+ || this._appliedDirectives.some(d => d.ofExtension() === undefined)
712
+ || this.hasNonExtensionInnerElements();
703
713
  }
704
714
 
705
715
  protected abstract hasNonExtensionInnerElements(): boolean;
@@ -1014,8 +1024,8 @@ export class CoreFeatures {
1014
1024
  this.byIdentity.set(feature.url.identity, feature);
1015
1025
  }
1016
1026
 
1017
- sourceFeature(element: DirectiveDefinition | NamedType): CoreFeature | undefined {
1018
- const isDirective = element instanceof DirectiveDefinition;
1027
+ sourceFeature(element: DirectiveDefinition | Directive | NamedType): CoreFeature | undefined {
1028
+ const isDirective = element instanceof DirectiveDefinition || element instanceof Directive;
1019
1029
  const splitted = element.name.split('__');
1020
1030
  if (splitted.length > 1) {
1021
1031
  return this.byAlias.get(splitted[0]);
@@ -1167,17 +1177,69 @@ export class Schema {
1167
1177
  return this.apiSchema;
1168
1178
  }
1169
1179
 
1170
- toGraphQLJSSchema(isSubgraph: boolean = false): GraphQLSchema {
1171
- // Obviously not super fast, but as long as we don't use this often on a hot path, that's probably ok.
1172
- if (!isSubgraph) {
1173
- return buildGraphqlSchemaFromAST(this.toAST());
1180
+ private emptyASTDefinitionsForExtensionsWithoutDefinition(): TypeSystemDefinitionNode[] {
1181
+ const nodes = [];
1182
+ if (this.schemaDefinition.hasExtensionElements() && !this.schemaDefinition.hasNonExtensionElements()) {
1183
+ const node: SchemaDefinitionNode = { kind: Kind.SCHEMA_DEFINITION, operationTypes: [] };
1184
+ nodes.push(node);
1185
+ }
1186
+ for (const type of this.types()) {
1187
+ if (type.hasExtensionElements() && !type.hasNonExtensionElements()) {
1188
+ const node: TypeDefinitionNode = {
1189
+ kind: type.astDefinitionKind,
1190
+ name: { kind: Kind.NAME, value: type.name },
1191
+ };
1192
+ nodes.push(node);
1193
+ }
1194
+ }
1195
+ return nodes;
1196
+ }
1197
+
1198
+ toGraphQLJSSchema(): GraphQLSchema {
1199
+ let ast = this.toAST();
1200
+
1201
+ // Note that AST generated by `this.toAST()` may not be fully graphQL valid because, in federation subgraphs, we accept
1202
+ // extensions that have no corresponding definitions. This won't fly however if we try to build a `GraphQLSchema`, so
1203
+ // we need to "fix" that problem. For that, we add empty definitions for every element that has extensions without
1204
+ // definitions (which is also what `fed1` was effectively doing).
1205
+ const additionalNodes = this.emptyASTDefinitionsForExtensionsWithoutDefinition();
1206
+ if (additionalNodes.length > 0) {
1207
+ ast = {
1208
+ kind: Kind.DOCUMENT,
1209
+ definitions: ast.definitions.concat(additionalNodes),
1210
+ }
1174
1211
  }
1175
1212
 
1176
- // Some subgraphs, especially federation 1 ones, cannot be properly converted to a GraphQLSchema because they are invalid graphQL.
1177
- // And the main culprit is type extensions that don't have a corresponding definition. So to avoid that problem, we print
1178
- // up the AST without extensions.
1179
- const ast = parse(printSchema(this, { ...defaultPrintOptions, mergeTypesAndExtensions: true }), { noLocation: true });
1180
- return buildGraphqlSchemaFromAST(ast);
1213
+ const graphQLSchema = buildGraphqlSchemaFromAST(ast);
1214
+ if (additionalNodes.length > 0) {
1215
+ // As mentionned, if we have extensions without definition, we _have_ to add an empty definition to be able to
1216
+ // build a `GraphQLSchema` object. But that also mean that we lose the information doing so, as we cannot
1217
+ // distinguish anymore that we have no definition. A method like `graphQLSchemaToAST` for instance, would
1218
+ // include a definition in particular, and that could a bit surprised (and could lead to an hard-to-find bug
1219
+ // in the worst case if you were expecting it that something like `graphQLSchemaToAST(buildSchema(defs).toGraphQLJSSchema())`
1220
+ // gives you back the original `defs`).
1221
+ // So to avoid this, we manually delete the definition `astNode` post-construction on the created schema if
1222
+ // we had not definition. This should break users of the resulting schema since `astNode` is allowed to be `undefined`,
1223
+ // but it allows `graphQLSchemaToAST` to make the proper distinction in general.
1224
+ for (const node of additionalNodes) {
1225
+ switch (node.kind) {
1226
+ case Kind.SCHEMA_DEFINITION:
1227
+ graphQLSchema.astNode = undefined;
1228
+ break;
1229
+ case Kind.SCALAR_TYPE_DEFINITION:
1230
+ case Kind.OBJECT_TYPE_DEFINITION:
1231
+ case Kind.INTERFACE_TYPE_DEFINITION:
1232
+ case Kind.ENUM_TYPE_DEFINITION:
1233
+ case Kind.UNION_TYPE_DEFINITION:
1234
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION:
1235
+ const type = graphQLSchema.getType(node.name.value);
1236
+ if (type) {
1237
+ type.astNode = undefined;
1238
+ }
1239
+ }
1240
+ }
1241
+ }
1242
+ return graphQLSchema;
1181
1243
  }
1182
1244
 
1183
1245
  get schemaDefinition(): SchemaDefinition {
@@ -1525,6 +1587,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1525
1587
  readonly kind = 'SchemaDefinition' as const;
1526
1588
  protected readonly _roots = new MapWithCachedArrays<SchemaRootKind, RootType>();
1527
1589
  protected readonly _extensions = new Set<Extension<SchemaDefinition>>();
1590
+ public preserveEmptyDefinition: boolean = false;
1528
1591
 
1529
1592
  roots(): readonly RootType[] {
1530
1593
  return this._roots.values();
@@ -1616,6 +1679,16 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1616
1679
  return extension;
1617
1680
  }
1618
1681
 
1682
+ hasExtensionElements(): boolean {
1683
+ return this._extensions.size > 0;
1684
+ }
1685
+
1686
+ hasNonExtensionElements(): boolean {
1687
+ return this.preserveEmptyDefinition
1688
+ || this._appliedDirectives.some((d) => d.ofExtension() === undefined)
1689
+ || this.roots().some((r) => r.ofExtension() === undefined);
1690
+ }
1691
+
1619
1692
  private removeRootType(rootType: RootType) {
1620
1693
  this._roots.delete(rootType.rootKind);
1621
1694
  removeReferenceToType(this, rootType.type);
@@ -1636,6 +1709,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1636
1709
 
1637
1710
  export class ScalarType extends BaseNamedType<OutputTypeReferencer | InputTypeReferencer, ScalarType> {
1638
1711
  readonly kind = 'ScalarType' as const;
1712
+ readonly astDefinitionKind = Kind.SCALAR_TYPE_DEFINITION;
1639
1713
 
1640
1714
  protected removeTypeReference(type: NamedType) {
1641
1715
  assert(false, `Scalar type ${this} can't reference other types; shouldn't be asked to remove reference to ${type}`);
@@ -1865,6 +1939,7 @@ abstract class FieldBasedType<T extends (ObjectType | InterfaceType) & NamedSche
1865
1939
 
1866
1940
  export class ObjectType extends FieldBasedType<ObjectType, ObjectTypeReferencer> {
1867
1941
  readonly kind = 'ObjectType' as const;
1942
+ readonly astDefinitionKind = Kind.OBJECT_TYPE_DEFINITION;
1868
1943
 
1869
1944
  /**
1870
1945
  * Whether this type is one of the schema root type (will return false if the type is detached).
@@ -1899,6 +1974,7 @@ export class ObjectType extends FieldBasedType<ObjectType, ObjectTypeReferencer>
1899
1974
 
1900
1975
  export class InterfaceType extends FieldBasedType<InterfaceType, InterfaceTypeReferencer> {
1901
1976
  readonly kind = 'InterfaceType' as const;
1977
+ readonly astDefinitionKind = Kind.INTERFACE_TYPE_DEFINITION;
1902
1978
 
1903
1979
  allImplementations(): (ObjectType | InterfaceType)[] {
1904
1980
  return setValues(this._referencers).filter(ref => ref.kind === 'ObjectType' || ref.kind === 'InterfaceType') as (ObjectType | InterfaceType)[];
@@ -1935,6 +2011,7 @@ export class UnionMember extends BaseExtensionMember<UnionType> {
1935
2011
 
1936
2012
  export class UnionType extends BaseNamedType<OutputTypeReferencer, UnionType> {
1937
2013
  readonly kind = 'UnionType' as const;
2014
+ readonly astDefinitionKind = Kind.UNION_TYPE_DEFINITION;
1938
2015
  protected readonly _members: MapWithCachedArrays<string, UnionMember> = new MapWithCachedArrays();
1939
2016
  private _typenameField?: FieldDefinition<UnionType>;
1940
2017
 
@@ -2057,6 +2134,7 @@ export class UnionType extends BaseNamedType<OutputTypeReferencer, UnionType> {
2057
2134
 
2058
2135
  export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
2059
2136
  readonly kind = 'EnumType' as const;
2137
+ readonly astDefinitionKind = Kind.ENUM_TYPE_DEFINITION;
2060
2138
  protected readonly _values: EnumValue[] = [];
2061
2139
 
2062
2140
  get values(): readonly EnumValue[] {
@@ -2128,6 +2206,7 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
2128
2206
 
2129
2207
  export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObjectType> {
2130
2208
  readonly kind = 'InputObjectType' as const;
2209
+ readonly astDefinitionKind = Kind.INPUT_OBJECT_TYPE_DEFINITION;
2131
2210
  private readonly _fields: Map<string, InputFieldDefinition> = new Map();
2132
2211
  private _cachedFieldsArray?: InputFieldDefinition[];
2133
2212
 
@@ -2984,6 +3063,51 @@ export class Directive<
2984
3063
  }
2985
3064
  }
2986
3065
 
3066
+ export function sameDirectiveApplication(application1: Directive<any, any>, application2: Directive<any, any>): boolean {
3067
+ return application1.name === application2.name && argumentsEquals(application1.arguments(), application2.arguments());
3068
+ }
3069
+
3070
+ /**
3071
+ * Checks whether the 2 provided "set" of directive applications are the same (same applications, regardless or order).
3072
+ */
3073
+ export function sameDirectiveApplications(applications1: Directive<any, any>[], applications2: Directive<any, any>[]): boolean {
3074
+ if (applications1.length !== applications2.length) {
3075
+ return false;
3076
+ }
3077
+
3078
+ for (const directive1 of applications1) {
3079
+ if (!applications2.some(directive2 => sameDirectiveApplication(directive1, directive2))) {
3080
+ return false;
3081
+ }
3082
+ }
3083
+ return true;
3084
+ }
3085
+
3086
+ /**
3087
+ * Checks whether a given array of directive applications (`maybeSubset`) is a sub-set of another array of directive applications (`applications`).
3088
+ *
3089
+ * Sub-set here means that all of the applications in `maybeSubset` appears in `applications`.
3090
+ */
3091
+ export function isDirectiveApplicationsSubset(applications: Directive<any, any>[], maybeSubset: Directive<any, any>[]): boolean {
3092
+ if (maybeSubset.length > applications.length) {
3093
+ return false;
3094
+ }
3095
+
3096
+ for (const directive1 of maybeSubset) {
3097
+ if (!applications.some(directive2 => sameDirectiveApplication(directive1, directive2))) {
3098
+ return false;
3099
+ }
3100
+ }
3101
+ return true;
3102
+ }
3103
+
3104
+ /**
3105
+ * Computes the difference between the set of directives applications `baseApplications` and the `toRemove` one.
3106
+ */
3107
+ export function directiveApplicationsSubstraction(baseApplications: Directive<any, any>[], toRemove: Directive<any, any>[]): Directive<any, any>[] {
3108
+ return baseApplications.filter((application) => !toRemove.some((other) => sameDirectiveApplication(application, other)));
3109
+ }
3110
+
2987
3111
  export class Variable {
2988
3112
  constructor(readonly name: string) {}
2989
3113
 
@@ -3257,6 +3381,7 @@ function copyOfExtension<T extends ExtendableElement>(
3257
3381
  }
3258
3382
 
3259
3383
  function copySchemaDefinitionInner(source: SchemaDefinition, dest: SchemaDefinition) {
3384
+ dest.preserveEmptyDefinition = source.preserveEmptyDefinition;
3260
3385
  const extensionsMap = copyExtensions(source, dest);
3261
3386
  for (const rootType of source.roots()) {
3262
3387
  copyOfExtension(extensionsMap, rootType, dest.setRoot(rootType.rootKind, rootType.type.name));
@@ -3271,6 +3396,7 @@ function copySchemaDefinitionInner(source: SchemaDefinition, dest: SchemaDefinit
3271
3396
  }
3272
3397
 
3273
3398
  function copyNamedTypeInner(source: NamedType, dest: NamedType) {
3399
+ dest.preserveEmptyDefinition = source.preserveEmptyDefinition;
3274
3400
  const extensionsMap = copyExtensions(source, dest);
3275
3401
  // Same as copyAppliedDirectives, but as the directive applies to the type, we need to remember if the application
3276
3402
  // is for the extension or not.
@@ -227,7 +227,7 @@ export function createEnumTypeSpecification({
227
227
  const actualValueNames = existing.values.map(v => v.name).sort((n1, n2) => n1.localeCompare(n2));
228
228
  if (!arrayEquals(expectedValueNames, actualValueNames)) {
229
229
  errors = errors.concat(ERRORS.TYPE_DEFINITION_INVALID.err({
230
- message: `Invalid definition of type ${name}: expected values [${expectedValueNames}] but found [${actualValueNames}].`,
230
+ message: `Invalid definition for type "${name}": expected values [${expectedValueNames.join(', ')}] but found [${actualValueNames.join(', ')}].`,
231
231
  nodes: existing.sourceAST
232
232
  }));
233
233
  }
package/src/federation.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  LinkDirectiveArgs,
62
62
  linkDirectiveDefaultName,
63
63
  linkIdentity,
64
+ FeatureUrl,
64
65
  } from "./coreSpec";
65
66
  import {
66
67
  FEDERATION_VERSIONS,
@@ -1406,10 +1407,48 @@ export class Subgraph {
1406
1407
  return false;
1407
1408
  }
1408
1409
 
1410
+ // If the query type only have our federation specific fields, then that (almost surely) means the original subgraph
1411
+ // had no Query type and so we save printing it.
1412
+ if (isObjectType(t) && t.isQueryRootType() && t.fields().filter((f) => !isFederationField(f)).length === 0) {
1413
+ return false;
1414
+ }
1415
+
1409
1416
  const core = this.schema.coreFeatures;
1410
1417
  return !core || core.sourceFeature(t)?.url.identity !== linkIdentity;
1411
1418
  }
1412
1419
 
1420
+ private isPrintedDirectiveApplication(d: Directive): boolean {
1421
+ // We print almost all directive application, but the one we skip is the `@link` to the link spec itself.
1422
+ // The reason is that it is one of the things that usually not provided by users but is instead auto-added
1423
+ // and so this keep the output a tad "cleaner".
1424
+ // Do note that it is only auto-added if it uses the `@link` name. If it is renamed, we need to include
1425
+ // the application (and more generally, if there is more argument set than just the url, we print
1426
+ // the directive to make sure we're not hidding something relevant).
1427
+ if (!this.schema.coreFeatures || d.name !== linkSpec.url.name) {
1428
+ return true;
1429
+ }
1430
+ const args = d.arguments();
1431
+ let urlArg: FeatureUrl | undefined = undefined;
1432
+ if ('url' in args) {
1433
+ try {
1434
+ urlArg = FeatureUrl.parse(args['url']);
1435
+ } catch (e) {
1436
+ // ignored on purpose: if the 'url' arg don't parse properly as a Feature url, then `urlArg` will
1437
+ // be `undefined` which we want.
1438
+ }
1439
+ }
1440
+ const isDefaultLinkToLink = urlArg?.identity === linkIdentity && Object.keys(args).length === 1;
1441
+ return !isDefaultLinkToLink;
1442
+ }
1443
+
1444
+ /**
1445
+ * Returns a representation of the subgraph without any auto-imported directive definitions or "federation private"
1446
+ * types and fiels (`_service` et al.).
1447
+ *
1448
+ * In other words, this will correspond to what a user would usually write.
1449
+ *
1450
+ * Note that if one just want a representation of the full schema, then it can simply call `printSchema(this.schema)`.
1451
+ */
1413
1452
  toString(basePrintOptions: PrintOptions = defaultPrintOptions) {
1414
1453
  return printSchema(
1415
1454
  this.schema,
@@ -1418,6 +1457,7 @@ export class Subgraph {
1418
1457
  directiveDefinitionFilter: (d) => this.isPrintedDirective(d),
1419
1458
  typeFilter: (t) => this.isPrintedType(t),
1420
1459
  fieldFilter: (f) => !isFederationField(f),
1460
+ directiveApplicationFilter: (d) => this.isPrintedDirectiveApplication(d),
1421
1461
  }
1422
1462
  );
1423
1463
  }
@@ -0,0 +1,138 @@
1
+ import {
2
+ DefinitionNode,
3
+ DirectiveDefinitionNode,
4
+ DocumentNode,
5
+ GraphQLDirective,
6
+ GraphQLNamedType,
7
+ GraphQLObjectType,
8
+ GraphQLSchema,
9
+ isIntrospectionType,
10
+ isSpecifiedDirective,
11
+ isSpecifiedScalarType,
12
+ Kind,
13
+ OperationTypeDefinitionNode,
14
+ OperationTypeNode,
15
+ parse,
16
+ printSchema,
17
+ printType,
18
+ SchemaDefinitionNode,
19
+ SchemaExtensionNode,
20
+ TypeDefinitionNode,
21
+ TypeExtensionNode
22
+ } from "graphql";
23
+ import { Maybe } from "graphql/jsutils/Maybe";
24
+ import { defaultRootName } from "./definitions";
25
+
26
+ const allOperationTypeNode = [ OperationTypeNode.QUERY, OperationTypeNode.MUTATION, OperationTypeNode.SUBSCRIPTION];
27
+
28
+ /**
29
+ * Converts a graphql-js schema into an equivalent AST document.
30
+ *
31
+ * Note importantly that this method is not, in general, equivalent to `parse(printSchema(schema))` in that
32
+ * the returned AST will contain directive _applications_ when those can be found in AST nodes linked by
33
+ * the elements of the provided schema.
34
+ */
35
+ export function graphQLJSSchemaToAST(schema: GraphQLSchema): DocumentNode {
36
+ const types = Object.values(schema.getTypeMap()).filter((type) => !isIntrospectionType(type) && !isSpecifiedScalarType(type));
37
+ const directives = schema.getDirectives().filter((directive) => !isSpecifiedDirective(directive));
38
+
39
+ const schemaASTs = toNodeArray(graphQLJSSchemaToSchemaDefinitionAST(schema));
40
+ const typesASTs = types.map((type) => toNodeArray(graphQLJSNamedTypeToAST(type))).flat();
41
+ const directivesASTs = directives.map((directive) => graphQLJSDirectiveToAST(directive));
42
+
43
+ return {
44
+ kind: Kind.DOCUMENT,
45
+ definitions: [...schemaASTs, ...typesASTs, ...directivesASTs],
46
+ }
47
+ }
48
+
49
+ function toNodeArray<TDef extends DefinitionNode, TExt extends DefinitionNode>({
50
+ definition,
51
+ extensions,
52
+ }: {
53
+ definition?: TDef,
54
+ extensions: readonly TExt[]}
55
+ ): readonly DefinitionNode[] {
56
+ return definition ? [definition, ...extensions] : extensions;
57
+ }
58
+
59
+ function maybe<T>(v: Maybe<T>): T | undefined {
60
+ return v ? v : undefined;
61
+ }
62
+
63
+ // Not exposing that one for now because it's a bit weirder API-wise (and take a `GraphqQLSchema` but only handle a specific subpart of it) .
64
+ function graphQLJSSchemaToSchemaDefinitionAST(schema: GraphQLSchema): { definition?: SchemaDefinitionNode, extensions: readonly SchemaExtensionNode[] } {
65
+ if (schema.astNode || schema.extensionASTNodes.length > 0) {
66
+ return {
67
+ definition: maybe(schema.astNode),
68
+ extensions: schema.extensionASTNodes,
69
+ };
70
+ } else {
71
+ let definition: SchemaDefinitionNode | undefined = undefined;
72
+ if (hasNonDefaultRootNames(schema)) {
73
+ const operationTypes: OperationTypeDefinitionNode[] = [];
74
+ for (const operation of allOperationTypeNode) {
75
+ const type = schema.getRootType(operation);
76
+ if (type) {
77
+ operationTypes.push({
78
+ kind: Kind.OPERATION_TYPE_DEFINITION,
79
+ operation,
80
+ type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value : type.name } },
81
+ });
82
+ }
83
+ }
84
+ definition = {
85
+ kind: Kind.SCHEMA_DEFINITION,
86
+ description: schema.description ? {
87
+ kind: Kind.STRING,
88
+ value: schema.description,
89
+ } : undefined,
90
+ operationTypes,
91
+ }
92
+ }
93
+ return {
94
+ definition,
95
+ extensions: [],
96
+ };
97
+ }
98
+ }
99
+
100
+ function hasNonDefaultRootNames(schema: GraphQLSchema): boolean {
101
+ return allOperationTypeNode.some((t) => isNonDefaultRootName(schema.getRootType(t), t));
102
+ }
103
+
104
+ function isNonDefaultRootName(type: Maybe<GraphQLObjectType>, operation: OperationTypeNode): boolean {
105
+ return !!type && type.name !== defaultRootName(operation);
106
+ }
107
+
108
+ export function graphQLJSNamedTypeToAST(type: GraphQLNamedType): { definition?: TypeDefinitionNode, extensions: readonly TypeExtensionNode[] } {
109
+ if (type.astNode || type.extensionASTNodes.length > 0) {
110
+ return {
111
+ definition: maybe(type.astNode),
112
+ extensions: type.extensionASTNodes,
113
+ };
114
+ } else {
115
+ // While we could theoretically manually build the AST, it's just simpler to print the type and parse it back.
116
+ return {
117
+ definition: parse(printType(type)).definitions[0] as TypeDefinitionNode,
118
+ extensions: [],
119
+ };
120
+ }
121
+ }
122
+
123
+ export function graphQLJSDirectiveToAST(directive: GraphQLDirective): DirectiveDefinitionNode {
124
+ if (directive.astNode) {
125
+ return directive.astNode;
126
+ } else {
127
+ // Note that the trick used for type of printing and parsing back is tad less convenient here because graphQL-js does not
128
+ // expose a direct way to print a directive alone. So we work-around it by built-in a "fake" schema with essentially just
129
+ // that directive.
130
+ const fakeSchema = new GraphQLSchema({
131
+ directives: [directive],
132
+ assumeValid: true,
133
+ });
134
+ const reparsed = parse(printSchema(fakeSchema));
135
+ return reparsed.definitions.find((def) => def.kind === Kind.DIRECTIVE_DEFINITION) as DirectiveDefinitionNode;
136
+ }
137
+ }
138
+
package/src/index.ts CHANGED
@@ -17,3 +17,4 @@ export * from './extractSubgraphsFromSupergraph';
17
17
  export * from './error';
18
18
  export * from './schemaUpgrader';
19
19
  export * from './suggestions';
20
+ export * from './graphQLJSSchemaToAST';
package/src/operations.ts CHANGED
@@ -41,6 +41,9 @@ import {
41
41
  CompositeType,
42
42
  typenameFieldName,
43
43
  NamedType,
44
+ sameDirectiveApplications,
45
+ isConditionalDirective,
46
+ isDirectiveApplicationsSubset,
44
47
  } from "./definitions";
45
48
  import { sameType } from "./types";
46
49
  import { assert, mapEntries, MapWithCachedArrays, MultiMap } from "./utils";
@@ -53,16 +56,7 @@ function validate(condition: any, message: () => string, sourceAST?: ASTNode): a
53
56
  }
54
57
 
55
58
  function haveSameDirectives<TElement extends OperationElement>(op1: TElement, op2: TElement): boolean {
56
- if (op1.appliedDirectives.length != op2.appliedDirectives.length) {
57
- return false;
58
- }
59
-
60
- for (const thisDirective of op1.appliedDirectives) {
61
- if (!op2.appliedDirectives.some(thatDirective => thisDirective.name === thatDirective.name && argumentsEquals(thisDirective.arguments(), thatDirective.arguments()))) {
62
- return false;
63
- }
64
- }
65
- return true;
59
+ return sameDirectiveApplications(op1.appliedDirectives, op2.appliedDirectives);
66
60
  }
67
61
 
68
62
  abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> extends DirectiveTargetElement<T> {
@@ -306,6 +300,13 @@ export function sameOperationPaths(p1: OperationPath, p2: OperationPath): boolea
306
300
  return true;
307
301
  }
308
302
 
303
+ /**
304
+ * Returns all the "conditional" directive applications (`@skip` and `@include`) in the provided path.
305
+ */
306
+ export function conditionalDirectivesInOperationPath(path: OperationPath): Directive<any, any>[] {
307
+ return path.map((e) => e.appliedDirectives).flat().filter((d) => isConditionalDirective(d));
308
+ }
309
+
309
310
  export function concatOperationPaths(head: OperationPath, tail: OperationPath): OperationPath {
310
311
  // While this is mainly a simple array concatenation, we optimize slightly by recognizing if the
311
312
  // tail path starts by a fragment selection that is useless given the end of the head path.
@@ -316,14 +317,21 @@ export function concatOperationPaths(head: OperationPath, tail: OperationPath):
316
317
  return head;
317
318
  }
318
319
  const lastOfHead = head[head.length - 1];
319
- const firstOfTail = tail[0];
320
- if (isUselessFollowupElement(lastOfHead, firstOfTail)) {
320
+ const conditionals = conditionalDirectivesInOperationPath(head);
321
+ let firstOfTail = tail[0];
322
+ // Note that in practice, we may be able to eliminate a few elements at the beginning of the path
323
+ // due do conditionals ('@skip' and '@include'). Indeed, a (tail) path crossing multiple conditions
324
+ // may start with: [ ... on X @include(if: $c1), ... on X @ksip(if: $c2), (...)], but if `head`
325
+ // already ends on type `X` _and_ both the conditions on `$c1` and `$c2` are alredy found on `head`,
326
+ // then we can remove both fragments in `tail`.
327
+ while (firstOfTail && isUselessFollowupElement(lastOfHead, firstOfTail, conditionals)) {
321
328
  tail = tail.slice(1);
329
+ firstOfTail = tail[0];
322
330
  }
323
331
  return head.concat(tail);
324
332
  }
325
333
 
326
- function isUselessFollowupElement(first: OperationElement, followup: OperationElement): boolean {
334
+ function isUselessFollowupElement(first: OperationElement, followup: OperationElement, conditionals: Directive<any, any>[]): boolean {
327
335
  const typeOfFirst = first.kind === 'Field'
328
336
  ? baseType(first.definition.type!)
329
337
  : first.typeCondition;
@@ -333,7 +341,7 @@ function isUselessFollowupElement(first: OperationElement, followup: OperationEl
333
341
  return !!typeOfFirst
334
342
  && followup.kind === 'FragmentElement'
335
343
  && !!followup.typeCondition
336
- && followup.appliedDirectives.length === 0
344
+ && (followup.appliedDirectives.length === 0 || isDirectiveApplicationsSubset(conditionals, followup.appliedDirectives))
337
345
  && sameType(typeOfFirst, followup.typeCondition);
338
346
  }
339
347
 
@@ -766,7 +774,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
766
774
  this._selections.add(key, toAdd);
767
775
  ++this._selectionCount;
768
776
  this._cachedSelections = undefined;
769
- return selection;
777
+ return toAdd;
770
778
  }
771
779
 
772
780
  addPath(path: OperationPath) {
@@ -1128,9 +1136,27 @@ export class FieldSelection extends Freezable<FieldSelection> {
1128
1136
 
1129
1137
  updateForAddingTo(selectionSet: SelectionSet): FieldSelection {
1130
1138
  const updatedField = this.field.updateForAddingTo(selectionSet);
1131
- return this.field === updatedField
1132
- ? this.cloneIfFrozen()
1133
- : new FieldSelection(updatedField, this.selectionSet?.cloneIfFrozen());
1139
+ if (this.field === updatedField) {
1140
+ return this.cloneIfFrozen();
1141
+ }
1142
+
1143
+ // We create a new selection that not only uses the updated field, but also ensures
1144
+ // the underlying selection set uses the updated field type as parent type.
1145
+ const updatedBaseType = baseType(updatedField.definition.type!);
1146
+ let updatedSelectionSet : SelectionSet | undefined;
1147
+ if (this.selectionSet && this.selectionSet.parentType !== updatedBaseType) {
1148
+ assert(isCompositeType(updatedBaseType), `Expected ${updatedBaseType.coordinate} to be composite but ${updatedBaseType.kind}`);
1149
+ updatedSelectionSet = new SelectionSet(updatedBaseType);
1150
+ // Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
1151
+ // effect of the type change.
1152
+ for (const selection of this.selectionSet.selections()) {
1153
+ updatedSelectionSet.add(selection);
1154
+ }
1155
+ } else {
1156
+ updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
1157
+ }
1158
+
1159
+ return new FieldSelection(updatedField, updatedSelectionSet);
1134
1160
  }
1135
1161
 
1136
1162
  toSelectionNode(): FieldNode {