@apollo/federation-internals 2.0.2-alpha.1 → 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.
- package/CHANGELOG.md +6 -27
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +76 -49
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts +19 -3
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +110 -10
- package/dist/definitions.js.map +1 -1
- package/dist/directiveAndTypeSpecification.js +1 -1
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/federation.d.ts +1 -0
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +20 -0
- package/dist/federation.js.map +1 -1
- package/dist/graphQLJSSchemaToAST.d.ts +8 -0
- package/dist/graphQLJSSchemaToAST.d.ts.map +1 -0
- package/dist/graphQLJSSchemaToAST.js +96 -0
- package/dist/graphQLJSSchemaToAST.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +2 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +29 -18
- package/dist/operations.js.map +1 -1
- package/dist/print.d.ts +2 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +23 -15
- package/dist/print.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/definitions.test.ts +138 -1
- package/src/__tests__/graphQLJSSchemaToAST.test.ts +156 -0
- package/src/__tests__/schemaUpgrader.test.ts +0 -2
- package/src/__tests__/subgraphValidation.test.ts +58 -0
- package/src/buildSchema.ts +138 -47
- package/src/definitions.ts +146 -16
- package/src/directiveAndTypeSpecification.ts +1 -1
- package/src/federation.ts +40 -0
- package/src/graphQLJSSchemaToAST.ts +138 -0
- package/src/index.ts +1 -0
- package/src/operations.ts +44 -18
- package/src/print.ts +30 -15
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/definitions.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
if (!
|
|
1173
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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[] {
|
|
@@ -2106,7 +2184,11 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
|
|
|
2106
2184
|
}
|
|
2107
2185
|
|
|
2108
2186
|
protected removeInnerElements(): void {
|
|
2109
|
-
|
|
2187
|
+
// Make a copy, since EnumValue.remove() will modify this._values.
|
|
2188
|
+
const values = Array.from(this._values);
|
|
2189
|
+
for (const value of values) {
|
|
2190
|
+
value.remove();
|
|
2191
|
+
}
|
|
2110
2192
|
}
|
|
2111
2193
|
|
|
2112
2194
|
protected hasNonExtensionInnerElements(): boolean {
|
|
@@ -2124,6 +2206,7 @@ export class EnumType extends BaseNamedType<OutputTypeReferencer, EnumType> {
|
|
|
2124
2206
|
|
|
2125
2207
|
export class InputObjectType extends BaseNamedType<InputTypeReferencer, InputObjectType> {
|
|
2126
2208
|
readonly kind = 'InputObjectType' as const;
|
|
2209
|
+
readonly astDefinitionKind = Kind.INPUT_OBJECT_TYPE_DEFINITION;
|
|
2127
2210
|
private readonly _fields: Map<string, InputFieldDefinition> = new Map();
|
|
2128
2211
|
private _cachedFieldsArray?: InputFieldDefinition[];
|
|
2129
2212
|
|
|
@@ -2980,6 +3063,51 @@ export class Directive<
|
|
|
2980
3063
|
}
|
|
2981
3064
|
}
|
|
2982
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
|
+
|
|
2983
3111
|
export class Variable {
|
|
2984
3112
|
constructor(readonly name: string) {}
|
|
2985
3113
|
|
|
@@ -3253,6 +3381,7 @@ function copyOfExtension<T extends ExtendableElement>(
|
|
|
3253
3381
|
}
|
|
3254
3382
|
|
|
3255
3383
|
function copySchemaDefinitionInner(source: SchemaDefinition, dest: SchemaDefinition) {
|
|
3384
|
+
dest.preserveEmptyDefinition = source.preserveEmptyDefinition;
|
|
3256
3385
|
const extensionsMap = copyExtensions(source, dest);
|
|
3257
3386
|
for (const rootType of source.roots()) {
|
|
3258
3387
|
copyOfExtension(extensionsMap, rootType, dest.setRoot(rootType.rootKind, rootType.type.name));
|
|
@@ -3267,6 +3396,7 @@ function copySchemaDefinitionInner(source: SchemaDefinition, dest: SchemaDefinit
|
|
|
3267
3396
|
}
|
|
3268
3397
|
|
|
3269
3398
|
function copyNamedTypeInner(source: NamedType, dest: NamedType) {
|
|
3399
|
+
dest.preserveEmptyDefinition = source.preserveEmptyDefinition;
|
|
3270
3400
|
const extensionsMap = copyExtensions(source, dest);
|
|
3271
3401
|
// Same as copyAppliedDirectives, but as the directive applies to the type, we need to remember if the application
|
|
3272
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
|
|
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
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
|
-
|
|
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
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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 {
|