diff --git a/composition/src/ast/ast.ts b/composition/src/ast/ast.ts index 9f4f260652..c70dd40fbf 100644 --- a/composition/src/ast/ast.ts +++ b/composition/src/ast/ast.ts @@ -1,5 +1,6 @@ import { BooleanValueNode, + ConstDirectiveNode, ConstValueNode, DirectiveDefinitionNode, EnumTypeDefinitionNode, @@ -10,6 +11,7 @@ import { InputObjectTypeDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, + InterfaceTypeExtensionNode, IntValueNode, Kind, NamedTypeNode, @@ -22,7 +24,6 @@ import { UnionTypeDefinitionNode, } from 'graphql'; import { federationUnexpectedNodeKindError } from '../errors/errors'; -import { InterfaceTypeExtensionNode } from 'graphql/index'; function deepCopyFieldsAndInterfaces( node: InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode | ObjectTypeExtensionNode, @@ -69,15 +70,36 @@ export function deepCopyTypeNode(node: TypeNode, parentName: string, fieldName: throw new Error(`Field ${parentName}.${fieldName} has more than 30 layers of nesting, or there is a cyclical error.`); } +export type MutableDirectiveDefinitionNode = { + arguments?: InputValueDefinitionNode[]; + description?: StringValueNode; + kind: Kind.DIRECTIVE_DEFINITION; + locations: NameNode[]; + name: NameNode; + repeatable: boolean; +}; + +export function directiveDefinitionNodeToMutable(node: DirectiveDefinitionNode): MutableDirectiveDefinitionNode { + return { + arguments: node.arguments ? [...node.arguments] : undefined, + description: node.description ? { ...node.description } : undefined, + kind: node.kind, + locations: [...node.locations], + name: { ...node.name }, + repeatable: node.repeatable, + }; +} + export type MutableEnumTypeDefinitionNode = { - description?: StringValueNode, - kind: Kind.ENUM_TYPE_DEFINITION, - name: NameNode, - values: MutableEnumValueDefinitionNode[], + description?: StringValueNode; + directives?: ConstDirectiveNode[]; + kind: Kind.ENUM_TYPE_DEFINITION; + name: NameNode; + values: MutableEnumValueDefinitionNode[]; }; export function enumTypeDefinitionNodeToMutable(node: EnumTypeDefinitionNode): MutableEnumTypeDefinitionNode { - const values: EnumValueDefinitionNode[] = []; + const values: MutableEnumValueDefinitionNode[] = []; if (node.values) { for (const value of node.values) { values.push(enumValueDefinitionNodeToMutable(value)); @@ -93,6 +115,7 @@ export function enumTypeDefinitionNodeToMutable(node: EnumTypeDefinitionNode): M export type MutableEnumValueDefinitionNode = { description?: StringValueNode; + directives?: ConstDirectiveNode[]; kind: Kind.ENUM_VALUE_DEFINITION; name: NameNode; }; @@ -106,18 +129,19 @@ export function enumValueDefinitionNodeToMutable(node: EnumValueDefinitionNode): } export type MutableFieldDefinitionNode = { - arguments: MutableInputValueDefinitionNode[], - description?: StringValueNode, - kind: Kind.FIELD_DEFINITION, - name: NameNode, - type: TypeNode, + arguments: MutableInputValueDefinitionNode[]; + description?: StringValueNode; + directives?: ConstDirectiveNode[]; + kind: Kind.FIELD_DEFINITION; + name: NameNode; + type: TypeNode; }; export function fieldDefinitionNodeToMutable(node: FieldDefinitionNode, parentName: string): MutableFieldDefinitionNode { const args: MutableInputValueDefinitionNode[] = []; if (node.arguments) { for (const argument of node.arguments) { - args.push(inputValueDefinitionNodeToMutable(argument, node.name.value)); // TODO better error for arguments + args.push(inputValueDefinitionNodeToMutable(argument, node.name.value)); } } return { @@ -130,7 +154,8 @@ export function fieldDefinitionNodeToMutable(node: FieldDefinitionNode, parentNa } export type MutableInputObjectTypeDefinitionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; fields: InputValueDefinitionNode[]; kind: Kind.INPUT_OBJECT_TYPE_DEFINITION; name: NameNode; @@ -153,10 +178,11 @@ export function inputObjectTypeDefinitionNodeToMutable(node: InputObjectTypeDefi export type MutableInputValueDefinitionNode = { defaultValue?: ConstValueNode; - description?: StringValueNode, - kind: Kind.INPUT_VALUE_DEFINITION, - name: NameNode, - type: TypeNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; + kind: Kind.INPUT_VALUE_DEFINITION; + name: NameNode; + type: TypeNode; } export function inputValueDefinitionNodeToMutable(node: InputValueDefinitionNode, parentName: string): MutableInputValueDefinitionNode { @@ -170,11 +196,12 @@ export function inputValueDefinitionNodeToMutable(node: InputValueDefinitionNode } export type MutableInterfaceTypeDefinitionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; fields: FieldDefinitionNode[]; interfaces: NamedTypeNode[]; - kind: Kind.INTERFACE_TYPE_DEFINITION, - name: NameNode, + kind: Kind.INTERFACE_TYPE_DEFINITION; + name: NameNode; } export function interfaceTypeDefinitionNodeToMutable(node: InterfaceTypeDefinitionNode): MutableInterfaceTypeDefinitionNode { @@ -191,7 +218,8 @@ export function interfaceTypeDefinitionNodeToMutable(node: InterfaceTypeDefiniti } export type MutableObjectTypeDefinitionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; fields: FieldDefinitionNode[]; interfaces: NamedTypeNode[]; kind: Kind.OBJECT_TYPE_DEFINITION; @@ -212,7 +240,8 @@ export function objectTypeDefinitionNodeToMutable(node: ObjectTypeDefinitionNode } export type MutableObjectTypeExtensionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; fields: FieldDefinitionNode[]; interfaces: NamedTypeNode[]; kind: Kind.OBJECT_TYPE_EXTENSION; @@ -245,6 +274,7 @@ export function objectTypeExtensionNodeToMutableDefinitionNode(node: ObjectTypeE export type MutableScalarTypeDefinitionNode = { description?: StringValueNode; + directives?: ConstDirectiveNode[]; kind: Kind.SCALAR_TYPE_DEFINITION; name: NameNode; }; @@ -266,7 +296,8 @@ export type MutableTypeNode = { export const maximumTypeNesting = 30; export type MutableUnionTypeDefinitionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; kind: Kind.UNION_TYPE_DEFINITION; name: NameNode; types: NamedTypeNode[]; diff --git a/composition/src/ast/utils.ts b/composition/src/ast/utils.ts index 3cc61f2c45..9a58abeb65 100644 --- a/composition/src/ast/utils.ts +++ b/composition/src/ast/utils.ts @@ -1,4 +1,5 @@ import { + ConstDirectiveNode, FieldDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, @@ -10,6 +11,7 @@ import { OperationTypeNode, } from 'graphql'; import { + MutableDirectiveDefinitionNode, MutableEnumTypeDefinitionNode, MutableEnumValueDefinitionNode, MutableFieldDefinitionNode, @@ -27,17 +29,24 @@ import { ENUM_VALUE_UPPER, EXTENDS, FIELD_DEFINITION_UPPER, + FIELD_UPPER, FIELDS, + FRAGMENT_DEFINITION_UPPER, + FRAGMENT_SPREAD_UPPER, + INLINE_FRAGMENT_UPPER, INPUT_OBJECT_UPPER, INTERFACE_UPPER, KEY, MUTATION, + MUTATION_UPPER, OBJECT_UPPER, QUERY, + QUERY_UPPER, SCALAR_UPPER, SCHEMA_UPPER, SHAREABLE, SUBSCRIPTION, + SUBSCRIPTION_UPPER, } from '../utils/string-constants'; import { duplicateInterfaceError, @@ -55,12 +64,40 @@ import { } from '../errors/errors'; import { getOrThrowError } from '../utils/utils'; +export const EXECUTABLE_DIRECTIVE_LOCATIONS = new Set([ + FIELD_UPPER, FRAGMENT_DEFINITION_UPPER, FRAGMENT_SPREAD_UPPER, + INLINE_FRAGMENT_UPPER, MUTATION_UPPER, QUERY_UPPER, SUBSCRIPTION_UPPER, +]); + export enum MergeMethod { UNION, INTERSECTION, CONSISTENT, } +export type PersistedDirectivesContainer = { + directives: Map; + tags: Map; +}; + +export type ArgumentContainer = { + includeDefaultValue: boolean; + node: MutableInputValueDefinitionNode; + requiredSubgraphs: Set; + subgraphs: Set; +}; + +export type ArgumentMap = Map; + +export type DirectiveContainer = { + arguments: ArgumentMap; + executableLocations: Set; + node: MutableDirectiveDefinitionNode; + subgraphs: Set; +}; + +export type DirectiveMap = Map; + export type EntityContainer = { fields: Set; keys: Set; @@ -69,30 +106,23 @@ export type EntityContainer = { export type EnumContainer = { appearances: number; - values: EnumValueMap; + directives: PersistedDirectivesContainer; kind: Kind.ENUM_TYPE_DEFINITION; node: MutableEnumTypeDefinitionNode; + values: EnumValueMap; }; export type EnumValueContainer = { appearances: number; + directives: PersistedDirectivesContainer; node: MutableEnumValueDefinitionNode; }; export type EnumValueMap = Map; -export type ArgumentContainer = { - includeDefaultValue: boolean; - node: MutableInputValueDefinitionNode; - requiredSubgraphs: Set; - subgraphs: Set; -}; - -export type ArgumentMap = Map; - export type FieldContainer = { - appearances: number; arguments: ArgumentMap; + directives: PersistedDirectivesContainer; isShareable: boolean; node: MutableFieldDefinitionNode; rootTypeName: string; @@ -104,6 +134,7 @@ export type FieldMap = Map; export type InputValueContainer = { appearances: number; + directives: PersistedDirectivesContainer; includeDefaultValue: boolean; node: MutableInputValueDefinitionNode; }; @@ -112,13 +143,14 @@ export type InputValueMap = Map; export type InputObjectContainer = { appearances: number; + directives: PersistedDirectivesContainer; fields: InputValueMap; kind: Kind.INPUT_OBJECT_TYPE_DEFINITION; node: MutableInputObjectTypeDefinitionNode; }; export type InterfaceContainer = { - appearances: number; + directives: PersistedDirectivesContainer; fields: FieldMap; interfaces: Set; kind: Kind.INTERFACE_TYPE_DEFINITION; @@ -127,7 +159,7 @@ export type InterfaceContainer = { }; export type ObjectContainer = { - appearances: number; + directives: PersistedDirectivesContainer; fields: FieldMap; entityKeys: Set; interfaces: Set; @@ -138,7 +170,7 @@ export type ObjectContainer = { }; export type ObjectExtensionContainer = { - appearances: number; + directives: PersistedDirectivesContainer; fields: FieldMap; entityKeys: Set; interfaces: Set; @@ -165,18 +197,20 @@ export type PotentiallyUnresolvableField = { }; export type ScalarContainer = { - appearances: number; + directives: PersistedDirectivesContainer; kind: Kind.SCALAR_TYPE_DEFINITION; node: MutableScalarTypeDefinitionNode; }; export type UnionContainer = { - appearances: number; + directives: PersistedDirectivesContainer; kind: Kind.UNION_TYPE_DEFINITION; members: Set; node: MutableUnionTypeDefinitionNode; }; +export type ChildContainer = FieldContainer | InputValueContainer | EnumValueContainer; + export type ParentContainer = | EnumContainer | InputObjectContainer @@ -185,6 +219,8 @@ export type ParentContainer = | UnionContainer | ScalarContainer; +export type NodeContainer = ChildContainer | ParentContainer; + export type ExtensionContainer = ObjectExtensionContainer; export type ParentMap = Map; @@ -448,7 +484,7 @@ export function stringToNameNode(value: string): NameNode { }; } -export function stringToNameNodes(values: string[]): NameNode[] { +export function stringArrayToNameNodeArray(values: string[]): NameNode[] { const nameNodes: NameNode[] = []; for (const value of values) { nameNodes.push(stringToNameNode(value)); @@ -456,6 +492,14 @@ export function stringToNameNodes(values: string[]): NameNode[] { return nameNodes; } +export function setToNameNodeArray(set: Set): NameNode[] { + const nameNodes: NameNode[] = []; + for (const value of set) { + nameNodes.push(stringToNameNode(value)); + } + return nameNodes; +} + export function stringToNamedTypeNode(value: string): NamedTypeNode { return { kind: Kind.NAMED_TYPE, @@ -523,3 +567,50 @@ export function isKindAbstract(kind: Kind) { export function getInlineFragmentString(parentTypeName: string): string { return ` ... on ${parentTypeName} `; } + +export function extractNameNodeStringsToSet(nodes: readonly NameNode[] | NameNode[], set: Set): Set { + for (const node of nodes) { + set.add(node.value); + } + return set; +} + +export function extractExecutableDirectiveLocations( + nodes: readonly NameNode[] | NameNode[], set: Set, +): Set { + for (const node of nodes) { + const name = node.value; + if (EXECUTABLE_DIRECTIVE_LOCATIONS.has(name)) { + set.add(name); + } + } + return set; +} + +export function mergeExecutableDirectiveLocations( + nodes: readonly NameNode[] | NameNode[], directiveContainer: DirectiveContainer, +): Set { + const mergedSet = new Set(); + for (const node of nodes) { + const name = node.value; + if (directiveContainer.executableLocations.has(name)) { + mergedSet.add(name); + } + } + directiveContainer.executableLocations = mergedSet; + return mergedSet; +} + +export function pushPersistedDirectivesToNode(container: NodeContainer) { + const persistedDirectives: ConstDirectiveNode[] = [...container.directives.tags.values()]; + for (const directives of container.directives.directives.values()) { + persistedDirectives.push(...directives); + } + container.node.directives = persistedDirectives; + return container.node; +} + +export function getNodeWithPersistedDirectives(container: ParentContainer) { + pushPersistedDirectivesToNode(container); + return container.node; +} \ No newline at end of file diff --git a/composition/src/errors/errors.ts b/composition/src/errors/errors.ts index b74536da5e..80e43ac00d 100644 --- a/composition/src/errors/errors.ts +++ b/composition/src/errors/errors.ts @@ -265,6 +265,14 @@ export function invalidUnionError(unionName: string): Error { return new Error(`Union "${unionName}" must have at least one member.`); } +export function invalidArgumentNumberErrorMessage(expected: number, actual: number): string { + return ` Expected ${expected} argument` + (expected === 1 ? '' : 's') + ` but received ${actual}.`; +} + +export const invalidTagDirectiveError = new Error(` + Expected the @tag directive to have a single required argument "name" of the type "String!" +`); + export function invalidDirectiveError( directiveName: string, hostPath: string, @@ -540,15 +548,17 @@ export function unimplementedInterfaceFieldsError( ); } -export function invalidRequiredArgumentsError(fieldPath: string, errors: InvalidRequiredArgument[]): Error { - let message = `The field "${fieldPath}" could not be federated because:\n`; +export function invalidRequiredArgumentsError( + typeString: string, path: string, errors: InvalidRequiredArgument[], +): Error { + let message = `The ${typeString} "${path}" could not be federated because:\n`; for (const error of errors) { message += ` The argument "${error.argumentName}" is required in the following subgraph` + (error.requiredSubgraphs.length > 1 ? 's' : '' ) +': "' + error.requiredSubgraphs.join(`", "`) + `"\n` + ` However, the argument "${error.argumentName}" is not defined in the following subgraph` + (error.missingSubgraphs.length > 1 ? 's' : '' ) +': "' + error.missingSubgraphs.join(`", "`) + `"\n` + - ` If an argument is required on a field in any one subgraph, it must be at least defined as optional on all` + - ` other definitions of that field in all other subgraphs.\n` + ` If an argument is required on a ${typeString} in any one subgraph, it must be at least defined as optional` + + ` on all other definitions of that ${typeString} in all other subgraphs.\n` } return new Error(message); } diff --git a/composition/src/federation/federation-factory.ts b/composition/src/federation/federation-factory.ts index d5e5b631f3..a59ac35dab 100644 --- a/composition/src/federation/federation-factory.ts +++ b/composition/src/federation/federation-factory.ts @@ -2,6 +2,7 @@ import { MultiGraph } from 'graphology'; import { allSimplePaths } from 'graphology-simple-path'; import { buildASTSchema, + ConstDirectiveNode, ConstValueNode, DirectiveDefinitionNode, DocumentNode, @@ -18,12 +19,15 @@ import { } from 'graphql'; import { ConstValueNodeWithValue, + directiveDefinitionNodeToMutable, enumTypeDefinitionNodeToMutable, enumValueDefinitionNodeToMutable, fieldDefinitionNodeToMutable, inputObjectTypeDefinitionNodeToMutable, inputValueDefinitionNodeToMutable, interfaceTypeDefinitionNodeToMutable, + MutableEnumValueDefinitionNode, + MutableInputValueDefinitionNode, MutableTypeDefinitionNode, objectTypeDefinitionNodeToMutable, objectTypeExtensionNodeToMutable, @@ -34,24 +38,32 @@ import { import { ArgumentContainer, ArgumentMap, + DirectiveContainer, + DirectiveMap, EntityContainer, EnumValueContainer, ExtensionContainer, extractEntityKeys, + extractExecutableDirectiveLocations, extractInterfaces, FieldContainer, getInlineFragmentString, + getNodeWithPersistedDirectives, InputValueContainer, InterfaceContainer, isNodeShareable, + mergeExecutableDirectiveLocations, MergeMethod, ObjectContainer, ObjectExtensionContainer, ObjectLikeContainer, ParentContainer, ParentMap, + PersistedDirectivesContainer, PotentiallyUnresolvableField, + pushPersistedDirectivesToNode, RootTypeField, + setToNameNodeArray, stringToNamedTypeNode, } from '../ast/utils'; import { @@ -69,6 +81,7 @@ import { invalidRequiredArgumentsError, invalidSubgraphNameErrorMessage, invalidSubgraphNamesError, + invalidTagDirectiveError, invalidUnionError, minimumSubgraphRequirementError, noBaseTypeExtensionError, @@ -92,17 +105,22 @@ import { InternalSubgraph, Subgraph, validateSubgraphName, - walkSubgraphToCollectObjects, - walkSubgraphToCollectOperationsAndFields, + walkSubgraphToCollectFields, + walkSubgraphToCollectObjectLikesAndDirectiveDefinitions, walkSubgraphToFederate, } from '../subgraph/subgraph'; import { DEFAULT_MUTATION, DEFAULT_QUERY, DEFAULT_SUBSCRIPTION, + DIRECTIVE_DEFINITION, + FIELD, FIELD_NAME, + FRAGMENT_REPRESENTATION, + INACCESSIBLE, INLINE_FRAGMENT, QUERY, + TAG, } from '../utils/string-constants'; import { doSetsHaveAnyOverlap, @@ -169,15 +187,18 @@ export class FederationFactory { areFieldsShareable = false; argumentTypeNameSet = new Set(); argumentConfigurations: ArgumentConfigurationData[] = []; + executableDirectives = new Set(); parentTypeName = ''; + persistedDirectives = new Set([INACCESSIBLE, TAG]); currentSubgraphName = ''; childName = ''; - directiveDefinitions = new Map(); + directiveDefinitions: DirectiveMap = new Map(); entityMap = new Map(); errors: Error[] = []; extensions = new Map(); graph: MultiGraph = new MultiGraph(); graphEdges = new Set(); + graphPaths = new Map>(); inputFieldTypeNameSet = new Set(); isCurrentParentEntity = false; isCurrentParentInterface = false; @@ -219,8 +240,8 @@ export class FederationFactory { populateMultiGraphAndRenameOperations(subgraphs: InternalSubgraph[]) { for (const subgraph of subgraphs) { this.currentSubgraphName = subgraph.name; - walkSubgraphToCollectObjects(this, subgraph); - walkSubgraphToCollectOperationsAndFields(this, subgraph); + walkSubgraphToCollectObjectLikesAndDirectiveDefinitions(this, subgraph); + walkSubgraphToCollectFields(this, subgraph); } } @@ -308,18 +329,18 @@ export class FederationFactory { } // TODO validation of default values - upsertArgumentsForFieldNode(node: FieldDefinitionNode, existingFieldNode: FieldContainer) { + upsertArguments(node: DirectiveDefinitionNode | FieldDefinitionNode, argumentMap: ArgumentMap): ArgumentMap { if (!node.arguments) { - return; + return argumentMap; } for (const arg of node.arguments) { const argName = arg.name.value; const argPath = `${node.name.value}(${argName}...)`; this.argumentTypeNameSet.add(getNamedTypeForChild(argPath, arg.type)); const isRequired = isTypeRequired(arg.type); - const existingArg = existingFieldNode.arguments.get(argName); + const existingArg = argumentMap.get(argName); if (!existingArg) { - existingFieldNode.arguments.set(argName, { + argumentMap.set(argName, { includeDefaultValue: !!arg.defaultValue, node: inputValueDefinitionNodeToMutable(arg, this.childName), requiredSubgraphs: this.upsertRequiredSubgraph(new Set(), isRequired), @@ -348,24 +369,7 @@ export class FederationFactory { } this.compareAndValidateArgumentDefaultValues(existingArg, arg); } - } - - extractArgumentsFromFieldNode(node: FieldDefinitionNode, args: ArgumentMap): ArgumentMap { - if (!node.arguments) { - return args; - } - for (const arg of node.arguments) { - const argName = arg.name.value; - const argPath = `${node.name.value}(${argName}...)`; - args.set(argName, { - includeDefaultValue: !!arg.defaultValue, - node: inputValueDefinitionNodeToMutable(arg, this.childName), - requiredSubgraphs: this.upsertRequiredSubgraph(new Set(), isTypeRequired(arg.type)), - subgraphs: new Set([this.currentSubgraphName]), - }); - this.argumentTypeNameSet.add(getNamedTypeForChild(argPath, arg.type)); - } - return args; + return argumentMap; } addConcreteTypesForInterface(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode) { @@ -409,14 +413,33 @@ export class FederationFactory { ); } + getAllSimplePaths(responseTypeName: string): string[][] { + if (responseTypeName === this.parentTypeName) { + return [[this.parentTypeName]]; + } + const responsePaths = this.graphPaths.get(responseTypeName); + if (!responsePaths) { + const allPaths = allSimplePaths(this.graph, responseTypeName, this.parentTypeName) + this.graphPaths.set(responseTypeName, new Map([ + [this.parentTypeName, allPaths] + ])); + return allPaths; + } + const pathsToParent = responsePaths.get(this.parentTypeName); + if (pathsToParent) { + return pathsToParent; + } + const allParentPaths = allSimplePaths(this.graph, responseTypeName, this.parentTypeName); + responsePaths.set(this.parentTypeName, allParentPaths); + return allParentPaths; + } + addPotentiallyUnresolvableField(parent: ObjectContainer | ObjectExtensionContainer, fieldName: string) { const fieldContainer = getOrThrowError(parent.fields, fieldName); - for (const [responseTypeName, operation] of this.rootTypeFieldsByResponseTypeName) { + for (const [responseTypeName, rootTypeFields] of this.rootTypeFieldsByResponseTypeName) { + const paths = this.getAllSimplePaths(responseTypeName); // If the operation response type has no path to the parent type, continue - const paths = allSimplePaths(this.graph, responseTypeName, this.parentTypeName); - if (responseTypeName === this.parentTypeName) { - paths.push([this.parentTypeName]); - } else if (paths.length < 1) { + if (paths!.length < 1) { continue; } // Construct all possible paths to the unresolvable field but with the fieldName relationship between nodes @@ -447,7 +470,7 @@ export class FederationFactory { resolverPath += fieldName; // If the field could have fields itself, add ellipsis if (this.graph.hasNode(fieldContainer.rootTypeName)) { - resolverPath += ' { ... }'; + resolverPath += FRAGMENT_REPRESENTATION; } partialResolverPaths.push(resolverPath); } @@ -455,21 +478,21 @@ export class FederationFactory { return; } // Each of these operations returns a type that has a path to the parent - for (const [operationFieldPath, operationField] of operation) { + for (const [rootTypePath, rootTypeField] of rootTypeFields) { // If the operation is defined in a subgraph that the field is defined, it is resolvable - if (doSetsHaveAnyOverlap(fieldContainer.subgraphs, operationField.subgraphs)) { + if (doSetsHaveAnyOverlap(fieldContainer.subgraphs, rootTypeField.subgraphs)) { continue; } const fullResolverPaths: string[] = []; // The field is still resolvable if it's defined and resolved in another graph (but that isn't yet known) // Consequently, the subgraphs must be compared later to determine that the field is always resolvable for (const partialResolverPath of partialResolverPaths) { - fullResolverPaths.push(`${operationFieldPath}${operationField.inlineFragment}${partialResolverPath}`); + fullResolverPaths.push(`${rootTypePath}${rootTypeField.inlineFragment}${partialResolverPath}`); } const potentiallyUnresolvableField: PotentiallyUnresolvableField = { fieldContainer, fullResolverPaths, - rootTypeField: operationField, + rootTypeField: rootTypeField, }; // The parent might already have unresolvable fields that have already been added @@ -483,6 +506,35 @@ export class FederationFactory { } } + upsertDirectiveNode(node: DirectiveDefinitionNode) { + const directiveName = node.name.value; + const directiveDefinition = this.directiveDefinitions.get(directiveName); + if (directiveDefinition) { + if (!this.executableDirectives.has(directiveName)) { + return; + } + if (mergeExecutableDirectiveLocations(node.locations, directiveDefinition).size < 1) { + this.executableDirectives.delete(directiveName); + return; + } + this.upsertArguments(node, directiveDefinition.arguments); + directiveDefinition.node.description = directiveDefinition.node.description || node.description; + directiveDefinition.node.repeatable = directiveDefinition.node.repeatable && node.repeatable; + directiveDefinition.subgraphs.add(this.currentSubgraphName); + return; + } + const executableLocations = extractExecutableDirectiveLocations(node.locations, new Set()); + this.directiveDefinitions.set(directiveName, { + arguments: this.upsertArguments(node, new Map()), + executableLocations, + node: directiveDefinitionNodeToMutable(node), + subgraphs: new Set([this.currentSubgraphName]), + }); + if (executableLocations.size > 0) { + this.executableDirectives.add(directiveName); + } + } + upsertFieldNode(node: FieldDefinitionNode) { const parent = this.isCurrentParentExtensionType ? getOrThrowError(this.extensions, this.parentTypeName) @@ -501,7 +553,7 @@ export class FederationFactory { const existingFieldNode = fieldMap.get(this.childName); const entityParent = this.entityMap.get(this.parentTypeName); if (existingFieldNode) { - existingFieldNode.appearances += 1; + this.extractPersistedDirectives(node.directives || [], existingFieldNode.directives); existingFieldNode.node.description = existingFieldNode.node.description || node.description; existingFieldNode.subgraphs.add(this.currentSubgraphName); existingFieldNode.subgraphsByShareable.set(this.currentSubgraphName, isFieldShareable); @@ -521,7 +573,7 @@ export class FederationFactory { incompatibleChildTypesError(this.parentTypeName, this.childName, typeErrors[0], typeErrors[1]), ); } - this.upsertArgumentsForFieldNode(node, existingFieldNode); + this.upsertArguments(node, existingFieldNode.arguments); // If the parent is not an interface and both fields are not shareable, is it is a shareable error if (!this.isCurrentParentInterface && (!existingFieldNode.isShareable || !isFieldShareable)) { const shareableErrorTypeNames = this.shareableErrorTypeNames.get(this.parentTypeName); @@ -535,8 +587,14 @@ export class FederationFactory { } this.outputFieldTypeNameSet.add(fieldRootTypeName); fieldMap.set(this.childName, { - appearances: 1, - arguments: this.extractArgumentsFromFieldNode(node, new Map()), + arguments: this.upsertArguments(node, new Map()), + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), isShareable: isFieldShareable, node: fieldDefinitionNodeToMutable(node, this.parentTypeName), rootTypeName: fieldRootTypeName, @@ -566,38 +624,47 @@ export class FederationFactory { throw incompatibleParentKindFatalError(this.parentTypeName, Kind.ENUM_TYPE_DEFINITION, parent.kind); } const enumValues = parent.values; - const enumValue = enumValues.get(this.childName); - if (enumValue) { - enumValue.node.description = enumValue.node.description || node.description; - enumValue.appearances += 1; + const enumValueContainer = enumValues.get(this.childName); + if (enumValueContainer) { + this.extractPersistedDirectives(node.directives || [], enumValueContainer.directives); + enumValueContainer.node.description = enumValueContainer.node.description || node.description; + enumValueContainer.appearances += 1; return; } enumValues.set(this.childName, { appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), node: enumValueDefinitionNodeToMutable(node), }); return; case Kind.INPUT_VALUE_DEFINITION: if (!parent || !this.isParentInputObject) { - // TODO handle directives + // these are arguments to a directive return; } if (parent.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION) { throw incompatibleParentKindFatalError(this.parentTypeName, Kind.INPUT_OBJECT_TYPE_DEFINITION, parent.kind); } const inputValues = parent.fields; - const inputValue = inputValues.get(this.childName); - if (inputValue) { - inputValue.appearances += 1; - inputValue.node.description = inputValue.node.description || node.description; + const inputValueContainer = inputValues.get(this.childName); + if (inputValueContainer) { + this.extractPersistedDirectives(node.directives || [], inputValueContainer.directives); + inputValueContainer.appearances += 1; + inputValueContainer.node.description = inputValueContainer.node.description || node.description; const { typeErrors, typeNode } = getMostRestrictiveMergedTypeNode( - inputValue.node.type, + inputValueContainer.node.type, node.type, this.parentTypeName, this.childName, ); if (typeNode) { - inputValue.node.type = typeNode; + inputValueContainer.node.type = typeNode; } else { if (!typeErrors || typeErrors.length < 2) { throw fieldTypeMergeFatalError(this.childName); @@ -613,6 +680,13 @@ export class FederationFactory { this.inputFieldTypeNameSet.add(inputValueNamedType); inputValues.set(this.childName, { appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), includeDefaultValue: !!node.defaultValue, node: inputValueDefinitionNodeToMutable(node, this.parentTypeName), }); @@ -627,7 +701,7 @@ export class FederationFactory { const parent = this.parentMap.get(parentTypeName); if (parent) { parent.node.description = parent.node.description || node.description; - parent.appearances += 1; + this.extractPersistedDirectives(node.directives || [], parent.directives); } switch (node.kind) { case Kind.ENUM_TYPE_DEFINITION: @@ -635,10 +709,18 @@ export class FederationFactory { if (parent.kind !== node.kind) { throw incompatibleParentKindFatalError(parentTypeName, node.kind, parent.kind); } + parent.appearances += 1; return; } this.parentMap.set(parentTypeName, { appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), values: new Map(), kind: node.kind, node: enumTypeDefinitionNodeToMutable(node), @@ -649,10 +731,18 @@ export class FederationFactory { if (parent.kind !== node.kind) { throw incompatibleParentKindFatalError(parentTypeName, node.kind, parent.kind); } + parent.appearances += 1; return; } this.parentMap.set(parentTypeName, { appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), fields: new Map(), kind: node.kind, node: inputObjectTypeDefinitionNodeToMutable(node), @@ -670,7 +760,13 @@ export class FederationFactory { const nestedInterfaces = new Set(); extractInterfaces(node, nestedInterfaces); this.parentMap.set(parentTypeName, { - appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), fields: new Map(), interfaces: nestedInterfaces, kind: node.kind, @@ -686,7 +782,13 @@ export class FederationFactory { return; } this.parentMap.set(parentTypeName, { - appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), kind: node.kind, node: scalarTypeDefinitionNodeToMutable(node), }); @@ -706,7 +808,13 @@ export class FederationFactory { const entityKeys = new Set(); extractEntityKeys(node, entityKeys); this.parentMap.set(parentTypeName, { - appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), fields: new Map(), entityKeys, interfaces, @@ -729,7 +837,13 @@ export class FederationFactory { return; } this.parentMap.set(parentTypeName, { - appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), kind: node.kind, members: new Set(node.types?.map((member) => member.name.value)), node: unionTypeDefinitionNodeToMutable(node), @@ -797,7 +911,7 @@ export class FederationFactory { if (!this.graphEdges.has(rootTypeFieldPath)) { this.graph.addEdge(this.parentTypeName, concreteTypeName, { fieldName }); } - // Always upsert the operation field node to record subgraph appearances + // Always upsert the root type field node to record subgraph appearances this.upsertConcreteObjectLikeRootTypeFieldNode( fieldName, fieldTypeName, @@ -843,25 +957,29 @@ export class FederationFactory { if (extension.kind !== Kind.OBJECT_TYPE_EXTENSION) { throw incompatibleParentKindFatalError(this.parentTypeName, Kind.OBJECT_TYPE_EXTENSION, extension.kind); } - extension.appearances += 1; extension.subgraphs.add(this.currentSubgraphName); extractInterfaces(node, extension.interfaces); + this.extractPersistedDirectives(node.directives || [], extension.directives); return; } // build a new extension - const interfaces = new Set(); - extractInterfaces(node, interfaces); - const entityKeys = new Set(); - extractEntityKeys(node, entityKeys); + const interfaces = extractInterfaces(node, new Set()); + const entityKeys = extractEntityKeys(node, new Set()); this.extensions.set(this.parentTypeName, { - appearances: 1, - subgraphs: new Set([this.currentSubgraphName]), + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), + entityKeys, + fields: new Map(), + interfaces, isRootType: this.isParentRootType, kind: Kind.OBJECT_TYPE_EXTENSION, node: objectTypeExtensionNodeToMutable(node), - fields: new Map(), - entityKeys, - interfaces, + subgraphs: new Set([this.currentSubgraphName]), }); } @@ -952,18 +1070,15 @@ export class FederationFactory { return interfaces; } - getFieldDefinitionNodeWithMergedArguments(fieldContainer: FieldContainer, parentTypeName: string): FieldDefinitionNode { - if (!fieldContainer.arguments) { - return fieldContainer.node; - } - const fieldName = fieldContainer.node.name.value; - const fieldPath = `${parentTypeName}.${fieldName}`; - const args: InputValueDefinitionNode[] = []; - const errors: InvalidRequiredArgument[] = []; - const argumentNames: string[] = []; - for (const argumentContainer of fieldContainer.arguments.values()) { + mergeArguments( + container: FieldContainer | DirectiveContainer, + args: MutableInputValueDefinitionNode[], + errors: InvalidRequiredArgument[], + argumentNames?: string[], + ) { + for (const argumentContainer of container.arguments.values()) { const missingSubgraphs = getEntriesNotInHashSet( - fieldContainer.subgraphs, argumentContainer.subgraphs, + container.subgraphs, argumentContainer.subgraphs, ); const argumentName = argumentContainer.node.name.value; if (missingSubgraphs.length > 0) { @@ -982,10 +1097,50 @@ export class FederationFactory { argumentContainer.node.defaultValue = argumentContainer.includeDefaultValue ? argumentContainer.node.defaultValue : undefined; args.push(argumentContainer.node); - argumentNames.push(argumentName); + if (argumentNames) { + argumentNames.push(argumentName); + } } + } + + addValidExecutableDirectiveDefinition( + directiveName: string, directiveContainer: DirectiveContainer, definitions: MutableTypeDefinitionNode[], + ) { + if (!this.executableDirectives.has(directiveName)) { + return; + } + if (this.subgraphs.length !== directiveContainer.subgraphs.size) { + return; + } + directiveContainer.node.locations = setToNameNodeArray(directiveContainer.executableLocations); + if (!directiveContainer.arguments) { + definitions.push(directiveContainer.node); + return; + } + const args: MutableInputValueDefinitionNode[] = []; + const errors: InvalidRequiredArgument[] = []; + this.mergeArguments(directiveContainer, args, errors); if (errors.length > 0) { - this.errors.push(invalidRequiredArgumentsError(fieldPath, errors)); + this.errors.push(invalidRequiredArgumentsError(DIRECTIVE_DEFINITION, directiveName, errors)); + return; + } + directiveContainer.node.arguments = args; + definitions.push(directiveContainer.node) + } + + getMergedFieldDefinitionNode(fieldContainer: FieldContainer, parentTypeName: string): FieldDefinitionNode { + if (!fieldContainer.arguments) { + return fieldContainer.node; + } + pushPersistedDirectivesToNode(fieldContainer); + const fieldName = fieldContainer.node.name.value; + const fieldPath = `${parentTypeName}.${fieldName}`; + const args: MutableInputValueDefinitionNode[] = []; + const errors: InvalidRequiredArgument[] = []; + const argumentNames: string[] = []; + this.mergeArguments(fieldContainer, args, errors, argumentNames); + if (errors.length > 0) { + this.errors.push(invalidRequiredArgumentsError(FIELD, fieldPath, errors)); } else if (argumentNames.length > 0) { this.argumentConfigurations.push({ argumentNames, @@ -997,6 +1152,51 @@ export class FederationFactory { return fieldContainer.node; } + // tags with the same name string are merged + mergeTagDirectives(directive: ConstDirectiveNode, map: Map) { + // the directive has been validated in the normalizer + if (!directive.arguments || directive.arguments.length !== 1) { + this.errors.push(invalidTagDirectiveError); // should never happen + return; + } + const nameArgument = directive.arguments[0].value; + if (nameArgument.kind !== Kind.STRING) { + this.errors.push(invalidTagDirectiveError); // should never happen + return; + } + map.set(nameArgument.value, directive); + } + + extractPersistedDirectives( + directives: readonly ConstDirectiveNode[], container: PersistedDirectivesContainer, + ): PersistedDirectivesContainer { + if (directives.length < 1) { + return container; + } + for (const directive of directives) { + const directiveName = directive.name.value; + if (!this.persistedDirectives.has(directiveName)) { + continue; + } + if (directiveName === TAG) { + this.mergeTagDirectives(directive, container.tags); + continue; + } + const existingDirectives = container.directives.get(directiveName); + if (!existingDirectives) { + container.directives.set(directiveName, [directive]); + continue; + } + // Naïvely ignore non-repeatable directives + const definition = getOrThrowError(this.directiveDefinitions, directiveName); + if (!definition.node.repeatable) { + continue; + } + existingDirectives.push(directive); + } + return container; + } + federate(): FederationResultContainer { this.populateMultiGraphAndRenameOperations(this.subgraphs); const factory = this; @@ -1007,8 +1207,13 @@ export class FederationFactory { } this.validatePotentiallyUnresolvableFields(); const definitions: MutableTypeDefinitionNode[] = []; - for (const definition of this.directiveDefinitions.values()) { - definitions.push(definition); + for (const [directiveName, directiveContainer] of this.directiveDefinitions) { + if (this.persistedDirectives.has(directiveName)) { + definitions.push(directiveContainer.node); + continue; + } + // The definitions must be present in all subgraphs to kept in the federated graph + this.addValidExecutableDirectiveDefinition(directiveName, directiveContainer, definitions); } for (const [typeName, extension] of this.extensions) { if (extension.isRootType && !this.parentMap.has(typeName)) { @@ -1023,7 +1228,6 @@ export class FederationFactory { if (baseObject.kind !== Kind.OBJECT_TYPE_DEFINITION) { throw incompatibleParentKindFatalError(typeName, Kind.OBJECT_TYPE_DEFINITION, baseObject.kind); } - // TODO check directives for (const [fieldName, field] of extension.fields) { const baseField = baseObject.fields.get(fieldName); if (!baseField) { @@ -1053,82 +1257,86 @@ export class FederationFactory { this.errors.push(shareableFieldDefinitionsError(parent, children)); } const objectLikeContainersWithInterfaces: ObjectLikeContainer[] = []; - for (const parent of this.parentMap.values()) { - const parentTypeName = parent.node.name.value; - switch (parent.kind) { + for (const parentContainer of this.parentMap.values()) { + const parentTypeName = parentContainer.node.name.value; + switch (parentContainer.kind) { case Kind.ENUM_TYPE_DEFINITION: - const values: EnumValueDefinitionNode[] = []; + const values: MutableEnumValueDefinitionNode[] = []; const mergeMethod = this.getEnumMergeMethod(parentTypeName); - for (const value of parent.values.values()) { + for (const enumValueContainer of parentContainer.values.values()) { + pushPersistedDirectivesToNode(enumValueContainer); switch (mergeMethod) { case MergeMethod.CONSISTENT: - if (value.appearances < parent.appearances) { + if (enumValueContainer.appearances < parentContainer.appearances) { this.errors.push(incompatibleSharedEnumError(parentTypeName)); } - values.push(value.node); + values.push(enumValueContainer.node); break; case MergeMethod.INTERSECTION: - if (value.appearances === parent.appearances) { - values.push(value.node); + if (enumValueContainer.appearances === parentContainer.appearances) { + values.push(enumValueContainer.node); } break; default: - values.push(value.node); + values.push(enumValueContainer.node); break; } } - parent.node.values = values; - definitions.push(parent.node); + parentContainer.node.values = values; + definitions.push(getNodeWithPersistedDirectives(parentContainer)); break; case Kind.INPUT_OBJECT_TYPE_DEFINITION: const inputValues: InputValueDefinitionNode[] = []; - for (const value of parent.fields.values()) { - if (parent.appearances === value.appearances) { - inputValues.push(value.node); - } else if (isTypeRequired(value.node.type)) { - this.errors.push(federationRequiredInputFieldError(parentTypeName, value.node.name.value)); + for (const inputValueContainer of parentContainer.fields.values()) { + pushPersistedDirectivesToNode(inputValueContainer); + if (parentContainer.appearances === inputValueContainer.appearances) { + inputValues.push(inputValueContainer.node); + } else if (isTypeRequired(inputValueContainer.node.type)) { + this.errors.push(federationRequiredInputFieldError(parentTypeName, inputValueContainer.node.name.value)); break; } } - parent.node.fields = inputValues; - definitions.push(parent.node); + parentContainer.node.fields = inputValues; + definitions.push(getNodeWithPersistedDirectives(parentContainer)); break; case Kind.INTERFACE_TYPE_DEFINITION: const interfaceFields: FieldDefinitionNode[] = []; - for (const fieldContainer of parent.fields.values()) { - interfaceFields.push(this.getFieldDefinitionNodeWithMergedArguments(fieldContainer, parentTypeName)); + for (const fieldContainer of parentContainer.fields.values()) { + interfaceFields.push(this.getMergedFieldDefinitionNode(fieldContainer, parentTypeName)); } - parent.node.fields = interfaceFields; + parentContainer.node.fields = interfaceFields; + pushPersistedDirectivesToNode(parentContainer); // Interface implementations can only be evaluated after they've been fully merged - if (parent.interfaces.size > 0) { - objectLikeContainersWithInterfaces.push(parent); + if (parentContainer.interfaces.size > 0) { + objectLikeContainersWithInterfaces.push(parentContainer); } else { - definitions.push(parent.node); + definitions.push(parentContainer.node); } break; case Kind.OBJECT_TYPE_DEFINITION: const fields: FieldDefinitionNode[] = []; - for (const fieldContainer of parent.fields.values()) { - fields.push(this.getFieldDefinitionNodeWithMergedArguments(fieldContainer, parentTypeName)); + for (const fieldContainer of parentContainer.fields.values()) { + fields.push(this.getMergedFieldDefinitionNode(fieldContainer, parentTypeName)); } - parent.node.fields = fields; + parentContainer.node.fields = fields; + pushPersistedDirectivesToNode(parentContainer); // Interface implementations can only be evaluated after they've been fully merged - if (parent.interfaces.size > 0) { - objectLikeContainersWithInterfaces.push(parent); + if (parentContainer.interfaces.size > 0) { + objectLikeContainersWithInterfaces.push(parentContainer); } else { - definitions.push(parent.node); + definitions.push(parentContainer.node); } break; case Kind.SCALAR_TYPE_DEFINITION: - definitions.push(parent.node); + definitions.push(getNodeWithPersistedDirectives(parentContainer)); break; case Kind.UNION_TYPE_DEFINITION: const types: NamedTypeNode[] = []; - for (const memberName of parent.members) { + for (const memberName of parentContainer.members) { types.push(stringToNamedTypeNode(memberName)); } - parent.node.types = types; - definitions.push(parent.node); + parentContainer.node.types = types; + definitions.push(getNodeWithPersistedDirectives(parentContainer)); break; } } diff --git a/composition/src/normalization/normalization-factory.ts b/composition/src/normalization/normalization-factory.ts index 6bbf7c6237..631cda77f8 100644 --- a/composition/src/normalization/normalization-factory.ts +++ b/composition/src/normalization/normalization-factory.ts @@ -598,24 +598,29 @@ export class NormalizationFactory { normalize(document: DocumentNode) { const factory = this; + /* factory.allDirectiveDefinitions is initialized with v1 directive definitions, and v2 definitions are only added + after the visitor has visited the entire schema and the subgraph is known to be a V2 graph. Consequently, + alldirectiveDefinitions cannot be used to check for duplicate definitions, and another set (below) is required */ + const definedDirectives = new Set(); visit(document, { DirectiveDefinition: { enter(node) { const name = node.name.value; - // TODO These sets would potentially allow the user to define these directives more than once - // Add our definitions rather than the existing ones + if (definedDirectives.has(name)) { + factory.errors.push(duplicateDirectiveDefinitionError(name)); + return false; + } else { + definedDirectives.add(name); + } + // Normalize federation directives by replacing them with predefined definitions if (VERSION_TWO_DIRECTIVES.has(name)) { factory.isSubgraphVersionTwo = true; return false; } + // The V1 directives are always injected if (VERSION_ONE_DIRECTIVES.has(name)) { return false; } - const directiveDefinition = factory.allDirectiveDefinitions.get(name); - if (directiveDefinition) { - factory.errors.push(duplicateDirectiveDefinitionError(name)); - return false; - } factory.allDirectiveDefinitions.set(name, node); factory.customDirectiveDefinitions.set(name, node); return false; @@ -793,6 +798,10 @@ export class NormalizationFactory { return; } const name = node.name.value; + const valueRootTypeName = getNamedTypeForChild(`${factory.parentTypeName}.${name}`, node.type); + if (!BASE_SCALARS.has(valueRootTypeName)) { + factory.referencedTypeNames.add(valueRootTypeName); + } const parent = factory.isCurrentParentExtension ? getOrThrowError(factory.extensions, factory.parentTypeName) : getOrThrowError(factory.parents, factory.parentTypeName); diff --git a/composition/src/normalization/utils.ts b/composition/src/normalization/utils.ts index 7e672c8307..9214c28ecd 100644 --- a/composition/src/normalization/utils.ts +++ b/composition/src/normalization/utils.ts @@ -29,6 +29,7 @@ import { FIELD_DEFINITION_UPPER, FIELD_UPPER, FRAGMENT_DEFINITION_UPPER, + FRAGMENT_SPREAD_UPPER, INLINE_FRAGMENT_UPPER, INPUT_FIELD_DEFINITION_UPPER, INPUT_OBJECT_UPPER, @@ -371,6 +372,11 @@ export function areNodeKindAndDirectiveLocationCompatible( return true; } break; + case FRAGMENT_SPREAD_UPPER: + if (kind === Kind.FRAGMENT_SPREAD) { + return true; + } + break; case SCALAR_UPPER: if (kind === Kind.SCALAR_TYPE_DEFINITION || kind === Kind.SCALAR_TYPE_EXTENSION) { return true; diff --git a/composition/src/subgraph/subgraph.ts b/composition/src/subgraph/subgraph.ts index 8c95069236..6a9b6803c1 100644 --- a/composition/src/subgraph/subgraph.ts +++ b/composition/src/subgraph/subgraph.ts @@ -39,11 +39,16 @@ export function validateSubgraphName( } // Places the object-like nodes into the multigraph including the concrete types for abstract types -export function walkSubgraphToCollectObjects( +export function walkSubgraphToCollectObjectLikesAndDirectiveDefinitions( factory: FederationFactory, subgraph: InternalSubgraph, ) { subgraph.definitions = visit(subgraph.definitions, { + DirectiveDefinition: { + enter(node) { + factory.upsertDirectiveNode(node); + }, + }, InterfaceTypeDefinition: { enter(node) { factory.upsertParentNode(node); @@ -102,7 +107,7 @@ export function walkSubgraphToCollectObjects( }); } -export function walkSubgraphToCollectOperationsAndFields( +export function walkSubgraphToCollectFields( factory: FederationFactory, subgraph: Subgraph, ) { @@ -202,14 +207,9 @@ export function walkSubgraphToCollectOperationsAndFields( export function walkSubgraphToFederate(subgraph: DocumentNode, factory: FederationFactory) { visit(subgraph, { - DirectiveDefinition: { - enter(node) { - factory.directiveDefinitions.set(node.name.value, node); // TODO - }, - }, Directive: { enter() { - return false; // TODO + return false; }, }, EnumTypeDefinition: { diff --git a/composition/src/utils/constants.ts b/composition/src/utils/constants.ts index 51b2b82999..e36e4aadeb 100644 --- a/composition/src/utils/constants.ts +++ b/composition/src/utils/constants.ts @@ -1,5 +1,5 @@ import { DirectiveDefinitionNode, Kind } from 'graphql'; -import { stringToNamedTypeNode, stringToNameNode, stringToNameNodes } from '../ast/utils'; +import { stringArrayToNameNodeArray, stringToNamedTypeNode, stringToNameNode } from '../ast/utils'; import { ARGUMENT_DEFINITION_UPPER, DEPRECATED, @@ -30,6 +30,7 @@ export const BASE_SCALARS = new Set(['Boolean', 'Float', 'ID', 'Int', 'S export const VERSION_ONE_DIRECTIVES = new Set([DEPRECATED, EXTENDS, EXTERNAL, KEY, PROVIDES, REQUIRES, TAG]); export const VERSION_TWO_DIRECTIVES = new Set([INACCESSIBLE, SHAREABLE]); + export const BASE_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ /* directive @deprecated(reason: String = "No longer supported") on ARGUMENT_DEFINITION | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION @@ -47,7 +48,7 @@ export const BASE_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ }, ], kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([ + locations: stringArrayToNameNodeArray([ ARGUMENT_DEFINITION_UPPER, ENUM_VALUE_UPPER, FIELD_DEFINITION_UPPER, @@ -59,14 +60,14 @@ export const BASE_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ // directive @extends on INTERFACE | OBJECT { kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([INTERFACE_UPPER, OBJECT_UPPER]), + locations: stringArrayToNameNodeArray([INTERFACE_UPPER, OBJECT_UPPER]), name: stringToNameNode(EXTENDS), repeatable: false, }, // directive @external on FIELD_DEFINITION | OBJECT { kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([FIELD_DEFINITION_UPPER, OBJECT_UPPER]), + locations: stringArrayToNameNodeArray([FIELD_DEFINITION_UPPER, OBJECT_UPPER]), name: stringToNameNode(EXTERNAL), repeatable: false, }, @@ -139,7 +140,7 @@ export const BASE_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ }, ], kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([ + locations: stringArrayToNameNodeArray([ ARGUMENT_DEFINITION_UPPER, ENUM_UPPER, ENUM_VALUE_UPPER, @@ -157,12 +158,30 @@ export const BASE_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ ]; export const VERSION_TWO_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ - /* directive @inaccessible on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION |INPUT_OBJECT | + // @composeDirective is currently unimplemented + /* directive @composeDirective(name: String!) repeatable on SCHEMA */ + // { + // arguments: [ + // { + // kind: Kind.INPUT_VALUE_DEFINITION, + // name: stringToNameNode(NAME), + // type: { + // kind: Kind.NON_NULL_TYPE, + // type: stringToNamedTypeNode(STRING_TYPE), + // }, + // }, + // ], + // kind: Kind.DIRECTIVE_DEFINITION, + // locations: stringToNameNodes([SCHEMA]), + // name: stringToNameNode(COMPOSE_DIRECTIVE), + // repeatable: true, + // }, + /* directive @inaccessible on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_OBJECT | INPUT_FIELD_DEFINITION | INTERFACE | OBJECT | SCALAR | UNION */ { kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([ + locations: stringArrayToNameNodeArray([ ARGUMENT_DEFINITION_UPPER, ENUM_UPPER, ENUM_VALUE_UPPER, @@ -180,7 +199,7 @@ export const VERSION_TWO_DIRECTIVE_DEFINITIONS: DirectiveDefinitionNode[] = [ // directive @shareable on FIELD_DEFINITION | OBJECT { kind: Kind.DIRECTIVE_DEFINITION, - locations: stringToNameNodes([FIELD_DEFINITION_UPPER, OBJECT_UPPER]), + locations: stringArrayToNameNodeArray([FIELD_DEFINITION_UPPER, OBJECT_UPPER]), name: stringToNameNode(SHAREABLE), repeatable: false, }, diff --git a/composition/src/utils/string-constants.ts b/composition/src/utils/string-constants.ts index cc04e5cd48..3d7432a41b 100644 --- a/composition/src/utils/string-constants.ts +++ b/composition/src/utils/string-constants.ts @@ -1,18 +1,23 @@ export const ARGUMENT_DEFINITION_UPPER = 'ARGUMENT_DEFINITION'; +export const COMPOSE_DIRECTIVE = 'composeDirective'; export const DEFAULT_MUTATION = 'Mutation'; export const DEFAULT_QUERY = 'Query'; export const DEFAULT_SUBSCRIPTION = 'Subscription'; export const DEPRECATED = 'deprecated'; +export const DIRECTIVE_DEFINITION = 'directive definition'; export const ENUM_UPPER = 'ENUM'; export const ENUM_VALUE_UPPER = 'ENUM_VALUE'; export const EXTERNAL = 'external'; export const EXTENDS = 'extends'; +export const FIELD = 'field'; export const FIELD_UPPER = 'FIELD'; export const FIELD_NAME = 'fieldName'; export const FIELD_SET = 'FieldSet'; export const FIELDS = 'fields'; export const FIELD_DEFINITION_UPPER = 'FIELD_DEFINITION'; export const FRAGMENT_DEFINITION_UPPER = 'FRAGMENT_DEFINITION'; +export const FRAGMENT_REPRESENTATION = ' { ... }'; +export const FRAGMENT_SPREAD_UPPER = 'FRAGMENT_SPREAD'; export const INLINE_FRAGMENT = 'inlineFragment'; export const INLINE_FRAGMENT_UPPER = 'INLINE_FRAGMENT'; export const INPUT_FIELD_DEFINITION_UPPER = 'INPUT_FIELD_DEFINITION'; diff --git a/composition/tests/arguments.test.ts b/composition/tests/arguments.test.ts index e06fc13c83..3e07617af2 100644 --- a/composition/tests/arguments.test.ts +++ b/composition/tests/arguments.test.ts @@ -12,7 +12,8 @@ import { } from '../src'; import { Kind, parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionTwoBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionTwoPersistedBaseSchema } from './utils/utils'; +import { FIELD } from '../src/utils/string-constants'; describe('Argument federation tests', () => { const argName = 'input'; @@ -27,7 +28,7 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -48,7 +49,7 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -69,7 +70,7 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -90,7 +91,7 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -155,8 +156,8 @@ describe('Argument federation tests', () => { test('that if an argument is optional but not included in all subgraphs, it is not present in the federated graph', () => { const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST!)).toBe( - normalizeString(versionTwoBaseSchema + ` + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( + normalizeString(versionTwoPersistedBaseSchema + ` interface Interface { field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean): String } @@ -186,7 +187,7 @@ describe('Argument federation tests', () => { missingSubgraphs: ['subgraph-c'], requiredSubgraphs: ['subgraph-a'], }]; - expect(errors![0]).toStrictEqual(invalidRequiredArgumentsError('Interface.field', errorArrayOne)); + expect(errors![0]).toStrictEqual(invalidRequiredArgumentsError(FIELD, 'Interface.field', errorArrayOne)); const errorArrayTwo: InvalidRequiredArgument[] = [{ argumentName: 'requiredInAll', missingSubgraphs: ['subgraph-c'], @@ -196,7 +197,7 @@ describe('Argument federation tests', () => { missingSubgraphs: ['subgraph-c'], requiredSubgraphs: ['subgraph-a'], }]; - expect(errors![1]).toStrictEqual(invalidRequiredArgumentsError('Object.field', errorArrayTwo)); + expect(errors![1]).toStrictEqual(invalidRequiredArgumentsError(FIELD,'Object.field', errorArrayTwo)); }); test('that if an argument is not a valid input type or defined more than once, an error is returned', () => { diff --git a/composition/tests/entities.test.ts b/composition/tests/entities.test.ts index d340494f16..e8f70b55fe 100644 --- a/composition/tests/entities.test.ts +++ b/composition/tests/entities.test.ts @@ -1,6 +1,6 @@ import { federateSubgraphs, RootTypeField, Subgraph, unresolvableFieldError } from '../src'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionOneBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionOnePersistedBaseSchema } from './utils/utils'; import { parse } from 'graphql'; describe('Entities federation tests', () => { @@ -10,7 +10,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + ` + versionOnePersistedBaseSchema + ` type Query { dummy: String! } @@ -41,7 +41,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + ` + versionOnePersistedBaseSchema + ` type Query { dummy: String! trainer: Trainer! @@ -98,7 +98,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { trainer: Trainer! @@ -134,7 +134,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { trainer: Trainer! @@ -170,7 +170,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + ` + versionOnePersistedBaseSchema + ` type Trainer { id: Int! pokemon: [Pokemon!]! diff --git a/composition/tests/enums.test.ts b/composition/tests/enums.test.ts index a54c833ff4..655b864d6b 100644 --- a/composition/tests/enums.test.ts +++ b/composition/tests/enums.test.ts @@ -1,7 +1,7 @@ import { federateSubgraphs, incompatibleSharedEnumError, Subgraph } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionTwoBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionTwoPersistedBaseSchema } from './utils/utils'; describe('Enum federation tests', () => { const parentName = 'Instruction'; @@ -12,7 +12,7 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -34,7 +34,7 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -58,7 +58,7 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } @@ -80,7 +80,7 @@ describe('Enum federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` type Query { dummy: String! } diff --git a/composition/tests/federation-factory.test.ts b/composition/tests/federation-factory.test.ts index 16d193669a..9174d6cd6e 100644 --- a/composition/tests/federation-factory.test.ts +++ b/composition/tests/federation-factory.test.ts @@ -4,8 +4,8 @@ import { describe, expect, test } from 'vitest'; import { documentNodeToNormalizedString, normalizeString, - versionOneBaseSchema, - versionTwoBaseSchema, + versionOnePersistedBaseSchema, + versionTwoPersistedBaseSchema, } from './utils/utils'; describe('FederationFactory tests', () => { @@ -57,12 +57,8 @@ describe('FederationFactory tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` - directive @myDirective(a: String!) on FIELD_DEFINITION - directive @hello on FIELD_DEFINITION - directive @override(from: String!) on FIELD_DEFINITION - interface SkuItf { sku: String } @@ -77,7 +73,7 @@ describe('FederationFactory tests', () => { type Panda { name: ID! - favoriteFood: String + favoriteFood: String @tag(name: "nom-nom-nom") } enum ShippingClass { @@ -96,7 +92,7 @@ describe('FederationFactory tests', () => { } type User { - email: ID! + email: ID! @tag(name: "test-from-users") totalProductsCreated: Int name: String } @@ -114,7 +110,7 @@ describe('FederationFactory tests', () => { variation: ProductVariation dimensions: ProductDimension createdBy: User - hidden: String + hidden: String @inaccessible oldField: String reviewsCount: Int! reviewsScore: Float! @@ -122,7 +118,7 @@ describe('FederationFactory tests', () => { } type Product implements ProductItf & SkuItf { - id: ID! + id: ID! @tag(name: "hi-from-products") sku: String name: String package: String @@ -145,7 +141,7 @@ describe('FederationFactory tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { pokemon: [Pokemon] @@ -185,7 +181,7 @@ describe('FederationFactory tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { string: String @@ -200,8 +196,7 @@ describe('FederationFactory tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` type Query { string: String } @@ -209,6 +204,52 @@ describe('FederationFactory tests', () => { ), ); }); + + test('that tag and inaccessible directives are persisted in the federated schema', () => { + const { errors, federationResult } = federateSubgraphs([subgraphI, subgraphJ]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( + normalizeString( + versionTwoPersistedBaseSchema + ` + interface I @tag(name: "interface1") @tag(name: "interface2") @inaccessible { + field: String @tag(name: "field1") @tag(name: "field2") @inaccessible + } + + type Query @tag(name: "object2") @tag(name: "object1") { + dummy: String @tag(name: "field1") @tag(name: "field2") + } + + enum E @tag(name: "enum1") @tag(name: "enum2") @inaccessible { + A @tag(name: "enum value2") @tag(name: "enum value1") @inaccessible + } + + input In @tag(name: "input object1") @tag(name: "input object2") @inaccessible { + field: String @tag(name: "input value2") @tag(name: "input value1") @inaccessible + } + + scalar S @tag(name: "scalar1") @tag(name: "scalar2") @inaccessible + + type O implements I @tag(name: "object2") @tag(name: "object1") @inaccessible { + field: String @tag(name: "field1") @tag(name: "field2") @inaccessible + } + `, + ), + ); + }); + + test('that valid executable directives are merged and persisted in the federated graph', () => { + const { errors, federationResult } = federateSubgraphs([subgraphK, subgraphL]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( + normalizeString(versionOnePersistedBaseSchema + ` + directive @executableDirective(requiredArgInAll: String!, requiredArgInSome: Int!, optionalArgInAll: Float) on FIELD + + type Query { + dummy: String + } + `), + ); + }); }); const subgraphA = { @@ -447,4 +488,76 @@ const subgraphF: Subgraph = { string: String } `), +}; + +const subgraphI: Subgraph = { + name: 'subgraph-i', + url: '', + definitions: parse(` + type Query @shareable @tag(name: "object2") @tag(name: "object1") @tag(name: "object1") { + dummy: String @tag(name: "field1") @tag(name: "field1") @tag(name: "field2") + } + + enum E @tag(name: "enum1") @inaccessible @tag(name: "enum1") @tag(name: "enum2") { + A @tag(name: "enum value2") @tag(name: "enum value2") @tag(name: "enum value1") @inaccessible + } + + input In @tag(name: "input object1") @tag(name: "input object1") @tag(name: "input object2") { + field: String @tag(name: "input value2") @tag(name: "input value2") @tag(name: "input value1") @inaccessible + } + + interface I @tag(name: "interface1") @inaccessible @tag(name: "interface1") @tag(name: "interface2") { + field: String @tag(name: "field1") @tag(name: "field1") @inaccessible @tag(name: "field2") + } + + type O implements I @inaccessible @tag(name: "object2") @tag(name: "object1") @tag(name: "object1") @shareable { + field: String @tag(name: "field1") @inaccessible @tag(name: "field1") @tag(name: "field2") + } + + scalar S @tag(name: "scalar1") @tag(name: "scalar2") @inaccessible @tag(name: "scalar1") + `), +}; + +const subgraphJ: Subgraph = { + name: 'subgraph-j', + url: '', + definitions: parse(` + enum E @inaccessible @tag(name: "enum2") @tag(name: "enum2") @tag(name: "enum1") { + A @tag(name: "enum value1") @tag(name: "enum value2") @tag(name: "enum value1") + } + + input In @tag(name: "input object1") @inaccessible @tag(name: "input object1") @tag(name: "input object2") { + field: String @tag(name: "input value1") @inaccessible @tag(name: "input value2") @tag(name: "input value1") + } + + interface I @tag(name: "interface1") @tag(name: "interface1") @inaccessible @tag(name: "interface2") { + field: String @inaccessible @tag(name: "field1") @tag(name: "field1") @tag(name: "field2") + } + + type O implements I @tag(name: "object2") @shareable @tag(name: "object1") @inaccessible @tag(name: "object1") { + field: String @tag(name: "field1") @tag(name: "field1") @inaccessible @tag(name: "field2") + } + + scalar S @tag(name: "scalar1") @tag(name: "scalar2") @tag(name: "scalar1") @inaccessible + `), +}; + +const subgraphK: Subgraph = { + name: 'subgraph-k', + url: '', + definitions: parse(` + directive @executableDirective(requiredArgInAll: String!, requiredArgInSome: Int!, optionalArgInAll: Float, optionalArg: Boolean) on FIELD | SCHEMA | FIELD_DEFINITION + + type Query { + dummy: String + } + `), +}; + +const subgraphL: Subgraph = { + name: 'subgraph-l', + url: '', + definitions: parse(` + directive @executableDirective(requiredArgInAll: String!, requiredArgInSome: Int, optionalArgInAll: Float) on FIELD | OBJECT + `), }; \ No newline at end of file diff --git a/composition/tests/inputs.test.ts b/composition/tests/inputs.test.ts index ddca28cefc..1194c479ec 100644 --- a/composition/tests/inputs.test.ts +++ b/composition/tests/inputs.test.ts @@ -1,7 +1,7 @@ import { federateSubgraphs, federationRequiredInputFieldError, Subgraph } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionOneBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionOnePersistedBaseSchema } from './utils/utils'; describe('Input federation tests', () => { test('that inputs merge by intersection if the removed fields are nullable', () => { @@ -10,7 +10,7 @@ describe('Input federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + ` + versionOnePersistedBaseSchema + ` type Query { dummy: String! } diff --git a/composition/tests/interfaces.test.ts b/composition/tests/interfaces.test.ts index a73d692d2a..a813501cd9 100644 --- a/composition/tests/interfaces.test.ts +++ b/composition/tests/interfaces.test.ts @@ -8,7 +8,7 @@ import { } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionTwoBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionTwoPersistedBaseSchema } from './utils/utils'; describe('Interface tests', () => { describe('Normalization tests', () => { @@ -166,7 +166,7 @@ describe('Interface tests', () => { const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` interface Character { name: String! age: Int! @@ -199,7 +199,7 @@ describe('Interface tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` interface Character { name: String! age: Int! @@ -230,7 +230,7 @@ describe('Interface tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` interface Character { isFriend: Boolean! } diff --git a/composition/tests/normalization.test.ts b/composition/tests/normalization.test.ts index 1e34b2220f..b09f722f46 100644 --- a/composition/tests/normalization.test.ts +++ b/composition/tests/normalization.test.ts @@ -882,11 +882,10 @@ describe('Normalization tests', () => { const subgraphString = normalizationResult!.subgraphString; expect(normalizeString(subgraphString!)).toBe( normalizeString( - versionTwoBaseSchema + - ` + versionTwoBaseSchema + ` directive @myDirective(a: String!) on FIELD_DEFINITION directive @hello on FIELD_DEFINITION - + type Query { allProducts: [ProductItf] product(id: ID!): ProductItf diff --git a/composition/tests/queries.test.ts b/composition/tests/queries.test.ts index d62cd2c803..acd2892fd2 100644 --- a/composition/tests/queries.test.ts +++ b/composition/tests/queries.test.ts @@ -4,8 +4,8 @@ import { describe, expect, test } from 'vitest'; import { documentNodeToNormalizedString, normalizeString, - versionOneBaseSchema, - versionTwoBaseSchema, + versionOnePersistedBaseSchema, + versionTwoPersistedBaseSchema, } from './utils/utils'; describe('Query federation tests', () => { @@ -15,7 +15,7 @@ describe('Query federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { query: Nested @@ -126,7 +126,7 @@ describe('Query federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { friend: Friend @@ -147,7 +147,7 @@ describe('Query federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` interface Human { name: String! @@ -214,7 +214,7 @@ describe('Query federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` union Human = Friend | Enemy diff --git a/composition/tests/unions.test.ts b/composition/tests/unions.test.ts index 6786655635..dfd3244b0a 100644 --- a/composition/tests/unions.test.ts +++ b/composition/tests/unions.test.ts @@ -1,7 +1,7 @@ import { federateSubgraphs, invalidUnionError, Subgraph } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionOneBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionOnePersistedBaseSchema } from './utils/utils'; describe('Union federation tests', () => { test('that unions merge by union', () => { @@ -10,8 +10,7 @@ describe('Union federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` union Starters = Bulbasaur | Squirtle | Charmander | Chikorita | Totodile | Cyndaquil type Bulbasaur { diff --git a/composition/tests/utils/utils.ts b/composition/tests/utils/utils.ts index cb918e58be..2ea4fe2638 100644 --- a/composition/tests/utils/utils.ts +++ b/composition/tests/utils/utils.ts @@ -9,6 +9,7 @@ export function documentNodeToNormalizedString(document: DocumentNode): string { return normalizeString(print(document)); } +// The V1 definitions that are required during normalization export const versionOneBaseSchema = ` directive @deprecated(reason: String = "No longer supported") on ARGUMENT_DEFINITION | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION directive @extends on INTERFACE | OBJECT @@ -19,11 +20,22 @@ export const versionOneBaseSchema = ` directive @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION `; +// The V1 definitions that are persisted in the raw federated schema +export const versionOnePersistedBaseSchema = ` + directive @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION +`; + +// The V2 definitions that are required during normalization export const versionTwoBaseSchema = versionOneBaseSchema + ` directive @inaccessible on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION directive @shareable on FIELD_DEFINITION | OBJECT `; +// The V2 definitions that are persisted in the raw federated schema +export const versionTwoPersistedBaseSchema = versionOnePersistedBaseSchema + ` + directive @inaccessible on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION +`; + export function createSubgraph(name: string, schemaString: string): Subgraph { return { definitions: parse(schemaString), diff --git a/demo/example_queries.graphql b/demo/example_queries.graphql index e772d3f170..bbf53be5a3 100644 --- a/demo/example_queries.graphql +++ b/demo/example_queries.graphql @@ -8,12 +8,27 @@ query { surname # resolved through employees subgraph location + # resolved through family subgraph + hasChildren + # maritalStatus can return null + maritalStatus + nationality + # pets can return null + pets { + class + gender + name + ... on Cat { + type + } + ... on Dog { + breed + } + ... on Alligator { + dangerous + } + } } - # resolved through family subgraph - hasChildren - # can be null - maritalStatus - nationality # resolved through employees subgraph role { department @@ -25,21 +40,6 @@ query { operatorType } } - # resolved through family subgraph (can be null) - pets { - class - gender - name - ... on Cat { - type - } - ... on Dog { - breed - } - ... on Alligator { - dangerous - } - } # resolved through hobbies subgraph hobbies { ... on Exercise { @@ -67,6 +67,7 @@ query { # resolved through products subgraph products } + # can return null employee(id: 1) { # resolved through employees subgraph id