diff --git a/composition/.gitignore b/composition/.gitignore index 3765735299..1777a8d178 100644 --- a/composition/.gitignore +++ b/composition/.gitignore @@ -1,4 +1,5 @@ dist node_modules .env -.eslintcache \ No newline at end of file +.eslintcache +tests/unstaged-tests \ No newline at end of file diff --git a/composition/src/ast/ast.ts b/composition/src/ast/ast.ts index 9f4f260652..13fd00518b 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,7 @@ import { UnionTypeDefinitionNode, } from 'graphql'; import { federationUnexpectedNodeKindError } from '../errors/errors'; -import { InterfaceTypeExtensionNode } from 'graphql/index'; +import { formatDescription } from './utils'; function deepCopyFieldsAndInterfaces( node: InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode | ObjectTypeExtensionNode, @@ -69,22 +71,43 @@ 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: formatDescription(node.description), + 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)); } } return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: node.kind, name: { ...node.name }, values, @@ -93,36 +116,38 @@ export function enumTypeDefinitionNodeToMutable(node: EnumTypeDefinitionNode): M export type MutableEnumValueDefinitionNode = { description?: StringValueNode; + directives?: ConstDirectiveNode[]; kind: Kind.ENUM_VALUE_DEFINITION; name: NameNode; }; export function enumValueDefinitionNodeToMutable(node: EnumValueDefinitionNode): MutableEnumValueDefinitionNode { return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: node.kind, name: { ...node.name }, } } 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 { arguments: args, - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: node.kind, name: { ...node.name }, type: deepCopyTypeNode(node.type, parentName, node.name.value), @@ -130,7 +155,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; @@ -144,7 +170,7 @@ export function inputObjectTypeDefinitionNodeToMutable(node: InputObjectTypeDefi } } return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), fields, kind: node.kind, name: { ...node.name }, @@ -153,16 +179,17 @@ 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 { return { defaultValue: node.defaultValue ? { ...node.defaultValue } : undefined, - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: node.kind, name: { ...node.name }, type: deepCopyTypeNode(node.type, parentName, node.name.value), @@ -170,11 +197,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 { @@ -182,7 +210,7 @@ export function interfaceTypeDefinitionNodeToMutable(node: InterfaceTypeDefiniti const interfaces: NamedTypeNode[] = []; deepCopyFieldsAndInterfaces(node, fields, interfaces); return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), fields, interfaces, kind: node.kind, @@ -191,7 +219,8 @@ export function interfaceTypeDefinitionNodeToMutable(node: InterfaceTypeDefiniti } export type MutableObjectTypeDefinitionNode = { - description?: StringValueNode, + description?: StringValueNode; + directives?: ConstDirectiveNode[]; fields: FieldDefinitionNode[]; interfaces: NamedTypeNode[]; kind: Kind.OBJECT_TYPE_DEFINITION; @@ -203,7 +232,7 @@ export function objectTypeDefinitionNodeToMutable(node: ObjectTypeDefinitionNode const interfaces: NamedTypeNode[] = []; deepCopyFieldsAndInterfaces(node, fields, interfaces); return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), fields, interfaces, kind: node.kind, @@ -212,7 +241,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,13 +275,14 @@ export function objectTypeExtensionNodeToMutableDefinitionNode(node: ObjectTypeE export type MutableScalarTypeDefinitionNode = { description?: StringValueNode; + directives?: ConstDirectiveNode[]; kind: Kind.SCALAR_TYPE_DEFINITION; name: NameNode; }; export function scalarTypeDefinitionNodeToMutable(node: ScalarTypeDefinitionNode): MutableScalarTypeDefinitionNode { return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: Kind.SCALAR_TYPE_DEFINITION, name: { ...node.name }, }; @@ -266,7 +297,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[]; @@ -280,7 +312,7 @@ export function unionTypeDefinitionNodeToMutable(node: UnionTypeDefinitionNode): } } return { - description: node.description ? { ...node.description } : undefined, + description: formatDescription(node.description), kind: node.kind, name: { ...node.name }, types, @@ -288,13 +320,14 @@ export function unionTypeDefinitionNodeToMutable(node: UnionTypeDefinitionNode): } export type MutableTypeDefinitionNode = - | MutableScalarTypeDefinitionNode - | MutableObjectTypeDefinitionNode - | MutableInterfaceTypeDefinitionNode - | MutableUnionTypeDefinitionNode + MutableDirectiveDefinitionNode | MutableEnumTypeDefinitionNode | MutableInputObjectTypeDefinitionNode - | DirectiveDefinitionNode; + | MutableInterfaceTypeDefinitionNode + | MutableObjectTypeDefinitionNode + | MutableScalarTypeDefinitionNode + | MutableUnionTypeDefinitionNode; + export type ObjectLikeTypeDefinitionNode = InterfaceTypeDefinitionNode diff --git a/composition/src/ast/utils.ts b/composition/src/ast/utils.ts index 30608d100b..3f82b646e5 100644 --- a/composition/src/ast/utils.ts +++ b/composition/src/ast/utils.ts @@ -1,4 +1,5 @@ import { + ConstDirectiveNode, FieldDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, @@ -8,18 +9,14 @@ import { ObjectTypeDefinitionNode, ObjectTypeExtensionNode, OperationTypeNode, + StringValueNode, + UnionTypeExtensionNode, } from 'graphql'; import { - MutableEnumTypeDefinitionNode, MutableEnumValueDefinitionNode, MutableFieldDefinitionNode, - MutableInputObjectTypeDefinitionNode, MutableInputValueDefinitionNode, - MutableInterfaceTypeDefinitionNode, - MutableObjectTypeDefinitionNode, - MutableObjectTypeExtensionNode, - MutableScalarTypeDefinitionNode, - MutableUnionTypeDefinitionNode, + MutableTypeDefinitionNode, ObjectLikeTypeDefinitionNode, } from './ast'; import { @@ -28,19 +25,26 @@ import { EXTENDS, FIELD_DEFINITION_UPPER, FIELDS, + FRAGMENT_DEFINITION_UPPER, + FRAGMENT_SPREAD_UPPER, + INLINE_FRAGMENT_UPPER, + INPUT_FIELD_DEFINITION_UPPER, INPUT_OBJECT_UPPER, INTERFACE_UPPER, KEY, MUTATION, + NAME, OBJECT_UPPER, QUERY, SCALAR_UPPER, SCHEMA_UPPER, SHAREABLE, SUBSCRIPTION, + UNION_UPPER, } from '../utils/string-constants'; import { duplicateInterfaceError, + expectedEntityError, invalidClosingBraceErrorMessage, invalidEntityKeyError, invalidGraphQLNameErrorMessage, @@ -54,132 +58,13 @@ import { unexpectedKindFatalError, } from '../errors/errors'; import { getOrThrowError } from '../utils/utils'; - -export enum MergeMethod { - UNION, - INTERSECTION, - CONSISTENT, -} - -export type EntityContainer = { - fields: Set; - keys: Set; - subgraphs: Set; -}; - -export type EnumContainer = { - appearances: number; - values: EnumValueMap; - kind: Kind.ENUM_TYPE_DEFINITION; - node: MutableEnumTypeDefinitionNode; -}; - -export type EnumValueContainer = { - appearances: number; - node: MutableEnumValueDefinitionNode; -}; - -export type EnumValueMap = Map; - -export type FieldContainer = { - appearances: number; - arguments: InputValueMap; - isShareable: boolean; - node: MutableFieldDefinitionNode; - rootTypeName: string; - subgraphs: Set; - subgraphsByShareable: Map; -}; - -export type FieldMap = Map; - -export type InputValueContainer = { - appearances: number; - includeDefaultValue: boolean; - node: MutableInputValueDefinitionNode; -}; - -export type InputValueMap = Map; - -export type InputObjectContainer = { - appearances: number; - fields: InputValueMap; - kind: Kind.INPUT_OBJECT_TYPE_DEFINITION; - node: MutableInputObjectTypeDefinitionNode; -}; - -export type InterfaceContainer = { - appearances: number; - fields: FieldMap; - interfaces: Set; - kind: Kind.INTERFACE_TYPE_DEFINITION; - node: MutableInterfaceTypeDefinitionNode; - subgraphs: Set; -}; - -export type ObjectContainer = { - appearances: number; - fields: FieldMap; - entityKeys: Set; - interfaces: Set; - isRootType: boolean; - kind: Kind.OBJECT_TYPE_DEFINITION; - node: MutableObjectTypeDefinitionNode; - subgraphs: Set; -}; - -export type ObjectExtensionContainer = { - appearances: number; - fields: FieldMap; - entityKeys: Set; - interfaces: Set; - isRootType: boolean; - kind: Kind.OBJECT_TYPE_EXTENSION; - node: MutableObjectTypeExtensionNode; - subgraphs: Set; -}; - -export type RootTypeField = { - inlineFragment: string; - path: string; - name: string; - parentTypeName: string; - responseType: string; - rootTypeName: string; - subgraphs: Set; -}; - -export type PotentiallyUnresolvableField = { - fieldContainer: FieldContainer; - fullResolverPaths: string[]; - rootTypeField: RootTypeField; -}; - -export type ScalarContainer = { - appearances: number; - kind: Kind.SCALAR_TYPE_DEFINITION; - node: MutableScalarTypeDefinitionNode; -}; - -export type UnionContainer = { - appearances: number; - kind: Kind.UNION_TYPE_DEFINITION; - members: Set; - node: MutableUnionTypeDefinitionNode; -}; - -export type ParentContainer = - | EnumContainer - | InputObjectContainer - | InterfaceContainer - | ObjectContainer - | UnionContainer - | ScalarContainer; - -export type ExtensionContainer = ObjectExtensionContainer; - -export type ParentMap = Map; - +import { UnionTypeDefinitionNode } from 'graphql/index'; +import { + DirectiveContainer, + EXECUTABLE_DIRECTIVE_LOCATIONS, + NodeContainer, + ParentContainer, +} from '../federation/utils'; export function isObjectLikeNodeEntity(node: ObjectLikeTypeDefinitionNode): boolean { // Interface entities are currently unsupported @@ -211,14 +96,19 @@ export function isNodeExtension(node: ObjectTypeDefinitionNode | InterfaceTypeDe export function extractEntityKeys( node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, keySet: Set, + errors: Error[], ): Set { if (!node.directives?.length) { return keySet; } + const typeName = node.name.value; for (const directive of node.directives) { if (directive.name.value === KEY) { if (!directive.arguments) { - throw new Error('Entity without arguments'); // TODO + errors.push(invalidKeyDirectiveError(typeName,[ + undefinedRequiredArgumentsErrorMessage(KEY, typeName, [NAME]) + ])); + continue; } for (const arg of directive.arguments) { if (arg.name.value !== FIELDS) { @@ -252,8 +142,9 @@ export function getEntityKeyExtractionResult(rawEntityKey: string, parentTypeNam let currentSegment = ''; let segmentEnded = true; let currentKey: EntityKey; + rawEntityKey = rawEntityKey.replaceAll(',', ' '); for (const char of rawEntityKey) { - currentKey = getOrThrowError(entityKeyMap, keyPath.join('.')); + currentKey = getOrThrowError(entityKeyMap, keyPath.join('.'), 'entityKeyMap'); switch (char) { case ' ': segmentEnded = true; @@ -327,10 +218,10 @@ export function getEntityKeyExtractionResults( node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, entityKeyMap: Map, ): EntityKeyExtractionResults { + const parentTypeName = node.name.value; if (!node.directives?.length) { - return { entityKeyMap, errors: [new Error('No directives found.')] }; // todo + return { entityKeyMap, errors: [expectedEntityError(parentTypeName)] }; } - const parentTypeName = node.name.value; const rawEntityKeys = new Set(); const errorMessages: string[] = []; for (const directive of node.directives) { @@ -438,7 +329,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)); @@ -446,6 +337,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, @@ -471,6 +370,14 @@ export function nodeKindToDirectiveLocation(kind: Kind): string { return ENUM_VALUE_UPPER; case Kind.FIELD_DEFINITION: return FIELD_DEFINITION_UPPER; + case Kind.FRAGMENT_DEFINITION: + return FRAGMENT_DEFINITION_UPPER; + case Kind.FRAGMENT_SPREAD: + return FRAGMENT_SPREAD_UPPER; + case Kind.INLINE_FRAGMENT: + return INLINE_FRAGMENT_UPPER; + case Kind.INPUT_VALUE_DEFINITION: + return INPUT_FIELD_DEFINITION_UPPER; case Kind.INPUT_OBJECT_TYPE_DEFINITION: // intentional fallthrough case Kind.INPUT_OBJECT_TYPE_EXTENSION: @@ -494,9 +401,9 @@ export function nodeKindToDirectiveLocation(kind: Kind): string { case Kind.UNION_TYPE_DEFINITION: // intentional fallthrough case Kind.UNION_TYPE_EXTENSION: - return SCHEMA_UPPER; + return UNION_UPPER; default: - throw new Error(`Unknown Kind "${kind}".`); // TODO + return kind; } } @@ -513,3 +420,113 @@ 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; +} + +export function addConcreteTypesForImplementedInterfaces( + node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode, + abstractToConcreteTypeNames: Map>, +) { + if (!node.interfaces || node.interfaces.length < 1) { + return; + } + const concreteTypeName = node.name.value; + for (const iFace of node.interfaces) { + const interfaceName = iFace.name.value; + const concreteTypes = abstractToConcreteTypeNames.get(interfaceName); + if (concreteTypes) { + concreteTypes.add(concreteTypeName); + } else { + abstractToConcreteTypeNames.set(interfaceName, new Set([concreteTypeName])); + } + } +} + +export function addConcreteTypesForUnion( + node: UnionTypeDefinitionNode | UnionTypeExtensionNode, abstractToConcreteTypeNames: Map>, +) { + if (!node.types || node.types.length < 1) { + return; + } + const unionName = node.name.value; + for (const member of node.types) { + const memberName = member.name.value; + const concreteTypes = abstractToConcreteTypeNames.get(unionName); + if (concreteTypes) { + concreteTypes.add(memberName); + } else { + abstractToConcreteTypeNames.set(unionName, new Set([memberName])); + } + } +} + +export function formatDescription(description?: StringValueNode): StringValueNode | undefined { + if (!description) { + return description; + } + let value = description.value; + if (description.block) { + const lines = value.split('\n'); + if (lines.length > 1) { + value = lines.map((line) => line.trimStart()).join('\n'); + } + } + return { ...description, value: value, block: true }; +} + +export function setLongestDescriptionForNode( + existingNode: MutableFieldDefinitionNode | MutableEnumValueDefinitionNode | MutableInputValueDefinitionNode | MutableTypeDefinitionNode, + newDescription?: StringValueNode, +) { + if (!newDescription) { + return; + } + if (!existingNode.description || newDescription.value.length > existingNode.description.value.length) { + existingNode.description = { ...newDescription, block: true }; + } +} \ No newline at end of file diff --git a/composition/src/errors/errors.ts b/composition/src/errors/errors.ts index 7726940a85..34472cf1d2 100644 --- a/composition/src/errors/errors.ts +++ b/composition/src/errors/errors.ts @@ -1,4 +1,4 @@ -import { nodeKindToDirectiveLocation, ObjectContainer, RootTypeField } from '../ast/utils'; +import { nodeKindToDirectiveLocation } from '../ast/utils'; import { ConstDirectiveNode, Kind, @@ -7,7 +7,15 @@ import { TypeDefinitionNode, TypeExtensionNode, } from 'graphql'; -import { ImplementationErrorsMap, numberToOrdinal } from '../utils/utils'; +import { + ImplementationErrorsMap, + InvalidArgument, + InvalidRequiredArgument, + kindToTypeString, + numberToOrdinal, +} from '../utils/utils'; +import { ObjectContainer, RootTypeFieldData } from '../federation/utils'; +import { QUOTATION_JOIN, UNION } from '../utils/string-constants'; export const minimumSubgraphRequirementError = new Error('At least one subgraph is required for federation.'); @@ -175,7 +183,7 @@ export function shareableFieldDefinitionsError(parent: ObjectContainer, children } if (shareableSubgraphs.length < 1) { errorMessages.push( - `\n The field "${fieldName}" is defined in the following subgraphs: "${shareableSubgraphs.join('", "')}".` + + `\n The field "${fieldName}" is defined in the following subgraphs: "${[...field.subgraphs].join('", "')}".` + `\n However, it it is not declared "@shareable" in any of them.`, ); } else { @@ -206,28 +214,30 @@ export function undefinedEntityKeyErrorMessage(fieldName: string, objectName: st } export function unresolvableFieldError( - rootTypeField: RootTypeField, + rootTypeFieldData: RootTypeFieldData, fieldName: string, - unresolvablePaths: string[], - fieldSubgraphs: string, + fieldSubgraphs: string[], + unresolvablePath: string, parentTypeName: string, ): Error { const fieldPath = `${parentTypeName}.${fieldName}`; return new Error( - `The following root path${unresolvablePaths.length > 1 ? 's are' : ' is'} unresolvable:\n ` + - unresolvablePaths.join('\n') + - `\nThis is because:\n` + - ` The root type field "${rootTypeField.path}" is defined in the following subgraphs: "` + - [...rootTypeField.subgraphs].join('", "') + `".\n` + - ` However, "${fieldPath}" is only defined in the following subgraphs: "${fieldSubgraphs}".\n` + - ` Consequently, "${fieldPath}" cannot be resolved through the root type field "${rootTypeField.path}".\n` + + `The path "${unresolvablePath}" cannot be resolved because:\n` + + ` The root type field "${rootTypeFieldData.path}" is defined in the following subgraph` + + (rootTypeFieldData.subgraphs.size > 1 ? 's' : '') + `: "` + + [...rootTypeFieldData.subgraphs].join(QUOTATION_JOIN) + `".\n` + + ` However, "${fieldPath}" is only defined in the following subgraph` + + (fieldSubgraphs.length > 1 ? 's' : '') + `: "` + fieldSubgraphs + `".\n` + + ` Consequently, "${fieldPath}" cannot be resolved through the root type field "${rootTypeFieldData.path}".\n` + `Potential solutions:\n` + - ` Convert "${parentTypeName}" into an entity using a "@key" directive.\n` + - ` Add the shareable root type field "${rootTypeField.path}" to the following subgraphs: "${fieldSubgraphs}".\n` + + ` Convert "${parentTypeName}" into an entity using the "@key" directive.\n` + + ` Add the shareable root type field "${rootTypeFieldData.path}" to ` + + (fieldSubgraphs.length > 1 ? 'one of the following subgraphs' : 'the following subgraph') + `: "` + + fieldSubgraphs.join(QUOTATION_JOIN) + `".\n` + ` For example (note that V1 fields are shareable by default and do not require a directive):\n` + - ` type ${rootTypeField.parentTypeName} {\n` + + ` type ${rootTypeFieldData.typeName} {\n` + ` ...\n` + - ` ${rootTypeField.name}: ${rootTypeField.responseType} @shareable\n` + + ` ${rootTypeFieldData.fieldName}: ${rootTypeFieldData.fieldTypeNodeString} @shareable\n` + ` }`, ); } @@ -259,6 +269,10 @@ export function invalidUnionError(unionName: string): Error { return new Error(`Union "${unionName}" must have at least one member.`); } +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, @@ -373,7 +387,7 @@ export function undefinedParentFatalError(parentTypeName: string): Error { export function unexpectedKindFatalError(typeName: string) { return new Error( - `Fatal: Unexpected type for ${typeName}`, + `Fatal: Unexpected type for "${typeName}"`, ); } @@ -383,18 +397,32 @@ export function invalidMultiGraphNodeFatalError(nodeName: string): Error { ); } -export function unexpectedParentKindErrorMessage(parentTypeName: string, expectedKind: Kind, actualKind: Kind): string { - return ( - ` Expected "${parentTypeName}" to be type ${expectedKind} but received "${actualKind}".` +export function incompatibleParentKindFatalError(parentTypeName: string, expectedKind: Kind, actualKind: Kind): Error { + return new Error( + `Fatal: Expected "${parentTypeName}" to be type ${kindToTypeString(expectedKind)}` + + ` but received "${kindToTypeString(actualKind)}".` ); } -export function incompatibleParentKindFatalError(parentTypeName: string, expectedKind: Kind, actualKind: Kind): Error { +export function fieldTypeMergeFatalError(fieldName: string) { return new Error( - `Fatal: Expected "${parentTypeName}" to be type ${expectedKind} but received "${actualKind}".` - ); + `Fatal: Unsuccessfully merged the cross-subgraph types of field "${fieldName}"` + + ` without producing a type error object.` + ) +} + +export function argumentTypeMergeFatalError(argumentName: string, fieldName: string) { + return new Error( + `Fatal: Unsuccessfully merged the cross-subgraph types of argument "${argumentName}" on field "${fieldName}"` + + ` without producing a type error object.` + ) } +export function unexpectedArgumentKindFatalError(argumentName: string, fieldName: string) { + return new Error( + `Fatal: Unexpected type for argument "${argumentName}" on field "${fieldName}".`, + ); +} export function unexpectedDirectiveLocationError(locationName: string): Error { return new Error( @@ -409,6 +437,18 @@ export function unexpectedTypeNodeKindError(childPath: string): Error { ); } +export function invalidKeyFatalError(key: K, mapName: string): Error { + return new Error( + `Fatal: Expected key "${key}" to exist in the map "${mapName}".` + ); +} + +export function unexpectedParentKindErrorMessage(parentTypeName: string, expectedTypeString: string, actualTypeString: string): string { + return ( + ` Expected "${parentTypeName}" to be type ${expectedTypeString} but received "${actualTypeString}".` + ); +} + export function objectInCompositeKeyWithoutSelectionsErrorMessage(fieldName: string, fieldTypeName: string): string { return ( ` The "fields" argument defines "${fieldName}", which is type "${fieldTypeName}, as part of a key.\n` + @@ -512,4 +552,68 @@ export function unimplementedInterfaceFieldsError( `The ${parentTypeString} "${parentTypeName}" has the following interface implementation errors:\n` + messages.join('\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 ${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); +} + +export function duplicateArgumentsError(fieldPath: string, duplicatedArguments: string[]): Error { + return new Error( + `The field "${fieldPath}" is invalid because:\n` + + ` The following argument` + (duplicatedArguments.length > 1 ? 's are' : ' is') + + ` defined more than once: "` + duplicatedArguments.join(`", "`) + `"\n` + ); +} + +export function invalidArgumentsError(fieldPath: string, invalidArguments: InvalidArgument[]): Error { + let message = `The field "${fieldPath}" is invalid because:\n` + + ` The named type (root type) of an input must be on of Enum, Input Object, or Scalar type.` + + ` For example: "Float", "[[String!]]!", or "[SomeInputObjectName]"\n` + for (const invalidArgument of invalidArguments) { + message += ` The argument "${invalidArgument.argumentName}" defines type "${invalidArgument.typeName}"` + + ` but the named type "${invalidArgument.namedType}" is type "` + invalidArgument.typeString + + `", which is not a valid input type.\n`; + } + return new Error(message); +} + +export const noQueryRootTypeError = new Error( + `A valid federated graph must have at least one populated query root type.\n` + + ` For example:\n` + + ` type Query {\n` + + ` dummy: String\n` + + ` }` +); + +export function unexpectedObjectResponseType(fieldPath: string, actualTypeString: string): Error { + return new Error( + `Expected the path "${fieldPath}" to have the response type` + + ` Enum, Interface, Object, Scalar, or Union but received ${actualTypeString}.` + ); +} + +export function noConcreteTypesForAbstractTypeError(typeString: string, abstractTypeName: string): Error { + return new Error( + `Expected ${typeString} "${abstractTypeName}" to define at least one ` + + (typeString === UNION ? 'member' : 'object that implements the interface') + + ` but received none` + ); +} + +export function expectedEntityError(typeName: string): Error { + return new Error( + `Expected object "${typeName}" to define a "key" directive, but it defines no directives.` + ); } \ No newline at end of file diff --git a/composition/src/federation/federation-factory.ts b/composition/src/federation/federation-factory.ts index bf5a9abcad..1002d74311 100644 --- a/composition/src/federation/federation-factory.ts +++ b/composition/src/federation/federation-factory.ts @@ -1,29 +1,31 @@ import { MultiGraph } from 'graphology'; -import { allSimplePaths } from 'graphology-simple-path'; import { buildASTSchema, + ConstDirectiveNode, ConstValueNode, DirectiveDefinitionNode, DocumentNode, EnumValueDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, - InterfaceTypeDefinitionNode, Kind, NamedTypeNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, TypeDefinitionNode, - UnionTypeDefinitionNode, + TypeNode, } from 'graphql'; import { ConstValueNodeWithValue, + directiveDefinitionNodeToMutable, enumTypeDefinitionNodeToMutable, enumValueDefinitionNodeToMutable, fieldDefinitionNodeToMutable, inputObjectTypeDefinitionNodeToMutable, inputValueDefinitionNodeToMutable, interfaceTypeDefinitionNodeToMutable, + MutableEnumValueDefinitionNode, + MutableInputValueDefinitionNode, MutableTypeDefinitionNode, objectTypeDefinitionNodeToMutable, objectTypeExtensionNodeToMutable, @@ -32,44 +34,44 @@ import { unionTypeDefinitionNodeToMutable, } from '../ast/ast'; import { - EntityContainer, - EnumValueContainer, - ExtensionContainer, extractEntityKeys, + extractExecutableDirectiveLocations, extractInterfaces, - FieldContainer, - getInlineFragmentString, - InputValueContainer, - InterfaceContainer, + getNodeWithPersistedDirectives, isNodeShareable, - MergeMethod, - ObjectContainer, - ObjectExtensionContainer, - ParentContainer, - ParentMap, - PotentiallyUnresolvableField, - RootTypeField, + mergeExecutableDirectiveLocations, + pushPersistedDirectivesToNode, + setLongestDescriptionForNode, + setToNameNodeArray, stringToNamedTypeNode, } from '../ast/utils'; import { + argumentTypeMergeFatalError, federationInvalidParentTypeError, federationRequiredInputFieldError, + fieldTypeMergeFatalError, incompatibleArgumentDefaultValueError, incompatibleArgumentDefaultValueTypeError, incompatibleArgumentTypesError, incompatibleChildTypesError, incompatibleParentKindFatalError, incompatibleSharedEnumError, - invalidMultiGraphNodeFatalError, + invalidRequiredArgumentsError, invalidSubgraphNameErrorMessage, invalidSubgraphNamesError, + invalidTagDirectiveError, invalidUnionError, minimumSubgraphRequirementError, noBaseTypeExtensionError, + noConcreteTypesForAbstractTypeError, + noQueryRootTypeError, shareableFieldDefinitionsError, subgraphValidationError, subgraphValidationFailureErrorMessage, + undefinedTypeError, + unexpectedArgumentKindFatalError, unexpectedKindFatalError, + unexpectedObjectResponseType, unimplementedInterfaceFieldsError, unresolvableFieldError, } from '../errors/errors'; @@ -79,93 +81,83 @@ import { getNamedTypeForChild, isTypeRequired, } from '../type-merging/type-merging'; -import { FederationResult } from './federation-result'; +import { + ArgumentContainer, + ArgumentMap, + DirectiveContainer, + DirectiveMap, + EntityContainer, + EnumValueContainer, + ExtensionContainer, + FederationResultContainer, + FieldContainer, + InputValueContainer, + InterfaceContainer, + MergeMethod, + ObjectContainer, + ObjectLikeContainer, + ParentContainer, + ParentMap, + PersistedDirectivesContainer, + RootTypeFieldData, +} from './utils'; import { InternalSubgraph, Subgraph, validateSubgraphName, - walkSubgraphToCollectObjects, - walkSubgraphToCollectOperationsAndFields, + walkSubgraphToCollectFields, + walkSubgraphToCollectObjectLikesAndDirectiveDefinitions, walkSubgraphToFederate, } from '../subgraph/subgraph'; import { DEFAULT_MUTATION, DEFAULT_QUERY, DEFAULT_SUBSCRIPTION, - FIELD_NAME, FRAGMENT_REPRESENTATION, - INLINE_FRAGMENT, + DEPRECATED, + DIRECTIVE_DEFINITION, + ENTITIES, + EXTENSIONS, + FIELD, + INACCESSIBLE, + PARENTS, + QUERY, + ROOT_TYPES, + SELECTION_REPRESENTATION, + TAG, } from '../utils/string-constants'; import { + addIterableValuesToSet, doSetsHaveAnyOverlap, + getEntriesNotInHashSet, getOrThrowError, + hasSimplePath, ImplementationErrors, InvalidFieldImplementation, - isTypeValidImplementation, + InvalidRequiredArgument, kindToTypeString, } from '../utils/utils'; -import { normalizeSubgraph } from '../normalization/normalization-factory'; import { printTypeNode } from '@graphql-tools/merge'; - -export function federateSubgraphs(subgraphs: Subgraph[]): FederationResult { - if (subgraphs.length < 1) { - throw minimumSubgraphRequirementError; - } - const normalizedSubgraphs: InternalSubgraph[] = []; - const validationErrors: Error[] = []; - const subgraphNames = new Set(); - const nonUniqueSubgraphNames = new Set(); - const invalidNameErrorMessages: string[] = []; - for (let i = 0; i < subgraphs.length; i++) { - const subgraph = subgraphs[i]; - const name = subgraph.name || `subgraph-${i}-${Date.now()}`; - if (!subgraph.name) { - invalidNameErrorMessages.push(invalidSubgraphNameErrorMessage(i, name)); - } else { - validateSubgraphName(subgraph.name, subgraphNames, nonUniqueSubgraphNames); - } - const { errors, normalizationResult } = normalizeSubgraph(subgraph.definitions); - if (errors) { - validationErrors.push(subgraphValidationError(name, errors)); - continue; - } - if (!normalizationResult) { - validationErrors.push(subgraphValidationError(name, [subgraphValidationFailureErrorMessage])); - continue; - } - normalizedSubgraphs.push({ - definitions: normalizationResult.subgraphAST, - isVersionTwo: normalizationResult.isVersionTwo, - name, - operationTypes: normalizationResult.operationTypes, - url: subgraph.url, - }); - } - const allErrors: Error[] = []; - if (invalidNameErrorMessages.length > 0 || nonUniqueSubgraphNames.size > 0) { - allErrors.push(invalidSubgraphNamesError([...nonUniqueSubgraphNames], invalidNameErrorMessages)); - } - allErrors.push(...validationErrors); - if (allErrors.length > 0) { - return { errors: allErrors }; - } - const federationFactory = new FederationFactory(normalizedSubgraphs); - return federationFactory.federate(); -} +import { ArgumentConfigurationData } from '../subgraph/field-configuration'; +import { BASE_SCALARS } from '../utils/constants'; +import { normalizeSubgraph } from '../normalization/normalization-factory'; export class FederationFactory { abstractToConcreteTypeNames = new Map>(); areFieldsShareable = false; argumentTypeNameSet = new Set(); + argumentConfigurations: ArgumentConfigurationData[] = []; + executableDirectives = new Set(); parentTypeName = ''; + persistedDirectives = new Set([DEPRECATED, INACCESSIBLE, TAG]); currentSubgraphName = ''; childName = ''; - directiveDefinitions = new Map(); - entityMap = new Map(); + directiveDefinitions: DirectiveMap = new Map(); + entities = new Map(); errors: Error[] = []; extensions = new Map(); graph: MultiGraph = new MultiGraph(); graphEdges = new Set(); - graphPaths = new Map>(); + graphPaths = new Map(); inputFieldTypeNameSet = new Set(); isCurrentParentEntity = false; isCurrentParentInterface = false; @@ -174,10 +166,8 @@ export class FederationFactory { isParentRootType = false; isParentInputObject = false; outputFieldTypeNameSet = new Set(); - parentMap: ParentMap = new Map(); - rootTypeFieldsByResponseTypeName = new Map>(); + parents: ParentMap = new Map(); rootTypeNames = new Set([DEFAULT_MUTATION, DEFAULT_QUERY, DEFAULT_SUBSCRIPTION]); - sharedRootTypeFieldDependentResponses = new Map(); subgraphs: InternalSubgraph[] = []; shareableErrorTypeNames = new Map>(); @@ -191,15 +181,15 @@ export class FederationFactory { upsertEntity(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode) { const typeName = node.name.value; - const entity = this.entityMap.get(typeName); + const entity = this.entities.get(typeName); if (entity) { - extractEntityKeys(node, entity.keys); + extractEntityKeys(node, entity.keys, this.errors); entity.subgraphs.add(this.currentSubgraphName); return; } - this.entityMap.set(typeName, { + this.entities.set(typeName, { fields: new Set(), - keys: extractEntityKeys(node, new Set()), + keys: extractEntityKeys(node, new Set(), this.errors), subgraphs: new Set([this.currentSubgraphName]), }); } @@ -207,15 +197,11 @@ 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); } } - isParentInterface(parent: ParentContainer): boolean { - return parent.kind === Kind.INTERFACE_TYPE_DEFINITION; - } - isFieldEntityKey(parent: ParentContainer | ExtensionContainer): boolean { if (parent.kind === Kind.OBJECT_TYPE_DEFINITION || parent.kind === Kind.OBJECT_TYPE_EXTENSION) { return parent.entityKeys.has(this.childName); @@ -263,13 +249,14 @@ export class FederationFactory { } } - compareAndValidateArgumentDefaultValues(existingArg: InputValueContainer, newArg: InputValueDefinitionNode) { + compareAndValidateArgumentDefaultValues(existingArg: ArgumentContainer, newArg: InputValueDefinitionNode) { const newDefaultValue = newArg.defaultValue; existingArg.node.defaultValue = existingArg.node.defaultValue || newDefaultValue; if (!existingArg.node.defaultValue || !newDefaultValue) { existingArg.includeDefaultValue = false; return; } + const argumentName = existingArg.node.name.value; const existingDefaultValue = existingArg.node.defaultValue; switch (existingDefaultValue.kind) { case Kind.LIST: // TODO @@ -284,100 +271,62 @@ export class FederationFactory { case Kind.FLOAT: case Kind.INT: case Kind.STRING: - this.validateArgumentDefaultValues(existingArg.node.name.value, existingDefaultValue, newDefaultValue); + this.validateArgumentDefaultValues(argumentName, existingDefaultValue, newDefaultValue); break; default: - throw new Error('Unexpected argument type'); // TODO + throw unexpectedArgumentKindFatalError(argumentName, this.childName); } } - upsertArgumentsForFieldNode(node: FieldDefinitionNode, existingFieldNode: FieldContainer) { - if (!node.arguments) { - return; - } - 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 existingArg = existingFieldNode.arguments.get(argName); - if (existingArg) { - existingArg.appearances += 1; - existingArg.node.description = existingArg.node.description || arg.description; - const { typeErrors, typeNode } = getMostRestrictiveMergedTypeNode( - existingArg.node.type, - arg.type, - this.childName, - argName, - ); - if (typeNode) { - existingArg.node.type = typeNode; - } else { - if (!typeErrors || typeErrors.length < 2) { - throw new Error(''); // TODO this should never happen - } - this.errors.push( - incompatibleArgumentTypesError(argName, this.parentTypeName, this.childName, typeErrors[0], typeErrors[1]), - ); - } - this.compareAndValidateArgumentDefaultValues(existingArg, arg); - return; - } - const newNode = inputValueDefinitionNodeToMutable(arg, this.childName); - // TODO validation of default values - existingFieldNode.arguments.set(argName, { - appearances: 1, - includeDefaultValue: !!arg.defaultValue, - node: newNode, - }); + upsertRequiredSubgraph(set: Set, isRequired: boolean): Set { + if (isRequired) { + set.add(this.currentSubgraphName); } + return set; } - extractArgumentsFromFieldNode(node: FieldDefinitionNode, args: Map) { + // TODO validation of default values + upsertArguments(node: DirectiveDefinitionNode | FieldDefinitionNode, argumentMap: ArgumentMap): ArgumentMap { if (!node.arguments) { - return; + return argumentMap; } - for (const arg of node.arguments) { - const argName = arg.name.value; + for (const argumentNode of node.arguments) { + const argName = argumentNode.name.value; const argPath = `${node.name.value}(${argName}...)`; - args.set(argName, { - appearances: 1, - includeDefaultValue: !!arg.defaultValue, - node: inputValueDefinitionNodeToMutable(arg, this.childName), - }); - this.argumentTypeNameSet.add(getNamedTypeForChild(argPath, arg.type)); - } - } - - addConcreteTypesForInterface(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode) { - if (!node.interfaces || node.interfaces.length < 1) { - return; - } - const concreteTypeName = node.name.value; - for (const iFace of node.interfaces) { - const interfaceName = iFace.name.value; - const concreteTypes = this.abstractToConcreteTypeNames.get(interfaceName); - if (concreteTypes) { - concreteTypes.add(concreteTypeName); - } else { - this.abstractToConcreteTypeNames.set(interfaceName, new Set([concreteTypeName])); + this.argumentTypeNameSet.add(getNamedTypeForChild(argPath, argumentNode.type)); + const isRequired = isTypeRequired(argumentNode.type); + const existingArgumentContainer = argumentMap.get(argName); + if (!existingArgumentContainer) { + argumentMap.set(argName, { + includeDefaultValue: !!argumentNode.defaultValue, + node: inputValueDefinitionNodeToMutable(argumentNode, this.childName), + requiredSubgraphs: this.upsertRequiredSubgraph(new Set(), isRequired), + subgraphs: new Set([this.currentSubgraphName]), + }); + continue; } - } - } - - addConcreteTypesForUnion(node: UnionTypeDefinitionNode) { - if (!node.types || node.types.length < 1) { - return; - } - const unionName = node.name.value; - for (const member of node.types) { - const memberName = member.name.value; - const concreteTypes = this.abstractToConcreteTypeNames.get(memberName); - if (concreteTypes) { - concreteTypes.add(memberName); + setLongestDescriptionForNode(existingArgumentContainer.node, argumentNode.description); + this.upsertRequiredSubgraph(existingArgumentContainer.requiredSubgraphs, isRequired); + existingArgumentContainer.subgraphs.add(this.currentSubgraphName); + const { typeErrors, typeNode } = getMostRestrictiveMergedTypeNode( + existingArgumentContainer.node.type, + argumentNode.type, + this.childName, + argName, + ); + if (typeNode) { + existingArgumentContainer.node.type = typeNode; } else { - this.abstractToConcreteTypeNames.set(unionName, new Set([memberName])); + if (!typeErrors || typeErrors.length < 2) { + throw argumentTypeMergeFatalError(argName, this.childName); + } + this.errors.push( + incompatibleArgumentTypesError(argName, this.parentTypeName, this.childName, typeErrors[0], typeErrors[1]), + ); } + this.compareAndValidateArgumentDefaultValues(existingArgumentContainer, argumentNode); } + return argumentMap; } isFieldShareable(node: FieldDefinitionNode, parent: ParentContainer | ExtensionContainer): boolean { @@ -389,107 +338,43 @@ 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, rootTypeFields] of this.rootTypeFieldsByResponseTypeName) { - const paths = this.getAllSimplePaths(responseTypeName); - // If the rootTypeFields response type has no path to the parent type, continue - if (paths.length < 1) { - continue; - } - // Construct all possible paths to the unresolvable field but with the fieldName relationship between nodes - const partialResolverPaths: string[] = []; - for (const path of paths) { - let hasEntityAncestor = false; - let resolverPath: string = ''; - for (let i = 0; i < path.length - 1; i++) { - const pathParent = path[i]; - // The field in question is resolvable if it has an entity ancestor within the same subgraph - // Unresolvable fields further up the chain will be handled elsewhere - const entity = this.entityMap.get(pathParent); - if (entity && entity.subgraphs.has(this.currentSubgraphName)) { - hasEntityAncestor = true; - break; - } - const edges = this.graph.edges(pathParent, path[i + 1])!; - // If there are multiple edges, pick the first one - const inlineFragment: string | undefined = this.graph.getEdgeAttribute(edges[0], INLINE_FRAGMENT); - const edgeName: string = this.graph.getEdgeAttribute(edges[0], FIELD_NAME); - // If the parent field is an abstract type, the child should be proceeded by an inline fragment - resolverPath += edgeName + (inlineFragment || '.'); - } - if (hasEntityAncestor) { - continue; - } - // Add the unresolvable field to each path - resolverPath += fieldName; - // If the field could have fields itself, add ellipsis - if (this.graph.hasNode(fieldContainer.rootTypeName)) { - resolverPath += FRAGMENT_REPRESENTATION; - } - partialResolverPaths.push(resolverPath); - } - if (partialResolverPaths.length < 1) { + upsertDirectiveNode(node: DirectiveDefinitionNode) { + const directiveName = node.name.value; + const directiveDefinition = this.directiveDefinitions.get(directiveName); + if (directiveDefinition) { + if (!this.executableDirectives.has(directiveName)) { return; } - // Each of these rootTypeFields returns a type that has a path to the parent - for (const [rootTypeFieldPath, rootTypeField] of rootTypeFields) { - // If the rootTypeField is defined in a subgraph that the field is defined, it is resolvable - 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(`${rootTypeFieldPath}${rootTypeField.inlineFragment}${partialResolverPath}`); - } - const potentiallyUnresolvableField: PotentiallyUnresolvableField = { - fieldContainer, - fullResolverPaths, - rootTypeField: rootTypeField, - }; - - // The parent might already have unresolvable fields that have already been added - const dependentResponsesByFieldName = this.sharedRootTypeFieldDependentResponses.get(this.parentTypeName); - if (dependentResponsesByFieldName) { - dependentResponsesByFieldName.push(potentiallyUnresolvableField); - return; - } - this.sharedRootTypeFieldDependentResponses.set(this.parentTypeName, [potentiallyUnresolvableField]); + if (mergeExecutableDirectiveLocations(node.locations, directiveDefinition).size < 1) { + this.executableDirectives.delete(directiveName); + return; } + this.upsertArguments(node, directiveDefinition.arguments); + setLongestDescriptionForNode(directiveDefinition.node, 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) - : getOrThrowError(this.parentMap, this.parentTypeName); + ? getOrThrowError(this.extensions, this.parentTypeName, EXTENSIONS) + : getOrThrowError(this.parents, this.parentTypeName, PARENTS); if ( - parent.kind !== Kind.OBJECT_TYPE_DEFINITION && - parent.kind !== Kind.INTERFACE_TYPE_DEFINITION && - parent.kind !== Kind.OBJECT_TYPE_EXTENSION + parent.kind !== Kind.OBJECT_TYPE_DEFINITION + && parent.kind !== Kind.INTERFACE_TYPE_DEFINITION + && parent.kind !== Kind.OBJECT_TYPE_EXTENSION ) { throw unexpectedKindFatalError(this.parentTypeName); } @@ -497,32 +382,31 @@ export class FederationFactory { const isFieldShareable = this.isFieldShareable(node, parent); const fieldPath = `${this.parentTypeName}.${this.childName}`; const fieldRootTypeName = getNamedTypeForChild(fieldPath, node.type); - const existingFieldNode = fieldMap.get(this.childName); - const entityParent = this.entityMap.get(this.parentTypeName); - if (existingFieldNode) { - existingFieldNode.appearances += 1; - existingFieldNode.node.description = existingFieldNode.node.description || node.description; - existingFieldNode.subgraphs.add(this.currentSubgraphName); - existingFieldNode.subgraphsByShareable.set(this.currentSubgraphName, isFieldShareable); + const existingFieldContainer = fieldMap.get(this.childName); + if (existingFieldContainer) { + this.extractPersistedDirectives(node.directives || [], existingFieldContainer.directives); + setLongestDescriptionForNode(existingFieldContainer.node, node.description); + existingFieldContainer.subgraphs.add(this.currentSubgraphName); + existingFieldContainer.subgraphsByShareable.set(this.currentSubgraphName, isFieldShareable); const { typeErrors, typeNode } = getLeastRestrictiveMergedTypeNode( - existingFieldNode.node.type, + existingFieldContainer.node.type, node.type, this.parentTypeName, this.childName, ); if (typeNode) { - existingFieldNode.node.type = typeNode; + existingFieldContainer.node.type = typeNode; } else { if (!typeErrors || typeErrors.length < 2) { - throw new Error(''); // TODO this should never happen + throw fieldTypeMergeFatalError(this.childName); } this.errors.push( incompatibleChildTypesError(this.parentTypeName, this.childName, typeErrors[0], typeErrors[1]), ); } - this.upsertArgumentsForFieldNode(node, existingFieldNode); + this.upsertArguments(node, existingFieldContainer.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)) { + if (!this.isCurrentParentInterface && (!existingFieldContainer.isShareable || !isFieldShareable)) { const shareableErrorTypeNames = this.shareableErrorTypeNames.get(this.parentTypeName); if (shareableErrorTypeNames) { shareableErrorTypeNames.add(this.childName); @@ -532,31 +416,26 @@ export class FederationFactory { } return; } - const args = new Map(); - this.extractArgumentsFromFieldNode(node, args); this.outputFieldTypeNameSet.add(fieldRootTypeName); fieldMap.set(this.childName, { - appearances: 1, - arguments: args, + 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, + namedTypeName: fieldRootTypeName, subgraphs: new Set([this.currentSubgraphName]), subgraphsByShareable: new Map([[this.currentSubgraphName, isFieldShareable]]), }); - - if ( - this.isParentRootType || - entityParent?.fields.has(this.childName) || - parent.kind === Kind.INTERFACE_TYPE_DEFINITION - ) { - return; - } - this.addPotentiallyUnresolvableField(parent, this.childName); } upsertValueNode(node: EnumValueDefinitionNode | InputValueDefinitionNode) { - const parent = this.parentMap.get(this.parentTypeName); + const parent = this.parents.get(this.parentTypeName); switch (node.kind) { case Kind.ENUM_VALUE_DEFINITION: if (!parent) { @@ -564,44 +443,53 @@ export class FederationFactory { throw federationInvalidParentTypeError(this.parentTypeName, this.childName); } if (parent.kind !== Kind.ENUM_TYPE_DEFINITION) { - throw incompatibleParentKindFatalError(this.parentTypeName, Kind.ENUM_TYPE_DEFINITION, parent.kind) + 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); + setLongestDescriptionForNode(enumValueContainer.node, 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; + setLongestDescriptionForNode(inputValueContainer.node, 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 new Error(''); // TODO this should never happen + throw fieldTypeMergeFatalError(this.childName); } this.errors.push( incompatibleChildTypesError(this.parentTypeName, this.childName, typeErrors[0], typeErrors[1]), @@ -614,6 +502,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), }); @@ -625,10 +520,10 @@ export class FederationFactory { upsertParentNode(node: TypeDefinitionNode) { const parentTypeName = node.name.value; - const parent = this.parentMap.get(parentTypeName); + const parent = this.parents.get(parentTypeName); if (parent) { - parent.node.description = parent.node.description || node.description; - parent.appearances += 1; + setLongestDescriptionForNode(parent.node, node.description); + this.extractPersistedDirectives(node.directives || [], parent.directives); } switch (node.kind) { case Kind.ENUM_TYPE_DEFINITION: @@ -636,10 +531,18 @@ export class FederationFactory { if (parent.kind !== node.kind) { throw incompatibleParentKindFatalError(parentTypeName, node.kind, parent.kind); } + parent.appearances += 1; return; } - this.parentMap.set(parentTypeName, { + this.parents.set(parentTypeName, { appearances: 1, + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), values: new Map(), kind: node.kind, node: enumTypeDefinitionNodeToMutable(node), @@ -650,10 +553,18 @@ export class FederationFactory { if (parent.kind !== node.kind) { throw incompatibleParentKindFatalError(parentTypeName, node.kind, parent.kind); } + parent.appearances += 1; return; } - this.parentMap.set(parentTypeName, { + this.parents.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,8 +581,14 @@ export class FederationFactory { } const nestedInterfaces = new Set(); extractInterfaces(node, nestedInterfaces); - this.parentMap.set(parentTypeName, { - appearances: 1, + this.parents.set(parentTypeName, { + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), fields: new Map(), interfaces: nestedInterfaces, kind: node.kind, @@ -686,8 +603,14 @@ export class FederationFactory { } return; } - this.parentMap.set(parentTypeName, { - appearances: 1, + this.parents.set(parentTypeName, { + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), kind: node.kind, node: scalarTypeDefinitionNodeToMutable(node), }); @@ -698,16 +621,22 @@ export class FederationFactory { throw incompatibleParentKindFatalError(parentTypeName, node.kind, parent.kind); } extractInterfaces(node, parent.interfaces); - extractEntityKeys(node, parent.entityKeys); + extractEntityKeys(node, parent.entityKeys, this.errors); parent.subgraphs.add(this.currentSubgraphName); return; } const interfaces = new Set(); extractInterfaces(node, interfaces); const entityKeys = new Set(); - extractEntityKeys(node, entityKeys); - this.parentMap.set(parentTypeName, { - appearances: 1, + extractEntityKeys(node, entityKeys, this.errors); + this.parents.set(parentTypeName, { + directives: this.extractPersistedDirectives( + node.directives || [], + { + directives: new Map(), + tags: new Map(), + }, + ), fields: new Map(), entityKeys, interfaces, @@ -729,8 +658,14 @@ export class FederationFactory { node.types?.forEach((member) => parent.members.add(member.name.value)); return; } - this.parentMap.set(parentTypeName, { - appearances: 1, + this.parents.set(parentTypeName, { + 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), @@ -739,133 +674,71 @@ export class FederationFactory { } } - upsertConcreteObjectLikeOperationFieldNode( - fieldName: string, - fieldTypeName: string, - operationFieldPath: string, - responseType: string, - concreteTypeName = fieldTypeName, - hasAbstractParent = false, - ) { - const operationFields = this.rootTypeFieldsByResponseTypeName.get(concreteTypeName); - if (!operationFields) { - this.rootTypeFieldsByResponseTypeName.set( - concreteTypeName, - new Map([ - [ - operationFieldPath, - { - inlineFragment: hasAbstractParent ? getInlineFragmentString(concreteTypeName) : '.', - name: fieldName, - parentTypeName: this.parentTypeName, - path: operationFieldPath, - responseType, - rootTypeName: fieldTypeName, - subgraphs: new Set([this.currentSubgraphName]), - }, - ], - ]), - ); - return; - } - const operationField = operationFields.get(operationFieldPath); - if (operationField) { - operationField.subgraphs.add(this.currentSubgraphName); - return; - } - operationFields.set(operationFieldPath, { - inlineFragment: hasAbstractParent ? getInlineFragmentString(concreteTypeName) : '.', - name: fieldName, - parentTypeName: this.parentTypeName, - path: operationFieldPath, - responseType, - rootTypeName: fieldTypeName, - subgraphs: new Set([this.currentSubgraphName]), - }); - } - - upsertAbstractObjectLikeOperationFieldNode( - fieldName: string, - fieldTypeName: string, - operationFieldPath: string, - responseType: string, - concreteTypeNames: Set, - ) { - for (const concreteTypeName of concreteTypeNames) { - if (!this.graph.hasNode(concreteTypeName)) { - throw invalidMultiGraphNodeFatalError(concreteTypeName); // should never happen - } - if (!this.graphEdges.has(operationFieldPath)) { - this.graph.addEdge(this.parentTypeName, concreteTypeName, { fieldName }); - } - // Always upsert the operation field node to record subgraph appearances - this.upsertConcreteObjectLikeOperationFieldNode( - fieldName, - fieldTypeName, - operationFieldPath, - responseType, - concreteTypeName, - true, - ); - } - // Add the path so the edges are not added again - this.graphEdges.add(operationFieldPath); - } - - validatePotentiallyUnresolvableFields() { - if (this.sharedRootTypeFieldDependentResponses.size < 1) { - return; - } - for (const [parentTypeName, potentiallyUnresolvableFields] of this.sharedRootTypeFieldDependentResponses) { - for (const potentiallyUnresolvableField of potentiallyUnresolvableFields) { - // There is no issue if the field is resolvable from at least one subgraph - const operationField = potentiallyUnresolvableField.rootTypeField; - const fieldContainer = potentiallyUnresolvableField.fieldContainer; - if (doSetsHaveAnyOverlap(fieldContainer.subgraphs, operationField.subgraphs)) { - continue; - } - const fieldSubgraphs = [...fieldContainer.subgraphs].join('", "'); - this.errors.push( - unresolvableFieldError( - operationField, - fieldContainer.node.name.value, - potentiallyUnresolvableField.fullResolverPaths, - fieldSubgraphs, - parentTypeName, - ), - ); - } - } - } - upsertExtensionNode(node: ObjectTypeExtensionNode) { const extension = this.extensions.get(this.parentTypeName); if (extension) { 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.errors); 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]), }); } + isTypeValidImplementation(originalType: TypeNode, implementationType: TypeNode): boolean { + if (originalType.kind === Kind.NON_NULL_TYPE) { + if (implementationType.kind !== Kind.NON_NULL_TYPE) { + return false; + } + return this.isTypeValidImplementation(originalType.type, implementationType.type); + } + if (implementationType.kind === Kind.NON_NULL_TYPE) { + return this.isTypeValidImplementation(originalType, implementationType.type); + } + switch (originalType.kind) { + case Kind.NAMED_TYPE: + if (implementationType.kind === Kind.NAMED_TYPE) { + const originalTypeName = originalType.name.value; + const implementationTypeName = implementationType.name.value; + if (originalTypeName === implementationTypeName) { + return true; + } + const concreteTypes = this.abstractToConcreteTypeNames.get(originalTypeName); + if (!concreteTypes) { + return false; + } + return concreteTypes.has(implementationTypeName); + } + return false; + default: + if (implementationType.kind === Kind.LIST_TYPE) { + return this.isTypeValidImplementation(originalType.type, implementationType.type); + } + return false; + } + } + getAndValidateImplementedInterfaces(container: ObjectContainer | InterfaceContainer): NamedTypeNode[] { const interfaces: NamedTypeNode[] = []; if (container.interfaces.size < 1) { @@ -874,7 +747,11 @@ export class FederationFactory { const implementationErrorsMap = new Map(); for (const interfaceName of container.interfaces) { interfaces.push(stringToNamedTypeNode(interfaceName)); - const interfaceContainer = getOrThrowError(this.parentMap, interfaceName); + const interfaceContainer = this.parents.get(interfaceName); + if (!interfaceContainer) { + this.errors.push(undefinedTypeError(interfaceName)); + continue; + } if (interfaceContainer.kind !== Kind.INTERFACE_TYPE_DEFINITION) { throw incompatibleParentKindFatalError(interfaceName, Kind.INTERFACE_TYPE_DEFINITION, interfaceContainer.kind); } @@ -898,7 +775,7 @@ export class FederationFactory { unimplementedArguments: new Set(), }; // The implemented field type must be equally or more restrictive than the original interface field type - if (!isTypeValidImplementation(interfaceField.node.type, containerField.node.type)) { + if (!this.isTypeValidImplementation(interfaceField.node.type, containerField.node.type)) { hasErrors = true; hasNestedErrors = true; invalidFieldImplementation.implementedResponseType = printTypeNode(containerField.node.type); @@ -947,13 +824,313 @@ export class FederationFactory { } if (implementationErrorsMap.size) { this.errors.push(unimplementedInterfaceFieldsError( - container.node.name.value, kindToTypeString(container.kind), implementationErrorsMap + container.node.name.value, kindToTypeString(container.kind), implementationErrorsMap, )); } return interfaces; } - federate(): FederationResult { + mergeArguments( + container: FieldContainer | DirectiveContainer, + args: MutableInputValueDefinitionNode[], + errors: InvalidRequiredArgument[], + argumentNames?: string[], + ) { + for (const argumentContainer of container.arguments.values()) { + const missingSubgraphs = getEntriesNotInHashSet( + container.subgraphs, argumentContainer.subgraphs, + ); + const argumentName = argumentContainer.node.name.value; + if (missingSubgraphs.length > 0) { + // Required arguments must be defined in all subgraphs that define the field + if (argumentContainer.requiredSubgraphs.size > 0) { + errors.push({ + argumentName, + missingSubgraphs, + requiredSubgraphs: [...argumentContainer.requiredSubgraphs] + }); + } + // If the argument is always optional, but it's not defined in all subgraphs that define the field, + // the argument should not be included in the federated graph + continue; + } + argumentContainer.node.defaultValue = argumentContainer.includeDefaultValue + ? argumentContainer.node.defaultValue : undefined; + args.push(argumentContainer.node); + 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(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, + fieldName, + typeName: parentTypeName + }); + } + fieldContainer.node.arguments = args; + 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, 'directiveDefinitions', + ); + if (!definition.node.repeatable) { + continue; + } + existingDirectives.push(directive); + } + return container; + } + + entityAncestor(entityAncestors: string[], fieldSubgraphs: Set, parentTypeName: string): boolean { + if (!this.graph.hasNode(parentTypeName)) { + return false; + } + for (const entityAncestorName of entityAncestors) { + const path = `${entityAncestorName}.${parentTypeName}`; + if (this.graphPaths.get(path)) { + return true; + } + if (entityAncestorName === parentTypeName) { + const hasOverlap = doSetsHaveAnyOverlap( + fieldSubgraphs, + getOrThrowError(this.entities, entityAncestorName, ENTITIES).subgraphs + ); + this.graphPaths.set(path, hasOverlap); + return hasOverlap; + } + if (hasSimplePath(this.graph, entityAncestorName, parentTypeName)) { + this.graphPaths.set(path, true) + return true; + } + this.graphPaths.set(path, false); + } + return false; + } + + evaluateResolvabilityOfObject( + parentContainer: ObjectContainer, + rootTypeFieldData: RootTypeFieldData, + currentFieldPath: string, + evaluatedObjectLikes: Set, + entityAncestors: string[], + isParentAbstract = false, + ) { + const parentTypeName = parentContainer.node.name.value; + if (evaluatedObjectLikes.has(parentTypeName)) { + return; + } + for (const [fieldName, fieldContainer] of parentContainer.fields) { + const fieldNamedTypeName = fieldContainer.namedTypeName; + if (ROOT_TYPES.has(fieldNamedTypeName)) { + continue; + } + // Avoid an infinite loop with self-referential objects + if (evaluatedObjectLikes.has(fieldNamedTypeName)) { + continue; + } + const isFieldResolvable = doSetsHaveAnyOverlap(rootTypeFieldData.subgraphs, fieldContainer.subgraphs); + const newCurrentFieldPath = currentFieldPath + (isParentAbstract ? ' ' : '.') + fieldName; + const entity = this.entities.get(fieldNamedTypeName); + if (isFieldResolvable || this.entityAncestor(entityAncestors, fieldContainer.subgraphs, parentTypeName)) { + // The base scalars are not in this.parentMap + if (BASE_SCALARS.has(fieldNamedTypeName)) { + continue; + } + const childContainer = getOrThrowError(this.parents, fieldNamedTypeName, PARENTS); + switch (childContainer.kind) { + case Kind.ENUM_TYPE_DEFINITION: + // intentional fallthrough + case Kind.SCALAR_TYPE_DEFINITION: + continue; + case Kind.OBJECT_TYPE_DEFINITION: + this.evaluateResolvabilityOfObject( + childContainer, + rootTypeFieldData, + newCurrentFieldPath, + new Set([...evaluatedObjectLikes, parentTypeName]), + entity ? [...entityAncestors, fieldNamedTypeName] : [...entityAncestors], + ); + continue; + case Kind.INTERFACE_TYPE_DEFINITION: + // intentional fallthrough + case Kind.UNION_TYPE_DEFINITION: + this.evaluateResolvabilityOfAbstractType( + fieldNamedTypeName, + childContainer.kind, + rootTypeFieldData, + newCurrentFieldPath, + new Set([...evaluatedObjectLikes, parentTypeName]), + entity ? [...entityAncestors, fieldNamedTypeName] : [...entityAncestors], + fieldContainer.subgraphs, + ); + continue; + default: + this.errors.push(unexpectedObjectResponseType(newCurrentFieldPath, kindToTypeString(childContainer.kind))); + continue; + } + } + if (BASE_SCALARS.has(fieldNamedTypeName)) { + this.errors.push(unresolvableFieldError( + rootTypeFieldData, + fieldName, + [...fieldContainer.subgraphs], + newCurrentFieldPath, + parentTypeName, + )); + continue; + } + const childContainer = getOrThrowError(this.parents, fieldNamedTypeName, PARENTS); + switch (childContainer.kind) { + case Kind.ENUM_TYPE_DEFINITION: + // intentional fallthrough + case Kind.SCALAR_TYPE_DEFINITION: + this.errors.push(unresolvableFieldError( + rootTypeFieldData, + fieldName, + [...fieldContainer.subgraphs], + newCurrentFieldPath, + parentTypeName, + )); + continue; + case Kind.INTERFACE_TYPE_DEFINITION: + // intentional fallthrough + case Kind.UNION_TYPE_DEFINITION: + // intentional fallthrough + case Kind.OBJECT_TYPE_DEFINITION: + this.errors.push(unresolvableFieldError( + rootTypeFieldData, + fieldName, + [...fieldContainer.subgraphs], + newCurrentFieldPath + SELECTION_REPRESENTATION, + parentTypeName, + )); + continue; + default: + this.errors.push(unexpectedObjectResponseType(newCurrentFieldPath, kindToTypeString(childContainer.kind))); + } + } + } + + evaluateResolvabilityOfAbstractType( + fieldNamedTypeName: string, + fieldNamedTypeKind: Kind, + rootTypeFieldData: RootTypeFieldData, + currentFieldPath: string, + evaluatedObjectLikes: Set, + entityAncestors: string[], + parentSubgraphs: Set, + ) { + if (evaluatedObjectLikes.has(fieldNamedTypeName)) { + return; + } + const concreteTypeNames = this.abstractToConcreteTypeNames.get(fieldNamedTypeName); + if (!concreteTypeNames) { + noConcreteTypesForAbstractTypeError(kindToTypeString(fieldNamedTypeKind), fieldNamedTypeName) + return; + } + for (const concreteTypeName of concreteTypeNames) { + if (evaluatedObjectLikes.has(concreteTypeName)) { + continue; + } + const concreteParentContainer = getOrThrowError(this.parents, concreteTypeName, PARENTS); + if (concreteParentContainer.kind !== Kind.OBJECT_TYPE_DEFINITION) { + // throw + continue; + } + if (!doSetsHaveAnyOverlap(concreteParentContainer.subgraphs, parentSubgraphs)) { + continue; + } + const entity = this.entities.get(concreteTypeName); + this.evaluateResolvabilityOfObject( + concreteParentContainer, + rootTypeFieldData, + currentFieldPath + ` ... on ` + concreteTypeName, + new Set([...evaluatedObjectLikes, fieldNamedTypeName]), + entity ? [...entityAncestors, concreteTypeName] : [...entityAncestors], + true + ); + } + } + + federate(): FederationResultContainer { this.populateMultiGraphAndRenameOperations(this.subgraphs); const factory = this; for (const subgraph of this.subgraphs) { @@ -961,16 +1138,20 @@ export class FederationFactory { this.currentSubgraphName = subgraph.name; walkSubgraphToFederate(subgraph.definitions, factory); } - 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)) { + if (extension.isRootType && !this.parents.has(typeName)) { this.upsertParentNode(objectTypeExtensionNodeToMutableDefinitionNode(extension.node)); } - const baseObject = this.parentMap.get(typeName); + const baseObject = this.parents.get(typeName); if (!baseObject) { this.errors.push(noBaseTypeExtensionError(typeName)); continue; @@ -979,150 +1160,247 @@ 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) { - baseObject.fields.set(fieldName, field); + for (const [extensionFieldName, extensionFieldContainer] of extension.fields) { + const baseFieldContainer = baseObject.fields.get(extensionFieldName); + if (!baseFieldContainer) { + baseObject.fields.set(extensionFieldName, extensionFieldContainer); continue; } - if (baseField.isShareable && field.isShareable) { + if (baseFieldContainer.isShareable && extensionFieldContainer.isShareable) { + setLongestDescriptionForNode(baseFieldContainer.node, extensionFieldContainer.node.description); + addIterableValuesToSet(extensionFieldContainer.subgraphs, baseFieldContainer.subgraphs); continue; } const parent = this.shareableErrorTypeNames.get(typeName); if (parent) { - parent.add(fieldName); + parent.add(extensionFieldName); continue; } - this.shareableErrorTypeNames.set(typeName, new Set([fieldName])); + this.shareableErrorTypeNames.set(typeName, new Set([extensionFieldName])); } for (const interfaceName of extension.interfaces) { baseObject.interfaces.add(interfaceName); } } for (const [parentTypeName, children] of this.shareableErrorTypeNames) { - const parent = getOrThrowError(this.parentMap, parentTypeName); + const parent = getOrThrowError(this.parents, parentTypeName, PARENTS); if (parent.kind !== Kind.OBJECT_TYPE_DEFINITION) { throw incompatibleParentKindFatalError(parentTypeName, Kind.OBJECT_TYPE_DEFINITION, parent.kind); } this.errors.push(shareableFieldDefinitionsError(parent, children)); } - for (const parent of this.parentMap.values()) { - const parentName = parent.node.name.value; - switch (parent.kind) { + const objectLikeContainersWithInterfaces: ObjectLikeContainer[] = []; + for (const parentContainer of this.parents.values()) { + const parentTypeName = parentContainer.node.name.value; + switch (parentContainer.kind) { case Kind.ENUM_TYPE_DEFINITION: - const values: EnumValueDefinitionNode[] = []; - const mergeMethod = this.getEnumMergeMethod(parentName); - for (const value of parent.values.values()) { + const values: MutableEnumValueDefinitionNode[] = []; + const mergeMethod = this.getEnumMergeMethod(parentTypeName); + for (const enumValueContainer of parentContainer.values.values()) { + pushPersistedDirectivesToNode(enumValueContainer); switch (mergeMethod) { case MergeMethod.CONSISTENT: - if (value.appearances < parent.appearances) { - this.errors.push(incompatibleSharedEnumError(parentName)); + 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)) { - // TODO append to errors - throw federationRequiredInputFieldError(parentName, 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 field of parent.fields.values()) { - if (field.arguments) { - const args: InputValueDefinitionNode[] = []; - for (const arg of field.arguments.values()) { - arg.node.defaultValue = arg.includeDefaultValue ? arg.node.defaultValue : undefined; - args.push(arg.node); - } - field.node.arguments = args; - } - interfaceFields.push(field.node); + for (const fieldContainer of parentContainer.fields.values()) { + interfaceFields.push(this.getMergedFieldDefinitionNode(fieldContainer, parentTypeName)); } - const otherInterfaces: NamedTypeNode[] = []; - for (const iFace of parent.interfaces) { - otherInterfaces.push({ - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: iFace, - }, - }); + parentContainer.node.fields = interfaceFields; + pushPersistedDirectivesToNode(parentContainer); + // Interface implementations can only be evaluated after they've been fully merged + if (parentContainer.interfaces.size > 0) { + objectLikeContainersWithInterfaces.push(parentContainer); + } else { + definitions.push(parentContainer.node); } - parent.node.interfaces = otherInterfaces; - parent.node.fields = interfaceFields; - definitions.push(parent.node); break; case Kind.OBJECT_TYPE_DEFINITION: const fields: FieldDefinitionNode[] = []; - for (const field of parent.fields.values()) { - if (field.arguments) { - const args: InputValueDefinitionNode[] = []; - for (const arg of field.arguments.values()) { - arg.node.defaultValue = arg.includeDefaultValue ? arg.node.defaultValue : undefined; - args.push(arg.node); - } - field.node.arguments = args; - } - fields.push(field.node); + for (const fieldContainer of parentContainer.fields.values()) { + fields.push(this.getMergedFieldDefinitionNode(fieldContainer, parentTypeName)); + } + parentContainer.node.fields = fields; + pushPersistedDirectivesToNode(parentContainer); + // Interface implementations can only be evaluated after they've been fully merged + if (parentContainer.interfaces.size > 0) { + objectLikeContainersWithInterfaces.push(parentContainer); + } else { + definitions.push(parentContainer.node); } - parent.node.fields = fields; - parent.node.interfaces = this.getAndValidateImplementedInterfaces(parent); - definitions.push(parent.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 member of parent.members) { - types.push({ - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: member, - }, - }); + 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; } } + for (const container of objectLikeContainersWithInterfaces) { + container.node.interfaces = this.getAndValidateImplementedInterfaces(container); + definitions.push(container.node); + } + if (!this.parents.has(QUERY)) { + this.errors.push(noQueryRootTypeError); + } + // return any composition errors before checking whether all fields are resolvable if (this.errors.length > 0) { - return { - errors: this.errors, - }; + return { errors: this.errors }; + } + for (const rootTypeName of ROOT_TYPES) { + const rootTypeContainer = this.parents.get(rootTypeName); + if (!rootTypeContainer || rootTypeContainer.kind !== Kind.OBJECT_TYPE_DEFINITION) { + continue; + } + // If a root type field returns a Scalar or Enum, track it so that it is not evaluated it again + const evaluatedNamedTypes = new Set(BASE_SCALARS); + for (const [rootTypeFieldName, rootTypeFieldContainer] of rootTypeContainer.fields) { + const rootTypeFieldNamedTypeName = rootTypeFieldContainer.namedTypeName; + if (evaluatedNamedTypes.has(rootTypeFieldNamedTypeName)) { + continue; + } + const childContainer = getOrThrowError(this.parents, rootTypeFieldNamedTypeName, PARENTS); + const fieldPath = `${rootTypeName}.${rootTypeFieldName}`; + const rootTypeFieldData: RootTypeFieldData = { + fieldName: rootTypeFieldName, + fieldTypeNodeString: printTypeNode(rootTypeFieldContainer.node.type), + path: fieldPath, + typeName: rootTypeName, + subgraphs: rootTypeFieldContainer.subgraphs, + }; + const evaluatedObjectLikes = new Set(); + switch (childContainer.kind) { + case Kind.ENUM_TYPE_DEFINITION: + // intentional fallthrough + case Kind.SCALAR_TYPE_DEFINITION: + // Root type fields whose response type is an Enums and Scalars will always be resolvable + // Consequently, subsequent checks can be skipped + evaluatedNamedTypes.add(rootTypeFieldNamedTypeName); + continue; + case Kind.OBJECT_TYPE_DEFINITION: + this.evaluateResolvabilityOfObject( + childContainer, + rootTypeFieldData, + fieldPath, + evaluatedObjectLikes, + this.entities.has(rootTypeFieldNamedTypeName) ? [rootTypeFieldNamedTypeName] : [], + ); + continue; + case Kind.INTERFACE_TYPE_DEFINITION: + // intentional fallthrough + case Kind.UNION_TYPE_DEFINITION: + this.evaluateResolvabilityOfAbstractType( + rootTypeFieldNamedTypeName, + childContainer.kind, + rootTypeFieldData, + fieldPath, + evaluatedObjectLikes, + this.entities.has(rootTypeFieldNamedTypeName) ? [rootTypeFieldNamedTypeName] : [], + rootTypeFieldContainer.subgraphs, + ); + continue; + default: + this.errors.push(unexpectedObjectResponseType(fieldPath, kindToTypeString(childContainer.kind))); + } + } + } + if (this.errors.length > 0) { + return { errors: this.errors }; } const newAst: DocumentNode = { kind: Kind.DOCUMENT, definitions, }; return { - federatedGraphAST: newAst, - federatedGraphSchema: buildASTSchema(newAst), + federationResult: { + argumentConfigurations: this.argumentConfigurations, + federatedGraphAST: newAst, + federatedGraphSchema: buildASTSchema(newAst), + } }; } } + +export function federateSubgraphs(subgraphs: Subgraph[]): FederationResultContainer { + if (subgraphs.length < 1) { + throw minimumSubgraphRequirementError; + } + const normalizedSubgraphs: InternalSubgraph[] = []; + const validationErrors: Error[] = []; + const subgraphNames = new Set(); + const nonUniqueSubgraphNames = new Set(); + const invalidNameErrorMessages: string[] = []; + for (let i = 0; i < subgraphs.length; i++) { + const subgraph = subgraphs[i]; + const name = subgraph.name || `subgraph-${i}-${Date.now()}`; + if (!subgraph.name) { + invalidNameErrorMessages.push(invalidSubgraphNameErrorMessage(i, name)); + } else { + validateSubgraphName(subgraph.name, subgraphNames, nonUniqueSubgraphNames); + } + const { errors, normalizationResult } = normalizeSubgraph(subgraph.definitions); + if (errors) { + validationErrors.push(subgraphValidationError(name, errors)); + continue; + } + if (!normalizationResult) { + validationErrors.push(subgraphValidationError(name, [subgraphValidationFailureErrorMessage])); + continue; + } + normalizedSubgraphs.push({ + definitions: normalizationResult.subgraphAST, + isVersionTwo: normalizationResult.isVersionTwo, + name, + operationTypes: normalizationResult.operationTypes, + url: subgraph.url, + }); + } + const allErrors: Error[] = []; + if (invalidNameErrorMessages.length > 0 || nonUniqueSubgraphNames.size > 0) { + allErrors.push(invalidSubgraphNamesError([...nonUniqueSubgraphNames], invalidNameErrorMessages)); + } + allErrors.push(...validationErrors); + if (allErrors.length > 0) { + return { errors: allErrors }; + } + const federationFactory = new FederationFactory(normalizedSubgraphs); + return federationFactory.federate(); +} \ No newline at end of file diff --git a/composition/src/federation/federation-result.ts b/composition/src/federation/federation-result.ts deleted file mode 100644 index 3fbf75be33..0000000000 --- a/composition/src/federation/federation-result.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DocumentNode, GraphQLSchema } from 'graphql'; - -export type FederationResult = { - errors?: Error[]; - federatedGraphAST?: DocumentNode; - federatedGraphSchema?: GraphQLSchema; -} \ No newline at end of file diff --git a/composition/src/federation/utils.ts b/composition/src/federation/utils.ts new file mode 100644 index 0000000000..cf6099c3db --- /dev/null +++ b/composition/src/federation/utils.ts @@ -0,0 +1,187 @@ +import { ConstDirectiveNode, DocumentNode, GraphQLSchema, Kind } from 'graphql'; +import { ArgumentConfigurationData } from '../subgraph/field-configuration'; +import { + MutableDirectiveDefinitionNode, + MutableEnumTypeDefinitionNode, + MutableEnumValueDefinitionNode, + MutableFieldDefinitionNode, + MutableInputObjectTypeDefinitionNode, + MutableInputValueDefinitionNode, + MutableInterfaceTypeDefinitionNode, + MutableObjectTypeDefinitionNode, + MutableObjectTypeExtensionNode, + MutableScalarTypeDefinitionNode, + MutableUnionTypeDefinitionNode, +} from '../ast/ast'; +import { + FIELD_UPPER, + FRAGMENT_DEFINITION_UPPER, + FRAGMENT_SPREAD_UPPER, + INLINE_FRAGMENT_UPPER, + MUTATION_UPPER, + QUERY_UPPER, + SUBSCRIPTION_UPPER, +} from '../utils/string-constants'; + +export type FederationResultContainer = { + errors?: Error[]; + federationResult?: FederationResult; +}; + +export type FederationResult = { + argumentConfigurations: ArgumentConfigurationData[]; + federatedGraphAST: DocumentNode; + federatedGraphSchema: GraphQLSchema; +} + +export type RootTypeFieldData = { + fieldName: string; + fieldTypeNodeString: string; + path: string; + typeName: string; + subgraphs: Set; +}; + +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; + subgraphs: Set; +}; + +export type EnumContainer = { + appearances: number; + 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 FieldContainer = { + arguments: ArgumentMap; + directives: PersistedDirectivesContainer; + isShareable: boolean; + node: MutableFieldDefinitionNode; + namedTypeName: string; + subgraphs: Set; + subgraphsByShareable: Map; +}; + +export type FieldMap = Map; + +export type InputValueContainer = { + appearances: number; + directives: PersistedDirectivesContainer; + includeDefaultValue: boolean; + node: MutableInputValueDefinitionNode; +}; + +export type InputValueMap = Map; + +export type InputObjectContainer = { + appearances: number; + directives: PersistedDirectivesContainer; + fields: InputValueMap; + kind: Kind.INPUT_OBJECT_TYPE_DEFINITION; + node: MutableInputObjectTypeDefinitionNode; +}; + +export type InterfaceContainer = { + directives: PersistedDirectivesContainer; + fields: FieldMap; + interfaces: Set; + kind: Kind.INTERFACE_TYPE_DEFINITION; + node: MutableInterfaceTypeDefinitionNode; + subgraphs: Set; +}; + +export type ObjectContainer = { + directives: PersistedDirectivesContainer; + fields: FieldMap; + entityKeys: Set; + interfaces: Set; + isRootType: boolean; + kind: Kind.OBJECT_TYPE_DEFINITION; + node: MutableObjectTypeDefinitionNode; + subgraphs: Set; +}; + +export type ObjectExtensionContainer = { + directives: PersistedDirectivesContainer; + fields: FieldMap; + entityKeys: Set; + interfaces: Set; + isRootType: boolean; + kind: Kind.OBJECT_TYPE_EXTENSION; + node: MutableObjectTypeExtensionNode; + subgraphs: Set; +}; + +export type ScalarContainer = { + directives: PersistedDirectivesContainer; + kind: Kind.SCALAR_TYPE_DEFINITION; + node: MutableScalarTypeDefinitionNode; +}; + +export type UnionContainer = { + directives: PersistedDirectivesContainer; + kind: Kind.UNION_TYPE_DEFINITION; + members: Set; + node: MutableUnionTypeDefinitionNode; +}; + +export type ChildContainer = FieldContainer | InputValueContainer | EnumValueContainer; + +export type ParentContainer = + | EnumContainer + | InputObjectContainer + | InterfaceContainer + | ObjectContainer + | UnionContainer + | ScalarContainer; + +export type NodeContainer = ChildContainer | ParentContainer; +export type ExtensionContainer = ObjectExtensionContainer; +export type ParentMap = Map; +export type ObjectLikeContainer = ObjectContainer | InterfaceContainer; \ No newline at end of file diff --git a/composition/src/index.ts b/composition/src/index.ts index 2b8e41abb6..4a443da93f 100644 --- a/composition/src/index.ts +++ b/composition/src/index.ts @@ -1,9 +1,10 @@ -export * from './normalization/normalization-factory' -export * from './federation/federation-factory' -export * from './federation/federation-result' -export * from './subgraph/subgraph' -export * from './errors/errors' -export * from './utils/utils' -export * from './ast/utils' -export * from './ast/ast' -export * from './type-merging/type-merging' \ No newline at end of file +export * from './normalization/normalization-factory'; +export * from './federation/federation-factory'; +export * from './federation/utils'; +export * from './subgraph/field-configuration'; +export * from './subgraph/subgraph'; +export * from './errors/errors'; +export * from './utils/utils'; +export * from './ast/utils'; +export * from './ast/ast'; +export * from './type-merging/type-merging'; \ No newline at end of file diff --git a/composition/src/normalization/normalization-factory.ts b/composition/src/normalization/normalization-factory.ts index f52bd13c2d..0963d28331 100644 --- a/composition/src/normalization/normalization-factory.ts +++ b/composition/src/normalization/normalization-factory.ts @@ -21,12 +21,16 @@ import { SchemaExtensionNode, TypeDefinitionNode, TypeExtensionNode, + TypeNode, visit, } from 'graphql'; import { + addConcreteTypesForImplementedInterfaces, + addConcreteTypesForUnion, areBaseAndExtensionKindsCompatible, EntityKey, extractInterfaces, + formatDescription, getEntityKeyExtractionResults, isNodeExtension, isObjectLikeNodeEntity, @@ -46,6 +50,7 @@ import { getDirectiveDefinitionArgumentSets, inputObjectContainerToNode, InputObjectExtensionContainer, + InputValidationContainer, InputValueContainer, ObjectExtensionContainer, ObjectLikeContainer, @@ -75,11 +80,12 @@ import { getEntriesNotInHashSet, getOrThrowError, ImplementationErrors, + InvalidArgument, InvalidFieldImplementation, - isTypeValidImplementation, kindToTypeString, } from '../utils/utils'; import { + duplicateArgumentsError, duplicateDirectiveDefinitionError, duplicateEnumValueDefinitionError, duplicateFieldDefinitionError, @@ -92,6 +98,7 @@ import { incompatibleExtensionError, incompatibleExtensionKindsError, incompatibleParentKindFatalError, + invalidArgumentsError, invalidDirectiveError, invalidDirectiveLocationErrorMessage, invalidKeyDirectiveArgumentErrorMessage, @@ -109,10 +116,22 @@ import { unexpectedKindFatalError, unimplementedInterfaceFieldsError, } from '../errors/errors'; -import { EXTENDS, KEY, SCHEMA } from '../utils/string-constants'; +import { + ENTITIES_FIELD, + EXTENDS, + EXTENSIONS, + KEY, + OPERATION_TO_DEFAULT, + PARENTS, + ROOT_TYPES, + SCHEMA, + SERVICE, + SERVICE_FIELD, +} from '../utils/string-constants'; import { buildASTSchema } from '../buildASTSchema/buildASTSchema'; import { ConfigurationData, ConfigurationDataMap } from '../subgraph/field-configuration'; import { printTypeNode } from '@graphql-tools/merge'; +import { inputValueDefinitionNodeToMutable, MutableInputValueDefinitionNode } from '../ast/ast'; export type NormalizationResult = { configurationDataMap: ConfigurationDataMap; @@ -145,6 +164,7 @@ export function normalizeSubgraph(document: DocumentNode): NormalizationResultCo } export class NormalizationFactory { + abstractToConcreteTypeNames = new Map>(); allDirectiveDefinitions = new Map(); customDirectiveDefinitions = new Map(); errors: Error[] = []; @@ -152,6 +172,7 @@ export class NormalizationFactory { operationTypeNames = new Map(); parents: ParentMap = new Map(); parentTypeName = ''; + parentsWithChildArguments = new Set(); extensions: ExtensionMap = new Map(); isChild = false; isCurrentParentExtension = false; @@ -172,23 +193,61 @@ export class NormalizationFactory { }; } + validateInputNamedType(namedType: string): InputValidationContainer { + if (BASE_SCALARS.has(namedType)) { + return { hasUnhandledError: false, typeString: '' }; + } + const parentContainer = this.parents.get(namedType); + if (!parentContainer) { + this.errors.push(undefinedTypeError(namedType)); + return { hasUnhandledError: false, typeString: '' }; + } + switch (parentContainer.kind) { + case Kind.ENUM_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_DEFINITION: + return { hasUnhandledError: false, typeString: '' }; + default: + return { hasUnhandledError: true, typeString: kindToTypeString(parentContainer.kind) }; + } + } + extractArguments( node: FieldDefinitionNode, - map: Map, - ): Map { + argumentByName: Map, + fieldPath: string, + ): Map { if (!node.arguments) { - return map; + return argumentByName; } + this.parentsWithChildArguments.add(this.parentTypeName); + const duplicatedArguments = new Set(); for (const argumentNode of node.arguments) { const argumentName = argumentNode.name.value; - if (map.has(argumentName)) { - // TODO - this.errors.push(new Error('duplicate argument')); + if (argumentByName.has(argumentName)) { + duplicatedArguments.add(argumentName); continue; } - map.set(argumentName, argumentNode); + argumentByName.set(argumentName, inputValueDefinitionNodeToMutable(argumentNode, this.parentTypeName)); + } + if (duplicatedArguments.size > 0) { + this.errors.push(duplicateArgumentsError(fieldPath, [...duplicatedArguments])); + } + return argumentByName; + } + + validateArguments(fieldContainer: FieldContainer, fieldPath: string){ + const invalidArguments: InvalidArgument[] = []; + for (const [argumentName, argumentNode] of fieldContainer.arguments) { + const namedType = getNamedTypeForChild(fieldPath + `(${argumentName}...)`, argumentNode.type); + const { hasUnhandledError, typeString } = this.validateInputNamedType(namedType) + if (hasUnhandledError) { + invalidArguments.push({ argumentName, namedType, typeString, typeName: printTypeNode(argumentNode.type) }); + } + } + if (invalidArguments.length > 0) { + this.errors.push(invalidArgumentsError(fieldPath, invalidArguments)); } - return map; } extractDirectives( @@ -471,13 +530,46 @@ export class NormalizationFactory { } } + isTypeValidImplementation(originalType: TypeNode, implementationType: TypeNode): boolean { + if (originalType.kind === Kind.NON_NULL_TYPE) { + if (implementationType.kind !== Kind.NON_NULL_TYPE) { + return false; + } + return this.isTypeValidImplementation(originalType.type, implementationType.type); + } + if (implementationType.kind === Kind.NON_NULL_TYPE) { + return this.isTypeValidImplementation(originalType, implementationType.type); + } + switch (originalType.kind) { + case Kind.NAMED_TYPE: + if (implementationType.kind === Kind.NAMED_TYPE) { + const originalTypeName = originalType.name.value; + const implementationTypeName = implementationType.name.value; + if (originalTypeName === implementationTypeName) { + return true; + } + const concreteTypes = this.abstractToConcreteTypeNames.get(originalTypeName); + if (!concreteTypes) { + return false; + } + return concreteTypes.has(implementationTypeName); + } + return false; + default: + if (implementationType.kind === Kind.LIST_TYPE) { + return this.isTypeValidImplementation(originalType.type, implementationType.type); + } + return false; + } + } + validateInterfaceImplementations(container: ObjectLikeContainer) { if (container.interfaces.size < 1) { return; } const implementationErrorsMap = new Map(); for (const interfaceName of container.interfaces) { - const interfaceContainer = getOrThrowError(this.parents, interfaceName); + const interfaceContainer = getOrThrowError(this.parents, interfaceName, PARENTS); if (interfaceContainer.kind !== Kind.INTERFACE_TYPE_DEFINITION) { throw incompatibleParentKindFatalError(interfaceName, Kind.INTERFACE_TYPE_DEFINITION, interfaceContainer.kind); } @@ -501,7 +593,7 @@ export class NormalizationFactory { unimplementedArguments: new Set(), }; // The implemented field type must be equally or more restrictive than the original interface field type - if (!isTypeValidImplementation(interfaceField.node.type, containerField.node.type)) { + if (!this.isTypeValidImplementation(interfaceField.node.type, containerField.node.type)) { hasErrors = true; hasNestedErrors = true; invalidFieldImplementation.implementedResponseType = printTypeNode(containerField.node.type); @@ -555,24 +647,30 @@ 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(); + let isCurrentParentRootType: boolean = false; 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; @@ -600,7 +698,7 @@ export class NormalizationFactory { } factory.parentTypeName = name; factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), kind: node.kind, name: node.name, @@ -641,8 +739,8 @@ export class NormalizationFactory { enter(node) { const name = node.name.value; const parent = factory.isCurrentParentExtension - ? getOrThrowError(factory.extensions, factory.parentTypeName) - : getOrThrowError(factory.parents, factory.parentTypeName); + ? getOrThrowError(factory.extensions, factory.parentTypeName, EXTENSIONS) + : getOrThrowError(factory.parents, factory.parentTypeName, PARENTS); if (parent.kind !== Kind.ENUM_TYPE_DEFINITION && parent.kind !== Kind.ENUM_TYPE_EXTENSION) { throw unexpectedKindFatalError(name); } @@ -656,13 +754,16 @@ export class NormalizationFactory { parent.values.set(name, { directives: factory.extractDirectives(node, new Map()), name, - node, + node: { ...node, description: formatDescription(node.description) }, }); }, }, FieldDefinition: { enter(node) { const name = node.name.value; + if (isCurrentParentRootType && (name === SERVICE_FIELD || name === ENTITIES_FIELD)) { + return false; + } const fieldPath = `${factory.parentTypeName}.${name}`; factory.isChild = true; const fieldRootType = getNamedTypeForChild(fieldPath, node.type); @@ -670,8 +771,8 @@ export class NormalizationFactory { factory.referencedTypeNames.add(fieldRootType); } const parent = factory.isCurrentParentExtension - ? getOrThrowError(factory.extensions, factory.parentTypeName) - : getOrThrowError(factory.parents, factory.parentTypeName); + ? getOrThrowError(factory.extensions, factory.parentTypeName, EXTENSIONS) + : getOrThrowError(factory.parents, factory.parentTypeName, PARENTS); if ( parent.kind !== Kind.OBJECT_TYPE_DEFINITION && parent.kind !== Kind.OBJECT_TYPE_EXTENSION && @@ -687,11 +788,18 @@ export class NormalizationFactory { factory.errors.push(error); return; } + // recreate the node so the argument descriptions are updated parent.fields.set(name, { - arguments: factory.extractArguments(node, new Map), + arguments: factory.extractArguments(node, new Map(), fieldPath), directives: factory.extractDirectives(node, new Map()), name, - node, + node: { + ...node, + arguments: node.arguments?.map((arg) => ({ + ...arg, + description: formatDescription(arg.description), + })), + } }); }, leave() { @@ -707,7 +815,7 @@ export class NormalizationFactory { } factory.parentTypeName = name; factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), fields: new Map(), kind: node.kind, @@ -750,9 +858,13 @@ 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); + ? getOrThrowError(factory.extensions, factory.parentTypeName, EXTENSIONS) + : getOrThrowError(factory.parents, factory.parentTypeName, PARENTS); if (parent.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION && parent.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION) { throw unexpectedKindFatalError(factory.parentTypeName); } @@ -763,7 +875,7 @@ export class NormalizationFactory { parent.fields.set(name, { directives: factory.extractDirectives(node, new Map()), name, - node, + node: { ...node, description: formatDescription(node.description) }, }); }, }, @@ -779,7 +891,7 @@ export class NormalizationFactory { return false; } factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), fields: new Map(), interfaces: extractInterfaces(node, new Set(), factory.errors), @@ -805,7 +917,12 @@ export class NormalizationFactory { ObjectTypeDefinition: { enter(node) { const name = node.name.value; + if (name === SERVICE) { + return false; + } + isCurrentParentRootType = ROOT_TYPES.has(name); factory.parentTypeName = name; + addConcreteTypesForImplementedInterfaces(node, factory.abstractToConcreteTypeNames); // handling for @extends directive if (isNodeExtension(node)) { return factory.handleObjectLikeExtension(node); @@ -816,7 +933,7 @@ export class NormalizationFactory { } const isEntity = isObjectLikeNodeEntity(node); factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), fields: new Map(), interfaces: extractInterfaces(node, new Set(), factory.errors), @@ -841,16 +958,24 @@ export class NormalizationFactory { }, leave() { factory.isCurrentParentExtension = false; + isCurrentParentRootType = false; factory.parentTypeName = ''; }, }, ObjectTypeExtension: { enter(node) { - factory.parentTypeName = node.name.value; + const name = node.name.value; + if (name === SERVICE) { + return false; + } + isCurrentParentRootType = ROOT_TYPES.has(name); + factory.parentTypeName = name; + addConcreteTypesForImplementedInterfaces(node, factory.abstractToConcreteTypeNames); return factory.handleObjectLikeExtension(node); }, leave() { factory.isCurrentParentExtension = false; + isCurrentParentRootType = false; factory.parentTypeName = ''; }, }, @@ -887,7 +1012,7 @@ export class NormalizationFactory { return false; } factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), kind: Kind.SCALAR_TYPE_DEFINITION, name: node.name, @@ -917,7 +1042,7 @@ export class NormalizationFactory { SchemaDefinition: { enter(node) { factory.extractDirectives(node, factory.schemaDefinition.directives); - factory.schemaDefinition.description = factory.schemaDefinition.description || node.description; + factory.schemaDefinition.description = node.description; }, }, SchemaExtension: { @@ -938,8 +1063,9 @@ export class NormalizationFactory { factory.errors.push(noDefinedUnionMembersError(name)); return false; } + addConcreteTypesForUnion(node, factory.abstractToConcreteTypeNames); factory.parents.set(name, { - description: node.description, + description: formatDescription(node.description), directives: factory.extractDirectives(node, new Map()), kind: node.kind, name: node.name, @@ -958,6 +1084,7 @@ export class NormalizationFactory { factory.errors.push(); return false; } + addConcreteTypesForUnion(node, factory.abstractToConcreteTypeNames); if (extension) { if (extension.kind !== Kind.UNION_TYPE_EXTENSION) { factory.errors.push(incompatibleExtensionKindsError(node, extension.kind)); @@ -997,90 +1124,106 @@ export class NormalizationFactory { const configurationDataMap = new Map(); const validExtensionOrphans = new Set(); const parentsToIgnore = new Set(); - for (const [typeName, extension] of this.extensions) { - const entity = this.entityMap.get(typeName); + for (const [extensionTypeName, extensionContainer] of this.extensions) { + const entity = this.entityMap.get(extensionTypeName); const configurationData: ConfigurationData = { fieldNames: new Set(), isRootNode: !!entity, - selectionSets: entity ? [...entity.keys()] : [], - typeName, + typeName: extensionTypeName, }; - if (extension.kind === Kind.OBJECT_TYPE_EXTENSION) { - addIterableValuesToSet(extension.fields.keys(), configurationData.fieldNames); - configurationDataMap.set(typeName, configurationData); + if (entity) { + configurationData.keys = [...entity.keys()].map((selectionSet) => ({ + fieldName: '', selectionSet, + })); + } + if (extensionContainer.kind === Kind.OBJECT_TYPE_EXTENSION) { + if (this.operationTypeNames.has(extensionTypeName)) { + extensionContainer.fields.delete(SERVICE_FIELD); + extensionContainer.fields.delete(ENTITIES_FIELD); + } + addIterableValuesToSet(extensionContainer.fields.keys(), configurationData.fieldNames); + configurationDataMap.set(extensionTypeName, configurationData); } - const baseType = this.parents.get(typeName); + const baseType = this.parents.get(extensionTypeName); if (!baseType) { - if (extension.kind !== Kind.OBJECT_TYPE_EXTENSION) { - this.errors.push(noBaseTypeExtensionError(typeName)); + if (extensionContainer.kind !== Kind.OBJECT_TYPE_EXTENSION) { + this.errors.push(noBaseTypeExtensionError(extensionTypeName)); } else { - validateEntityKeys(this, typeName, true); - this.validateInterfaceImplementations(extension); - validExtensionOrphans.add(typeName); - definitions.push(objectLikeContainerToNode(this, extension)); + validateEntityKeys(this, extensionTypeName, true); + this.validateInterfaceImplementations(extensionContainer); + validExtensionOrphans.add(extensionTypeName); + definitions.push(objectLikeContainerToNode(this, extensionContainer)); } continue; } - if (!areBaseAndExtensionKindsCompatible(baseType.kind, extension.kind, typeName)) { - this.errors.push(incompatibleExtensionError(typeName, baseType.kind, extension.kind)); + if (!areBaseAndExtensionKindsCompatible(baseType.kind, extensionContainer.kind, extensionTypeName)) { + this.errors.push(incompatibleExtensionError(extensionTypeName, baseType.kind, extensionContainer.kind)); continue; } switch (baseType.kind) { case Kind.ENUM_TYPE_DEFINITION: - const enumExtension = extension as EnumExtensionContainer; + const enumExtension = extensionContainer as EnumExtensionContainer; for (const [valueName, enumValueDefinitionNode] of enumExtension.values) { if (!baseType.values.has(valueName)) { baseType.values.set(valueName, enumValueDefinitionNode); continue; } - this.errors.push(duplicateEnumValueDefinitionError(valueName, typeName)); + this.errors.push(duplicateEnumValueDefinitionError(valueName, extensionTypeName)); } definitions.push(enumContainerToNode(this, baseType, enumExtension)); break; case Kind.INPUT_OBJECT_TYPE_DEFINITION: - const inputExtension = extension as InputObjectExtensionContainer; + const inputExtension = extensionContainer as InputObjectExtensionContainer; for (const [fieldName, inputValueDefinitionNode] of inputExtension.fields) { if (!baseType.fields.has(fieldName)) { baseType.fields.set(fieldName, inputValueDefinitionNode); continue; } - this.errors.push(duplicateFieldDefinitionError(fieldName, typeName)); + this.errors.push(duplicateFieldDefinitionError(fieldName, extensionTypeName)); } definitions.push(inputObjectContainerToNode(this, baseType, inputExtension)); break; case Kind.INTERFACE_TYPE_DEFINITION: // intentional fallthrough case Kind.OBJECT_TYPE_DEFINITION: - const objectLikeExtension = extension as ObjectLikeExtensionContainer; + const objectLikeExtension = extensionContainer as ObjectLikeExtensionContainer; + if (this.operationTypeNames.has(extensionTypeName)) { + objectLikeExtension.fields.delete(SERVICE_FIELD); + objectLikeExtension.fields.delete(ENTITIES_FIELD); + } for (const [fieldName, fieldContainer] of objectLikeExtension.fields) { + if (fieldContainer.arguments.size > 0) { + // Arguments can only be fully validated once all parents types are known + this.validateArguments(fieldContainer, `${extensionTypeName}.${fieldName}`); + } if (baseType.fields.has(fieldName)) { - this.errors.push(duplicateFieldDefinitionError(fieldName, typeName)); + this.errors.push(duplicateFieldDefinitionError(fieldName, extensionTypeName)); continue; } baseType.fields.set(fieldName, fieldContainer); configurationData.fieldNames.add(fieldName); } - validateEntityKeys(this, typeName); - this.mergeUniqueInterfaces(objectLikeExtension.interfaces, baseType.interfaces, typeName); + validateEntityKeys(this, extensionTypeName); + this.mergeUniqueInterfaces(objectLikeExtension.interfaces, baseType.interfaces, extensionTypeName); this.validateInterfaceImplementations(baseType); - configurationDataMap.set(typeName, configurationData); + configurationDataMap.set(extensionTypeName, configurationData); definitions.push(objectLikeContainerToNode(this, baseType, objectLikeExtension)); break; case Kind.SCALAR_TYPE_DEFINITION: - definitions.push(scalarContainerToNode(this, baseType, extension as ScalarExtensionContainer)); + definitions.push(scalarContainerToNode(this, baseType, extensionContainer as ScalarExtensionContainer)); break; case Kind.UNION_TYPE_DEFINITION: - const unionExtension = extension as UnionExtensionContainer; + const unionExtension = extensionContainer as UnionExtensionContainer; definitions.push(unionContainerToNode(this, baseType, unionExtension)); break; default: - throw unexpectedKindFatalError(typeName); + throw unexpectedKindFatalError(extensionTypeName); } // At this point, the base type has been dealt with, so it doesn't need to be dealt with again - parentsToIgnore.add(typeName); + parentsToIgnore.add(extensionTypeName); } - for (const [typeName, parentContainer] of this.parents) { - if (parentsToIgnore.has(typeName)) { + for (const [parentTypeName, parentContainer] of this.parents) { + if (parentsToIgnore.has(parentTypeName)) { continue; } switch (parentContainer.kind) { @@ -1093,17 +1236,36 @@ export class NormalizationFactory { case Kind.INTERFACE_TYPE_DEFINITION: // intentional fallthrough case Kind.OBJECT_TYPE_DEFINITION: - const entity = this.entityMap.get(typeName); + const entity = this.entityMap.get(parentTypeName); + if (this.operationTypeNames.has(parentTypeName)) { + parentContainer.fields.delete(SERVICE_FIELD); + parentContainer.fields.delete(ENTITIES_FIELD); + } + if (this.parentsWithChildArguments.has(parentTypeName)) { + const parentContainer = getOrThrowError(this.parents, parentTypeName, PARENTS); + if (parentContainer.kind !== Kind.OBJECT_TYPE_DEFINITION + && parentContainer.kind !== Kind.INTERFACE_TYPE_DEFINITION) { + continue; + } + for (const [fieldName, fieldContainer] of parentContainer.fields) { + // Arguments can only be fully validated once all parents types are known + this.validateArguments(fieldContainer, `${parentTypeName}.${fieldName}`); + } + } const configurationData: ConfigurationData = { fieldNames: new Set(), isRootNode: !!entity, - selectionSets: entity ? [...entity.keys()] : [], - typeName, + typeName: parentTypeName, }; + if (entity) { + configurationData.keys = [...entity.keys()].map((selectionSet) => ({ + fieldName: '', selectionSet, + })); + } addIterableValuesToSet(parentContainer.fields.keys(), configurationData.fieldNames); - validateEntityKeys(this, typeName); + validateEntityKeys(this, parentTypeName); this.validateInterfaceImplementations(parentContainer); - configurationDataMap.set(typeName, configurationData); + configurationDataMap.set(parentTypeName, configurationData); definitions.push(objectLikeContainerToNode(this, parentContainer)); break; case Kind.SCALAR_TYPE_DEFINITION: @@ -1113,15 +1275,16 @@ export class NormalizationFactory { definitions.push(unionContainerToNode(this, parentContainer)); break; default: - throw unexpectedKindFatalError(typeName); + throw unexpectedKindFatalError(parentTypeName); } } // Check that explicitly defined operations types are valid objects and that their fields are also valid for (const operationType of Object.values(OperationTypeNode)) { const node = this.schemaDefinition.operationTypes.get(operationType); - const defaultTypeName = getOrThrowError(operationTypeNodeToDefaultType, operationType); + const defaultTypeName = getOrThrowError(operationTypeNodeToDefaultType, operationType, OPERATION_TO_DEFAULT); // If an operation type name was not declared, use the default - const operationTypeName = node ? getNamedTypeForChild(`schema.${operationType}`, node.type) : defaultTypeName; + const operationTypeName = node ? getNamedTypeForChild(`schema.${operationType}`, node.type) + : defaultTypeName; // If a custom type is used, the default type should not be defined if ( operationTypeName !== defaultTypeName && @@ -1159,7 +1322,7 @@ export class NormalizationFactory { this.errors.push(operationDefinitionError(operationTypeName, operationType, container.kind)); continue; } - // Operations whose response type is an extension orphan could be valid through a federated graph + // Root types fields whose response type is an extension orphan could be valid through a federated graph // However, the field would have to be shareable to ever be valid TODO for (const fieldContainer of container.fields.values()) { const fieldName = fieldContainer.name; @@ -1176,7 +1339,11 @@ export class NormalizationFactory { } } for (const referencedTypeName of this.referencedTypeNames) { - if (!this.parents.has(referencedTypeName) && !this.entityMap.has(referencedTypeName)) { + if (this.parents.has(referencedTypeName) || this.entityMap.has(referencedTypeName)) { + continue; + } + const extension = this.extensions.get(referencedTypeName); + if (!extension || extension.kind !== Kind.OBJECT_TYPE_EXTENSION) { this.errors.push(undefinedTypeError(referencedTypeName)); } } diff --git a/composition/src/normalization/utils.ts b/composition/src/normalization/utils.ts index 4abd8df2b9..efa6873e4c 100644 --- a/composition/src/normalization/utils.ts +++ b/composition/src/normalization/utils.ts @@ -20,7 +20,7 @@ import { StringValueNode, UnionTypeDefinitionNode, } from 'graphql'; -import { mapToArrayOfValues } from '../utils/utils'; +import { kindToTypeString, mapToArrayOfValues } from '../utils/utils'; import { EntityKey, setToNamedTypeNodeArray } from '../ast/utils'; import { ARGUMENT_DEFINITION_UPPER, @@ -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; @@ -443,7 +449,8 @@ function validateEntityKey( } const fieldPath = `${objectTypeName}.${fieldName}`; const fieldTypeName = getNamedTypeForChild(fieldPath, field.node.type); - if (factory.parents.has(fieldTypeName)) { + const parentContainer = factory.parents.get(fieldTypeName); + if (parentContainer && parentContainer.kind === Kind.OBJECT_TYPE_DEFINITION) { errorMessages.push(objectInCompositeKeyWithoutSelectionsErrorMessage(fieldName, fieldTypeName)); } } @@ -467,12 +474,14 @@ function validateBaseObjectEntityKey( entityKey: EntityKey, errorMessages: string[], ) { - const object = factory.parents.get(objectTypeName); + const object = factory.parents.get(objectTypeName) || factory.extensions.get(objectTypeName); if (!object) { throw undefinedParentFatalError(objectTypeName); } - if (object.kind !== Kind.OBJECT_TYPE_DEFINITION) { - errorMessages.push(unexpectedParentKindErrorMessage(objectTypeName, object.kind, Kind.OBJECT_TYPE_DEFINITION)); + if (object.kind !== Kind.OBJECT_TYPE_DEFINITION && object.kind !== Kind.OBJECT_TYPE_EXTENSION) { + errorMessages.push(unexpectedParentKindErrorMessage( + objectTypeName, 'object or object extension', kindToTypeString(object.kind)), + ); return; } validateEntityKey(factory, entityKey, object, objectTypeName, errorMessages); @@ -505,7 +514,8 @@ export function getDirectiveDefinitionArgumentSets( for (const argument of args) { const argumentName = argument.name.value; allArguments.add(argumentName); - if (argument.type.kind === Kind.NON_NULL_TYPE) { + // If the definition defines a default argument, it's not necessary to include it + if (argument.type.kind === Kind.NON_NULL_TYPE && !argument.defaultValue) { requiredArguments.add(argumentName); } } @@ -533,3 +543,8 @@ export function getDefinedArgumentsForDirective( } return definedArguments; } + +export type InputValidationContainer = { + hasUnhandledError: boolean; + typeString: string; +}; diff --git a/composition/src/subgraph/field-configuration.ts b/composition/src/subgraph/field-configuration.ts index a47e6d4d77..fcb997bb37 100644 --- a/composition/src/subgraph/field-configuration.ts +++ b/composition/src/subgraph/field-configuration.ts @@ -1,8 +1,21 @@ export type ConfigurationDataMap = Map; +export type RequiredFieldConfiguration = { + fieldName: string; + selectionSet: string; +} + export type ConfigurationData = { fieldNames: Set; isRootNode: boolean; - selectionSets: string[]; + provides?: RequiredFieldConfiguration[]; + keys?: RequiredFieldConfiguration[]; + requires?: RequiredFieldConfiguration[]; + typeName: string; +}; + +export type ArgumentConfigurationData = { + argumentNames: string[]; + fieldName: string; typeName: string; }; \ No newline at end of file diff --git a/composition/src/subgraph/subgraph.ts b/composition/src/subgraph/subgraph.ts index c6cb5d4d8c..1e92e6c5d9 100644 --- a/composition/src/subgraph/subgraph.ts +++ b/composition/src/subgraph/subgraph.ts @@ -1,8 +1,8 @@ import { DocumentNode, OperationTypeNode, visit } from 'graphql'; import { FederationFactory } from '../federation/federation-factory'; import { - getInlineFragmentString, - isKindAbstract, + addConcreteTypesForImplementedInterfaces, + addConcreteTypesForUnion, isNodeShareable, isObjectLikeNodeEntity, operationTypeNodeToDefaultType, @@ -10,8 +10,7 @@ import { } from '../ast/utils'; import { getNamedTypeForChild } from '../type-merging/type-merging'; import { getOrThrowError } from '../utils/utils'; -import { printTypeNode } from '@graphql-tools/merge'; -import { ENTITIES_FIELD, SERVICE, SERVICE_FIELD } from '../utils/string-constants'; +import { ENTITIES, ENTITIES_FIELD, OPERATION_TO_DEFAULT, SERVICE, SERVICE_FIELD } from '../utils/string-constants'; export type Subgraph = { definitions: DocumentNode; @@ -40,8 +39,16 @@ export function validateSubgraphName( } // Places the object-like nodes into the multigraph including the concrete types for abstract types -export function walkSubgraphToCollectObjects(factory: FederationFactory, subgraph: InternalSubgraph) { +export function walkSubgraphToCollectObjectLikesAndDirectiveDefinitions( + factory: FederationFactory, + subgraph: InternalSubgraph, +) { subgraph.definitions = visit(subgraph.definitions, { + DirectiveDefinition: { + enter(node) { + factory.upsertDirectiveNode(node); + }, + }, InterfaceTypeDefinition: { enter(node) { factory.upsertParentNode(node); @@ -50,12 +57,10 @@ export function walkSubgraphToCollectObjects(factory: FederationFactory, subgrap ObjectTypeDefinition: { enter(node) { const name = node.name.value; - if (name === SERVICE) { - return false; - } const operationType = subgraph.operationTypes.get(name); - const parentTypeName = operationType ? getOrThrowError(operationTypeNodeToDefaultType, operationType) : name; - factory.addConcreteTypesForInterface(node); + const parentTypeName = operationType + ? getOrThrowError(operationTypeNodeToDefaultType, operationType, OPERATION_TO_DEFAULT) : name; + addConcreteTypesForImplementedInterfaces(node, factory.abstractToConcreteTypeNames); if (!factory.graph.hasNode(parentTypeName)) { factory.graph.addNode(parentTypeName); } @@ -75,8 +80,9 @@ export function walkSubgraphToCollectObjects(factory: FederationFactory, subgrap enter(node) { const name = node.name.value; const operationType = subgraph.operationTypes.get(name); - const parentTypeName = operationType ? getOrThrowError(operationTypeNodeToDefaultType, operationType) : name; - factory.addConcreteTypesForInterface(node); + const parentTypeName = operationType + ? getOrThrowError(operationTypeNodeToDefaultType, operationType, OPERATION_TO_DEFAULT) : name; + addConcreteTypesForImplementedInterfaces(node, factory.abstractToConcreteTypeNames); if (!factory.graph.hasNode(parentTypeName)) { factory.graph.addNode(parentTypeName); } @@ -95,20 +101,20 @@ export function walkSubgraphToCollectObjects(factory: FederationFactory, subgrap UnionTypeDefinition: { enter(node) { factory.upsertParentNode(node); - factory.addConcreteTypesForUnion(node); + addConcreteTypesForUnion(node, factory.abstractToConcreteTypeNames); }, }, }); } -export function walkSubgraphToCollectOperationsAndFields(factory: FederationFactory, subgraph: Subgraph) { +export function walkSubgraphToCollectFields( + factory: FederationFactory, + subgraph: Subgraph, +) { let isCurrentParentRootType = false; visit(subgraph.definitions, { ObjectTypeDefinition: { enter(node) { - if (node.name.value === SERVICE) { - return false; - } isCurrentParentRootType = factory.isObjectRootType(node); factory.isCurrentParentEntity = isObjectLikeNodeEntity(node); factory.parentTypeName = node.name.value; @@ -132,73 +138,10 @@ export function walkSubgraphToCollectOperationsAndFields(factory: FederationFact FieldDefinition: { enter(node) { const fieldName = node.name.value; - if(isCurrentParentRootType){ - if(fieldName === SERVICE_FIELD || fieldName === ENTITIES_FIELD){ - return false - } - } - const fieldPath = `${factory.parentTypeName}.${fieldName}`; - const fieldRootTypeName = getNamedTypeForChild(fieldPath, node.type); - // If a node exists in the multigraph, it's a concrete object type - // Only add the edge if it hasn't already been added through another subgraph - if (factory.graph.hasNode(fieldRootTypeName) && !factory.graphEdges.has(fieldPath)) { - factory.graph.addEdge(factory.parentTypeName, fieldRootTypeName, { fieldName }); - factory.graphEdges.add(fieldPath); - } if (factory.isCurrentParentEntity) { - const entity = getOrThrowError(factory.entityMap, factory.parentTypeName); + const entity = getOrThrowError(factory.entities, factory.parentTypeName, ENTITIES); entity.fields.add(fieldName); } - if (!isCurrentParentRootType) { - // If the response type is a concrete field or the path has already been added, there's nothing further to do - if (factory.graph.hasNode(fieldRootTypeName) || factory.graphEdges.has(fieldPath)) { - return false; - } - // If the field is an abstract response type, an edge for each concrete response type must be added - factory.graphEdges.add(fieldPath); - const concreteTypeNames = factory.abstractToConcreteTypeNames.get(fieldRootTypeName); - // It is possible for an interface to have no implementers - if (!concreteTypeNames) { - return false; - } - for (const concreteTypeName of concreteTypeNames) { - factory.graph.addEdge(factory.parentTypeName, concreteTypeName, { - fieldName, - inlineFragment: getInlineFragmentString(concreteTypeName), - }); - } - return false; - } - // If the operation returns a concrete type, upsert the field - // This also records the appearance of this field in the current subgraph - if (factory.graph.hasNode(fieldRootTypeName)) { - factory.upsertConcreteObjectLikeOperationFieldNode( - fieldName, - fieldRootTypeName, - fieldPath, - printTypeNode(node.type), - ); - return false; - } - const parentContainer = factory.parentMap.get(fieldRootTypeName); - // If the field is not an abstract response type, it is not an object-like, so return - if (!parentContainer || !isKindAbstract(parentContainer.kind)) { - return false; - } - // At his point, it is known that this field is an abstract response type on an operation - const concreteTypes = factory.abstractToConcreteTypeNames.get(fieldRootTypeName); - // It is possible for an interface to have no implementers - if (!concreteTypes) { - return false; - } - // Upsert response types and add edges from the operation to each possible concrete type for the abstract field - factory.upsertAbstractObjectLikeOperationFieldNode( - fieldName, - fieldRootTypeName, - fieldPath, - printTypeNode(node.type), - concreteTypes, - ); return false; }, }, @@ -213,14 +156,9 @@ export function walkSubgraphToCollectOperationsAndFields(factory: FederationFact 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: { @@ -243,14 +181,34 @@ export function walkSubgraphToFederate(subgraph: DocumentNode, factory: Federati }, FieldDefinition: { enter(node) { - const name = node.name.value; - if (factory.isParentRootType) { - if (name === SERVICE_FIELD || name === ENTITIES_FIELD) { - return false; - } - } - factory.childName = name; + const fieldName = node.name.value; + const fieldPath = `${factory.parentTypeName}.${fieldName}`; + const fieldNamedTypeName = getNamedTypeForChild(fieldPath, node.type); + if (factory.isParentRootType && (fieldName === SERVICE_FIELD || fieldName === ENTITIES_FIELD)) { + return false; + } + factory.childName = fieldName; factory.upsertFieldNode(node); + if (!factory.graph.hasNode(factory.parentTypeName) || factory.graphEdges.has(fieldPath)) { + return; + } + factory.graphEdges.add(fieldPath); + // If the parent node is never an entity, add the child edge + // Otherwise, only add the child edge if the child is a field on a subgraph where the object is an entity + const entity = factory.entities.get(factory.parentTypeName); + if (entity && !entity.fields.has(fieldName)) { + return; + } + const concreteTypeNames = factory.abstractToConcreteTypeNames.get(fieldNamedTypeName); + if (concreteTypeNames) { + for (const concreteTypeName of concreteTypeNames) { + factory.graph.addEdge(factory.parentTypeName, concreteTypeName); + } + } + if (!factory.graph.hasNode(fieldNamedTypeName)) { + return; + } + factory.graph.addEdge(factory.parentTypeName, fieldNamedTypeName); }, leave() { factory.childName = ''; diff --git a/composition/src/utils/constants.ts b/composition/src/utils/constants.ts index 51b2b82999..8a4227f2dc 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, @@ -25,11 +25,14 @@ import { UNION_UPPER, } from './string-constants'; -export const BASE_SCALARS = new Set(['Boolean', 'Float', 'ID', 'Int', 'String']); +export const BASE_SCALARS = new Set( + ['_Any', '_Entities', 'Boolean', 'Float', 'ID', 'Int', 'String'], +); 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 +50,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 +62,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 +142,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 +160,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 +201,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 f4b5e2aa19..b8fac4be43 100644 --- a/composition/src/utils/string-constants.ts +++ b/composition/src/utils/string-constants.ts @@ -1,19 +1,24 @@ 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 ENTITIES = 'entities'; export const ENTITIES_FIELD = '_entities'; export const ENUM_UPPER = 'ENUM'; export const ENUM_VALUE_UPPER = 'ENUM_VALUE'; export const EXTERNAL = 'external'; export const EXTENDS = 'extends'; +export const EXTENSIONS = 'extensions' +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_SPREAD_UPPER = 'FRAGMENT_SPREAD'; export const FRAGMENT_REPRESENTATION = ' { ... }'; export const INLINE_FRAGMENT = 'inlineFragment'; export const INLINE_FRAGMENT_UPPER = 'INLINE_FRAGMENT'; @@ -25,14 +30,18 @@ export const KEY = 'key'; export const MUTATION = 'Mutation'; export const MUTATION_UPPER = 'MUTATION'; export const NAME = 'name'; +export const OPERATION_TO_DEFAULT = 'operationTypeNodeToDefaultType'; export const OBJECT_UPPER = 'OBJECT'; +export const PARENTS = 'parents'; export const PROVIDES = 'provides'; export const QUERY = 'Query'; export const QUERY_UPPER = 'QUERY'; +export const QUOTATION_JOIN = `", "`; export const REQUIRES = 'requires'; export const SCALAR_UPPER = 'SCALAR'; export const SCHEMA = 'schema'; export const SCHEMA_UPPER = 'SCHEMA'; +export const SELECTION_REPRESENTATION = ' { ... }'; export const SERVICE = '_Service'; export const SERVICE_FIELD = '_service'; export const SHAREABLE = 'shareable'; @@ -40,5 +49,8 @@ export const STRING_TYPE = 'String'; export const SUBSCRIPTION = 'Subscription'; export const SUBSCRIPTION_UPPER = 'SUBSCRIPTION'; export const TAG = 'tag'; +export const UNION = 'union'; export const UNION_UPPER = 'UNION'; -export const VARIABLE_DEFINITION_UPPER = 'VARIABLE_DEFINITION'; \ No newline at end of file +export const VARIABLE_DEFINITION_UPPER = 'VARIABLE_DEFINITION'; + +export const ROOT_TYPES = new Set([MUTATION, QUERY, SUBSCRIPTION]); \ No newline at end of file diff --git a/composition/src/utils/utils.ts b/composition/src/utils/utils.ts index 392c2070f9..b040012d1c 100644 --- a/composition/src/utils/utils.ts +++ b/composition/src/utils/utils.ts @@ -1,4 +1,7 @@ -import { Kind, TypeNode } from 'graphql'; +import { Kind } from 'graphql'; +import { FIELD, UNION } from './string-constants'; +import { MultiGraph } from 'graphology'; +import { invalidKeyFatalError } from '../errors/errors'; export function areSetsEqual(set: Set, other: Set): boolean { if (set.size !== other.size) { @@ -12,10 +15,10 @@ export function areSetsEqual(set: Set, other: Set): boolean { return true; } -export function getOrThrowError(map: Map, key: K): V { +export function getOrThrowError(map: Map, key: K, mapName: string): V { const value = map.get(key); if (value === undefined) { - throw new Error(`Expected the key ${key} to exist in map ${map}.`); // TODO + throw invalidKeyFatalError(key, mapName) } return value; } @@ -88,10 +91,14 @@ export function kindToTypeString(kind: Kind): string { return 'enum'; case Kind.ENUM_TYPE_EXTENSION: return 'enum extension'; + case Kind.FIELD_DEFINITION: + return FIELD; case Kind.INPUT_OBJECT_TYPE_DEFINITION: return 'input object'; case Kind.INPUT_OBJECT_TYPE_EXTENSION: return 'input object extension'; + case Kind.INPUT_VALUE_DEFINITION: + return 'input value'; case Kind.INTERFACE_TYPE_DEFINITION: return 'interface'; case Kind.INTERFACE_TYPE_EXTENSION: @@ -105,7 +112,7 @@ export function kindToTypeString(kind: Kind): string { case Kind.SCALAR_TYPE_EXTENSION: return 'scalar extension'; case Kind.UNION_TYPE_DEFINITION: - return 'union'; + return UNION; case Kind.UNION_TYPE_EXTENSION: return 'union extension'; default: @@ -113,30 +120,6 @@ export function kindToTypeString(kind: Kind): string { } } -export function isTypeValidImplementation(originalType: TypeNode, implementationType: TypeNode): boolean { - if (originalType.kind === Kind.NON_NULL_TYPE) { - if (implementationType.kind !== Kind.NON_NULL_TYPE) { - return false; - } - return isTypeValidImplementation(originalType.type, implementationType.type); - } - if (implementationType.kind === Kind.NON_NULL_TYPE) { - return isTypeValidImplementation(originalType, implementationType.type); - } - switch (originalType.kind) { - case Kind.NAMED_TYPE: - if (implementationType.kind === Kind.NAMED_TYPE) { - return originalType.name.value === implementationType.name.value; - } - return false; - default: - if (implementationType.kind === Kind.LIST_TYPE) { - return isTypeValidImplementation(originalType.type, implementationType.type); - } - return false; - } -} - export type InvalidArgumentImplementation = { actualType: string; argumentName: string; @@ -156,4 +139,79 @@ export type ImplementationErrors = { unimplementedFields: string[]; }; -export type ImplementationErrorsMap = Map; \ No newline at end of file +export type ImplementationErrorsMap = Map; + +export type InvalidRequiredArgument = { + argumentName: string; + missingSubgraphs: string[]; + requiredSubgraphs: string[]; +}; + +export type InvalidArgument = { + argumentName: string; + namedType: string; + typeName: string; + typeString: string; +}; + +class StackSet { + set = new Set(); + stack: string[] = []; + + constructor(value: string) { + this.push(value); + } + + has(value: string): boolean { + return this.set.has(value); + } + + push(value: string) { + this.stack.push(value); + this.set.add(value); + } + + pop() { + const value = this.stack.pop(); + if (value) { + this.set.delete(value); + } + }; +} + +export function hasSimplePath(graph: MultiGraph, source: string, target: string): boolean { + if (!graph.hasNode(source) || !graph.hasNode(target)) { + return false; + } + + const stack = [graph.outboundNeighbors(source)]; + const visited = new StackSet(source); + let children, child; + + while (stack.length > 0) { + children = stack[stack.length - 1]; + child = children.pop(); + + if (!child) { + stack.pop(); + visited.pop(); + continue; + } + if (visited.has(child)) { + continue; + } + + if (child === target) { + return true; + } + + visited.push(child); + + if (!visited.has(target)) { + stack.push(graph.outboundNeighbors(child)); + } else { + visited.pop(); + } + } + return false; +} \ No newline at end of file diff --git a/composition/tests/arguments.test.ts b/composition/tests/arguments.test.ts index d587bcad7d..3e07617af2 100644 --- a/composition/tests/arguments.test.ts +++ b/composition/tests/arguments.test.ts @@ -1,13 +1,19 @@ import { + duplicateArgumentsError, federateSubgraphs, incompatibleArgumentDefaultValueError, incompatibleArgumentDefaultValueTypeError, incompatibleArgumentTypesError, + invalidArgumentsError, + InvalidRequiredArgument, + invalidRequiredArgumentsError, + normalizeSubgraphFromString, Subgraph, } 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'; @@ -15,84 +21,96 @@ describe('Argument federation tests', () => { const childName = 'field'; test('that equal arguments merge', () => { - const result = federateSubgraphs([ + const { errors, federationResult } = federateSubgraphs([ subgraphWithArgument('subgraph-a', 'String'), subgraphWithArgument('subgraph-b', 'String'), ]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: String): String - } + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: String): String + } `, ), ); }); test('that arguments merge into their most restrictive form #1', () => { - const result = federateSubgraphs([ + const { errors, federationResult } = federateSubgraphs([ subgraphWithArgument('subgraph-a', 'Float!'), subgraphWithArgument('subgraph-b', 'Float'), ]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Float!): String - } + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Float!): String + } `, ), ); }); test('that if not all arguments have a default value, the default value is ignored', () => { - const result = federateSubgraphs([ + const { errors, federationResult } = federateSubgraphs([ subgraphWithArgument('subgraph-a', 'Int'), subgraphWithArgumentAndDefaultValue('subgraph-b', 'Int', '1337'), ]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Int): String - } + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Int): String + } `, ), ); }); test('that if all arguments have the same default value, the default value is included', () => { - const result = federateSubgraphs([ + const { errors, federationResult } = federateSubgraphs([ subgraphWithArgumentAndDefaultValue('subgraph-a', 'Boolean', 'false'), subgraphWithArgumentAndDefaultValue('subgraph-b', 'Boolean', 'false'), ]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Boolean = false): String - } + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Boolean = false): String + } `, ), ); }); test('that if arguments of the same name are not the same type, an error is returned`', () => { - const result = federateSubgraphs([ + const { errors } = federateSubgraphs([ subgraphWithArgument('subgraph-a', 'String'), subgraphWithArgument('subgraph-b', 'Float'), ]); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).deep.equal( + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( incompatibleArgumentTypesError(argName, parentName, childName, 'String', 'Float'), ); }); @@ -100,46 +118,138 @@ describe('Argument federation tests', () => { test('that if arguments have different string-converted default values, an error is returned`', () => { const expectedType = '1'; const actualType = '2'; - const result = federateSubgraphs([ + const { errors } = federateSubgraphs([ subgraphWithArgumentAndDefaultValue('subgraph-a', 'Int', expectedType), subgraphWithArgumentAndDefaultValue('subgraph-b', 'Int', actualType), ]); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).deep.equal( + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( incompatibleArgumentDefaultValueError(argName, parentName, childName, expectedType, actualType), ); }); test('that if arguments have different boolean default values, an error is returned`', () => { - const result = federateSubgraphs([ + const { errors } = federateSubgraphs([ subgraphWithArgumentAndDefaultValue('subgraph-a', 'Boolean', 'true'), subgraphWithArgumentAndDefaultValue('subgraph-b', 'Boolean', 'false'), ]); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).deep.equal( + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( incompatibleArgumentDefaultValueError(argName, parentName, childName, true, false), ); }); test('that if arguments have incompatible default values, an error is returned', () => { - const result = federateSubgraphs([ + const { errors } = federateSubgraphs([ subgraphWithArgumentAndDefaultValue('subgraph-a', 'Boolean', '1'), subgraphWithArgumentAndDefaultValue('subgraph-b', 'Boolean', 'false'), ]); - expect(result.errors).toHaveLength(2); - expect(result.errors![0]).deep.equal( + expect(errors).toHaveLength(2); + expect(errors![0]).toStrictEqual( incompatibleArgumentDefaultValueTypeError(argName, parentName, childName, Kind.INT, Kind.BOOLEAN), ); - expect(result.errors![1]).deep.equal( + expect(errors![1]).toStrictEqual( incompatibleArgumentDefaultValueError(argName, parentName, childName, '1', false), ); }); + + 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(versionTwoPersistedBaseSchema + ` + interface Interface { + field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean): String + } + + type Query { + dummy: String! + } + + type Object implements Interface { + field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean): String + } + `, + ), + ); + }); + + test('that if a required argument is not defined in all definitions of a field, an error is returned', () => { + const { errors } = federateSubgraphs([subgraphA, subgraphC]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(2); + const errorArrayOne: InvalidRequiredArgument[] = [{ + argumentName: 'requiredInAll', + missingSubgraphs: ['subgraph-c'], + requiredSubgraphs: ['subgraph-a'], + }, { + argumentName: 'requiredOrOptionalInAll', + missingSubgraphs: ['subgraph-c'], + requiredSubgraphs: ['subgraph-a'], + }]; + expect(errors![0]).toStrictEqual(invalidRequiredArgumentsError(FIELD, 'Interface.field', errorArrayOne)); + const errorArrayTwo: InvalidRequiredArgument[] = [{ + argumentName: 'requiredInAll', + missingSubgraphs: ['subgraph-c'], + requiredSubgraphs: ['subgraph-a'], + }, { + argumentName: 'requiredOrOptionalInAll', + missingSubgraphs: ['subgraph-c'], + requiredSubgraphs: ['subgraph-a'], + }]; + 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', () => { + const { errors } = normalizeSubgraphFromString(` + enum Enum { + A + B + C + } + + input Input { + a: String! + b: Int! + c: Float! + } + + type AnotherObject { + a: String! + b: Int! + c: Float! + } + + type Object { + field(argOne: Enum!, argTwo: Input!, argThree: AnotherObject! argThree: String!, argOne: Enum!): String! + } + `); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(2); + expect(errors![0]).toStrictEqual(duplicateArgumentsError( + 'Object.field', + ['argThree', 'argOne'], + )); + expect(errors![1]).toStrictEqual(invalidArgumentsError( + 'Object.field', + [{ + argumentName: 'argThree', + namedType: 'AnotherObject', + typeName: 'AnotherObject!', + typeString: 'object', + }], + )); + }); }); const subgraphWithArgument = (name: string, typeName: string): Subgraph => ({ name, url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + type Object @shareable { field(input: ${typeName}): String } @@ -150,8 +260,58 @@ const subgraphWithArgumentAndDefaultValue = (name: string, typeName: string, def name, url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + type Object @shareable { field(input: ${typeName} = ${defaultValue}): String } `), }); + +const subgraphA = { + name: 'subgraph-a', + url: '', + definitions: parse(` + type Query { + dummy: String! @shareable + } + + interface Interface { + field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean, optionalInSome: Float): String + } + + type Object implements Interface @shareable { + field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean, optionalInSome: Float): String + } + `), +}; + +const subgraphB = { + name: 'subgraph-b', + url: '', + definitions: parse(` + interface Interface { + field(requiredInAll: Int!, requiredOrOptionalInAll: String, optionalInAll: Boolean): String + } + + type Object implements Interface @shareable { + field(requiredInAll: Int!, requiredOrOptionalInAll: String, optionalInAll: Boolean): String + } + `), +}; + +const subgraphC = { + name: 'subgraph-c', + url: '', + definitions: parse(` + interface Interface { + field(optionalInAll: Boolean): String + } + + type Object implements Interface @shareable { + field(optionalInAll: Boolean): String + } + `), +}; \ No newline at end of file diff --git a/composition/tests/entities.test.ts b/composition/tests/entities.test.ts index d2405c6f64..4c1165064a 100644 --- a/composition/tests/entities.test.ts +++ b/composition/tests/entities.test.ts @@ -1,17 +1,20 @@ -import { federateSubgraphs, RootTypeField, Subgraph, unresolvableFieldError } from '../src'; +import { federateSubgraphs, RootTypeFieldData, 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', () => { test('that entities merge successfully', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` + type Query { + dummy: String! + } + type Trainer { id: Int! details: Details! @@ -33,13 +36,17 @@ describe('Entities federation tests', () => { }); test('that an entity and non-declared entity merge if the non-entity is resolvable', () => { - const result = federateSubgraphs([subgraphA, subgraphC]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphC]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` + type Query { + dummy: String! + trainer: Trainer! + } + type Trainer { id: Int! details: Details! @@ -51,10 +58,6 @@ describe('Entities federation tests', () => { age: Int! } - type Query { - trainer: Trainer! - } - type Pokemon { name: String! level: Int! @@ -65,37 +68,34 @@ describe('Entities federation tests', () => { }); test('that if an unresolvable field appears in the first subgraph, it returns an error', () => { - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'trainer', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'trainer', + fieldTypeNodeString: 'Trainer!', path: 'Query.trainer', - parentTypeName: 'Query', - responseType: 'Trainer!', - rootTypeName: 'Trainer', subgraphs: new Set(['subgraph-e']), + typeName: 'Query', }; const result = federateSubgraphs([subgraphD, subgraphE]); expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(3); - expect(result.errors![0]).deep.equal( - unresolvableFieldError(rootTypeField, 'details', ['Query.trainer.details { ... }'], 'subgraph-d', 'Trainer'), - ); - // TODO these errors should not happen because it's the parent that's the problem - expect(result.errors![1]).deep.equal( - unresolvableFieldError(rootTypeField, 'name', ['Query.trainer.details.name'], 'subgraph-d', 'Details'), - ); - expect(result.errors![2]).deep.equal( - unresolvableFieldError(rootTypeField, 'age', ['Query.trainer.details.age'], 'subgraph-d', 'Details'), + expect(result.errors).toHaveLength(1); + expect(result.errors![0]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'details', + ['subgraph-d'], + 'Query.trainer.details { ... }', + 'Trainer' + ), ); }); test('that ancestors of resolvable entities are also determined to be resolvable', () => { - const result = federateSubgraphs([subgraphC, subgraphF]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphC, subgraphF]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { trainer: Trainer! @@ -126,12 +126,12 @@ describe('Entities federation tests', () => { }); test('that ancestors of resolvable entities that are not in the same subgraph return an error', () => { - const result = federateSubgraphs([subgraphC, subgraphF]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphC, subgraphF]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { trainer: Trainer! @@ -162,13 +162,12 @@ describe('Entities federation tests', () => { }); test('that V1 and V2 entities merge successfully', () => { - const result = federateSubgraphs([subgraphB, subgraphG]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphB, subgraphG]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` type Trainer { id: Int! pokemon: [Pokemon!]! @@ -179,6 +178,10 @@ describe('Entities federation tests', () => { name: String! level: Int! } + + type Query { + dummy: String! + } type Details { name: String! @@ -194,6 +197,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! + } + type Trainer @key(fields: "id") { id: Int! details: Details! @@ -296,6 +303,10 @@ const subgraphG: Subgraph = { name: 'subgraph-g', url: '', definitions: parse(` + type Query { + dummy: String! + } + extend type Trainer @key(fields: "id") { id: Int! details: Details! diff --git a/composition/tests/enums.test.ts b/composition/tests/enums.test.ts index abde1d8009..655b864d6b 100644 --- a/composition/tests/enums.test.ts +++ b/composition/tests/enums.test.ts @@ -1,19 +1,22 @@ import { federateSubgraphs, incompatibleSharedEnumError, Subgraph } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionOneBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionTwoPersistedBaseSchema } from './utils/utils'; describe('Enum federation tests', () => { const parentName = 'Instruction'; test('that enums merge by union if unused in inputs or arguments', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -26,13 +29,16 @@ describe('Enum federation tests', () => { }); test('that enums merge by intersection if used as an input', () => { - const result = federateSubgraphs([subgraphA, subgraphC]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphC]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -47,13 +53,16 @@ describe('Enum federation tests', () => { }); test('that enums merge by intersection if used as an argument', () => { - const result = federateSubgraphs([subgraphA, subgraphF]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphF]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT } @@ -67,13 +76,15 @@ describe('Enum federation tests', () => { }); test('that enums must be consistent if used as both an input and output', () => { - const result = federateSubgraphs([subgraphC, subgraphD]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + const { errors, federationResult } = federateSubgraphs([subgraphC, subgraphD]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoPersistedBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -103,6 +114,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + enum Instruction { FIGHT POKEMON @@ -125,6 +140,10 @@ const subgraphC = { name: 'subgraph-c', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + enum Instruction { FIGHT POKEMON diff --git a/composition/tests/federation-factory.test.ts b/composition/tests/federation-factory.test.ts index 03b2b53d63..feb7ecd375 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', () => { @@ -53,35 +53,15 @@ describe('FederationFactory tests', () => { }); test('that subgraphs are federated #1', () => { - const result = federateSubgraphs([pandas, products, reviews, users]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + const { errors, federationResult } = federateSubgraphs([pandas, products, reviews, users]); + 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 } - - interface ProductItf implements SkuItf { - id: ID! - sku: String - name: String - package: String - variation: ProductVariation - dimensions: ProductDimension - createdBy: User - hidden: String - oldField: String - reviewsCount: Int! - reviewsScore: Float! - reviews: [Review!]! - } type Query { allPandas: [Panda] @@ -93,22 +73,7 @@ describe('FederationFactory tests', () => { type Panda { name: ID! - favoriteFood: String - } - - type Product implements ProductItf & SkuItf { - id: ID! - sku: String - name: String - package: String - variation: ProductVariation - dimensions: ProductDimension - createdBy: User - hidden: String - reviewsScore: Float! - oldField: String - reviewsCount: Int! - reviews: [Review!]! + favoriteFood: String @tag(name: "nom-nom-nom") } enum ShippingClass { @@ -127,7 +92,7 @@ describe('FederationFactory tests', () => { } type User { - email: ID! + email: ID! @tag(name: "test-from-users") totalProductsCreated: Int name: String } @@ -136,18 +101,47 @@ describe('FederationFactory tests', () => { id: Int! body: String! } + + interface ProductItf implements SkuItf { + id: ID! + sku: String + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String @inaccessible + oldField: String @deprecated(reason: "refactored out") + reviewsCount: Int! + reviewsScore: Float! + reviews: [Review!]! + } + + type Product implements ProductItf & SkuItf { + id: ID! @tag(name: "hi-from-products") + sku: String + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String + reviewsScore: Float! + oldField: String + reviewsCount: Int! + reviews: [Review!]! + } `, ), ); }); test('that subgraphs are federated #2', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { pokemon: [Pokemon] @@ -183,11 +177,11 @@ describe('FederationFactory tests', () => { }); test('that root types are promoted', () => { - const { errors, federatedGraphAST } = federateSubgraphs([subgraphE]); + const { errors, federationResult } = federateSubgraphs([subgraphE]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federatedGraphAST!)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` type Query { string: String @@ -198,12 +192,11 @@ describe('FederationFactory tests', () => { }); test('that custom root types are renamed', () => { - const { errors, federatedGraphAST } = federateSubgraphs([subgraphF]); + const { errors, federationResult } = federateSubgraphs([subgraphF]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federatedGraphAST!)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` type Query { string: String } @@ -213,12 +206,11 @@ describe('FederationFactory tests', () => { }); test('that service object, entities and service fields are not included in the federated graph', () => { - const { errors, federatedGraphAST } = federateSubgraphs([subgraphG, subgraphH]); + const { errors, federationResult } = federateSubgraphs([subgraphG, subgraphH]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federatedGraphAST!)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` union _Entity = User type Query { @@ -234,6 +226,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 = { @@ -521,3 +559,75 @@ const subgraphH: Subgraph = { scalar _Any `), }; + +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/federation-test-data.ts b/composition/tests/federation-test-data.ts deleted file mode 100644 index 11926c7838..0000000000 --- a/composition/tests/federation-test-data.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Subgraph } from '../src'; -import { parse } from 'graphql'; - -export const baseSchema: Subgraph = { - name: 'base-schema', - url: '', - definitions: parse(` - directive @key(fields: String!) on OBJECT | INTERFACE - - directive @external on OBJECT | FIELD_DEFINITION - - directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - - directive @provides(fields: String!) on FIELD_DEFINITION - - directive @requires(fields: String!) on FIELD_DEFINITION - - directive @shareable on OBJECT | FIELD_DEFINITION - - type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! - } - - scalar _Entity - - scalar _Any - - type _Service { - """ - The sdl representing the federated service capabilities. Includes federation directives, removes federation types, and includes rest of full schema after schema directives have been applied - """ - sdl: String - } - `), -}; diff --git a/composition/tests/field-configuration.test.ts b/composition/tests/field-configuration.test.ts index 48b3ac9601..2d355a05e7 100644 --- a/composition/tests/field-configuration.test.ts +++ b/composition/tests/field-configuration.test.ts @@ -1,204 +1,206 @@ import { describe, expect, test } from 'vitest'; -import { normalizeSubgraphFromString } from '../src'; -import { ConfigurationData } from '../src/subgraph/field-configuration'; +import { ConfigurationData, federateSubgraphs, normalizeSubgraphFromString } from '../src'; +import { createSubgraph } from './utils/utils'; describe('Field Configuration tests', () => { - test('that field configuration for employees.graphql is correctly generated', () => { - const { errors, normalizationResult } = normalizeSubgraphFromString(employees); - expect(errors).toBeUndefined(); - expect(normalizationResult).toBeDefined(); - const configurationDataMap = normalizationResult!.configurationDataMap; - expect(configurationDataMap).toStrictEqual(new Map([ - ['Query', { - fieldNames: new Set(['employee', 'employees', 'team_mates']), - isRootNode: true, - selectionSets: [], - typeName: 'Query', - }], - ['RoleType', { - fieldNames: new Set(['department', 'title']), - isRootNode: false, - selectionSets: [], - typeName: 'RoleType', - }], - ['Identifiable', { - fieldNames: new Set(['id']), - isRootNode: false, - selectionSets: [], - typeName: 'Identifiable', - }], - ['Engineer', { - fieldNames: new Set(['department', 'engineerType', 'title']), - isRootNode: false, - selectionSets: [], - typeName: 'Engineer', - }], - ['Marketer', { - fieldNames: new Set(['department', 'title']), - isRootNode: false, - selectionSets: [], - typeName: 'Marketer', - }], - ['Operator', { - fieldNames: new Set(['department', 'operatorType', 'title']), - isRootNode: false, - selectionSets: [], - typeName: 'Operator', - }], - ['Details', { - fieldNames: new Set(['forename', 'location', 'surname']), - isRootNode: false, - selectionSets: [], - typeName: 'Details', - }], - ['Employee', { - fieldNames: new Set(['details', 'id', 'role']), - isRootNode: true, - selectionSets: ['id'], - typeName: 'Employee', - }], - ])); + describe('Normalization tests' ,() => { + test('that field configuration for employees.graphql is correctly generated', () => { + const { errors, normalizationResult } = normalizeSubgraphFromString(employees); + expect(errors).toBeUndefined(); + expect(normalizationResult).toBeDefined(); + const configurationDataMap = normalizationResult!.configurationDataMap; + expect(configurationDataMap).toStrictEqual(new Map([ + ['Query', { + fieldNames: new Set(['employee', 'employees', 'team_mates']), + isRootNode: true, + typeName: 'Query', + }], + ['RoleType', { + fieldNames: new Set(['department', 'title']), + isRootNode: false, + typeName: 'RoleType', + }], + ['Identifiable', { + fieldNames: new Set(['id']), + isRootNode: false, + typeName: 'Identifiable', + }], + ['Engineer', { + fieldNames: new Set(['department', 'engineerType', 'title']), + isRootNode: false, + typeName: 'Engineer', + }], + ['Marketer', { + fieldNames: new Set(['department', 'title']), + isRootNode: false, + typeName: 'Marketer', + }], + ['Operator', { + fieldNames: new Set(['department', 'operatorType', 'title']), + isRootNode: false, + typeName: 'Operator', + }], + ['Details', { + fieldNames: new Set(['forename', 'location', 'surname']), + isRootNode: false, + typeName: 'Details', + }], + ['Employee', { + fieldNames: new Set(['details', 'id', 'role']), + isRootNode: true, + keys: [{ fieldName: '', selectionSet: 'id' }], + typeName: 'Employee', + }], + ])); + }); + + test('that field configuration for family.graphql is correctly generated', () => { + const { errors, normalizationResult } = normalizeSubgraphFromString(family); + expect(errors).toBeUndefined(); + expect(normalizationResult).toBeDefined(); + const configurationDataMap = normalizationResult!.configurationDataMap; + expect(configurationDataMap).toStrictEqual(new Map([ + ['Animal', { + fieldNames: new Set(['class', 'gender']), + isRootNode: false, + typeName: 'Animal', + }], + ['Pet', { + fieldNames: new Set(['class', 'gender', 'name']), + isRootNode: false, + typeName: 'Pet', + }], + ['Alligator', { + fieldNames: new Set(['class', 'dangerous', 'gender', 'name']), + isRootNode: false, + typeName: 'Alligator', + }], + ['Cat', { + fieldNames: new Set(['class', 'gender', 'name', 'type']), + isRootNode: false, + typeName: 'Cat', + }], + ['Dog', { + fieldNames: new Set(['breed', 'class', 'gender', 'name']), + isRootNode: false, + typeName: 'Dog', + }], + ['Mouse', { + fieldNames: new Set(['class', 'gender', 'name']), + isRootNode: false, + typeName: 'Mouse', + }], + ['Pony', { + fieldNames: new Set(['class', 'gender', 'name']), + isRootNode: false, + typeName: 'Pony', + }], + ['Details', { + fieldNames: new Set(['forename', 'surname']), + isRootNode: false, + typeName: 'Details', + }], + ['Employee', { + fieldNames: new Set(['details', 'id', 'hasChildren', 'maritalStatus', 'nationality', 'pets']), + isRootNode: true, + keys: [{ fieldName: '', selectionSet: 'id', }], + typeName: 'Employee', + }], + ])); + }); + + test('that field configuration for hobbies.graphql is correctly generated', () => { + const { errors, normalizationResult } = normalizeSubgraphFromString(hobbies); + expect(errors).toBeUndefined(); + expect(normalizationResult).toBeDefined(); + const configurationDataMap = normalizationResult!.configurationDataMap; + expect(configurationDataMap).toStrictEqual(new Map([ + ['Exercise', { + fieldNames: new Set(['category']), + isRootNode: false, + typeName: 'Exercise', + }], + ['Experience', { + fieldNames: new Set(['yearsOfExperience']), + isRootNode: false, + typeName: 'Experience', + }], + ['Flying', { + fieldNames: new Set(['planeModels', 'yearsOfExperience']), + isRootNode: false, + typeName: 'Flying', + }], + ['Gaming', { + fieldNames: new Set(['genres', 'name', 'yearsOfExperience']), + isRootNode: false, + typeName: 'Gaming', + }], + ['Other', { + fieldNames: new Set(['name']), + isRootNode: false, + typeName: 'Other', + }], + ['Programming', { + fieldNames: new Set(['languages']), + isRootNode: false, + typeName: 'Programming', + }], + ['Travelling', { + fieldNames: new Set(['countriesLived']), + isRootNode: false, + typeName: 'Travelling', + }], + ['Employee', { + fieldNames: new Set(['id', 'hobbies']), + isRootNode: true, + keys: [{ fieldName: '', selectionSet: 'id', }], + typeName: 'Employee', + }], + ])); + }); + + test('that field configuration for products.graphql is correctly generated', () => { + const { errors, normalizationResult } = normalizeSubgraphFromString(products); + expect(errors).toBeUndefined(); + expect(normalizationResult).toBeDefined(); + const configurationDataMap = normalizationResult!.configurationDataMap; + expect(configurationDataMap).toStrictEqual(new Map([ + ['Employee', { + fieldNames: new Set(['id', 'products']), + isRootNode: true, + keys: [{ fieldName: '', selectionSet: 'id', }], + typeName: 'Employee', + }], + ])); + }); }); - test('that field configuration for family.graphql is correctly generated', () => { - const { errors, normalizationResult } = normalizeSubgraphFromString(family); - expect(errors).toBeUndefined(); - expect(normalizationResult).toBeDefined(); - const configurationDataMap = normalizationResult!.configurationDataMap; - expect(configurationDataMap).toStrictEqual(new Map([ - ['Animal', { - fieldNames: new Set(['class', 'gender']), - isRootNode: false, - selectionSets: [], - typeName: 'Animal', - }], - ['Pet', { - fieldNames: new Set(['class', 'gender', 'name']), - isRootNode: false, - selectionSets: [], - typeName: 'Pet', - }], - ['Alligator', { - fieldNames: new Set(['class', 'dangerous', 'gender', 'name']), - isRootNode: false, - selectionSets: [], - typeName: 'Alligator', - }], - ['Cat', { - fieldNames: new Set(['class', 'gender', 'name', 'type']), - isRootNode: false, - selectionSets: [], - typeName: 'Cat', - }], - ['Dog', { - fieldNames: new Set(['breed', 'class', 'gender', 'name']), - isRootNode: false, - selectionSets: [], - typeName: 'Dog', - }], - ['Mouse', { - fieldNames: new Set(['class', 'gender', 'name']), - isRootNode: false, - selectionSets: [], - typeName: 'Mouse', - }], - ['Pony', { - fieldNames: new Set(['class', 'gender', 'name']), - isRootNode: false, - selectionSets: [], - typeName: 'Pony', - }], - ['Details', { - fieldNames: new Set(['forename', 'surname']), - isRootNode: false, - selectionSets: [], - typeName: 'Details', - }], - ['Employee', { - fieldNames: new Set(['details', 'id', 'hasChildren', 'maritalStatus', 'nationality', 'pets']), - isRootNode: true, - selectionSets: ['id'], - typeName: 'Employee', - }], - ])); - }); - - test('that field configuration for hobbies.graphql is correctly generated', () => { - const { errors, normalizationResult } = normalizeSubgraphFromString(hobbies); - expect(errors).toBeUndefined(); - expect(normalizationResult).toBeDefined(); - const configurationDataMap = normalizationResult!.configurationDataMap; - expect(configurationDataMap).toStrictEqual(new Map([ - ['Exercise', { - fieldNames: new Set(['category']), - isRootNode: false, - selectionSets: [], - typeName: 'Exercise', - }], - ['Experience', { - fieldNames: new Set(['yearsOfExperience']), - isRootNode: false, - selectionSets: [], - typeName: 'Experience', - }], - ['Flying', { - fieldNames: new Set(['planeModels', 'yearsOfExperience']), - isRootNode: false, - selectionSets: [], - typeName: 'Flying', - }], - ['Gaming', { - fieldNames: new Set(['genres', 'name', 'yearsOfExperience']), - isRootNode: false, - selectionSets: [], - typeName: 'Gaming', - }], - ['Other', { - fieldNames: new Set(['name']), - isRootNode: false, - selectionSets: [], - typeName: 'Other', - }], - ['Programming', { - fieldNames: new Set(['languages']), - isRootNode: false, - selectionSets: [], - typeName: 'Programming', - }], - ['Travelling', { - fieldNames: new Set(['countriesLived']), - isRootNode: false, - selectionSets: [], - typeName: 'Travelling', - }], - ['Employee', { - fieldNames: new Set(['id', 'hobbies']), - isRootNode: true, - selectionSets: ['id'], - typeName: 'Employee', - }], - ])); - }); - - test('that field configuration for products.graphql is correctly generated', () => { - const { errors, normalizationResult } = normalizeSubgraphFromString(products); - expect(errors).toBeUndefined(); - expect(normalizationResult).toBeDefined(); - const configurationDataMap = normalizationResult!.configurationDataMap; - expect(configurationDataMap).toStrictEqual(new Map([ - ['Employee', { - fieldNames: new Set(['id', 'products']), - isRootNode: true, - selectionSets: ['id'], - typeName: 'Employee', - }], - ])); + describe('Federation tests', () => { + test('that argument configurations are correctly generated', () => { + const { errors, federationResult } = federateSubgraphs([ + createSubgraph('employees', employees), createSubgraph('family', family), + createSubgraph('hobbies', hobbies), createSubgraph('products', products), + ]); + expect(errors).toBeUndefined(); + expect(federationResult!.argumentConfigurations).toStrictEqual([ + { + argumentNames: ['id'], + fieldName: 'employee', + typeName: 'Query', + }, + { + argumentNames: ['team'], + fieldName: 'team_mates', + typeName: 'Query', + }, + ]) + }); }); }); const employees = ` type Query { - employee(id: Int!): Employee! + employee(id: Int!): Employee employees: [Employee!]! team_mates(team: Department!): [Employee!]! } diff --git a/composition/tests/inputs.test.ts b/composition/tests/inputs.test.ts index 5d373399b4..1194c479ec 100644 --- a/composition/tests/inputs.test.ts +++ b/composition/tests/inputs.test.ts @@ -1,17 +1,20 @@ 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', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` + type Query { + dummy: String! + } + input TechnicalMachine { move: String! number: Int! @@ -21,14 +24,13 @@ describe('Input federation tests', () => { ); }); - // TODO shouldn't be a throw test('that a required input object field that is omitted from the federated graph returns an error', () => { - // const result = federateSubgraphs([subgraphA, subgraphC]); const parentName = 'TechnicalMachine'; const fieldName = 'move'; - expect(() => federateSubgraphs([subgraphA, subgraphC])).toThrowError( - federationRequiredInputFieldError(parentName, fieldName).message, - ); + const { errors } = federateSubgraphs([subgraphA, subgraphC]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual(federationRequiredInputFieldError(parentName, fieldName)); }); }); @@ -36,6 +38,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! + } + input TechnicalMachine { move: String! number: Int! diff --git a/composition/tests/interfaces.test.ts b/composition/tests/interfaces.test.ts index 70b98a3bc4..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', () => { @@ -161,19 +161,22 @@ describe('Interface tests', () => { describe('Federation tests', () => { test('that interfaces merge by union', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + - ` + versionTwoPersistedBaseSchema + ` interface Character { name: String! age: Int! isFriend: Boolean! } + type Query { + dummy: String! + } + type Trainer implements Character { name: String! age: Int! @@ -192,12 +195,11 @@ describe('Interface tests', () => { }); test('that interfaces and implementations merge by union', () => { - const { errors, federatedGraphAST } = federateSubgraphs([subgraphA, subgraphC]); + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphC]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federatedGraphAST!)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - ` + versionTwoPersistedBaseSchema + ` interface Character { name: String! age: Int! @@ -208,6 +210,10 @@ describe('Interface tests', () => { name: String! } + type Query { + dummy: String! + } + type Trainer implements Character & Human { name: String! age: Int! @@ -220,15 +226,19 @@ describe('Interface tests', () => { }); test('that nested interfaces merge by union', () => { - const { errors, federatedGraphAST} = federateSubgraphs([subgraphC, subgraphD]); + const { errors, federationResult } = federateSubgraphs([subgraphC, subgraphD]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federatedGraphAST!)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + ` + versionTwoPersistedBaseSchema + ` interface Character { isFriend: Boolean! } + type Query { + dummy: String! + } + interface Human implements Character { name: String! isFriend: Boolean! @@ -303,6 +313,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + interface Character { name: String! } @@ -343,6 +357,10 @@ const subgraphC: Subgraph = { name: 'subgraph-c', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + interface Character { isFriend: Boolean! } @@ -382,6 +400,10 @@ const subgraphE: Subgraph = { name: 'subgraph-e', url: '', definitions: parse(` + type Query { + dummy: String! + } + interface Animal { sounds(a: String!, b: Int!): [String] } 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 b019900328..fb4b2c7339 100644 --- a/composition/tests/queries.test.ts +++ b/composition/tests/queries.test.ts @@ -1,21 +1,21 @@ -import { federateSubgraphs, RootTypeField, Subgraph, unresolvableFieldError } from '../src'; +import { federateSubgraphs, RootTypeFieldData, Subgraph, unresolvableFieldError } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; import { documentNodeToNormalizedString, normalizeString, - versionOneBaseSchema, - versionTwoBaseSchema, + versionOnePersistedBaseSchema, + versionTwoPersistedBaseSchema, } from './utils/utils'; describe('Query federation tests', () => { test('that shared queries that return a nested type that is only resolvable over multiple subgraphs are valid', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { query: Nested @@ -43,90 +43,109 @@ describe('Query federation tests', () => { }); test('that unshared queries that return a nested type that cannot be resolved in a single subgraph returns an error', () => { - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'query', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'query', + fieldTypeNodeString: 'Nested', path: 'Query.query', - parentTypeName: 'Query', - responseType: 'Nested', - rootTypeName: 'Nested', subgraphs: new Set(['subgraph-b']), + typeName: 'Query', }; - const result = federateSubgraphs([subgraphB, subgraphC]); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'name', ['Query.query.nest.nest.nest.name'], 'subgraph-c', 'Nested4'), + const { errors } = federateSubgraphs([subgraphB, subgraphC]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'name', + ['subgraph-c'], + 'Query.query.nest.nest.nest.name', + 'Nested4', + ), ); }); test('that unresolvable fields return an error', () => { - const parentTypeName = 'Friend'; - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'friend', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'friend', + fieldTypeNodeString: 'Friend', path: 'Query.friend', - parentTypeName: 'Query', - responseType: parentTypeName, - rootTypeName: parentTypeName, subgraphs: new Set(['subgraph-d']), + typeName: 'Query', }; - const result = federateSubgraphs([subgraphD, subgraphF]); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'age', ['Query.friend.age'], 'subgraph-f', parentTypeName), + const { errors } = federateSubgraphs([subgraphD, subgraphF]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'age', + ['subgraph-f'], + 'Query.friend.age', + 'Friend', + ), ); }); test('that unresolvable fields that are the first fields to be added still return an error', () => { - const parentTypeName = 'Friend'; - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'friend', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'friend', + fieldTypeNodeString: 'Friend', path: 'Query.friend', - parentTypeName: 'Query', - responseType: parentTypeName, - rootTypeName: parentTypeName, subgraphs: new Set(['subgraph-d']), + typeName: 'Query', }; - const result = federateSubgraphs([subgraphF, subgraphD]); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'age', ['Query.friend.age'], 'subgraph-f', parentTypeName), + const { errors } = federateSubgraphs([subgraphF, subgraphD]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'age', + ['subgraph-f'], + 'Query.friend.age', + 'Friend', + ), ); }); test('that multiple unresolved fields return an error for each', () => { - const parentTypeName = 'Friend'; - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'friend', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'friend', + fieldTypeNodeString: 'Friend', path: 'Query.friend', - parentTypeName: 'Query', - responseType: parentTypeName, - rootTypeName: parentTypeName, subgraphs: new Set(['subgraph-d']), + typeName: 'Query', }; - const result = federateSubgraphs([subgraphD, subgraphF, subgraphG]); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(2); - expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'age', ['Query.friend.age'], 'subgraph-f', parentTypeName), + const { errors } = federateSubgraphs([subgraphD, subgraphF, subgraphG]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(2); + expect(errors![0]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'age', + ['subgraph-f'], + 'Query.friend.age', + 'Friend', + ), ); - expect(result.errors![1]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'hobbies', ['Query.friend.hobbies'], 'subgraph-g', parentTypeName), + expect(errors![1]).toStrictEqual( + unresolvableFieldError( + rootTypeFieldData, + 'hobbies', + ['subgraph-g'], + 'Query.friend.hobbies', + 'Friend', + ), ); }); test('that shared queries that return a type that is only resolvable over multiple subgraphs are valid', () => { - const result = federateSubgraphs([subgraphD, subgraphE]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphD, subgraphE]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + + versionTwoPersistedBaseSchema + ` type Query { friend: Friend @@ -142,12 +161,12 @@ describe('Query federation tests', () => { }); test('that shared queries that return an interface that is only resolvable over multiple subgraphs are valid', () => { - const result = federateSubgraphs([subgraphH, subgraphI]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphH, subgraphI]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` interface Human { name: String! @@ -168,53 +187,56 @@ describe('Query federation tests', () => { }); test('that queries that return interfaces whose constituent types are unresolvable return an error', () => { - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'humans', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'humans', + fieldTypeNodeString: '[Human]', path: 'Query.humans', - parentTypeName: 'Query', - responseType: '[Human]', - rootTypeName: 'Human', subgraphs: new Set(['subgraph-i']), + typeName: 'Query', }; const result = federateSubgraphs([subgraphI, subgraphJ]); expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'name', ['Query.humans ... on Friend name'], 'subgraph-j', 'Friend'), + unresolvableFieldError( + rootTypeFieldData, + 'name', + ['subgraph-j'], + 'Query.humans ... on Friend name', + 'Friend', + ), ); }); test('that queries that return nested interfaces whose constituent types are unresolvable return an error', () => { - const rootTypeField: RootTypeField = { - inlineFragment: '', - name: 'humans', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'humans', + fieldTypeNodeString: '[Human]', path: 'Query.humans', - parentTypeName: 'Query', - responseType: '[Human]', - rootTypeName: 'Human', subgraphs: new Set(['subgraph-k']), + typeName: 'Query', }; - const result = federateSubgraphs([subgraphK, subgraphL]); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - expect(result.errors![0]).toStrictEqual( + const { errors } = federateSubgraphs([subgraphK, subgraphL]); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors![0]).toStrictEqual( unresolvableFieldError( - rootTypeField, + rootTypeFieldData, 'age', - ['Query.humans ... on Friend pets ... on Cat age'], - 'subgraph-l', + ['subgraph-l'], + 'Query.humans ... on Friend pets ... on Cat age', 'Cat', ), ); }); test('that shared queries that return a union that is only resolvable over multiple subgraphs are valid', () => { - const result = federateSubgraphs([subgraphM, subgraphN]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphM, subgraphN]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + + versionOnePersistedBaseSchema + ` union Human = Friend | Enemy @@ -235,22 +257,98 @@ describe('Query federation tests', () => { }); test('that queries that return unions whose constituent types are unresolvable return an error', () => { - const rootTypeField: RootTypeField = { - inlineFragment: ' ... on Enemy ', - name: 'humans', + const rootTypeFieldData: RootTypeFieldData = { + fieldName: 'humans', + fieldTypeNodeString: '[Human]', path: 'Query.humans', - parentTypeName: 'Query', - responseType: '[Human]', - rootTypeName: 'Human', subgraphs: new Set(['subgraph-o']), + typeName: 'Query', }; const result = federateSubgraphs([subgraphO, subgraphP]); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); expect(result.errors![0]).toStrictEqual( - unresolvableFieldError(rootTypeField, 'age', ['Query.humans ... on Enemy age'], 'subgraph-p', 'Enemy'), + unresolvableFieldError( + rootTypeFieldData, + 'age', + ['subgraph-p'], + 'Query.humans ... on Enemy age', + 'Enemy', + ), ); }); + + test('that an entity ancestor provides access to an otherwise unreachable field', () => { + const { errors, federationResult } = federateSubgraphs([subgraphQ, subgraphR]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe(normalizeString( + versionOnePersistedBaseSchema + ` + type Query { + entity: SometimesEntity! + } + + type SometimesEntity { + id: ID! + object: Object! + } + + type Object { + nestedObject: NestedObject! + } + + type NestedObject { + name: String! + age: Int! + } + `)); + }); + + test('that a nested self-referential type does not create an infinite validation loop', () => { + const { errors, federationResult } = federateSubgraphs([subgraphS, subgraphD]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe(normalizeString( + versionTwoPersistedBaseSchema + ` + type Query { + object: Object! + friend: Friend + } + + type Object { + nestedObject: NestedObject! + } + + type NestedObject { + object: Object! + } + + type Friend { + name: String! + } + `)); + }); + + test('that unreachable interface implementations do not return an error', () => { + const { errors, federationResult } = federateSubgraphs([subgraphT, subgraphU]); + expect(errors).toBeUndefined(); + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe(normalizeString( + versionOnePersistedBaseSchema + ` + interface Interface { + field: String! + } + + type Query { + query: Interface! + } + + type Object implements Interface { + field: String! + } + + type OtherObject implements Interface { + field: String! + } + `)); + }); }); const subgraphA: Subgraph = { @@ -327,7 +425,7 @@ const subgraphC: Subgraph = { `), }; -const subgraphD = { +const subgraphD: Subgraph = { name: 'subgraph-d', url: '', definitions: parse(` @@ -341,7 +439,7 @@ const subgraphD = { `), }; -const subgraphE = { +const subgraphE: Subgraph = { name: 'subgraph-e', url: '', definitions: parse(` @@ -355,7 +453,7 @@ const subgraphE = { `), }; -const subgraphF = { +const subgraphF: Subgraph = { name: 'subgraph-f', url: '', definitions: parse(` @@ -365,7 +463,7 @@ const subgraphF = { `), }; -const subgraphG = { +const subgraphG: Subgraph = { name: 'subgraph-g', url: '', definitions: parse(` @@ -375,7 +473,7 @@ const subgraphG = { `), }; -const subgraphH = { +const subgraphH: Subgraph = { name: 'subgraph-h', url: '', definitions: parse(` @@ -393,7 +491,7 @@ const subgraphH = { `), }; -const subgraphI = { +const subgraphI: Subgraph = { name: 'subgraph-i', url: '', definitions: parse(` @@ -411,7 +509,7 @@ const subgraphI = { `), }; -const subgraphJ = { +const subgraphJ: Subgraph = { name: 'subgraph-j', url: '', definitions: parse(` @@ -425,7 +523,7 @@ const subgraphJ = { `), }; -const subgraphK = { +const subgraphK: Subgraph = { name: 'subgraph-k', url: '', definitions: parse(` @@ -453,7 +551,7 @@ const subgraphK = { `), }; -const subgraphL = { +const subgraphL: Subgraph = { name: 'subgraph-l', url: '', definitions: parse(` @@ -477,7 +575,7 @@ const subgraphL = { `), }; -const subgraphM = { +const subgraphM: Subgraph = { name: 'subgraph-m', url: '', definitions: parse(` @@ -493,7 +591,7 @@ const subgraphM = { `), }; -const subgraphN = { +const subgraphN: Subgraph = { name: 'subgraph-n', url: '', definitions: parse(` @@ -509,7 +607,7 @@ const subgraphN = { `), }; -const subgraphO = { +const subgraphO: Subgraph = { name: 'subgraph-o', url: '', definitions: parse(` @@ -529,7 +627,7 @@ const subgraphO = { `), }; -const subgraphP = { +const subgraphP: Subgraph = { name: 'subgraph-p', url: '', definitions: parse(` @@ -540,3 +638,96 @@ const subgraphP = { } `), }; + +const subgraphQ = { + name: 'subgraph-q', + url: '', + definitions: parse(` + type Query { + entity: SometimesEntity! + } + + type SometimesEntity { + id: ID! + object: Object! + } + + type Object { + nestedObject: NestedObject! + } + + type NestedObject { + name: String! + } + `), +}; + +const subgraphR = { + name: 'subgraph-r', + url: '', + definitions: parse(` + type SometimesEntity @key(fields: "id") { + id: ID! + object: Object! + } + + type Object { + nestedObject: NestedObject! + } + + type NestedObject { + age: Int! + } + `), +}; + +const subgraphS = { + name: 'subgraph-s', + url: '', + definitions: parse(` + type Query { + object: Object! + } + + type Object { + nestedObject: NestedObject! + } + + type NestedObject { + object: Object! + } + `), +}; + +const subgraphT = { + name: 'subgraph-t', + url: '', + definitions: parse(` + type Query { + query: Interface! + } + + interface Interface { + field: String! + } + + type Object implements Interface { + field: String! + } + `), +}; + +const subgraphU = { + name: 'subgraph-u', + url: '', + definitions: parse(` + interface Interface { + field: String! + } + + type OtherObject implements Interface { + field: String! + } + `), +}; + diff --git a/composition/tests/tsconfig.json b/composition/tests/tsconfig.json new file mode 100644 index 0000000000..2a5cf7b931 --- /dev/null +++ b/composition/tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ESNext" + }, +} \ No newline at end of file diff --git a/composition/tests/unions.test.ts b/composition/tests/unions.test.ts index adce887cd5..dfd3244b0a 100644 --- a/composition/tests/unions.test.ts +++ b/composition/tests/unions.test.ts @@ -1,17 +1,16 @@ 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', () => { - const result = federateSubgraphs([subgraphA, subgraphB]); - expect(result.errors).toBeUndefined(); - const federatedGraph = result.federatedGraphAST!; + const { errors, federationResult } = federateSubgraphs([subgraphA, subgraphB]); + expect(errors).toBeUndefined(); + const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOnePersistedBaseSchema + ` union Starters = Bulbasaur | Squirtle | Charmander | Chikorita | Totodile | Cyndaquil type Bulbasaur { @@ -25,6 +24,10 @@ describe('Union federation tests', () => { type Charmander { name: String! } + + type Query { + starter: Starters + } type Chikorita { name: String! @@ -43,9 +46,9 @@ describe('Union federation tests', () => { }); test('that unions with no members throw an error', () => { - const result = federateSubgraphs([subgraphB, subgraphC]); - expect(result.errors).toBeDefined(); - expect(result.errors![0].message).equals(invalidUnionError('Starters').message); + const { errors } = federateSubgraphs([subgraphB, subgraphC]); + expect(errors).toBeDefined(); + expect(errors![0].message).equals(invalidUnionError('Starters').message); }); }); @@ -66,6 +69,10 @@ const subgraphA = { type Charmander { name: String! } + + type Query { + starter: Starters + } `), }; @@ -73,6 +80,10 @@ const subgraphB = { name: 'subgraph-b', url: '', definitions: parse(` + type Query { + starter: Starters + } + union Starters = Chikorita | Totodile | Cyndaquil type Chikorita { diff --git a/composition/tests/utils/utils.ts b/composition/tests/utils/utils.ts index b0a1f89ac9..ec5560846f 100644 --- a/composition/tests/utils/utils.ts +++ b/composition/tests/utils/utils.ts @@ -1,4 +1,5 @@ -import { DocumentNode, print } from 'graphql'; +import { DocumentNode, parse, print } from 'graphql'; +import { Subgraph } from '../../src'; export function normalizeString(input: string): string { return input.replaceAll(/\n| {2,}/g, ''); @@ -8,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 @@ -18,7 +20,27 @@ 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 @deprecated(reason: String = "No longer supported") on ARGUMENT_DEFINITION | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION + 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 -`; \ No newline at end of file +`; + +// 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), + name, + url: '', + }; +} \ No newline at end of file diff --git a/composition/tsconfig.json b/composition/tsconfig.json index 365515d7fd..bf9c043a05 100644 --- a/composition/tsconfig.json +++ b/composition/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "target": "ES2022", "declaration": true, "outDir": "./dist", "module": "commonjs", diff --git a/connect/src/wg/cosmo/node/v1/node-NodeService_connectquery.ts b/connect/src/wg/cosmo/node/v1/node-NodeService_connectquery.ts index 6aa01dc981..b943a66693 100644 --- a/connect/src/wg/cosmo/node/v1/node-NodeService_connectquery.ts +++ b/connect/src/wg/cosmo/node/v1/node-NodeService_connectquery.ts @@ -3,9 +3,9 @@ /* eslint-disable */ // @ts-nocheck -import { createQueryService } from "@connectrpc/connect-query"; -import { MethodKind } from "@bufbuild/protobuf"; -import { GetConfigRequest, GetConfigResponse } from "./node_pb.js"; +import { createQueryService } from '@connectrpc/connect-query'; +import { MethodKind } from '@bufbuild/protobuf'; +import { GetConfigRequest, GetConfigResponse } from './node_pb.js'; export const typeName = "wg.cosmo.node.v1.NodeService"; diff --git a/connect/src/wg/cosmo/node/v1/node_connect.ts b/connect/src/wg/cosmo/node/v1/node_connect.ts index d992932869..9fafaf6da8 100644 --- a/connect/src/wg/cosmo/node/v1/node_connect.ts +++ b/connect/src/wg/cosmo/node/v1/node_connect.ts @@ -3,8 +3,8 @@ /* eslint-disable */ // @ts-nocheck -import { GetConfigRequest, GetConfigResponse } from "./node_pb.js"; -import { MethodKind } from "@bufbuild/protobuf"; +import { GetConfigRequest, GetConfigResponse } from './node_pb.js'; +import { MethodKind } from '@bufbuild/protobuf'; /** * @generated from service wg.cosmo.node.v1.NodeService diff --git a/connect/src/wg/cosmo/node/v1/node_pb.ts b/connect/src/wg/cosmo/node/v1/node_pb.ts index 46fffc3daa..fe23388ce7 100644 --- a/connect/src/wg/cosmo/node/v1/node_pb.ts +++ b/connect/src/wg/cosmo/node/v1/node_pb.ts @@ -3,9 +3,16 @@ /* eslint-disable */ // @ts-nocheck -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; -import { EnumStatusCode } from "../../common_pb.js"; +import type { + BinaryReadOptions, + FieldList, + JsonReadOptions, + JsonValue, + PartialMessage, + PlainMessage, +} from '@bufbuild/protobuf'; +import { Message, proto3, protoInt64 } from '@bufbuild/protobuf'; +import { EnumStatusCode } from '../../common_pb.js'; /** * @generated from enum wg.cosmo.node.v1.ArgumentRenderConfiguration @@ -467,9 +474,19 @@ export class DataSourceConfiguration extends Message { id = ""; /** - * @generated from field: repeated wg.cosmo.node.v1.RequiredField required_fields = 10; + * @generated from field: repeated wg.cosmo.node.v1.RequiredField keys = 10; */ - requiredFields: RequiredField[] = []; + keys: RequiredField[] = []; + + /** + * @generated from field: repeated wg.cosmo.node.v1.RequiredField provides = 11; + */ + provides: RequiredField[] = []; + + /** + * @generated from field: repeated wg.cosmo.node.v1.RequiredField requires = 12; + */ + requires: RequiredField[] = []; constructor(data?: PartialMessage) { super(); @@ -488,7 +505,9 @@ export class DataSourceConfiguration extends Message { { no: 7, name: "directives", kind: "message", T: DirectiveConfiguration, repeated: true }, { no: 8, name: "request_timeout_seconds", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, { no: 9, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 10, name: "required_fields", kind: "message", T: RequiredField, repeated: true }, + { no: 10, name: "keys", kind: "message", T: RequiredField, repeated: true }, + { no: 11, name: "provides", kind: "message", T: RequiredField, repeated: true }, + { no: 12, name: "requires", kind: "message", T: RequiredField, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): DataSourceConfiguration { @@ -523,25 +542,10 @@ export class FieldConfiguration extends Message { fieldName = ""; /** - * @generated from field: bool disable_default_field_mapping = 3; - */ - disableDefaultFieldMapping = false; - - /** - * @generated from field: repeated string path = 4; - */ - path: string[] = []; - - /** - * @generated from field: repeated wg.cosmo.node.v1.ArgumentConfiguration arguments_configuration = 6; + * @generated from field: repeated wg.cosmo.node.v1.ArgumentConfiguration arguments_configuration = 3; */ argumentsConfiguration: ArgumentConfiguration[] = []; - /** - * @generated from field: bool unescape_response_json = 7; - */ - unescapeResponseJson = false; - constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -552,10 +556,7 @@ export class FieldConfiguration extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "type_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "field_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "disable_default_field_mapping", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, - { no: 4, name: "path", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 6, name: "arguments_configuration", kind: "message", T: ArgumentConfiguration, repeated: true }, - { no: 7, name: "unescape_response_json", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 3, name: "arguments_configuration", kind: "message", T: ArgumentConfiguration, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): FieldConfiguration { @@ -589,21 +590,6 @@ export class ArgumentConfiguration extends Message { */ sourceType = ArgumentSource.OBJECT_FIELD; - /** - * @generated from field: repeated string source_path = 3; - */ - sourcePath: string[] = []; - - /** - * @generated from field: wg.cosmo.node.v1.ArgumentRenderConfiguration render_configuration = 4; - */ - renderConfiguration = ArgumentRenderConfiguration.RENDER_ARGUMENT_DEFAULT; - - /** - * @generated from field: string rename_type_to = 5; - */ - renameTypeTo = ""; - constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -614,9 +600,6 @@ export class ArgumentConfiguration extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "source_type", kind: "enum", T: proto3.getEnumType(ArgumentSource) }, - { no: 3, name: "source_path", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 4, name: "render_configuration", kind: "enum", T: proto3.getEnumType(ArgumentRenderConfiguration) }, - { no: 5, name: "rename_type_to", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): ArgumentConfiguration { diff --git a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts index ea8aa8fd77..829811c7a7 100644 --- a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts +++ b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts @@ -5,10 +5,73 @@ /* eslint-disable */ // @ts-nocheck -import { createQueryService } from "@connectrpc/connect-query"; -import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; -import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFederatedSubgraphSDLByNameRequest, GetFederatedSubgraphSDLByNameResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; -import { GetConfigRequest, GetConfigResponse } from "../../node/v1/node_pb.js"; +import { createQueryService } from '@connectrpc/connect-query'; +import { MethodIdempotency, MethodKind } from '@bufbuild/protobuf'; +import { + CheckFederatedGraphRequest, + CheckFederatedGraphResponse, + CheckSubgraphSchemaRequest, + CheckSubgraphSchemaResponse, + CreateAPIKeyRequest, + CreateAPIKeyResponse, + CreateFederatedGraphRequest, + CreateFederatedGraphResponse, + CreateFederatedGraphTokenRequest, + CreateFederatedGraphTokenResponse, + CreateFederatedSubgraphRequest, + CreateFederatedSubgraphResponse, + DeleteAPIKeyRequest, + DeleteAPIKeyResponse, + DeleteFederatedGraphRequest, + DeleteFederatedGraphResponse, + DeleteFederatedSubgraphRequest, + DeleteFederatedSubgraphResponse, + FixSubgraphSchemaRequest, + FixSubgraphSchemaResponse, + GetAnalyticsViewRequest, + GetAnalyticsViewResponse, + GetAPIKeysRequest, + GetAPIKeysResponse, + GetCheckDetailsRequest, + GetCheckDetailsResponse, + GetChecksByFederatedGraphNameRequest, + GetChecksByFederatedGraphNameResponse, + GetDashboardAnalyticsViewRequest, + GetDashboardAnalyticsViewResponse, + GetFederatedGraphByNameRequest, + GetFederatedGraphByNameResponse, + GetFederatedGraphChangelogRequest, + GetFederatedGraphChangelogResponse, + GetFederatedGraphSDLByNameRequest, + GetFederatedGraphSDLByNameResponse, + GetFederatedGraphsRequest, + GetFederatedGraphsResponse, + GetFederatedSubgraphSDLByNameRequest, + GetFederatedSubgraphSDLByNameResponse, + GetOrganizationMembersRequest, + GetOrganizationMembersResponse, + GetSubgraphByNameRequest, + GetSubgraphByNameResponse, + GetSubgraphsRequest, + GetSubgraphsResponse, + GetTraceRequest, + GetTraceResponse, + InviteUserRequest, + InviteUserResponse, + MigrateFromApolloRequest, + MigrateFromApolloResponse, + PublishFederatedSubgraphRequest, + PublishFederatedSubgraphResponse, + RemoveInvitationRequest, + RemoveInvitationResponse, + UpdateFederatedGraphRequest, + UpdateFederatedGraphResponse, + UpdateSubgraphRequest, + UpdateSubgraphResponse, + WhoAmIRequest, + WhoAmIResponse, +} from './platform_pb.js'; +import { GetConfigRequest, GetConfigResponse } from '../../node/v1/node_pb.js'; export const typeName = "wg.cosmo.platform.v1.PlatformService"; diff --git a/connect/src/wg/cosmo/platform/v1/platform_connect.ts b/connect/src/wg/cosmo/platform/v1/platform_connect.ts index 0063d77af7..e34c057332 100644 --- a/connect/src/wg/cosmo/platform/v1/platform_connect.ts +++ b/connect/src/wg/cosmo/platform/v1/platform_connect.ts @@ -5,9 +5,72 @@ /* eslint-disable */ // @ts-nocheck -import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFederatedSubgraphSDLByNameRequest, GetFederatedSubgraphSDLByNameResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; -import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; -import { GetConfigRequest, GetConfigResponse } from "../../node/v1/node_pb.js"; +import { + CheckFederatedGraphRequest, + CheckFederatedGraphResponse, + CheckSubgraphSchemaRequest, + CheckSubgraphSchemaResponse, + CreateAPIKeyRequest, + CreateAPIKeyResponse, + CreateFederatedGraphRequest, + CreateFederatedGraphResponse, + CreateFederatedGraphTokenRequest, + CreateFederatedGraphTokenResponse, + CreateFederatedSubgraphRequest, + CreateFederatedSubgraphResponse, + DeleteAPIKeyRequest, + DeleteAPIKeyResponse, + DeleteFederatedGraphRequest, + DeleteFederatedGraphResponse, + DeleteFederatedSubgraphRequest, + DeleteFederatedSubgraphResponse, + FixSubgraphSchemaRequest, + FixSubgraphSchemaResponse, + GetAnalyticsViewRequest, + GetAnalyticsViewResponse, + GetAPIKeysRequest, + GetAPIKeysResponse, + GetCheckDetailsRequest, + GetCheckDetailsResponse, + GetChecksByFederatedGraphNameRequest, + GetChecksByFederatedGraphNameResponse, + GetDashboardAnalyticsViewRequest, + GetDashboardAnalyticsViewResponse, + GetFederatedGraphByNameRequest, + GetFederatedGraphByNameResponse, + GetFederatedGraphChangelogRequest, + GetFederatedGraphChangelogResponse, + GetFederatedGraphSDLByNameRequest, + GetFederatedGraphSDLByNameResponse, + GetFederatedGraphsRequest, + GetFederatedGraphsResponse, + GetFederatedSubgraphSDLByNameRequest, + GetFederatedSubgraphSDLByNameResponse, + GetOrganizationMembersRequest, + GetOrganizationMembersResponse, + GetSubgraphByNameRequest, + GetSubgraphByNameResponse, + GetSubgraphsRequest, + GetSubgraphsResponse, + GetTraceRequest, + GetTraceResponse, + InviteUserRequest, + InviteUserResponse, + MigrateFromApolloRequest, + MigrateFromApolloResponse, + PublishFederatedSubgraphRequest, + PublishFederatedSubgraphResponse, + RemoveInvitationRequest, + RemoveInvitationResponse, + UpdateFederatedGraphRequest, + UpdateFederatedGraphResponse, + UpdateSubgraphRequest, + UpdateSubgraphResponse, + WhoAmIRequest, + WhoAmIResponse, +} from './platform_pb.js'; +import { MethodIdempotency, MethodKind } from '@bufbuild/protobuf'; +import { GetConfigRequest, GetConfigResponse } from '../../node/v1/node_pb.js'; /** * @generated from service wg.cosmo.platform.v1.PlatformService diff --git a/connect/src/wg/cosmo/platform/v1/platform_pb.ts b/connect/src/wg/cosmo/platform/v1/platform_pb.ts index 01bd57d8ee..5b8b965cde 100644 --- a/connect/src/wg/cosmo/platform/v1/platform_pb.ts +++ b/connect/src/wg/cosmo/platform/v1/platform_pb.ts @@ -5,9 +5,16 @@ /* eslint-disable */ // @ts-nocheck -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3, protoInt64 } from "@bufbuild/protobuf"; -import { EnumStatusCode } from "../../common_pb.js"; +import type { + BinaryReadOptions, + FieldList, + JsonReadOptions, + JsonValue, + PartialMessage, + PlainMessage, +} from '@bufbuild/protobuf'; +import { Message, proto3, protoInt64 } from '@bufbuild/protobuf'; +import { EnumStatusCode } from '../../common_pb.js'; /** * @generated from enum wg.cosmo.platform.v1.AnalyticsViewGroupName diff --git a/controlplane/src/core/auth-utils.ts b/controlplane/src/core/auth-utils.ts index 6f40e0a83e..96846229ff 100644 --- a/controlplane/src/core/auth-utils.ts +++ b/controlplane/src/core/auth-utils.ts @@ -4,8 +4,8 @@ import axios from 'axios'; import { eq } from 'drizzle-orm'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { PKCECodeChallenge, UserInfoEndpointResponse, UserSession } from '../types/index.js'; -import { sessions } from '../db/schema.js'; import * as schema from '../db/schema.js'; +import { sessions } from '../db/schema.js'; import { calculatePKCECodeChallenge, decodeJWT, diff --git a/controlplane/src/core/bufservices/PlatformService.ts b/controlplane/src/core/bufservices/PlatformService.ts index a442f63a2b..cce2c12f9c 100644 --- a/controlplane/src/core/bufservices/PlatformService.ts +++ b/controlplane/src/core/bufservices/PlatformService.ts @@ -18,8 +18,8 @@ import { FederatedGraphChangelog, FederatedGraphChangelogOutput, FixSubgraphSchemaResponse, - GetAPIKeysResponse, GetAnalyticsViewResponse, + GetAPIKeysResponse, GetCheckDetailsResponse, GetChecksByFederatedGraphNameResponse, GetDashboardAnalyticsViewResponse, @@ -39,7 +39,7 @@ import { UpdateSubgraphResponse, WhoAmIResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { OpenAIGraphql, buildRouterConfig } from '@wundergraph/cosmo-shared'; +import { buildRouterConfig, OpenAIGraphql } from '@wundergraph/cosmo-shared'; import { GraphApiKeyJwtPayload } from '../../types/index.js'; import { Composer } from '../composition/composer.js'; import { buildSchema, composeSubgraphs } from '../composition/composition.js'; @@ -670,8 +670,9 @@ export default function (opts: RouterOptions): Partial ({ name: s.name, url: s.routingUrl, @@ -54,10 +56,11 @@ export class Composer { ); return { + argumentConfigurations: result?.argumentConfigurations || [], name, targetID, - composedSchema: result.federatedGraphSchema ? printSchema(result.federatedGraphSchema) : undefined, - errors: result.errors || [], + composedSchema: result?.federatedGraphSchema ? printSchema(result.federatedGraphSchema) : undefined, + errors: errors || [], subgraphs: subgraphs.map((s) => ({ name: s.name, url: s.routingUrl, @@ -66,6 +69,7 @@ export class Composer { }; } catch (e: any) { return { + argumentConfigurations: [], name, targetID, errors: [e], @@ -103,13 +107,13 @@ export class Composer { } } - const result = composeSubgraphs(subgraphsToBeComposed); - + const { errors, federationResult: result } = composeSubgraphs(subgraphsToBeComposed); composedGraphs.push({ + argumentConfigurations: result?.argumentConfigurations || [], name: graph.name, targetID: graph.targetId, - composedSchema: result.federatedGraphSchema ? printSchema(result.federatedGraphSchema) : undefined, - errors: result.errors || [], + composedSchema: result?.federatedGraphSchema ? printSchema(result.federatedGraphSchema) : undefined, + errors: errors || [], subgraphs: subgraphs.map((s) => ({ name: s.name, url: s.routingUrl, @@ -118,6 +122,7 @@ export class Composer { }); } catch (e: any) { composedGraphs.push({ + argumentConfigurations: [], name: graph.name, targetID: graph.targetId, errors: [e], diff --git a/controlplane/src/core/composition/composition.ts b/controlplane/src/core/composition/composition.ts index d768587813..250a575f2c 100644 --- a/controlplane/src/core/composition/composition.ts +++ b/controlplane/src/core/composition/composition.ts @@ -1,15 +1,15 @@ import { federateSubgraphs, - Subgraph, + FederationResultContainer, NormalizationResultContainer, normalizeSubgraphFromString, - FederationResult, + Subgraph, } from '@wundergraph/composition'; /** * Composes a list of subgraphs into a single schema. */ -export function composeSubgraphs(subgraphs: Subgraph[]): FederationResult { +export function composeSubgraphs(subgraphs: Subgraph[]): FederationResultContainer { return federateSubgraphs(subgraphs); } diff --git a/controlplane/src/core/composition/updateComposedSchema.ts b/controlplane/src/core/composition/updateComposedSchema.ts index 956186978c..cb08116a4c 100644 --- a/controlplane/src/core/composition/updateComposedSchema.ts +++ b/controlplane/src/core/composition/updateComposedSchema.ts @@ -31,6 +31,7 @@ export const updateComposedSchema = async ({ let routerConfigJson: JsonValue = null; if (!hasErrors && composedGraph.composedSchema) { const routerConfig = buildRouterConfig({ + argumentConfigurations: composedGraph.argumentConfigurations, subgraphs: composedGraph.subgraphs, federatedSDL: composedGraph.composedSchema, }); diff --git a/controlplane/src/core/prometheus/client.ts b/controlplane/src/core/prometheus/client.ts index 012e14dd27..c16641748d 100644 --- a/controlplane/src/core/prometheus/client.ts +++ b/controlplane/src/core/prometheus/client.ts @@ -1,5 +1,5 @@ import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios'; -import { QueryResultType, Response, QueryRangeRequestParams, QueryRequestParams } from './types.js'; +import { QueryRangeRequestParams, QueryRequestParams, QueryResultType, Response } from './types.js'; export interface Options { /** diff --git a/controlplane/src/core/repositories/FederatedGraphRepository.ts b/controlplane/src/core/repositories/FederatedGraphRepository.ts index c471f88c74..70115ca333 100644 --- a/controlplane/src/core/repositories/FederatedGraphRepository.ts +++ b/controlplane/src/core/repositories/FederatedGraphRepository.ts @@ -1,5 +1,5 @@ import { JsonValue } from '@bufbuild/protobuf'; -import { SQL, and, asc, desc, eq, inArray, not, notExists, notInArray, sql } from 'drizzle-orm'; +import { and, asc, desc, eq, inArray, not, notExists, notInArray, SQL, sql } from 'drizzle-orm'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { RouterConfig } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; import { CompositionError, SchemaChange } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; @@ -20,7 +20,6 @@ import { GraphApiKeyDTO, Label, ListFilterOptions, - MigrationSubgraph, SchemaChangeType, } from '../../types/index.js'; import { updateComposedSchema } from '../composition/updateComposedSchema.js'; diff --git a/controlplane/src/core/repositories/SubgraphRepository.ts b/controlplane/src/core/repositories/SubgraphRepository.ts index 0a7a96adf2..b1e39cd9c3 100644 --- a/controlplane/src/core/repositories/SubgraphRepository.ts +++ b/controlplane/src/core/repositories/SubgraphRepository.ts @@ -1,6 +1,6 @@ import { CompositionError } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { joinLabel, splitLabel } from '@wundergraph/cosmo-shared'; -import { SQL, and, asc, eq, gt, inArray, lt, notInArray, sql } from 'drizzle-orm'; +import { and, asc, eq, gt, inArray, lt, notInArray, SQL, sql } from 'drizzle-orm'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import * as schema from '../../db/schema.js'; import { schemaChecks, schemaVersion, subgraphs, subgraphsToFederatedGraph, targets } from '../../db/schema.js'; diff --git a/controlplane/src/core/repositories/analytics/AnalyticsRequestViewRepository.ts b/controlplane/src/core/repositories/analytics/AnalyticsRequestViewRepository.ts index 223bb0f8e4..f26f996d7f 100644 --- a/controlplane/src/core/repositories/analytics/AnalyticsRequestViewRepository.ts +++ b/controlplane/src/core/repositories/analytics/AnalyticsRequestViewRepository.ts @@ -12,12 +12,12 @@ import { import { ClickHouseClient } from '../../clickhouse/index.js'; import { BaseFilters, - ColumnMetaData, buildAnalyticsViewColumns, buildAnalyticsViewFilters, buildCoercedFilterSqlStatement, buildColumnsFromNames, coerceFilterValues, + ColumnMetaData, fillColumnMetaData, } from './util.js'; diff --git a/controlplane/src/core/test-util.ts b/controlplane/src/core/test-util.ts index 424c1afd37..3acf4e149e 100644 --- a/controlplane/src/core/test-util.ts +++ b/controlplane/src/core/test-util.ts @@ -3,7 +3,7 @@ import postgres from 'postgres'; import nuid from 'nuid'; import { drizzle } from 'drizzle-orm/postgres-js'; import { ExpiresAt } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { Label, AuthContext } from '../types/index.js'; +import { AuthContext, Label } from '../types/index.js'; import * as schema from '../db/schema.js'; import { Authenticator } from './services/Authentication.js'; import { UserRepository } from './repositories/UserRepository.js'; diff --git a/controlplane/test/composition-errors.test.ts b/controlplane/test/composition-errors.test.ts index 8d31d790f2..032d68cf8c 100644 --- a/controlplane/test/composition-errors.test.ts +++ b/controlplane/test/composition-errors.test.ts @@ -8,8 +8,15 @@ import pino from 'pino'; import { PlatformService } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; -import { parse } from 'graphql'; +import { Kind, parse } from 'graphql'; import { joinLabel } from '@wundergraph/cosmo-shared'; +import { + ImplementationErrors, + incompatibleParentKindFatalError, + InvalidFieldImplementation, + noQueryRootTypeError, + unimplementedInterfaceFieldsError, +} from '@wundergraph/composition'; import database from '../src/core/plugins/database'; import routes from '../src/core/routes'; import { composeSubgraphs } from '../src/core/composition/composition'; @@ -295,7 +302,7 @@ describe('CompositionErrors', (ctx) => { ); }); - test.skip('Should cause composition errors if the subgraphs have no query', () => { + test('that an error is returned if the federated graph has no query root type', () => { const subgraph1 = { definitions: parse(` type TypeA { @@ -316,15 +323,14 @@ describe('CompositionErrors', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2]); + const { errors } = composeSubgraphs([subgraph1, subgraph2]); - expect(result.errors).toBeDefined(); - expect(result.errors?.[0].message).toBe( - 'No queries found in any subgraph: a supergraph must have a query root type.', - ); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors?.[0]).toStrictEqual(noQueryRootTypeError); }); - test.skip('Should cause an composition error when a type and a interface are defined with the same name in different subgraphs', () => { + test('Should cause an composition error when a type and a interface are defined with the same name in different subgraphs', () => { const subgraph1 = { definitions: parse(` type Query { @@ -349,26 +355,26 @@ describe('CompositionErrors', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2]); - - expect(result.errors).toBeDefined(); - expect(result.errors?.[0].message).toBe( - 'Type "SameName" has mismatched kind: it is defined as Object Type in subgraph "subgraph1" but Interface Type in subgraph "subgraph2"', - ); + expect(() => composeSubgraphs([subgraph1, subgraph2])) + .toThrow(incompatibleParentKindFatalError( + 'SameName', + Kind.OBJECT_TYPE_DEFINITION, + Kind.INTERFACE_TYPE_DEFINITION, + )); }); - test.skip('Should cause composition errors if a type does not implement one of its interface after merge', () => { + test('Should cause composition errors if a type does not implement one of its interface after merge', () => { const subgraph1 = { definitions: parse(` type Query { - x: [IntefaceA!] + x: [InterfaceA!] } - interface IntefaceA { + interface InterfaceA { a: Int } - type TypeA implements IntefaceA { + type TypeA implements InterfaceA { a: Int b: Int } @@ -379,11 +385,11 @@ describe('CompositionErrors', (ctx) => { const subgraph2 = { definitions: parse(` - interface IntefaceA { + interface InterfaceA { b: Int } - type TypeB implements IntefaceA { + type TypeB implements InterfaceA { b: Int } `), @@ -391,12 +397,19 @@ describe('CompositionErrors', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2]); - - expect(result.errors).toBeDefined(); - expect(result.errors?.[0].message).toBe( - 'Interface field "IntefaceA.a" is declared in subgraph "subgraph1" but type "TypeB", which implements "IntefaceA" only in subgraph "subgraph2" does not have field "a".', - ); + const { errors } = composeSubgraphs([subgraph1, subgraph2]); + + expect(errors).toBeDefined(); + expect(errors![0]).toStrictEqual(unimplementedInterfaceFieldsError( + 'TypeB', + 'object', + new Map([ + ['InterfaceA', { + invalidFieldImplementations: new Map(), + unimplementedFields: ['a'], + }] + ]), + )); }); test.skip('Should cause composition errors when merging completely inconsistent input types', () => { diff --git a/controlplane/test/graphql/federationV1/composedFederatedV1Graph.graphql b/controlplane/test/graphql/federationV1/composedFederatedV1Graph.graphql index 9e5f088388..abbbe19bec 100644 --- a/controlplane/test/graphql/federationV1/composedFederatedV1Graph.graphql +++ b/controlplane/test/graphql/federationV1/composedFederatedV1Graph.graphql @@ -1,13 +1,3 @@ -directive @extends on INTERFACE | OBJECT - -directive @external on FIELD_DEFINITION | OBJECT - -directive @key(fields: String!) repeatable on OBJECT - -directive @provides(fields: String!) on FIELD_DEFINITION - -directive @requires(fields: String!) on FIELD_DEFINITION - directive @tag( name: String! ) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION diff --git a/controlplane/test/graphql/federationV2/composedFederatedV2Graph.graphql b/controlplane/test/graphql/federationV2/composedFederatedV2Graph.graphql index 91207f1de0..7da21ac9c5 100644 --- a/controlplane/test/graphql/federationV2/composedFederatedV2Graph.graphql +++ b/controlplane/test/graphql/federationV2/composedFederatedV2Graph.graphql @@ -1,46 +1,13 @@ -directive @extends on INTERFACE | OBJECT - -directive @external on FIELD_DEFINITION | OBJECT - -directive @key(fields: String!) repeatable on OBJECT - -directive @provides(fields: String!) on FIELD_DEFINITION - -directive @requires(fields: String!) on FIELD_DEFINITION - directive @tag( name: String! ) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION 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 - -directive @myDirective(a: String!) on FIELD_DEFINITION - -directive @hello on FIELD_DEFINITION - -directive @override(from: String!) on FIELD_DEFINITION - interface SkuItf { sku: String } -interface ProductItf implements SkuItf { - id: ID! - sku: String - name: String - package: String - variation: ProductVariation - dimensions: ProductDimension - createdBy: User - hidden: String - oldField: String - reviewsCount: Int! - reviewsScore: Float! - reviews: [Review!]! -} - type Query { allPandas: [Panda] panda(name: ID!): Panda @@ -54,21 +21,6 @@ type Panda { favoriteFood: String } -type Product implements ProductItf & SkuItf { - id: ID! - sku: String - name: String - package: String - variation: ProductVariation - dimensions: ProductDimension - createdBy: User - hidden: String - reviewsScore: Float! - oldField: String - reviewsCount: Int! - reviews: [Review!]! -} - enum ShippingClass { STANDARD EXPRESS @@ -94,3 +46,33 @@ type Review { id: Int! body: String! } + +interface ProductItf implements SkuItf { + id: ID! + sku: String + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String + oldField: String @deprecated(reason: "refactored out") + reviewsCount: Int! + reviewsScore: Float! + reviews: [Review!]! +} + +type Product implements ProductItf & SkuItf { + id: ID! + sku: String + name: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User + hidden: String + reviewsScore: Float! + oldField: String + reviewsCount: Int! + reviews: [Review!]! +} diff --git a/controlplane/test/router-config.test.ts b/controlplane/test/router-config.test.ts index 39da9794bb..bb8aaf6f95 100644 --- a/controlplane/test/router-config.test.ts +++ b/controlplane/test/router-config.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, beforeAll, afterAll } from 'vitest'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import Fastify from 'fastify'; import { createConnectTransport } from '@connectrpc/connect-node'; @@ -10,6 +10,7 @@ import { pino } from 'pino'; import { NodeService } from '@wundergraph/cosmo-connect/dist/node/v1/node_connect'; import { joinLabel } from '@wundergraph/cosmo-shared'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { noQueryRootTypeError } from '@wundergraph/composition'; import routes from '../src/core/routes'; import database from '../src/core/plugins/database'; import { @@ -345,7 +346,9 @@ describe('Router Config', (ctx) => { ), }); - expect(publishPandaResp.response?.code).toBe(EnumStatusCode.OK); + expect(publishPandaResp.response?.code).toBe(EnumStatusCode.ERR_SUBGRAPH_COMPOSITION_FAILED); + expect(publishPandaResp.compositionErrors).toHaveLength(1); + expect(publishPandaResp.compositionErrors[0].message).toStrictEqual(noQueryRootTypeError.message); const createUsersSubgraph = await platformClient.createFederatedSubgraph({ name: usersSubgraph, diff --git a/controlplane/test/subgraph.test.ts b/controlplane/test/subgraph.test.ts index 69b6113dd6..a558dc0b04 100644 --- a/controlplane/test/subgraph.test.ts +++ b/controlplane/test/subgraph.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, beforeAll, afterAll } from 'vitest'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import Fastify from 'fastify'; import { createConnectTransport } from '@connectrpc/connect-node'; diff --git a/demo/employees/subgraph/generated/generated.go b/demo/employees/subgraph/generated/generated.go index f17789e05b..7cfc9362fc 100644 --- a/demo/employees/subgraph/generated/generated.go +++ b/demo/employees/subgraph/generated/generated.go @@ -374,7 +374,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var sources = []*ast.Source{ {Name: "../schema.graphqls", Input: `type Query { - employee(id: Int!): Employee! + employee(id: Int!): Employee employees: [Employee!]! team_mates(team: Department!): [Employee!]! } @@ -1342,14 +1342,11 @@ func (ec *executionContext) _Query_employee(ctx context.Context, field graphql.C return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } res := resTmp.(*model.Employee) fc.Result = res - return ec.marshalNEmployee2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋemployeesᚋsubgraphᚋmodelᚐEmployee(ctx, field.Selections, res) + return ec.marshalOEmployee2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋemployeesᚋsubgraphᚋmodelᚐEmployee(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_employee(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -3948,9 +3945,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_employee(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } return res } @@ -5086,6 +5080,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) marshalOEmployee2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋemployeesᚋsubgraphᚋmodelᚐEmployee(ctx context.Context, sel ast.SelectionSet, v *model.Employee) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Employee(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/demo/employees/subgraph/schema.graphqls b/demo/employees/subgraph/schema.graphqls index e90048722e..ce09f37cbd 100644 --- a/demo/employees/subgraph/schema.graphqls +++ b/demo/employees/subgraph/schema.graphqls @@ -1,5 +1,5 @@ type Query { - employee(id: Int!): Employee! + employee(id: Int!): Employee employees: [Employee!]! team_mates(team: Department!): [Employee!]! } 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 diff --git a/demo/family/subgraph/employees.go b/demo/family/subgraph/employees.go index 190df75657..f086d38464 100644 --- a/demo/family/subgraph/employees.go +++ b/demo/family/subgraph/employees.go @@ -8,212 +8,212 @@ var married = model.MaritalStatusMarried var employees = []*model.Employee{ { Details: &model.Details{ - Forename: "Jens", - Surname: "Neuse", + Forename: "Jens", + Surname: "Neuse", + HasChildren: true, + MaritalStatus: &married, + Nationality: model.NationalityGerman, }, - HasChildren: true, - MaritalStatus: &married, - Nationality: model.NationalityGerman, }, { Details: &model.Details{ - Forename: "Dustin", - Surname: "Deus", + Forename: "Dustin", + Surname: "Deus", + HasChildren: false, + MaritalStatus: &engaged, + Nationality: model.NationalityGerman, }, - HasChildren: false, - MaritalStatus: &engaged, - Nationality: model.NationalityGerman, }, { Details: &model.Details{ - Forename: "Stefan", - Surname: "Avram", - }, - HasChildren: false, - MaritalStatus: &engaged, - Nationality: model.NationalityAmerican, - Pets: []model.Pet{ - model.Alligator{ - Class: model.ClassReptile, - Gender: model.GenderUnknown, - Name: "Snappy", - Dangerous: "yes", + Forename: "Stefan", + Surname: "Avram", + HasChildren: false, + MaritalStatus: &engaged, + Nationality: model.NationalityAmerican, + Pets: []model.Pet{ + model.Alligator{ + Class: model.ClassReptile, + Gender: model.GenderUnknown, + Name: "Snappy", + Dangerous: "yes", + }, }, }, }, { Details: &model.Details{ - Forename: "Björn", - Surname: "Schwenzer", - }, - HasChildren: true, - MaritalStatus: &married, - Nationality: model.NationalityGerman, - Pets: []model.Pet{ - model.Dog{ - Breed: model.DogBreedGoldenRetriever, - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Abby", - }, - model.Pony{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Survivor", + Forename: "Björn", + Surname: "Schwenzer", + HasChildren: true, + MaritalStatus: &married, + Nationality: model.NationalityGerman, + Pets: []model.Pet{ + model.Dog{ + Breed: model.DogBreedGoldenRetriever, + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Abby", + }, + model.Pony{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Survivor", + }, }, }, }, { Details: &model.Details{ - Forename: "Sergiy", - Surname: "Petrunin", - }, - HasChildren: false, - MaritalStatus: &engaged, - Nationality: model.NationalityUkrainian, - Pets: []model.Pet{ - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Blotch", - Type: model.CatTypeStreet, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Grayone", - Type: model.CatTypeStreet, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Rusty", - Type: model.CatTypeStreet, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Manya", - Type: model.CatTypeHome, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Peach", - Type: model.CatTypeStreet, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Panda", - Type: model.CatTypeHome, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Mommy", - Type: model.CatTypeStreet, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Terry", - Type: model.CatTypeHome, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Tilda", - Type: model.CatTypeHome, - }, - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Vasya", - Type: model.CatTypeHome, + Forename: "Sergiy", + Surname: "Petrunin", + HasChildren: false, + MaritalStatus: &engaged, + Nationality: model.NationalityUkrainian, + Pets: []model.Pet{ + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Blotch", + Type: model.CatTypeStreet, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Grayone", + Type: model.CatTypeStreet, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Rusty", + Type: model.CatTypeStreet, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Manya", + Type: model.CatTypeHome, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Peach", + Type: model.CatTypeStreet, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Panda", + Type: model.CatTypeHome, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Mommy", + Type: model.CatTypeStreet, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Terry", + Type: model.CatTypeHome, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Tilda", + Type: model.CatTypeHome, + }, + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Vasya", + Type: model.CatTypeHome, + }, }, }, }, { Details: &model.Details{ - Forename: "Suvij", - Surname: "Surya", + Forename: "Suvij", + Surname: "Surya", + HasChildren: false, + Nationality: model.NationalityIndian, }, - HasChildren: false, - Nationality: model.NationalityIndian, }, { Details: &model.Details{ - Forename: "Nithin", - Surname: "Kumar", + Forename: "Nithin", + Surname: "Kumar", + HasChildren: false, + Nationality: model.NationalityIndian, }, - HasChildren: false, - Nationality: model.NationalityIndian, }, { Details: &model.Details{ - Forename: "Alberto", - Surname: "Garcia Hierro", - }, - HasChildren: true, - MaritalStatus: &married, - Nationality: model.NationalitySpanish, - Pets: []model.Pet{ - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderMale, - Name: "Thor", - Type: model.CatTypeHome, + Forename: "Alberto", + Surname: "Garcia Hierro", + HasChildren: true, + MaritalStatus: &married, + Nationality: model.NationalitySpanish, + Pets: []model.Pet{ + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderMale, + Name: "Thor", + Type: model.CatTypeHome, + }, }, }, }, { Details: &model.Details{ - Forename: "Eelco", - Surname: "Wiersma", - }, - HasChildren: false, - Nationality: model.NationalityDutch, - Pets: []model.Pet{ - model.Mouse{ - Class: model.ClassMammal, - Gender: model.GenderUnknown, - Name: "Vanson", + Forename: "Eelco", + Surname: "Wiersma", + HasChildren: false, + Nationality: model.NationalityDutch, + Pets: []model.Pet{ + model.Mouse{ + Class: model.ClassMammal, + Gender: model.GenderUnknown, + Name: "Vanson", + }, }, }, }, { Details: &model.Details{ - Forename: "Alexandra", - Surname: "Neuse", + Forename: "Alexandra", + Surname: "Neuse", + HasChildren: true, + MaritalStatus: &married, + Nationality: model.NationalityGerman, }, - HasChildren: true, - MaritalStatus: &married, - Nationality: model.NationalityGerman, }, { Details: &model.Details{ - Forename: "David", - Surname: "Stutt", - }, - HasChildren: false, - MaritalStatus: &married, - Nationality: model.NationalityEnglish, - Pets: []model.Pet{ - model.Cat{ - Class: model.ClassMammal, - Gender: model.GenderFemale, - Name: "Pepper", - Type: model.CatTypeHome, + Forename: "David", + Surname: "Stutt", + HasChildren: false, + MaritalStatus: &married, + Nationality: model.NationalityEnglish, + Pets: []model.Pet{ + model.Cat{ + Class: model.ClassMammal, + Gender: model.GenderFemale, + Name: "Pepper", + Type: model.CatTypeHome, + }, }, }, }, { Details: &model.Details{ - Forename: "Dani", - Surname: "Akash", + Forename: "Dani", + Surname: "Akash", + MaritalStatus: &engaged, + Nationality: model.NationalityIndian, }, - MaritalStatus: &engaged, - Nationality: model.NationalityIndian, }, } diff --git a/demo/family/subgraph/generated/generated.go b/demo/family/subgraph/generated/generated.go index 05d60b8c5a..811b8c9bc4 100644 --- a/demo/family/subgraph/generated/generated.go +++ b/demo/family/subgraph/generated/generated.go @@ -61,8 +61,12 @@ type ComplexityRoot struct { } Details struct { - Forename func(childComplexity int) int - Surname func(childComplexity int) int + Forename func(childComplexity int) int + HasChildren func(childComplexity int) int + MaritalStatus func(childComplexity int) int + Nationality func(childComplexity int) int + Pets func(childComplexity int) int + Surname func(childComplexity int) int } Dog struct { @@ -73,12 +77,8 @@ type ComplexityRoot struct { } Employee struct { - Details func(childComplexity int) int - HasChildren func(childComplexity int) int - ID func(childComplexity int) int - MaritalStatus func(childComplexity int) int - Nationality func(childComplexity int) int - Pets func(childComplexity int) int + Details func(childComplexity int) int + ID func(childComplexity int) int } Entity struct { @@ -189,6 +189,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Details.Forename(childComplexity), true + case "Details.hasChildren": + if e.complexity.Details.HasChildren == nil { + break + } + + return e.complexity.Details.HasChildren(childComplexity), true + + case "Details.maritalStatus": + if e.complexity.Details.MaritalStatus == nil { + break + } + + return e.complexity.Details.MaritalStatus(childComplexity), true + + case "Details.nationality": + if e.complexity.Details.Nationality == nil { + break + } + + return e.complexity.Details.Nationality(childComplexity), true + + case "Details.pets": + if e.complexity.Details.Pets == nil { + break + } + + return e.complexity.Details.Pets(childComplexity), true + case "Details.surname": if e.complexity.Details.Surname == nil { break @@ -231,13 +259,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Employee.Details(childComplexity), true - case "Employee.hasChildren": - if e.complexity.Employee.HasChildren == nil { - break - } - - return e.complexity.Employee.HasChildren(childComplexity), true - case "Employee.id": if e.complexity.Employee.ID == nil { break @@ -245,27 +266,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Employee.ID(childComplexity), true - case "Employee.maritalStatus": - if e.complexity.Employee.MaritalStatus == nil { - break - } - - return e.complexity.Employee.MaritalStatus(childComplexity), true - - case "Employee.nationality": - if e.complexity.Employee.Nationality == nil { - break - } - - return e.complexity.Employee.Nationality(childComplexity), true - - case "Employee.pets": - if e.complexity.Employee.Pets == nil { - break - } - - return e.complexity.Employee.Pets(childComplexity), true - case "Entity.findEmployeeByID": if e.complexity.Entity.FindEmployeeByID == nil { break @@ -521,16 +521,15 @@ enum Nationality { type Details { forename: String! @shareable surname: String! @shareable + hasChildren: Boolean! + maritalStatus: MaritalStatus + nationality: Nationality! + pets: [Pet] } type Employee @key(fields: "id") { details: Details @shareable id: Int! - # move to details eventually - hasChildren: Boolean! - maritalStatus: MaritalStatus - nationality: Nationality! - pets: [Pet] }`, BuiltIn: false}, {Name: "../../federation/directives.graphql", Input: ` directive @composeDirective(name: String!) repeatable on SCHEMA @@ -1132,8 +1131,8 @@ func (ec *executionContext) fieldContext_Details_surname(ctx context.Context, fi return fc, nil } -func (ec *executionContext) _Dog_breed(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Dog_breed(ctx, field) +func (ec *executionContext) _Details_hasChildren(ctx context.Context, field graphql.CollectedField, obj *model.Details) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Details_hasChildren(ctx, field) if err != nil { return graphql.Null } @@ -1146,7 +1145,7 @@ func (ec *executionContext) _Dog_breed(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Breed, nil + return obj.HasChildren, nil }) if err != nil { ec.Error(ctx, err) @@ -1158,26 +1157,26 @@ func (ec *executionContext) _Dog_breed(ctx context.Context, field graphql.Collec } return graphql.Null } - res := resTmp.(model.DogBreed) + res := resTmp.(bool) fc.Result = res - return ec.marshalNDogBreed2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐDogBreed(ctx, field.Selections, res) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Dog_breed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Details_hasChildren(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Dog", + Object: "Details", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type DogBreed does not have child fields") + return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Dog_class(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Dog_class(ctx, field) +func (ec *executionContext) _Details_maritalStatus(ctx context.Context, field graphql.CollectedField, obj *model.Details) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Details_maritalStatus(ctx, field) if err != nil { return graphql.Null } @@ -1190,38 +1189,35 @@ func (ec *executionContext) _Dog_class(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Class, nil + return obj.MaritalStatus, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(model.Class) + res := resTmp.(*model.MaritalStatus) fc.Result = res - return ec.marshalNClass2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐClass(ctx, field.Selections, res) + return ec.marshalOMaritalStatus2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐMaritalStatus(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Dog_class(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Details_maritalStatus(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Dog", + Object: "Details", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Class does not have child fields") + return nil, errors.New("field of type MaritalStatus does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Dog_gender(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Dog_gender(ctx, field) +func (ec *executionContext) _Details_nationality(ctx context.Context, field graphql.CollectedField, obj *model.Details) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Details_nationality(ctx, field) if err != nil { return graphql.Null } @@ -1234,7 +1230,7 @@ func (ec *executionContext) _Dog_gender(ctx context.Context, field graphql.Colle }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Gender, nil + return obj.Nationality, nil }) if err != nil { ec.Error(ctx, err) @@ -1246,26 +1242,26 @@ func (ec *executionContext) _Dog_gender(ctx context.Context, field graphql.Colle } return graphql.Null } - res := resTmp.(model.Gender) + res := resTmp.(model.Nationality) fc.Result = res - return ec.marshalNGender2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐGender(ctx, field.Selections, res) + return ec.marshalNNationality2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐNationality(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Dog_gender(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Details_nationality(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Dog", + Object: "Details", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Gender does not have child fields") + return nil, errors.New("field of type Nationality does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Dog_name(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Dog_name(ctx, field) +func (ec *executionContext) _Details_pets(ctx context.Context, field graphql.CollectedField, obj *model.Details) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Details_pets(ctx, field) if err != nil { return graphql.Null } @@ -1278,38 +1274,35 @@ func (ec *executionContext) _Dog_name(ctx context.Context, field graphql.Collect }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Name, nil + return obj.Pets, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(string) + res := resTmp.([]model.Pet) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOPet2ᚕgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐPet(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Dog_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Details_pets(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Dog", + Object: "Details", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") }, } return fc, nil } -func (ec *executionContext) _Employee_details(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_details(ctx, field) +func (ec *executionContext) _Dog_breed(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Dog_breed(ctx, field) if err != nil { return graphql.Null } @@ -1322,41 +1315,38 @@ func (ec *executionContext) _Employee_details(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Details, nil + return obj.Breed, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*model.Details) + res := resTmp.(model.DogBreed) fc.Result = res - return ec.marshalODetails2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐDetails(ctx, field.Selections, res) + return ec.marshalNDogBreed2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐDogBreed(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_details(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Dog_breed(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Employee", + Object: "Dog", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "forename": - return ec.fieldContext_Details_forename(ctx, field) - case "surname": - return ec.fieldContext_Details_surname(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Details", field.Name) + return nil, errors.New("field of type DogBreed does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Employee_id(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_id(ctx, field) +func (ec *executionContext) _Dog_class(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Dog_class(ctx, field) if err != nil { return graphql.Null } @@ -1369,7 +1359,7 @@ func (ec *executionContext) _Employee_id(ctx context.Context, field graphql.Coll }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ID, nil + return obj.Class, nil }) if err != nil { ec.Error(ctx, err) @@ -1381,26 +1371,26 @@ func (ec *executionContext) _Employee_id(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.(int) + res := resTmp.(model.Class) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNClass2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐClass(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Dog_class(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Employee", + Object: "Dog", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type Class does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Employee_hasChildren(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_hasChildren(ctx, field) +func (ec *executionContext) _Dog_gender(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Dog_gender(ctx, field) if err != nil { return graphql.Null } @@ -1413,7 +1403,7 @@ func (ec *executionContext) _Employee_hasChildren(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.HasChildren, nil + return obj.Gender, nil }) if err != nil { ec.Error(ctx, err) @@ -1425,26 +1415,26 @@ func (ec *executionContext) _Employee_hasChildren(ctx context.Context, field gra } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(model.Gender) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNGender2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐGender(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_hasChildren(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Dog_gender(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Employee", + Object: "Dog", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type Gender does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Employee_maritalStatus(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_maritalStatus(ctx, field) +func (ec *executionContext) _Dog_name(ctx context.Context, field graphql.CollectedField, obj *model.Dog) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Dog_name(ctx, field) if err != nil { return graphql.Null } @@ -1457,35 +1447,38 @@ func (ec *executionContext) _Employee_maritalStatus(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.MaritalStatus, nil + return obj.Name, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*model.MaritalStatus) + res := resTmp.(string) fc.Result = res - return ec.marshalOMaritalStatus2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐMaritalStatus(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_maritalStatus(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Dog_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Employee", + Object: "Dog", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type MaritalStatus does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Employee_nationality(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_nationality(ctx, field) +func (ec *executionContext) _Employee_details(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Employee_details(ctx, field) if err != nil { return graphql.Null } @@ -1498,38 +1491,49 @@ func (ec *executionContext) _Employee_nationality(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Nationality, nil + return obj.Details, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(model.Nationality) + res := resTmp.(*model.Details) fc.Result = res - return ec.marshalNNationality2githubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐNationality(ctx, field.Selections, res) + return ec.marshalODetails2ᚖgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐDetails(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_nationality(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Employee_details(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Employee", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Nationality does not have child fields") + switch field.Name { + case "forename": + return ec.fieldContext_Details_forename(ctx, field) + case "surname": + return ec.fieldContext_Details_surname(ctx, field) + case "hasChildren": + return ec.fieldContext_Details_hasChildren(ctx, field) + case "maritalStatus": + return ec.fieldContext_Details_maritalStatus(ctx, field) + case "nationality": + return ec.fieldContext_Details_nationality(ctx, field) + case "pets": + return ec.fieldContext_Details_pets(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Details", field.Name) }, } return fc, nil } -func (ec *executionContext) _Employee_pets(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Employee_pets(ctx, field) +func (ec *executionContext) _Employee_id(ctx context.Context, field graphql.CollectedField, obj *model.Employee) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Employee_id(ctx, field) if err != nil { return graphql.Null } @@ -1542,28 +1546,31 @@ func (ec *executionContext) _Employee_pets(ctx context.Context, field graphql.Co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Pets, nil + return obj.ID, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.([]model.Pet) + res := resTmp.(int) fc.Result = res - return ec.marshalOPet2ᚕgithubᚗcomᚋwundergraphᚋcosmoᚋdemoᚋfamilyᚋsubgraphᚋmodelᚐPet(ctx, field.Selections, res) + return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Employee_pets(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Employee_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Employee", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") + return nil, errors.New("field of type Int does not have child fields") }, } return fc, nil @@ -1612,14 +1619,6 @@ func (ec *executionContext) fieldContext_Entity_findEmployeeByID(ctx context.Con return ec.fieldContext_Employee_details(ctx, field) case "id": return ec.fieldContext_Employee_id(ctx, field) - case "hasChildren": - return ec.fieldContext_Employee_hasChildren(ctx, field) - case "maritalStatus": - return ec.fieldContext_Employee_maritalStatus(ctx, field) - case "nationality": - return ec.fieldContext_Employee_nationality(ctx, field) - case "pets": - return ec.fieldContext_Employee_pets(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Employee", field.Name) }, @@ -4194,6 +4193,20 @@ func (ec *executionContext) _Details(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { out.Invalids++ } + case "hasChildren": + out.Values[i] = ec._Details_hasChildren(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "maritalStatus": + out.Values[i] = ec._Details_maritalStatus(ctx, field, obj) + case "nationality": + out.Values[i] = ec._Details_nationality(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pets": + out.Values[i] = ec._Details_pets(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4289,20 +4302,6 @@ func (ec *executionContext) _Employee(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { out.Invalids++ } - case "hasChildren": - out.Values[i] = ec._Employee_hasChildren(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "maritalStatus": - out.Values[i] = ec._Employee_maritalStatus(ctx, field, obj) - case "nationality": - out.Values[i] = ec._Employee_nationality(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "pets": - out.Values[i] = ec._Employee_pets(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/demo/family/subgraph/model/models_gen.go b/demo/family/subgraph/model/models_gen.go index c0173a221c..1fe19f8be9 100644 --- a/demo/family/subgraph/model/models_gen.go +++ b/demo/family/subgraph/model/models_gen.go @@ -51,8 +51,12 @@ func (this Cat) GetName() string { return this.Name } func (Cat) IsAnimal() {} type Details struct { - Forename string `json:"forename"` - Surname string `json:"surname"` + Forename string `json:"forename"` + Surname string `json:"surname"` + HasChildren bool `json:"hasChildren"` + MaritalStatus *MaritalStatus `json:"maritalStatus,omitempty"` + Nationality Nationality `json:"nationality"` + Pets []Pet `json:"pets,omitempty"` } type Dog struct { @@ -70,12 +74,8 @@ func (this Dog) GetName() string { return this.Name } func (Dog) IsAnimal() {} type Employee struct { - Details *Details `json:"details,omitempty"` - ID int `json:"id"` - HasChildren bool `json:"hasChildren"` - MaritalStatus *MaritalStatus `json:"maritalStatus,omitempty"` - Nationality Nationality `json:"nationality"` - Pets []Pet `json:"pets,omitempty"` + Details *Details `json:"details,omitempty"` + ID int `json:"id"` } func (Employee) IsEntity() {} diff --git a/demo/family/subgraph/schema.graphqls b/demo/family/subgraph/schema.graphqls index b2c38a9b8a..13ff1602aa 100644 --- a/demo/family/subgraph/schema.graphqls +++ b/demo/family/subgraph/schema.graphqls @@ -84,14 +84,13 @@ enum Nationality { type Details { forename: String! @shareable surname: String! @shareable + hasChildren: Boolean! + maritalStatus: MaritalStatus + nationality: Nationality! + pets: [Pet] } type Employee @key(fields: "id") { details: Details @shareable id: Int! - # move to details eventually - hasChildren: Boolean! - maritalStatus: MaritalStatus - nationality: Nationality! - pets: [Pet] } \ No newline at end of file diff --git a/proto/wg/cosmo/node/v1/node.proto b/proto/wg/cosmo/node/v1/node.proto index 9d192f3f59..e94a5a3a1d 100644 --- a/proto/wg/cosmo/node/v1/node.proto +++ b/proto/wg/cosmo/node/v1/node.proto @@ -53,24 +53,20 @@ message DataSourceConfiguration { repeated DirectiveConfiguration directives = 7; int64 request_timeout_seconds = 8; string id = 9; - repeated RequiredField required_fields = 10; + repeated RequiredField keys = 10; + repeated RequiredField provides = 11; + repeated RequiredField requires = 12; } message FieldConfiguration { string type_name = 1; string field_name = 2; - bool disable_default_field_mapping = 3; - repeated string path = 4; - repeated ArgumentConfiguration arguments_configuration = 6; - bool unescape_response_json = 7; + repeated ArgumentConfiguration arguments_configuration = 3; } message ArgumentConfiguration { string name = 1; ArgumentSource source_type = 2; - repeated string source_path = 3; - ArgumentRenderConfiguration render_configuration = 4; - string rename_type_to = 5; } enum ArgumentRenderConfiguration { diff --git a/router/go.mod b/router/go.mod index 01ec0e3c2a..bc668ffe87 100644 --- a/router/go.mod +++ b/router/go.mod @@ -17,7 +17,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.14.4 github.com/tidwall/sjson v1.2.5 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230829102818-e2553cf069d3 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230831161009-3673bf02f054 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0 diff --git a/router/go.sum b/router/go.sum index deb0d0a54b..1e27a34405 100644 --- a/router/go.sum +++ b/router/go.sum @@ -307,8 +307,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230829102818-e2553cf069d3 h1:BZRss0FCEDbQL04kxX0YPsJghx6Y5Zzemf40BAxlRQM= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230829102818-e2553cf069d3/go.mod h1:jOEQFeTIDSAEWA//qrpSNjGYcCjMylvc/R/W8eM7+gY= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230831161009-3673bf02f054 h1:INyECNh422gaMB1aGuTAOa27+oI8jzX8oHRvOKLIpqk= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230831161009-3673bf02f054/go.mod h1:jOEQFeTIDSAEWA//qrpSNjGYcCjMylvc/R/W8eM7+gY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/router/pkg/factoryresolver/factoryresolver.go b/router/pkg/factoryresolver/factoryresolver.go index 073c47b6cf..4d3f4b70a5 100644 --- a/router/pkg/factoryresolver/factoryresolver.go +++ b/router/pkg/factoryresolver/factoryresolver.go @@ -233,9 +233,7 @@ func (l *Loader) Load(engineConfig *nodev1.EngineConfiguration, wgServerUrl stri var args []plan.ArgumentConfiguration for _, argumentConfiguration := range configuration.ArgumentsConfiguration { arg := plan.ArgumentConfiguration{ - Name: argumentConfiguration.Name, - SourcePath: argumentConfiguration.SourcePath, - RenameTypeTo: argumentConfiguration.RenameTypeTo, + Name: argumentConfiguration.Name, } switch argumentConfiguration.SourceType { case nodev1.ArgumentSource_FIELD_ARGUMENT: @@ -243,23 +241,12 @@ func (l *Loader) Load(engineConfig *nodev1.EngineConfiguration, wgServerUrl stri case nodev1.ArgumentSource_OBJECT_FIELD: arg.SourceType = plan.ObjectFieldSource } - switch argumentConfiguration.RenderConfiguration { - case nodev1.ArgumentRenderConfiguration_RENDER_ARGUMENT_DEFAULT: - arg.RenderConfig = plan.RenderArgumentDefault - case nodev1.ArgumentRenderConfiguration_RENDER_ARGUMENT_AS_ARRAY_CSV: - arg.RenderConfig = plan.RenderArgumentAsArrayCSV - case nodev1.ArgumentRenderConfiguration_RENDER_ARGUMENT_AS_GRAPHQL_VALUE: - arg.RenderConfig = plan.RenderArgumentAsGraphQLValue - } args = append(args, arg) } outConfig.Fields = append(outConfig.Fields, plan.FieldConfiguration{ - TypeName: configuration.TypeName, - FieldName: configuration.FieldName, - DisableDefaultMapping: configuration.DisableDefaultFieldMapping, - Path: configuration.Path, - Arguments: args, - UnescapeResponseJson: configuration.UnescapeResponseJson, + TypeName: configuration.TypeName, + FieldName: configuration.FieldName, + Arguments: args, }) } @@ -357,16 +344,34 @@ func (l *Loader) Load(engineConfig *nodev1.EngineConfiguration, wgServerUrl stri RenameTo: directive.DirectiveName, }) } - for _, requiredField := range in.RequiredFields { - out.RequiredFields = append(out.RequiredFields, plan.RequiredFieldsConfiguration{ - TypeName: requiredField.TypeName, - FieldName: requiredField.FieldName, - SelectionSet: requiredField.SelectionSet, + out.FederationMetaData = plan.FederationMetaData{ + Keys: nil, + Requires: nil, + Provides: nil, + } + for _, keyConfiguration := range in.Keys { + out.FederationMetaData.Keys = append(out.FederationMetaData.Keys, plan.FederationFieldConfiguration{ + TypeName: keyConfiguration.TypeName, + FieldName: keyConfiguration.FieldName, + SelectionSet: keyConfiguration.SelectionSet, + }) + } + for _, providesConfiguration := range in.Provides { + out.FederationMetaData.Provides = append(out.FederationMetaData.Provides, plan.FederationFieldConfiguration{ + TypeName: providesConfiguration.TypeName, + FieldName: providesConfiguration.FieldName, + SelectionSet: providesConfiguration.SelectionSet, + }) + } + for _, requiresConfiguration := range in.Requires { + out.FederationMetaData.Requires = append(out.FederationMetaData.Requires, plan.FederationFieldConfiguration{ + TypeName: requiresConfiguration.TypeName, + FieldName: requiresConfiguration.FieldName, + SelectionSet: requiresConfiguration.SelectionSet, }) } outConfig.DataSources = append(outConfig.DataSources, out) } - return &outConfig, nil } diff --git a/shared/src/router-config/builder.ts b/shared/src/router-config/builder.ts index dc3b4031f3..4c70c03f15 100644 --- a/shared/src/router-config/builder.ts +++ b/shared/src/router-config/builder.ts @@ -1,7 +1,7 @@ import crypto from 'node:crypto'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; -import { normalizeSubgraphFromString } from '@wundergraph/composition'; -import { GraphQLSchema, lexicographicSortSchema, parse } from 'graphql'; +import { ArgumentConfigurationData, normalizeSubgraphFromString } from '@wundergraph/composition'; +import { GraphQLSchema, lexicographicSortSchema } from 'graphql'; import { ConfigurationVariable, ConfigurationVariableKind, @@ -12,11 +12,15 @@ import { InternedString, RouterConfig, } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; -import { configuration, configurationDataMapToDataSourceConfiguration } from './graphql-configuration.js'; +import { + argumentConfigurationDatasToFieldConfigurations, + configurationDataMapToDataSourceConfiguration, +} from './graphql-configuration.js'; export interface Input { - subgraphs: Subgraph[]; + argumentConfigurations: ArgumentConfigurationData[]; federatedSDL: string; + subgraphs: Subgraph[]; } export interface Subgraph { @@ -54,15 +58,16 @@ export const buildRouterConfig = function (input: Input): RouterConfig { // IMPORTANT NOTE: printSchema and printSchemaWithDirectives promotes extension types to "full" types const upstreamSchema = internString(engineConfig, printSchemaWithDirectives(lexicographicSortSchema(schema))); - const { childNodes, rootNodes, requiredFields } = configurationDataMapToDataSourceConfiguration( + const { childNodes, rootNodes, keys, provides, requires } = configurationDataMapToDataSourceConfiguration( normalizationResult!.configurationDataMap, ); - const { fieldConfigs, typeConfigs } = configuration(parse(subgraph.sdl), true); const datasourceConfig = new DataSourceConfiguration({ id: subgraph.url, childNodes, rootNodes, - requiredFields, + keys, + provides, + requires, kind: DataSourceKind.GRAPHQL, customGraphql: { customScalarTypeFields: [], @@ -96,9 +101,8 @@ export const buildRouterConfig = function (input: Input): RouterConfig { requestTimeoutSeconds: BigInt(10), }); engineConfig.datasourceConfigurations.push(datasourceConfig); - engineConfig.fieldConfigurations.push(...fieldConfigs); - engineConfig.typeConfigurations.push(...typeConfigs); } + engineConfig.fieldConfigurations = argumentConfigurationDatasToFieldConfigurations(input.argumentConfigurations); engineConfig.graphqlSchema = input.federatedSDL; return new RouterConfig({ engineConfig, diff --git a/shared/src/router-config/graphql-configuration.ts b/shared/src/router-config/graphql-configuration.ts index 94892c86d0..461dba3d16 100644 --- a/shared/src/router-config/graphql-configuration.ts +++ b/shared/src/router-config/graphql-configuration.ts @@ -1,59 +1,47 @@ -import { - DocumentNode, - GraphQLSchema, - Kind, - OperationDefinitionNode, - parse, - SelectionSetNode, - TypeNode, - visit, -} from 'graphql'; +import { Kind, TypeNode } from 'graphql'; import { ArgumentConfiguration, - ArgumentRenderConfiguration, ArgumentSource, FieldConfiguration, RequiredField, - TypeConfiguration, TypeField, } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; -import { ConfigurationDataMap } from '@wundergraph/composition/dist/subgraph/field-configuration.js'; - -const DefaultJsonType = 'JSON'; - -export interface GraphQLConfiguration { - rootNodes: TypeField[]; - childNodes: TypeField[]; - fieldConfigs: FieldConfiguration[]; - typeConfigs: TypeConfiguration[]; -} - -export const configuration = (schema: DocumentNode, isFederationSubgraph: boolean): GraphQLConfiguration => { - const config: GraphQLConfiguration = { - rootNodes: [], - childNodes: [], - fieldConfigs: [], - typeConfigs: [], - }; - if (isFederationSubgraph) { - visitSchema(schema, config, true); - } else { - visitSchema(schema, config, false); - } - return config; -}; +import { ArgumentConfigurationData, ConfigurationDataMap, RequiredFieldConfiguration } from '@wundergraph/composition'; export type DataSourceConfiguration = { rootNodes: TypeField[]; childNodes: TypeField[]; - requiredFields: any; + provides: RequiredField[]; + keys: RequiredField[]; + requires: RequiredField[]; }; +function addRequiredFields( + requiredFields: RequiredFieldConfiguration[] | undefined, + target: RequiredField[], + typeName: string, +) { + if (!requiredFields) { + return; + } + for (const requiredField of requiredFields) { + target.push( + new RequiredField({ + typeName, + fieldName: requiredField.fieldName, + selectionSet: requiredField.selectionSet, + }), + ); + } +} + export function configurationDataMapToDataSourceConfiguration(dataMap: ConfigurationDataMap) { const output: DataSourceConfiguration = { rootNodes: [], childNodes: [], - requiredFields: [], + keys: [], + provides: [], + requires: [], }; for (const [typeName, data] of dataMap) { const fieldNames: string[] = [...data.fieldNames]; @@ -63,380 +51,35 @@ export function configurationDataMapToDataSourceConfiguration(dataMap: Configura } else { output.childNodes.push(typeField); } - for (const selectionSet of data.selectionSets) { - output.requiredFields.push(new RequiredField({ typeName, fieldName: '', selectionSet })); - } + addRequiredFields(data.keys, output.keys, typeName); + addRequiredFields(data.provides, output.provides, typeName); + addRequiredFields(data.requires, output.requires, typeName); } return output; } -interface JsonTypeField { - typeName: string; - fieldName: string; -} - -const visitSchema = (schema: DocumentNode, config: GraphQLConfiguration, isFederation: boolean) => { - let typeName: undefined | string; - let fieldName: undefined | string; - let isExtensionType = false; - let hasExtensionDirective = false; - let isEntity = false; - let isExternalField = false; - let entityFields: string[] = []; - const jsonFields: JsonTypeField[] = []; - - const jsonScalars = new Set([DefaultJsonType]); - - const RootNodeNames = rootNodeNames(schema, isFederation); - const isNodeRoot = (typeName: string) => { - return RootNodeNames.includes(typeName); - }; - - visit(schema, { - ObjectTypeDefinition: { - enter: (node) => { - typeName = node.name.value; - isExtensionType = false; - isEntity = false; - }, - leave: () => { - typeName = undefined; - isExtensionType = false; - hasExtensionDirective = false; - entityFields = []; - isEntity = false; - }, - }, - InterfaceTypeDefinition: { - enter: (node) => { - typeName = node.name.value; - isExtensionType = false; - isEntity = false; - }, - leave: () => { - typeName = undefined; - isExtensionType = false; - hasExtensionDirective = false; - entityFields = []; - isEntity = false; - }, - }, - ObjectTypeExtension: { - enter: (node) => { - typeName = node.name.value; - isExtensionType = true; - isEntity = false; - }, - leave: () => { - typeName = undefined; - isExtensionType = false; - hasExtensionDirective = false; - entityFields = []; - }, - }, - InterfaceTypeExtension: { - enter: (node) => { - typeName = node.name.value; - isExtensionType = true; - isEntity = false; - }, - leave: () => { - typeName = undefined; - isExtensionType = false; - hasExtensionDirective = false; - entityFields = []; - }, - }, - Directive: { - enter: (node) => { - switch (node.name.value) { - case 'extends': { - hasExtensionDirective = true; - return; - } - case 'key': { - isEntity = true; - if (!node.arguments) { - return; - } - const fields = node.arguments.find((arg) => arg.name.value === 'fields'); - if (!fields) { - return; - } - if (fields.value.kind !== 'StringValue') { - return; - } - const fieldsValue = fields.value.value; - const fieldsSelection = parseSelectionSet('{ ' + fieldsValue + ' }'); - for (const s of fieldsSelection.selections) { - if (s.kind !== 'Field') { - continue; - } - entityFields.push(s.name.value); - } - return; - } - case 'external': { - isExternalField = true; - } - } - }, - }, - FieldDefinition: { - enter: (node) => { - fieldName = node.name.value; - - if (jsonScalars.has(resolveNamedTypeName(node.type))) { - jsonFields.push({ typeName: typeName!, fieldName: fieldName! }); - } - }, - leave: () => { - if (typeName === undefined || fieldName === undefined) { - return; - } - const isRoot = isNodeRoot(typeName); - if (isRoot) { - addTypeField(config.rootNodes, typeName, fieldName); - } - - const isExtension = isExtensionType || hasExtensionDirective; - const isFederationRootNode = isExtension && isEntity && !isExternalField; - const isEntityField = entityFields.includes(fieldName); - - if (isEntity && !isExternalField) { - addTypeField(config.rootNodes, typeName, fieldName); - } - - if (isFederationRootNode) { - addTypeField(config.rootNodes, typeName, fieldName); - // addRequiredFields(typeName, fieldName, config, entityFields); - } - - if (!isRoot && !isFederationRootNode && !isExternalField) { - addTypeField(config.childNodes, typeName, fieldName); - } - - if (isExternalField && isEntityField) { - addTypeField(config.childNodes, typeName, fieldName); - } - - if (isEntity && !isEntityField && !isExternalField && !isFederationRootNode) { - // addRequiredFields(typeName, fieldName, config, entityFields); - } - - fieldName = undefined; - isExternalField = false; - }, - }, - InputValueDefinition: { - enter: (node) => { - if (!fieldName || !typeName) { - return; - } - addFieldArgument(typeName, fieldName, node.name.value, config); - }, - }, - }); - - addJsonFieldConfigurations(config, jsonFields); -}; - -const parseSelectionSet = (selectionSet: string): SelectionSetNode => { - const query = parse(selectionSet).definitions[0] as OperationDefinitionNode; - return query.selectionSet; -}; - -const rootNodeNames = (schema: DocumentNode, isFederation: boolean): string[] => { - const rootTypes = new Set(); - visit(schema, { - SchemaDefinition: { - enter: (node) => { - for (const operationType of node.operationTypes) { - rootTypes.add(operationType.type.name.value); - } - }, - }, - ObjectTypeDefinition: { - enter: (node) => { - switch (node.name.value) { - case 'Query': - case 'Mutation': - case 'Subscription': { - rootTypes.add(node.name.value); - } - } - }, - }, - ObjectTypeExtension: { - enter: (node) => { - if (!isFederation) { - return; - } - switch (node.name.value) { - case 'Query': - case 'Mutation': - case 'Subscription': { - rootTypes.add(node.name.value); - } - } - }, - }, - }); - - return [...rootTypes.values()]; -}; - -export const isRootType = (typeName: string, schema: GraphQLSchema): boolean => { - const queryType = schema.getQueryType(); - if (queryType && queryType.astNode && queryType.astNode.name.value === typeName) { - return true; - } - const mutationType = schema.getMutationType(); - if (mutationType && mutationType.astNode && mutationType.astNode.name.value === typeName) { - return true; - } - const subscriptionType = schema.getSubscriptionType(); - if (subscriptionType && subscriptionType.astNode && subscriptionType.astNode.name.value === typeName) { - return true; - } - const typeDefinition = schema.getType(typeName); - if ( - typeDefinition === undefined || - typeDefinition === null || - typeDefinition.astNode === undefined || - typeDefinition.astNode === null - ) { - return false; - } - return false; -}; - -const addTypeField = (typeFields: TypeField[], typeName: string, fieldName: string) => { - const i = typeFields.findIndex((t) => t.typeName === typeName); - if (i !== -1) { - addField(typeFields[i], fieldName); - return; - } - const typeField: TypeField = new TypeField({ - typeName, - fieldNames: [], - }); - addField(typeField, fieldName); - typeFields.push(typeField); -}; - -const addField = (typeField: TypeField, field: string) => { - const i = typeField.fieldNames.indexOf(field); - if (i !== -1) { - return; - } - typeField.fieldNames.push(field); -}; - -const addFieldArgument = (typeName: string, fieldName: string, argName: string, config: GraphQLConfiguration) => { - const arg: ArgumentConfiguration = new ArgumentConfiguration({ - name: argName, - sourceType: ArgumentSource.FIELD_ARGUMENT, - sourcePath: [], - renderConfiguration: ArgumentRenderConfiguration.RENDER_ARGUMENT_DEFAULT, - renameTypeTo: '', - }); - const field: FieldConfiguration | undefined = findField(config.fieldConfigs, typeName, fieldName); - if (!field) { - config.fieldConfigs.push( +export function argumentConfigurationDatasToFieldConfigurations( + datas: ArgumentConfigurationData[], +): FieldConfiguration[] { + const output: FieldConfiguration[] = []; + for (const data of datas) { + const argumentConfigurations: ArgumentConfiguration[] = data.argumentNames.map( + (argumentName: string) => + new ArgumentConfiguration({ + name: argumentName, + sourceType: ArgumentSource.FIELD_ARGUMENT, + }), + ); + output.push( new FieldConfiguration({ - typeName, - fieldName, - argumentsConfiguration: [arg], - disableDefaultFieldMapping: false, - path: [], - unescapeResponseJson: false, + argumentsConfiguration: argumentConfigurations, + fieldName: data.fieldName, + typeName: data.typeName, }), ); - return; - } - if (!field.argumentsConfiguration) { - field.argumentsConfiguration = [arg]; - return; - } - const i = field.argumentsConfiguration.findIndex((a: ArgumentConfiguration) => a.name === argName); - if (i !== -1) { - field.argumentsConfiguration[i] = arg; - return; - } - field.argumentsConfiguration.push(arg); -}; - -// const addRequiredFields = ( -// typeName: string, -// fieldName: string, -// config: GraphQLConfiguration, -// requiredFieldNames: string[], -// ) => { -// for (const f of requiredFieldNames) { -// addRequiredField(typeName, fieldName, config, f); -// } -// }; - -// const addRequiredField = ( -// typeName: string, -// fieldName: string, -// config: GraphQLConfiguration, -// requiredFieldName: string, -// ) => { -// const field: FieldConfiguration | undefined = findField(config.fieldConfigs, typeName, fieldName); -// if (!field) { -// config.fieldConfigs.push( -// new FieldConfiguration({ -// typeName, -// fieldName, -// argumentsConfiguration: [], -// path: [], -// disableDefaultFieldMapping: false, -// unescapeResponseJson: false, -// }), -// ); -// return; -// } -// if (!field.requiresFields) { -// field.requiresFields = [requiredFieldName]; -// return; -// } -// const exists = field.requiresFields.includes(requiredFieldName); -// if (exists) { -// return; -// } -// field.requiresFields.push(requiredFieldName); -// }; - -const addJsonFieldConfigurations = (config: GraphQLConfiguration, jsonFields: JsonTypeField[]) => { - for (const jsonField of jsonFields) { - const field: FieldConfiguration | undefined = findField( - config.fieldConfigs, - jsonField.typeName, - jsonField.fieldName, - ); - - if (field) { - field.unescapeResponseJson = true; - } else { - config.fieldConfigs.push( - new FieldConfiguration({ - typeName: jsonField.typeName, - fieldName: jsonField.fieldName, - argumentsConfiguration: [], - disableDefaultFieldMapping: false, - path: [], - unescapeResponseJson: true, - }), - ); - } } -}; - -const findField = (fields: FieldConfiguration[], typeName: string, fieldName: string) => { - return fields.find((f) => f.typeName === typeName && f.fieldName === fieldName); -}; + return output; +} const resolveNamedTypeName = (type: TypeNode): string => { switch (type.kind) { diff --git a/shared/test/__snapshots__/router.config.test.ts.snap b/shared/test/__snapshots__/router.config.test.ts.snap index 7bdbecd6d9..8ce3ca702d 100644 --- a/shared/test/__snapshots__/router.config.test.ts.snap +++ b/shared/test/__snapshots__/router.config.test.ts.snap @@ -50,7 +50,7 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`] }, \\"requestTimeoutSeconds\\": \\"10\\", \\"id\\": \\"https://wg-federation-demo-accounts.fly.dev/graphql\\", - \\"requiredFields\\": [ + \\"keys\\": [ { \\"typeName\\": \\"User\\", \\"selectionSet\\": \\"id\\" @@ -115,7 +115,7 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`] }, \\"requestTimeoutSeconds\\": \\"10\\", \\"id\\": \\"https://wg-federation-demo-products.fly.dev/graphql\\", - \\"requiredFields\\": [ + \\"keys\\": [ { \\"typeName\\": \\"Product\\", \\"selectionSet\\": \\"upc\\" @@ -177,7 +177,7 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`] }, \\"requestTimeoutSeconds\\": \\"10\\", \\"id\\": \\"https://wg-federation-demo-reviews.fly.dev/graphql\\", - \\"requiredFields\\": [ + \\"keys\\": [ { \\"typeName\\": \\"User\\", \\"selectionSet\\": \\"id\\" @@ -233,7 +233,7 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`] }, \\"requestTimeoutSeconds\\": \\"10\\", \\"id\\": \\"https://wg-federation-demo-inventory.fly.dev/graphql\\", - \\"requiredFields\\": [ + \\"keys\\": [ { \\"typeName\\": \\"Product\\", \\"selectionSet\\": \\"upc\\" @@ -241,36 +241,6 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`] ] } ], - \\"fieldConfigurations\\": [ - { - \\"typeName\\": \\"Query\\", - \\"fieldName\\": \\"topProducts\\", - \\"argumentsConfiguration\\": [ - { - \\"name\\": \\"first\\", - \\"sourceType\\": 1 - }, - { - \\"name\\": \\"random\\", - \\"sourceType\\": 1 - } - ] - }, - { - \\"typeName\\": \\"Mutation\\", - \\"fieldName\\": \\"setPrice\\", - \\"argumentsConfiguration\\": [ - { - \\"name\\": \\"upc\\", - \\"sourceType\\": 1 - }, - { - \\"name\\": \\"price\\", - \\"sourceType\\": 1 - } - ] - } - ], \\"graphqlSchema\\": \\"type Query {}\\", \\"stringStorage\\": { \\"56b22d2b228499be9dcb1bd0720bbf7d8348104e\\": \\"schema {\\\\n query: Query\\\\n}\\\\n\\\\ndirective @extends on INTERFACE | OBJECT\\\\n\\\\ndirective @external on FIELD_DEFINITION | OBJECT\\\\n\\\\ndirective @key(fields: String!) repeatable on OBJECT\\\\n\\\\ndirective @provides(fields: String!) on FIELD_DEFINITION\\\\n\\\\ndirective @requires(fields: String!) on FIELD_DEFINITION\\\\n\\\\ndirective @tag(name: String!) repeatable on ARGUMENT_DEFINITION | ENUM | ENUM_VALUE | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | INPUT_OBJECT | INTERFACE | OBJECT | SCALAR | UNION\\\\n\\\\ntype Query {\\\\n me: User\\\\n}\\\\n\\\\ntype User @key(fields: \\\\\\"id\\\\\\") {\\\\n id: ID!\\\\n name: String\\\\n username: String\\\\n}\\", diff --git a/shared/test/router.config.test.ts b/shared/test/router.config.test.ts index 303e285302..05c5e2c3a0 100644 --- a/shared/test/router.config.test.ts +++ b/shared/test/router.config.test.ts @@ -46,6 +46,7 @@ describe("Router Config Builder", () => { url: "https://wg-federation-demo-inventory.fly.dev/graphql", }; const routerConfig = buildRouterConfig({ + argumentConfigurations: [], subgraphs: [accounts, products, reviews, inventory], // Passed as it is to the router config federatedSDL: `type Query {}`, @@ -68,6 +69,7 @@ describe("Router Config Builder", () => { let error; try { buildRouterConfig({ + argumentConfigurations: [], subgraphs: [subgraph], federatedSDL: '', }); diff --git a/studio/src/components/analytics/getColumnData.tsx b/studio/src/components/analytics/getColumnData.tsx index 3092543594..1e5976166f 100644 --- a/studio/src/components/analytics/getColumnData.tsx +++ b/studio/src/components/analytics/getColumnData.tsx @@ -1,41 +1,27 @@ -import { cn } from "@/lib/utils"; -import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; -import { DropdownMenuItemProps } from "@radix-ui/react-dropdown-menu"; -import { ClipboardCopyIcon } from "@radix-ui/react-icons"; -import { ColumnDef } from "@tanstack/react-table"; -import { - AnalyticsViewColumn, - Unit, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import copy from "copy-to-clipboard"; -import { formatInTimeZone } from "date-fns-tz"; -import compact from "lodash/compact"; -import React, { ReactNode } from "react"; -import { SchemaViewer } from "../schmea-viewer"; -import { Button } from "../ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; +import { cn } from '@/lib/utils'; +import { EllipsisVerticalIcon } from '@heroicons/react/24/outline'; +import { DropdownMenuItemProps } from '@radix-ui/react-dropdown-menu'; +import { ClipboardCopyIcon } from '@radix-ui/react-icons'; +import { ColumnDef } from '@tanstack/react-table'; +import { AnalyticsViewColumn, Unit } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import copy from 'copy-to-clipboard'; +import { formatInTimeZone } from 'date-fns-tz'; +import compact from 'lodash/compact'; +import React, { ReactNode } from 'react'; +import { SchemaViewer } from '../schmea-viewer'; +import { Button } from '../ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; -import { useToast } from "../ui/use-toast"; -import { nanoTimestampToTime } from "./charts"; -import { defaultFilterFn } from "./defaultFilterFunction"; +} from '../ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; +import { useToast } from '../ui/use-toast'; +import { nanoTimestampToTime } from './charts'; +import { defaultFilterFn } from './defaultFilterFunction'; export const mapStatusCode: Record = { STATUS_CODE_UNSET: "Success", diff --git a/studio/src/components/app-provider.tsx b/studio/src/components/app-provider.tsx index b9aa18d0f0..93823a10b3 100644 --- a/studio/src/components/app-provider.tsx +++ b/studio/src/components/app-provider.tsx @@ -1,13 +1,9 @@ -import { Transport } from "@connectrpc/connect"; -import { TransportProvider } from "@connectrpc/connect-query"; -import { createConnectTransport } from "@connectrpc/connect-web"; -import { - QueryClient, - QueryClientProvider, - useQuery, -} from "@tanstack/react-query"; -import { useRouter } from "next/router"; -import { ReactNode, createContext, useEffect, useState } from "react"; +import { Transport } from '@connectrpc/connect'; +import { TransportProvider } from '@connectrpc/connect-query'; +import { createConnectTransport } from '@connectrpc/connect-web'; +import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { createContext, ReactNode, useEffect, useState } from 'react'; interface User { id: string; diff --git a/studio/src/components/compose-status.tsx b/studio/src/components/compose-status.tsx index edba772707..88521a5285 100644 --- a/studio/src/components/compose-status.tsx +++ b/studio/src/components/compose-status.tsx @@ -1,8 +1,4 @@ -import { - BoltIcon, - BoltSlashIcon, - CircleStackIcon, -} from "@heroicons/react/24/outline"; +import { BoltIcon, BoltSlashIcon, CircleStackIcon } from '@heroicons/react/24/outline'; export const ComposeStatus = ({ validGraph, diff --git a/studio/src/components/federatedgraphs-cards.tsx b/studio/src/components/federatedgraphs-cards.tsx index d69c438c9a..825e3e7063 100644 --- a/studio/src/components/federatedgraphs-cards.tsx +++ b/studio/src/components/federatedgraphs-cards.tsx @@ -1,46 +1,32 @@ -import { useFireworks } from "@/hooks/use-fireworks"; -import { SubmitHandler, useZodForm } from "@/hooks/use-form"; -import { docsBaseURL } from "@/lib/constants"; -import { useChartData } from "@/lib/insights-helpers"; -import { - ChevronDoubleRightIcon, - CommandLineIcon, -} from "@heroicons/react/24/outline"; -import { useMutation } from "@tanstack/react-query"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; -import { migrateFromApollo } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { FederatedGraph } from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import { getTime, parseISO, subDays } from "date-fns"; -import Link from "next/link"; -import { Dispatch, SetStateAction, useContext, useState } from "react"; -import { Line, LineChart, ResponsiveContainer, XAxis } from "recharts"; -import { z } from "zod"; -import { UserContext } from "./app-provider"; -import { ComposeStatusMessage } from "./compose-status"; -import { ComposeStatusBulb } from "./compose-status-bulb"; -import { EmptyState } from "./empty-state"; -import { TimeAgo } from "./time-ago"; -import { Button } from "./ui/button"; -import { Card } from "./ui/card"; -import { CLI } from "./ui/cli"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "./ui/dialog"; -import { Input } from "./ui/input"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "./ui/tooltip"; -import { useToast } from "./ui/use-toast"; -import { Logo } from "./logo"; -import { SiApollographql } from "react-icons/si"; -import { cn } from "@/lib/utils"; +import { useFireworks } from '@/hooks/use-fireworks'; +import { SubmitHandler, useZodForm } from '@/hooks/use-form'; +import { docsBaseURL } from '@/lib/constants'; +import { useChartData } from '@/lib/insights-helpers'; +import { ChevronDoubleRightIcon, CommandLineIcon } from '@heroicons/react/24/outline'; +import { useMutation } from '@tanstack/react-query'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { migrateFromApollo } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { FederatedGraph } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { getTime, parseISO, subDays } from 'date-fns'; +import Link from 'next/link'; +import { Dispatch, SetStateAction, useContext, useState } from 'react'; +import { Line, LineChart, ResponsiveContainer, XAxis } from 'recharts'; +import { z } from 'zod'; +import { UserContext } from './app-provider'; +import { ComposeStatusMessage } from './compose-status'; +import { ComposeStatusBulb } from './compose-status-bulb'; +import { EmptyState } from './empty-state'; +import { TimeAgo } from './time-ago'; +import { Button } from './ui/button'; +import { Card } from './ui/card'; +import { CLI } from './ui/cli'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog'; +import { Input } from './ui/input'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; +import { useToast } from './ui/use-toast'; +import { Logo } from './logo'; +import { SiApollographql } from 'react-icons/si'; +import { cn } from '@/lib/utils'; // this is required to render a blank line with LineChart const fallbackData = [ diff --git a/studio/src/components/layout/nav.tsx b/studio/src/components/layout/nav.tsx index f835cdfae9..bba6393e8f 100644 --- a/studio/src/components/layout/nav.tsx +++ b/studio/src/components/layout/nav.tsx @@ -1,24 +1,18 @@ -import { docsBaseURL } from "@/lib/constants"; -import { cn } from "@/lib/utils"; -import { Cross2Icon, HamburgerMenuIcon } from "@radix-ui/react-icons"; -import { useQuery } from "@tanstack/react-query"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { getFederatedGraphs } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { ReactNode, useContext, useState } from "react"; -import { Logo } from "../logo"; -import { ThemeToggle } from "../theme-toggle"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; -import { Separator } from "../ui/separator"; -import { UserMenu, UserMenuMobile } from "../user-menu"; -import { LayoutProps } from "./layout"; -import { UserContext } from "../app-provider"; +import { docsBaseURL } from '@/lib/constants'; +import { cn } from '@/lib/utils'; +import { Cross2Icon, HamburgerMenuIcon } from '@radix-ui/react-icons'; +import { useQuery } from '@tanstack/react-query'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { getFederatedGraphs } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { ReactNode, useContext, useState } from 'react'; +import { Logo } from '../logo'; +import { ThemeToggle } from '../theme-toggle'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; +import { Separator } from '../ui/separator'; +import { UserMenu, UserMenuMobile } from '../user-menu'; +import { LayoutProps } from './layout'; +import { UserContext } from '../app-provider'; export type NavLink = { title: string; @@ -96,7 +90,7 @@ const Graphs = () => { export const Nav = ({ children, links }: SideNavLayoutProps) => { const router = useRouter(); - const user = useContext(UserContext); + const user = useContext(UserContext); const [showMobileMenu, setShowMobileMenu] = useState(false); return ( diff --git a/studio/src/components/operations-overview.tsx b/studio/src/components/operations-overview.tsx index dd0cf3d1b0..594359c062 100644 --- a/studio/src/components/operations-overview.tsx +++ b/studio/src/components/operations-overview.tsx @@ -1,32 +1,23 @@ -import useWindowSize from "@/hooks/use-window-size"; -import { dateFormatter, useChartData } from "@/lib/insights-helpers"; -import { formatNumber } from "@/lib/utils"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useQuery } from "@tanstack/react-query"; -import { getDashboardAnalyticsView } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; +import useWindowSize from '@/hooks/use-window-size'; +import { dateFormatter, useChartData } from '@/lib/insights-helpers'; +import { formatNumber } from '@/lib/utils'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { useQuery } from '@tanstack/react-query'; import { - OperationRequestCount, - RequestSeriesItem, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import { useId, useMemo } from "react"; -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import BarList from "./analytics/barlist"; -import { EmptyState } from "./empty-state"; -import { Badge } from "./ui/badge"; -import { Button } from "./ui/button"; -import { Loader } from "./ui/loader"; -import { Separator } from "./ui/separator"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; -import { useRouter } from "next/router"; -import { constructAnalyticsTableQueryState } from "./analytics/constructAnalyticsTableQueryState"; + getDashboardAnalyticsView, +} from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { OperationRequestCount, RequestSeriesItem } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { useId, useMemo } from 'react'; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import BarList from './analytics/barlist'; +import { EmptyState } from './empty-state'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { Loader } from './ui/loader'; +import { Separator } from './ui/separator'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { useRouter } from 'next/router'; +import { constructAnalyticsTableQueryState } from './analytics/constructAnalyticsTableQueryState'; const valueFormatter = (number: number) => `${formatNumber(number)}`; diff --git a/studio/src/components/subgraphs-table.tsx b/studio/src/components/subgraphs-table.tsx index d25b8f524b..cffdf498d9 100644 --- a/studio/src/components/subgraphs-table.tsx +++ b/studio/src/components/subgraphs-table.tsx @@ -1,22 +1,12 @@ -import { docsBaseURL } from "@/lib/constants"; -import { CommandLineIcon } from "@heroicons/react/24/outline"; -import { formatDistanceToNow } from "date-fns"; -import Link from "next/link"; -import { - Subgraph, - FederatedGraph, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import { EmptyState } from "./empty-state"; -import { Badge } from "./ui/badge"; -import { CLISteps } from "./ui/cli"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "./ui/table"; +import { docsBaseURL } from '@/lib/constants'; +import { CommandLineIcon } from '@heroicons/react/24/outline'; +import { formatDistanceToNow } from 'date-fns'; +import Link from 'next/link'; +import { FederatedGraph, Subgraph } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { EmptyState } from './empty-state'; +import { Badge } from './ui/badge'; +import { CLISteps } from './ui/cli'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table'; export const Empty = ({ graph }: { graph?: FederatedGraph }) => { let label = "team=A"; diff --git a/studio/src/components/ui/dialog.tsx b/studio/src/components/ui/dialog.tsx index 51579a92e9..9c00daf828 100644 --- a/studio/src/components/ui/dialog.tsx +++ b/studio/src/components/ui/dialog.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { Cross2Icon } from "@radix-ui/react-icons"; +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { Cross2Icon } from '@radix-ui/react-icons'; -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils'; const Dialog = DialogPrimitive.Root; diff --git a/studio/src/hooks/use-fireworks.ts b/studio/src/hooks/use-fireworks.ts index dedfc78a2b..1f64d669d9 100644 --- a/studio/src/hooks/use-fireworks.ts +++ b/studio/src/hooks/use-fireworks.ts @@ -1,5 +1,5 @@ -import confetti from "canvas-confetti"; -import React from "react"; +import confetti from 'canvas-confetti'; +import React from 'react'; const fireworks = () => { const duration = 2 * 1000; diff --git a/studio/src/pages/[organizationSlug]/apikeys.tsx b/studio/src/pages/[organizationSlug]/apikeys.tsx index ff7bf63432..e5a37dceaa 100644 --- a/studio/src/pages/[organizationSlug]/apikeys.tsx +++ b/studio/src/pages/[organizationSlug]/apikeys.tsx @@ -1,68 +1,38 @@ -import { UserContext } from "@/components/app-provider"; -import { EmptyState } from "@/components/empty-state"; -import { getDashboardLayout } from "@/components/layout/dashboard-layout"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { UserContext } from '@/components/app-provider'; +import { EmptyState } from '@/components/empty-state'; +import { getDashboardLayout } from '@/components/layout/dashboard-layout'; +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Loader } from "@/components/ui/loader"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { useToast } from "@/components/ui/use-toast"; -import { SubmitHandler, useZodForm } from "@/hooks/use-form"; -import { NextPageWithLayout } from "@/lib/page"; -import { - KeyIcon, - EllipsisVerticalIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; -import { PlusIcon } from "@radix-ui/react-icons"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Loader } from '@/components/ui/loader'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { useToast } from '@/components/ui/use-toast'; +import { SubmitHandler, useZodForm } from '@/hooks/use-form'; +import { NextPageWithLayout } from '@/lib/page'; +import { EllipsisVerticalIcon, ExclamationTriangleIcon, KeyIcon } from '@heroicons/react/24/outline'; +import { PlusIcon } from '@radix-ui/react-icons'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; import { createAPIKey, deleteAPIKey, getAPIKeys, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { ExpiresAt } from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import copy from "copy-to-clipboard"; -import { format } from "date-fns"; -import Link from "next/link"; -import { - Dispatch, - SetStateAction, - useContext, - useEffect, - useState, -} from "react"; -import { FiCheck, FiCopy } from "react-icons/fi"; -import { z } from "zod"; -import { docsBaseURL } from "@/lib/constants"; -import { cn } from "@/lib/utils"; +} from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { ExpiresAt } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import copy from 'copy-to-clipboard'; +import { format } from 'date-fns'; +import Link from 'next/link'; +import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react'; +import { FiCheck, FiCopy } from 'react-icons/fi'; +import { z } from 'zod'; +import { docsBaseURL } from '@/lib/constants'; const CreateAPIKeyDialog = ({ setApiKey, diff --git a/studio/src/pages/[organizationSlug]/graph/[slug]/analytics/index.tsx b/studio/src/pages/[organizationSlug]/graph/[slug]/analytics/index.tsx index c9b912d1cd..598f7a5f7a 100644 --- a/studio/src/pages/[organizationSlug]/graph/[slug]/analytics/index.tsx +++ b/studio/src/pages/[organizationSlug]/graph/[slug]/analytics/index.tsx @@ -1,16 +1,16 @@ -import { DataTable } from "@/components/analytics/data-table"; -import { useAnalyticsQueryState } from "@/components/analytics/useAnalyticsQueryState"; -import { EmptyState } from "@/components/empty-state"; -import { GraphContext, getGraphLayout } from "@/components/layout/graph-layout"; -import { PageHeader } from "@/components/layout/head"; -import { TitleLayout } from "@/components/layout/title-layout"; -import { Button } from "@/components/ui/button"; -import { NextPageWithLayout } from "@/lib/page"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useQuery } from "@tanstack/react-query"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; -import { getAnalyticsView } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { useContext } from "react"; +import { DataTable } from '@/components/analytics/data-table'; +import { useAnalyticsQueryState } from '@/components/analytics/useAnalyticsQueryState'; +import { EmptyState } from '@/components/empty-state'; +import { getGraphLayout, GraphContext } from '@/components/layout/graph-layout'; +import { PageHeader } from '@/components/layout/head'; +import { TitleLayout } from '@/components/layout/title-layout'; +import { Button } from '@/components/ui/button'; +import { NextPageWithLayout } from '@/lib/page'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { useQuery } from '@tanstack/react-query'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { getAnalyticsView } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { useContext } from 'react'; export type OperationAnalytics = { name: string; diff --git a/studio/src/pages/[organizationSlug]/graph/[slug]/changelog.tsx b/studio/src/pages/[organizationSlug]/graph/[slug]/changelog.tsx index dd71e78168..483f8e39b9 100644 --- a/studio/src/pages/[organizationSlug]/graph/[slug]/changelog.tsx +++ b/studio/src/pages/[organizationSlug]/graph/[slug]/changelog.tsx @@ -1,29 +1,25 @@ -import { getGraphLayout } from "@/components/layout/graph-layout"; -import { PageHeader } from "@/components/layout/head"; -import { TitleLayout } from "@/components/layout/title-layout"; -import { Badge } from "@/components/ui/badge"; -import { NextPageWithLayout } from "@/lib/page"; -import { cn } from "@/lib/utils"; +import { getGraphLayout } from '@/components/layout/graph-layout'; +import { PageHeader } from '@/components/layout/head'; +import { TitleLayout } from '@/components/layout/title-layout'; +import { Badge } from '@/components/ui/badge'; +import { NextPageWithLayout } from '@/lib/page'; +import { cn } from '@/lib/utils'; +import { DotFilledIcon, ExclamationTriangleIcon, MinusIcon, PlusIcon, UpdateIcon } from '@radix-ui/react-icons'; +import { noCase } from 'change-case'; +import { format } from 'date-fns'; +import { useQuery } from '@tanstack/react-query'; import { - DotFilledIcon, - ExclamationTriangleIcon, - MinusIcon, - PlusIcon, - UpdateIcon, -} from "@radix-ui/react-icons"; -import { noCase } from "change-case"; -import { format } from "date-fns"; -import { useQuery } from "@tanstack/react-query"; -import { getFederatedGraphChangelog } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { useRouter } from "next/router"; -import { FederatedGraphChangelog } from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; -import { EmptyState } from "@/components/empty-state"; -import { Button } from "@/components/ui/button"; -import { Loader } from "@/components/ui/loader"; -import { CommandLineIcon } from "@heroicons/react/24/outline"; -import { docsBaseURL } from "@/lib/constants"; -import { CLI } from "@/components/ui/cli"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; + getFederatedGraphChangelog, +} from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { useRouter } from 'next/router'; +import { FederatedGraphChangelog } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; +import { EmptyState } from '@/components/empty-state'; +import { Button } from '@/components/ui/button'; +import { Loader } from '@/components/ui/loader'; +import { CommandLineIcon } from '@heroicons/react/24/outline'; +import { docsBaseURL } from '@/lib/constants'; +import { CLI } from '@/components/ui/cli'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; interface StructuredChangelog { changeType: string; diff --git a/studio/src/pages/[organizationSlug]/graph/[slug]/checks.tsx b/studio/src/pages/[organizationSlug]/graph/[slug]/checks.tsx index 807540b4fa..e2ecb152de 100644 --- a/studio/src/pages/[organizationSlug]/graph/[slug]/checks.tsx +++ b/studio/src/pages/[organizationSlug]/graph/[slug]/checks.tsx @@ -1,36 +1,19 @@ -import { EmptyState } from "@/components/empty-state"; -import { GraphContext, getGraphLayout } from "@/components/layout/graph-layout"; -import { PageHeader } from "@/components/layout/head"; -import { TitleLayout } from "@/components/layout/title-layout"; -import { SchemaViewer, SchemaViewerActions } from "@/components/schmea-viewer"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { CLI } from "@/components/ui/cli"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Loader } from "@/components/ui/loader"; -import { Separator } from "@/components/ui/separator"; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { docsBaseURL } from "@/lib/constants"; -import { NextPageWithLayout } from "@/lib/page"; -import { cn } from "@/lib/utils"; -import { - CommandLineIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; +import { EmptyState } from '@/components/empty-state'; +import { getGraphLayout, GraphContext } from '@/components/layout/graph-layout'; +import { PageHeader } from '@/components/layout/head'; +import { TitleLayout } from '@/components/layout/title-layout'; +import { SchemaViewer, SchemaViewerActions } from '@/components/schmea-viewer'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { CLI } from '@/components/ui/cli'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Loader } from '@/components/ui/loader'; +import { Separator } from '@/components/ui/separator'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { docsBaseURL } from '@/lib/constants'; +import { NextPageWithLayout } from '@/lib/page'; +import { cn } from '@/lib/utils'; +import { CommandLineIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { CheckCircledIcon, ChevronLeftIcon, @@ -38,18 +21,18 @@ import { CrossCircledIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon, -} from "@radix-ui/react-icons"; -import { useQuery } from "@tanstack/react-query"; -import { endOfDay, format, formatISO, startOfDay, subDays } from "date-fns"; -import { useRouter } from "next/router"; +} from '@radix-ui/react-icons'; +import { useQuery } from '@tanstack/react-query'; +import { endOfDay, format, formatISO, startOfDay, subDays } from 'date-fns'; +import { useRouter } from 'next/router'; import { getCheckDetails, getChecksByFederatedGraphName, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; -import { useCallback, useContext, useState } from "react"; -import { DatePickerWithRange } from "@/components/date-picker-with-range"; -import { DateRange } from "react-day-picker"; +} from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { useCallback, useContext } from 'react'; +import { DatePickerWithRange } from '@/components/date-picker-with-range'; +import { DateRange } from 'react-day-picker'; const Details = ({ id, graphName }: { id: string; graphName: string }) => { const { data, isLoading, error, refetch } = useQuery( diff --git a/studio/src/pages/[organizationSlug]/graphs.tsx b/studio/src/pages/[organizationSlug]/graphs.tsx index d41502c76e..5e5b45bf49 100644 --- a/studio/src/pages/[organizationSlug]/graphs.tsx +++ b/studio/src/pages/[organizationSlug]/graphs.tsx @@ -1,13 +1,13 @@ -import { EmptyState } from "@/components/empty-state"; -import { getDashboardLayout } from "@/components/layout/dashboard-layout"; -import { Button } from "@/components/ui/button"; -import { Loader } from "@/components/ui/loader"; -import { NextPageWithLayout } from "@/lib/page"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useQuery } from "@tanstack/react-query"; -import { getFederatedGraphs } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; -import { FederatedGraphsCards } from "@/components/federatedgraphs-cards"; +import { EmptyState } from '@/components/empty-state'; +import { getDashboardLayout } from '@/components/layout/dashboard-layout'; +import { Button } from '@/components/ui/button'; +import { Loader } from '@/components/ui/loader'; +import { NextPageWithLayout } from '@/lib/page'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { useQuery } from '@tanstack/react-query'; +import { getFederatedGraphs } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; +import { FederatedGraphsCards } from '@/components/federatedgraphs-cards'; const GraphsDashboardPage: NextPageWithLayout = () => { const { data, isLoading, error, refetch } = useQuery( diff --git a/studio/src/pages/[organizationSlug]/members.tsx b/studio/src/pages/[organizationSlug]/members.tsx index 3026610721..c1d09de544 100644 --- a/studio/src/pages/[organizationSlug]/members.tsx +++ b/studio/src/pages/[organizationSlug]/members.tsx @@ -1,31 +1,31 @@ -import { UserContext } from "@/components/app-provider"; -import { EmptyState } from "@/components/empty-state"; -import { getDashboardLayout } from "@/components/layout/dashboard-layout"; -import { Button } from "@/components/ui/button"; +import { UserContext } from '@/components/app-provider'; +import { EmptyState } from '@/components/empty-state'; +import { getDashboardLayout } from '@/components/layout/dashboard-layout'; +import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Loader } from "@/components/ui/loader"; -import { useToast } from "@/components/ui/use-toast"; -import { SubmitHandler, useZodForm } from "@/hooks/use-form"; -import { NextPageWithLayout } from "@/lib/page"; -import { cn } from "@/lib/utils"; -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common_pb"; +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Loader } from '@/components/ui/loader'; +import { useToast } from '@/components/ui/use-toast'; +import { SubmitHandler, useZodForm } from '@/hooks/use-form'; +import { NextPageWithLayout } from '@/lib/page'; +import { cn } from '@/lib/utils'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common_pb'; import { getOrganizationMembers, inviteUser, removeInvitation, -} from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; -import { sentenceCase } from "change-case"; -import { useContext } from "react"; -import { HiOutlineDotsVertical } from "react-icons/hi"; -import { z } from "zod"; +} from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery'; +import { sentenceCase } from 'change-case'; +import { useContext } from 'react'; +import { HiOutlineDotsVertical } from 'react-icons/hi'; +import { z } from 'zod'; const emailInputSchema = z.object({ email: z.string().email(), diff --git a/studio/src/pages/login.tsx b/studio/src/pages/login.tsx index 41f52fd8f8..724b77d040 100644 --- a/studio/src/pages/login.tsx +++ b/studio/src/pages/login.tsx @@ -1,9 +1,9 @@ -import { AuthLayout } from "@/components/layout/auth-layout"; -import { Logo } from "@/components/logo"; -import { Button } from "@/components/ui/button"; -import { docsBaseURL } from "@/lib/constants"; -import { NextPageWithLayout } from "@/lib/page"; -import Link from "next/link"; +import { AuthLayout } from '@/components/layout/auth-layout'; +import { Logo } from '@/components/logo'; +import { Button } from '@/components/ui/button'; +import { docsBaseURL } from '@/lib/constants'; +import { NextPageWithLayout } from '@/lib/page'; +import Link from 'next/link'; const LoginPage: NextPageWithLayout = () => { return (