diff --git a/.circleci/config.yml b/.circleci/config.yml index f50d5698..e5a4f33d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -126,12 +126,12 @@ references: env_neo4j40ce: &env_neo4j40ce NEO4J_DIST: 'community' NEO4J_VERSION: '4.0.0' - APOC_VERSION: '4.0.0-rc01' + APOC_VERSION: '4.0.0.4' DATASTORE_VERSION: '4_0' env_neo4j40ee: &env_neo4j40ee NEO4J_DIST: 'enterprise' NEO4J_VERSION: '4.0.0' - APOC_VERSION: '4.0.0-rc01' + APOC_VERSION: '4.0.0.4' DATASTORE_VERSION: '4_0' install_neo4j_steps: &install_neo4j_steps diff --git a/example/apollo-server/movies-schema.js b/example/apollo-server/movies-schema.js index e35356b3..5c4a5814 100644 --- a/example/apollo-server/movies-schema.js +++ b/example/apollo-server/movies-schema.js @@ -121,6 +121,8 @@ type CameraMan implements Person { cameraBuddy: Person @relation(name: "cameraBuddy", direction: "OUT") } +union MovieSearch = Movie | Genre | Book | User | OldCamera + type Query { Movie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float): [Movie] MoviesByYear(year: Int, first: Int = 10, offset: Int = 0): [Movie] diff --git a/src/augment/input-values.js b/src/augment/input-values.js index cf33cbaa..5a0d2eb8 100644 --- a/src/augment/input-values.js +++ b/src/augment/input-values.js @@ -10,8 +10,7 @@ import { import { isNeo4jTemporalType, isNeo4jPointType, - Neo4jTypeName, - isQueryTypeDefinition + Neo4jTypeName } from './types/types'; import { isCypherField } from './directives'; import { diff --git a/src/augment/resolvers.js b/src/augment/resolvers.js index 1827bd2c..0152b04a 100644 --- a/src/augment/resolvers.js +++ b/src/augment/resolvers.js @@ -57,10 +57,12 @@ export const augmentResolvers = ( // must implement __resolveInfo for every Interface type // we use "FRAGMENT_TYPE" key to identify the Interface implementation // type at runtime, so grab this value - const interfaceTypes = Object.keys(augmentedTypeMap).filter( - e => augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' + const derivedTypes = Object.keys(augmentedTypeMap).filter( + e => + augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' || + augmentedTypeMap[e].kind === 'UnionTypeDefinition' ); - interfaceTypes.map(e => { + derivedTypes.map(e => { resolvers[e] = {}; resolvers[e]['__resolveType'] = (obj, context, info) => { diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js index 1c5220d6..1113d2fe 100644 --- a/src/augment/types/node/node.js +++ b/src/augment/types/node/node.js @@ -38,7 +38,8 @@ import { isRelationshipType, isObjectTypeDefinition, isOperationTypeDefinition, - isQueryTypeDefinition + isQueryTypeDefinition, + isUnionTypeDefinition } from '../../types/types'; import { getPrimaryKey } from '../../../utils'; @@ -56,7 +57,8 @@ export const augmentNodeType = ({ }) => { if ( isObjectTypeDefinition({ definition }) || - isInterfaceTypeDefinition({ definition }) + isInterfaceTypeDefinition({ definition }) || + isUnionTypeDefinition({ definition }) ) { let [ nodeInputTypeMap, @@ -75,7 +77,8 @@ export const augmentNodeType = ({ if (!isIgnoredType) { if ( !isOperationTypeDefinition({ definition, operationTypeMap }) && - !isInterfaceTypeDefinition({ definition }) + !isInterfaceTypeDefinition({ definition }) && + !isUnionTypeDefinition({ definition }) ) { [propertyOutputFields, nodeInputTypeMap] = buildNeo4jSystemIDField({ definition, @@ -121,115 +124,121 @@ export const augmentNodeTypeFields = ({ operationTypeMap, config }) => { - const fields = definition.fields; - let isIgnoredType = true; - const propertyInputValues = []; let nodeInputTypeMap = {}; - if ( - !isQueryTypeDefinition({ - definition, - operationTypeMap - }) - ) { - nodeInputTypeMap[FilteringArgument.FILTER] = { - name: `_${typeName}Filter`, - fields: [] - }; - nodeInputTypeMap[OrderingArgument.ORDER_BY] = { - name: `_${typeName}Ordering`, - values: [] - }; - } - let propertyOutputFields = fields.reduce((outputFields, field) => { - let fieldType = field.type; - let fieldArguments = field.arguments; - const fieldDirectives = field.directives; - if (!isIgnoredField({ directives: fieldDirectives })) { - isIgnoredType = false; - const fieldName = field.name.value; - const unwrappedType = unwrapNamedType({ type: fieldType }); - const outputType = unwrappedType.name; - const outputDefinition = typeDefinitionMap[outputType]; - const outputKind = outputDefinition ? outputDefinition.kind : ''; - const outputTypeWrappers = unwrappedType.wrappers; - const relationshipDirective = getDirective({ - directives: fieldDirectives, - name: DirectiveDefinition.RELATION - }); - if ( - isPropertyTypeField({ - kind: outputKind, - type: outputType - }) - ) { - nodeInputTypeMap = augmentInputTypePropertyFields({ - inputTypeMap: nodeInputTypeMap, - fieldName, - fieldDirectives, - outputType, - outputKind, - outputTypeWrappers - }); - propertyInputValues.push({ - name: fieldName, - type: unwrappedType, - directives: fieldDirectives - }); - } else if (isNodeType({ definition: outputDefinition })) { - [ - fieldArguments, - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap - ] = augmentNodeTypeField({ - typeName, - definition, - fieldArguments, - fieldDirectives, - fieldName, - outputType, - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap, - config, - relationshipDirective, - outputTypeWrappers - }); - } else if (isRelationshipType({ definition: outputDefinition })) { - [ - fieldType, - fieldArguments, - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap - ] = augmentRelationshipTypeField({ - typeName, - definition, - fieldType, - fieldArguments, - fieldDirectives, - fieldName, - outputTypeWrappers, - outputType, - outputDefinition, - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap, - operationTypeMap, - config + let propertyOutputFields = []; + const propertyInputValues = []; + let isIgnoredType = true; + if (!isUnionTypeDefinition({ definition })) { + const fields = definition.fields; + if ( + !isQueryTypeDefinition({ + definition, + operationTypeMap + }) + ) { + nodeInputTypeMap[FilteringArgument.FILTER] = { + name: `_${typeName}Filter`, + fields: [] + }; + nodeInputTypeMap[OrderingArgument.ORDER_BY] = { + name: `_${typeName}Ordering`, + values: [] + }; + } + propertyOutputFields = fields.reduce((outputFields, field) => { + let fieldType = field.type; + let fieldArguments = field.arguments; + const fieldDirectives = field.directives; + if (!isIgnoredField({ directives: fieldDirectives })) { + isIgnoredType = false; + const fieldName = field.name.value; + const unwrappedType = unwrapNamedType({ type: fieldType }); + const outputType = unwrappedType.name; + const outputDefinition = typeDefinitionMap[outputType]; + const outputKind = outputDefinition ? outputDefinition.kind : ''; + const outputTypeWrappers = unwrappedType.wrappers; + const relationshipDirective = getDirective({ + directives: fieldDirectives, + name: DirectiveDefinition.RELATION }); + if ( + isPropertyTypeField({ + kind: outputKind, + type: outputType + }) + ) { + nodeInputTypeMap = augmentInputTypePropertyFields({ + inputTypeMap: nodeInputTypeMap, + fieldName, + fieldDirectives, + outputType, + outputKind, + outputTypeWrappers + }); + propertyInputValues.push({ + name: fieldName, + type: unwrappedType, + directives: fieldDirectives + }); + } else if (isNodeType({ definition: outputDefinition })) { + [ + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentNodeTypeField({ + typeName, + definition, + outputDefinition, + fieldArguments, + fieldDirectives, + fieldName, + outputType, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config, + relationshipDirective, + outputTypeWrappers + }); + } else if (isRelationshipType({ definition: outputDefinition })) { + [ + fieldType, + fieldArguments, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentRelationshipTypeField({ + typeName, + definition, + fieldType, + fieldArguments, + fieldDirectives, + fieldName, + outputTypeWrappers, + outputType, + outputDefinition, + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } } - } - outputFields.push({ - ...field, - type: fieldType, - arguments: fieldArguments - }); - return outputFields; - }, []); + outputFields.push({ + ...field, + type: fieldType, + arguments: fieldArguments + }); + return outputFields; + }, []); + } else { + isIgnoredType = false; + } return [ nodeInputTypeMap, propertyOutputFields, @@ -245,6 +254,7 @@ export const augmentNodeTypeFields = ({ const augmentNodeTypeField = ({ typeName, definition, + outputDefinition, fieldArguments, fieldDirectives, fieldName, @@ -257,52 +267,55 @@ const augmentNodeTypeField = ({ relationshipDirective, outputTypeWrappers }) => { - fieldArguments = augmentNodeTypeFieldArguments({ - fieldArguments, - fieldDirectives, - outputType, - outputTypeWrappers, - typeDefinitionMap, - config - }); - if ( - relationshipDirective && - !isQueryTypeDefinition({ definition, operationTypeMap }) - ) { - nodeInputTypeMap = augmentNodeQueryArgumentTypes({ - typeName, - fieldName, + if (!isUnionTypeDefinition({ definition: outputDefinition })) { + fieldArguments = augmentNodeTypeFieldArguments({ + definition, + fieldArguments, + fieldDirectives, outputType, outputTypeWrappers, - nodeInputTypeMap, - config - }); - const relationshipName = getRelationName(relationshipDirective); - const relationshipDirection = getRelationDirection(relationshipDirective); - // Assume direction OUT - let fromType = typeName; - let toType = outputType; - if (relationshipDirection === 'IN') { - let temp = fromType; - fromType = outputType; - toType = temp; - } - [ - typeDefinitionMap, - generatedTypeMap, - operationTypeMap - ] = augmentRelationshipMutationAPI({ - typeName, - fieldName, - outputType, - fromType, - toType, - relationshipName, typeDefinitionMap, - generatedTypeMap, - operationTypeMap, config }); + if ( + relationshipDirective && + !isQueryTypeDefinition({ definition, operationTypeMap }) + ) { + nodeInputTypeMap = augmentNodeQueryArgumentTypes({ + typeName, + fieldName, + outputType, + outputTypeWrappers, + nodeInputTypeMap, + config + }); + const relationshipName = getRelationName(relationshipDirective); + const relationshipDirection = getRelationDirection(relationshipDirective); + // Assume direction OUT + let fromType = typeName; + let toType = outputType; + if (relationshipDirection === 'IN') { + let temp = fromType; + fromType = outputType; + toType = temp; + } + [ + typeDefinitionMap, + generatedTypeMap, + operationTypeMap + ] = augmentRelationshipMutationAPI({ + typeName, + fieldName, + outputType, + fromType, + toType, + relationshipName, + typeDefinitionMap, + generatedTypeMap, + operationTypeMap, + config + }); + } } return [ fieldArguments, @@ -328,15 +341,25 @@ const augmentNodeTypeAPI = ({ operationTypeMap, config }) => { - [operationTypeMap, generatedTypeMap] = augmentNodeMutationAPI({ - definition, - typeName, - propertyInputValues, - generatedTypeMap, - operationTypeMap, - config - }); + if (!isUnionTypeDefinition({ definition })) { + [operationTypeMap, generatedTypeMap] = augmentNodeMutationAPI({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + operationTypeMap, + config + }); + generatedTypeMap = buildNodeSelectionInputType({ + definition, + typeName, + propertyInputValues, + generatedTypeMap, + config + }); + } [operationTypeMap, generatedTypeMap] = augmentNodeQueryAPI({ + definition, typeName, propertyInputValues, nodeInputTypeMap, @@ -345,13 +368,6 @@ const augmentNodeTypeAPI = ({ operationTypeMap, config }); - generatedTypeMap = buildNodeSelectionInputType({ - definition, - typeName, - propertyInputValues, - generatedTypeMap, - config - }); return [ propertyOutputFields, typeDefinitionMap, diff --git a/src/augment/types/node/query.js b/src/augment/types/node/query.js index 850f07be..5a5a5de8 100644 --- a/src/augment/types/node/query.js +++ b/src/augment/types/node/query.js @@ -12,7 +12,7 @@ import { useAuthDirective } from '../../directives'; import { shouldAugmentType } from '../../augment'; -import { OperationType } from '../../types/types'; +import { OperationType, isUnionTypeDefinition } from '../../types/types'; import { TypeWrappers, getFieldDefinition } from '../../fields'; import { FilteringArgument, @@ -39,6 +39,7 @@ const NodeQueryArgument = { * generated input or output types required for translation */ export const augmentNodeQueryAPI = ({ + definition, typeName, propertyInputValues, nodeInputTypeMap, @@ -52,6 +53,7 @@ export const augmentNodeQueryAPI = ({ if (shouldAugmentType(config, queryTypeNameLower, typeName)) { if (queryType) { operationTypeMap = buildNodeQueryField({ + definition, typeName, queryType, propertyInputValues, @@ -60,17 +62,19 @@ export const augmentNodeQueryAPI = ({ config }); } - generatedTypeMap = buildQueryOrderingEnumType({ - nodeInputTypeMap, - typeDefinitionMap, - generatedTypeMap - }); - generatedTypeMap = buildQueryFilteringInputType({ - typeName: `_${typeName}Filter`, - typeDefinitionMap, - generatedTypeMap, - inputTypeMap: nodeInputTypeMap - }); + if (!isUnionTypeDefinition({ definition })) { + generatedTypeMap = buildQueryOrderingEnumType({ + nodeInputTypeMap, + typeDefinitionMap, + generatedTypeMap + }); + generatedTypeMap = buildQueryFilteringInputType({ + typeName: `_${typeName}Filter`, + typeDefinitionMap, + generatedTypeMap, + inputTypeMap: nodeInputTypeMap + }); + } } return [operationTypeMap, generatedTypeMap]; }; @@ -135,6 +139,7 @@ export const augmentNodeQueryArgumentTypes = ({ * a given node type */ const buildNodeQueryField = ({ + definition, typeName, queryType, propertyInputValues, @@ -159,6 +164,7 @@ const buildNodeQueryField = ({ } }), args: buildNodeQueryArguments({ + definition, typeName, propertyInputValues, typeDefinitionMap @@ -179,38 +185,41 @@ const buildNodeQueryField = ({ * arguments of the Query type field for a given node type */ const buildNodeQueryArguments = ({ + definition, typeName, propertyInputValues, typeDefinitionMap }) => { - // Do not persist type wrappers - propertyInputValues = propertyInputValues.map(arg => - buildInputValue({ - name: buildName({ name: arg.name }), - type: buildNamedType({ - name: arg.type.name - }) - }) - ); - if (!propertyInputValues.some(field => field.name.value === '_id')) { - propertyInputValues.push( + if (!isUnionTypeDefinition({ definition })) { + // Do not persist type wrappers + propertyInputValues = propertyInputValues.map(arg => buildInputValue({ - name: buildName({ name: '_id' }), + name: buildName({ name: arg.name }), type: buildNamedType({ - name: GraphQLString.name + name: arg.type.name }) }) ); + if (!propertyInputValues.some(field => field.name.value === '_id')) { + propertyInputValues.push( + buildInputValue({ + name: buildName({ name: '_id' }), + type: buildNamedType({ + name: GraphQLString.name + }) + }) + ); + } + propertyInputValues = buildQueryFieldArguments({ + argumentMap: NodeQueryArgument, + fieldArguments: propertyInputValues, + outputType: typeName, + outputTypeWrappers: { + [TypeWrappers.LIST_TYPE]: true + }, + typeDefinitionMap + }); } - propertyInputValues = buildQueryFieldArguments({ - argumentMap: NodeQueryArgument, - fieldArguments: propertyInputValues, - outputType: typeName, - outputTypeWrappers: { - [TypeWrappers.LIST_TYPE]: true - }, - typeDefinitionMap - }); return propertyInputValues; }; diff --git a/src/augment/types/types.js b/src/augment/types/types.js index d35704c4..e6b9be95 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -64,7 +64,7 @@ export const Neo4jTypeName = `_Neo4j`; * An enum describing the names of fields computed and added to the input * and output type definitions representing non-scalar Neo4j property types */ -const Neo4jTypeFormatted = { +export const Neo4jTypeFormatted = { FORMATTED: 'formatted' }; @@ -87,9 +87,11 @@ export const Neo4jDataType = { [TemporalType.LOCALDATETIME]: 'Temporal', [SpatialType.POINT]: 'Spatial' }, + // TODO probably revise and remove... STRUCTURAL: { [Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType, - [Kind.INTERFACE_TYPE_DEFINITION]: Neo4jStructuralType + [Kind.INTERFACE_TYPE_DEFINITION]: Neo4jStructuralType, + [Kind.UNION_TYPE_DEFINITION]: Neo4jStructuralType } }; diff --git a/src/selections.js b/src/selections.js index 03ed7c54..7c773db9 100644 --- a/src/selections.js +++ b/src/selections.js @@ -16,7 +16,7 @@ import { isNeo4jTypeField, getNeo4jTypeArguments, removeIgnoredFields, - getDerivedTypeNames + getInterfaceDerivedTypeNames } from './utils'; import { customCypherField, @@ -25,7 +25,8 @@ import { nodeTypeFieldOnRelationType, neo4jType, neo4jTypeField, - derivedTypesParams + derivedTypesParams, + fragmentType } from './translate'; import { Kind } from 'graphql'; import { @@ -51,7 +52,15 @@ export function buildCypherSelection({ secondParentSelectionInfo = {} }) { if (!selections.length) return [initial, {}]; - selections = removeIgnoredFields(schemaType, selections); + const typeMap = resolveInfo.schema.getTypeMap(); + const schemaTypeName = schemaType.name; + const schemaTypeAstNode = typeMap[schemaTypeName].astNode; + const isUnionType = isUnionTypeDefinition({ + definition: schemaTypeAstNode + }); + if (!isUnionType) { + selections = removeIgnoredFields(schemaType, selections); + } let selectionFilters = filtersFromSelections( selections, resolveInfo.variableValues @@ -94,16 +103,12 @@ export function buildCypherSelection({ const [headSelection, ...tailSelections] = selections; const fieldName = headSelection && headSelection.name ? headSelection.name.value : ''; - const typeMap = resolveInfo.schema.getTypeMap(); - const schemaTypeName = schemaType.name; - const schemaTypeAstNode = typeMap[schemaType].astNode; const safeVariableName = safeVar(variableName); const usesFragments = isFragmentedSelection({ selections }); const isScalarType = isGraphqlScalarType(schemaType); - const schemaTypeField = !isScalarType - ? schemaType.getFields()[fieldName] - : {}; + const schemaTypeField = + !isScalarType && !isUnionType ? schemaType.getFields()[fieldName] : {}; const isInterfaceType = isInterfaceTypeDefinition({ definition: schemaTypeAstNode @@ -111,9 +116,6 @@ export function buildCypherSelection({ const isObjectType = isObjectTypeDefinition({ definition: schemaTypeAstNode }); - const isUnionType = isUnionTypeDefinition({ - definition: schemaTypeAstNode - }); const isFragmentedInterfaceType = usesFragments && isInterfaceType; const isFragmentedObjectType = usesFragments && isObjectType; const { statement: customCypherStatement } = cypherDirective( @@ -136,41 +138,46 @@ export function buildCypherSelection({ let translationConfig = undefined; if (isFragmentedInterfaceType || isUnionType || isFragmentedObjectType) { - const [schemaTypeFields, composedTypeMap] = mergeSelectionFragments({ + const [schemaTypeFields, derivedTypeMap] = mergeSelectionFragments({ schemaType, selections, isFragmentedObjectType, + isUnionType, + typeMap, resolveInfo }); const hasOnlySchemaTypeFragments = - schemaTypeFields.length > 0 && Object.keys(composedTypeMap).length === 0; + schemaTypeFields.length > 0 && Object.keys(derivedTypeMap).length === 0; if (hasOnlySchemaTypeFragments || isFragmentedObjectType) { tailParams.selections = schemaTypeFields; translationConfig = tailParams; } else if (isFragmentedInterfaceType || isUnionType) { - const implementingTypes = getComposedTypes({ + const derivedTypes = getDerivedTypes({ schemaTypeName, - schemaTypeAstNode, + derivedTypeMap, isFragmentedInterfaceType, isUnionType, - isFragmentedObjectType, resolveInfo }); // TODO Make this a new function once recurse is moved out of buildCypherSelection // so that we don't have to start passing recurse as an argument - const [fragmentedQuery, queryParams] = implementingTypes.reduce( - ([listComprehensions, params], implementingType) => { + const [fragmentedQuery, queryParams] = derivedTypes.reduce( + ([listComprehensions, params], derivedType) => { // Get merged selections of this implementing type - let mergedTypeSelections = composedTypeMap[implementingType]; - if (!mergedTypeSelections) { + if (!derivedTypeMap[derivedType]) { // If no fields of this implementing type were selected, // use at least any interface fields selected generally - mergedTypeSelections = schemaTypeFields; + derivedTypeMap[derivedType] = schemaTypeFields; } + const mergedTypeSelections = derivedTypeMap[derivedType]; if (mergedTypeSelections.length) { + const composedTypeDefinition = typeMap[derivedType].astNode; + const isInterfaceTypeFragment = isInterfaceTypeDefinition({ + definition: composedTypeDefinition + }); // If selections have been made for this type after merging if (isFragmentedInterfaceType || isUnionType) { - schemaType = resolveInfo.schema.getType(implementingType); + schemaType = resolveInfo.schema.getType(derivedType); } // TODO Refactor when recurse is moved out buildCypherSelection // Build the map projection for this implementing type @@ -183,10 +190,18 @@ export function buildCypherSelection({ if (isFragmentedInterfaceType || isUnionType) { // Build a more complex list comprehension for // this type, to be aggregated together later - fragmentedQuery = buildComposedTypeListComprehension({ - implementingType, + [ + fragmentedQuery, + queryParams + ] = buildComposedTypeListComprehension({ + derivedType, + isUnionType, + mergedTypeSelections, + queryParams, safeVariableName, - fragmentedQuery + isInterfaceTypeFragment, + fragmentedQuery, + resolveInfo }); } listComprehensions.push(fragmentedQuery); @@ -229,6 +244,9 @@ export function buildCypherSelection({ const isInterfaceTypeField = isInterfaceTypeDefinition({ definition: innerSchemaTypeAstNode }); + const isUnionTypeField = isUnionTypeDefinition({ + definition: innerSchemaTypeAstNode + }); if (isIntrospectionField) { // Schema meta fields(__schema, __typename, etc) translationConfig = { @@ -307,21 +325,26 @@ export function buildCypherSelection({ schemaType, fieldName ); + const isRelationshipField = relType && relDirection; + const isRelationshipTypeField = innerSchemaTypeRelation !== undefined; const usesFragments = isFragmentedSelection({ selections: fieldSelectionSet }); const isFragmentedObjectTypeField = isObjectTypeField && usesFragments; - const [schemaTypeFields, composedTypeMap] = mergeSelectionFragments({ + const [schemaTypeFields, derivedTypeMap] = mergeSelectionFragments({ schemaType: innerSchemaType, selections: fieldSelectionSet, isFragmentedObjectType: isFragmentedObjectTypeField, + isUnionType: isUnionTypeField, + typeMap, resolveInfo }); const fragmentTypeParams = derivedTypesParams({ isInterfaceType: isInterfaceTypeField, + isUnionType: isUnionTypeField, schema: resolveInfo.schema, - interfaceName: innerSchemaType.name, + schemaTypeName: innerSchemaType.name, usesFragments }); subSelection[1] = { ...subSelection[1], ...fragmentTypeParams }; @@ -333,9 +356,11 @@ export function buildCypherSelection({ paramIndex, schemaTypeRelation, isInterfaceTypeField, + isUnionTypeField, + isObjectTypeField, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, initial, fieldName, fieldType, @@ -365,7 +390,7 @@ export function buildCypherSelection({ schemaTypeRelation, parentSelectionInfo }); - } else if (relType && relDirection) { + } else if (isRelationshipField || isUnionTypeField) { // Object type field with relation directive [translationConfig, subSelection] = relationFieldOnNodeType({ initial, @@ -376,8 +401,10 @@ export function buildCypherSelection({ relType, nestedVariable, schemaTypeFields, - composedTypeMap, + derivedTypeMap, isInterfaceTypeField, + isUnionTypeField, + isObjectTypeField, usesFragments, innerSchemaType, paramIndex, @@ -413,8 +440,10 @@ export function buildCypherSelection({ schemaTypeRelation, innerSchemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, usesFragments, paramIndex, parentSelectionInfo, @@ -423,7 +452,7 @@ export function buildCypherSelection({ fieldArgs, cypherParams }); - } else if (innerSchemaTypeRelation) { + } else if (isRelationshipTypeField) { // Relation type field on node type (field payload types...) // and set subSelection to update field argument params [translationConfig, subSelection] = relationTypeFieldOnNodeType({ @@ -519,137 +548,376 @@ export const mergeSelectionFragments = ({ schemaType, selections, isFragmentedObjectType, + isUnionType, + typeMap, resolveInfo }) => { - let schemaTypeFields = []; - const composedTypeMap = {}; const schemaTypeName = schemaType.name; const fragmentDefinitions = resolveInfo.fragments; + let [schemaTypeFields, derivedTypeMap] = buildFragmentMaps({ + selections, + schemaTypeName, + isFragmentedObjectType, + fragmentDefinitions, + isUnionType, + typeMap, + resolveInfo + }); + // When querying an interface type using fragments, queries are made + // more specific if there is not at least 1 interface field selected. + // So the __typename field is removed here to prevent interpreting it + // as a field for which a value could be obtained from matched data. + // Otherwisez all interface type nodes would always be returned even + // when only using fragments to select fields on implementing types + const typeNameFieldIndex = schemaTypeFields.findIndex( + field => field.name && field.name.value === '__typename' + ); + if (typeNameFieldIndex !== -1) schemaTypeFields.splice(typeNameFieldIndex, 1); + return [schemaTypeFields, derivedTypeMap]; +}; + +const buildFragmentMaps = ({ + selections = [], + schemaTypeName, + isFragmentedObjectType, + fragmentDefinitions, + isUnionType, + typeMap = {}, + resolveInfo +}) => { + let schemaTypeFields = []; + let interfaceFragmentMap = {}; + let objectSelectionMap = {}; + let objectFragmentMap = {}; selections.forEach(selection => { - let fieldKind = selection.kind; + const fieldKind = selection.kind; if (fieldKind === Kind.FIELD) { schemaTypeFields.push(selection); - } else if ( - fieldKind === Kind.INLINE_FRAGMENT || - fieldKind === Kind.FRAGMENT_SPREAD - ) { - let fragmentSelections = []; - let typeCondition = ''; - if (fieldKind === Kind.FRAGMENT_SPREAD) { - const fragmentDefinition = fragmentDefinitions[selection.name.value]; - fragmentSelections = fragmentDefinition.selectionSet.selections; - typeCondition = fragmentDefinition.typeCondition; - } else { - fragmentSelections = selection.selectionSet.selections; - typeCondition = selection.typeCondition; - } - const typeName = typeCondition ? typeCondition.name.value : ''; - if (typeName) { - // For fragments on the same type containing the fragment or - // for inline fragments without type conditions + } else if (isSelectionFragment({ kind: fieldKind })) { + [ + schemaTypeFields, + interfaceFragmentMap, + objectSelectionMap, + objectFragmentMap + ] = aggregateFragmentedSelections({ + schemaTypeName, + selection, + fieldKind, + isUnionType, + schemaTypeFields, + objectFragmentMap, + interfaceFragmentMap, + objectSelectionMap, + fragmentDefinitions, + typeMap + }); + } + }); + // move into any interface type fragment, any fragments on object types implmenting it + const derivedTypeMap = mergeInterfacedObjectFragments({ + schemaTypeName, + schemaTypeFields, + isFragmentedObjectType, + objectSelectionMap, + objectFragmentMap, + interfaceFragmentMap, + resolveInfo + }); + // deduplicate relationship fields within fragments on the same type + Object.keys(derivedTypeMap).forEach(typeName => { + const allSelections = [...derivedTypeMap[typeName], ...schemaTypeFields]; + derivedTypeMap[typeName] = mergeFragmentedSelections({ + selections: allSelections + }); + }); + schemaTypeFields = mergeFragmentedSelections({ + selections: schemaTypeFields + }); + return [schemaTypeFields, derivedTypeMap]; +}; + +const aggregateFragmentedSelections = ({ + schemaTypeName, + selection, + fieldKind, + isUnionType, + schemaTypeFields, + objectFragmentMap, + interfaceFragmentMap, + objectSelectionMap, + fragmentDefinitions, + typeMap +}) => { + const typeName = getFragmentTypeName({ + selection, + kind: fieldKind, + fragmentDefinitions + }); + const fragmentSelections = getFragmentSelections({ + selection, + kind: fieldKind, + fragmentDefinitions + }); + if (typeName) { + if (fragmentSelections && fragmentSelections.length) { + const definition = typeMap[typeName] ? typeMap[typeName].astNode : {}; + if (isObjectTypeDefinition({ definition })) { if (typeName === schemaTypeName) { + // fragmented selections on the same type for which this is + // a selection set are aggregated into schemaTypeFields schemaTypeFields.push(...fragmentSelections); } else { - const typeSelections = composedTypeMap[typeName]; - // Initialize selection set array for this type - if (!typeSelections) { - composedTypeMap[typeName] = fragmentSelections; - } else { - // for aggregation of multiple fragments on the same type - composedTypeMap[typeName].push(...fragmentSelections); - } + if (!objectSelectionMap[typeName]) objectSelectionMap[typeName] = []; + // prepare an aggregation of fragmented selections used on object type + // if querying a union type, fragments on object types are merged with + // any interface type fragment implemented by them + objectSelectionMap[typeName].push(selection); + // initializes an array for the below progressive aggregation + if (!objectFragmentMap[typeName]) objectFragmentMap[typeName] = []; + // aggregates together all fragmented selections on this object type + objectFragmentMap[typeName].push(...fragmentSelections); + } + } else if (isInterfaceTypeDefinition({ definition })) { + if (typeName === schemaTypeName) { + // aggregates together all fragmented selections on this interface type + // to be multiplied over and deduplicated with any fragments on object + // types implementing the interface, within its selection set + schemaTypeFields.push(...fragmentSelections); + } else if (isUnionType) { + // only for interface fragments on union types, initializes an array + // for the below progressive aggregation + if (!interfaceFragmentMap[typeName]) + interfaceFragmentMap[typeName] = []; + // aggregates together all fragmented selections on this object type + interfaceFragmentMap[typeName].push(...fragmentSelections); } - } else { - // For inline untyped fragments on the same type, ex: ...{ title } - schemaTypeFields.push(...fragmentSelections); } } - }); - if (isFragmentedObjectType) { - // Composed object queries still only use a single map projection - composedTypeMap[schemaTypeName] = schemaTypeFields; + } else { + // For inline untyped fragments on the same type, ex: ...{ title } + schemaTypeFields.push(...fragmentSelections); } - Object.keys(composedTypeMap).forEach(typeName => { - composedTypeMap[typeName] = mergeFragmentedSelections({ - selections: [...composedTypeMap[typeName], ...schemaTypeFields] + return [ + schemaTypeFields, + interfaceFragmentMap, + objectSelectionMap, + objectFragmentMap + ]; +}; + +const mergeInterfacedObjectFragments = ({ + schemaTypeName, + schemaTypeFields, + isFragmentedObjectType, + objectSelectionMap, + objectFragmentMap, + interfaceFragmentMap, + resolveInfo +}) => { + Object.keys(interfaceFragmentMap).forEach(interfaceName => { + const derivedTypes = getInterfaceDerivedTypeNames( + resolveInfo.schema, + interfaceName + ); + derivedTypes.forEach(typeName => { + const implementingTypeFragments = objectSelectionMap[typeName]; + if (implementingTypeFragments) { + // aggregate into the selections in this aggregated interface type fragment, + // the aggregated selections of fragments on object types implmementing it + interfaceFragmentMap[interfaceName].push(...implementingTypeFragments); + // given the above aggregation into the interface type selections, + // remove the fragment on this implementing type that existed + // within the same selection set + delete objectFragmentMap[typeName]; + } }); + return interfaceFragmentMap; }); - schemaTypeFields = mergeFragmentedSelections({ - selections: schemaTypeFields - }); - // When querying an interface type using fragments, queries are made - // more specific if there is not at least 1 interface field selected. - // So the __typename field is removed here to prevent interpreting it - // as a field for which a value could be obtained from matched data. - // Otherwisez all interface type nodes would always be returned even - // when only using fragments to select fields on implementing types - const typeNameFieldIndex = schemaTypeFields.findIndex( - field => field.name && field.name.value === '__typename' - ); - if (typeNameFieldIndex !== -1) schemaTypeFields.splice(typeNameFieldIndex, 1); - return [schemaTypeFields, composedTypeMap]; + const derivedTypeMap = { ...objectFragmentMap, ...interfaceFragmentMap }; + if (isFragmentedObjectType) { + derivedTypeMap[schemaTypeName] = schemaTypeFields; + } + return derivedTypeMap; }; const mergeFragmentedSelections = ({ selections = [] }) => { - const mergedSelections = selections.reduce((merged, selection) => { - const fieldName = selection.name.value; - if (!merged[fieldName]) { - // initialize entry for this composing type - merged[fieldName] = selection; + const subSelecionFieldMap = {}; + const fragments = []; + selections.forEach(selection => { + const fieldKind = selection.kind; + if (fieldKind === Kind.FIELD) { + const fieldName = selection.name.value; + if (!subSelecionFieldMap[fieldName]) { + // initialize entry for this composing type + subSelecionFieldMap[fieldName] = selection; + } else { + const alreadySelected = subSelecionFieldMap[fieldName].selectionSet + ? subSelecionFieldMap[fieldName].selectionSet.selections + : []; + const selected = selection.selectionSet + ? selection.selectionSet.selections + : []; + // If the field has a subselection (relationship field) + if (alreadySelected.length && selected.length) { + const selections = [...alreadySelected, ...selected]; + subSelecionFieldMap[ + fieldName + ].selectionSet.selections = mergeFragmentedSelections({ + selections + }); + } + } } else { - // FIXME Deeply merge selection sets of fragments on the same type + // Persist all fragments, to be merged later + fragments.push(selection); } - return merged; - }, {}); - return Object.values(mergedSelections); + }); + // Return the aggregation of all fragments and merged relationship fields + return [...Object.values(subSelecionFieldMap), ...fragments]; }; -export const getComposedTypes = ({ +export const getDerivedTypes = ({ schemaTypeName, - schemaTypeAstNode, + derivedTypeMap, isFragmentedInterfaceType, isUnionType, - isFragmentedObjectType, resolveInfo }) => { - let implementingTypes = []; + let derivedTypes = []; if (isFragmentedInterfaceType) { // Get an array of all types implementing this interface type - implementingTypes = getDerivedTypeNames(resolveInfo.schema, schemaTypeName); + derivedTypes = getInterfaceDerivedTypeNames( + resolveInfo.schema, + schemaTypeName + ); } else if (isUnionType) { - implementingTypes = schemaTypeAstNode.types.reduce((types, type) => { - types.push(type.name.value); - return types; - }, []); - } else if (isFragmentedObjectType) { - implementingTypes.push(schemaTypeName); + derivedTypes = Object.keys(derivedTypeMap).sort(); } - return implementingTypes; + return derivedTypes; +}; + +export const getUnionDerivedTypes = ({ derivedTypeMap = {}, resolveInfo }) => { + const typeMap = resolveInfo.schema.getTypeMap(); + const fragmentDefinitions = resolveInfo.fragments; + const uniqueFragmentTypeMap = Object.entries(derivedTypeMap).reduce( + (uniqueFragmentTypeMap, [typeName, selections]) => { + const definition = typeMap[typeName].astNode; + if (isObjectTypeDefinition({ definition })) { + uniqueFragmentTypeMap[typeName] = true; + } else if (isInterfaceTypeDefinition({ definition })) { + if (hasFieldSelection({ selections })) { + // then use the interface name in the label predicate, + // as this is a case of a dynamic FRAGMENT_TYPE + uniqueFragmentTypeMap[typeName] = true; + } else if (isFragmentedSelection({ selections })) { + selections.forEach(selection => { + const kind = selection.kind; + if (isSelectionFragment({ kind })) { + const derivedTypeName = getFragmentTypeName({ + selection, + kind, + fragmentDefinitions + }); + if (derivedTypeName) { + uniqueFragmentTypeMap[derivedTypeName] = true; + } + } + }); + } + } + return uniqueFragmentTypeMap; + }, + {} + ); + const typeNames = Object.keys(uniqueFragmentTypeMap); + return typeNames.sort(); +}; + +const hasFieldSelection = ({ selections = [] }) => { + return selections.some(selection => { + const kind = selection.kind; + const name = selection.name ? selection.name.value : ''; + const isFieldSelection = + kind === Kind.FIELD || + (kind === Kind.INLINE_FRAGMENT && !selection.typeCondition); + return isFieldSelection && name !== '__typename'; + }); }; export const isFragmentedSelection = ({ selections }) => { - return selections.find( - selection => - selection.kind === Kind.INLINE_FRAGMENT || - selection.kind === Kind.FRAGMENT_SPREAD + return selections.find(selection => + isSelectionFragment({ kind: selection.kind }) ); }; +const isSelectionFragment = ({ kind = '' }) => + kind === Kind.INLINE_FRAGMENT || kind === Kind.FRAGMENT_SPREAD; + +const getFragmentTypeName = ({ selection, kind, fragmentDefinitions }) => { + let typeCondition = {}; + if (kind === Kind.FRAGMENT_SPREAD) { + const fragmentDefinition = fragmentDefinitions[selection.name.value]; + typeCondition = fragmentDefinition.typeCondition; + } else typeCondition = selection.typeCondition; + return typeCondition && typeCondition.name ? typeCondition.name.value : ''; +}; + +const getFragmentSelections = ({ selection, kind, fragmentDefinitions }) => { + let fragmentSelections = []; + if (kind === Kind.FRAGMENT_SPREAD) { + const fragmentDefinition = fragmentDefinitions[selection.name.value]; + fragmentSelections = fragmentDefinition.selectionSet.selections; + } else { + fragmentSelections = selection.selectionSet.selections; + } + return fragmentSelections; +}; + const buildComposedTypeListComprehension = ({ - implementingType, + derivedType, + isUnionType, safeVariableName, - fragmentedQuery + mergedTypeSelections, + queryParams, + isInterfaceTypeFragment, + fragmentedQuery = '', + resolveInfo }) => { - const fragmentTypeField = `FRAGMENT_TYPE: "${implementingType}"`; - const typeMapProjection = `${safeVariableName} { ${fragmentTypeField}${ - // When __typename is the only field selected not within a fragment, - // fragmentedQuery is undefined, so that we only provide the FRAGMENT_TYPE - fragmentedQuery ? `, ${fragmentedQuery}` : '' - } }`; - const typeListComprehension = `${safeVariableName} IN [${safeVariableName}] WHERE [label IN labels(${safeVariableName}) WHERE label = "${implementingType}" | TRUE]`; - return `[${typeListComprehension} | ${typeMapProjection}]`; + const staticFragmentTypeField = `FRAGMENT_TYPE: "${derivedType}"`; + let typeMapProjection = `${safeVariableName} { ${[ + staticFragmentTypeField, + fragmentedQuery + ].join(', ')} }`; + // For fragments on interface types implemented by unioned object types + if (isUnionType && isInterfaceTypeFragment) { + const usesFragments = isFragmentedSelection({ + selections: mergedTypeSelections + }); + if (usesFragments) { + typeMapProjection = fragmentedQuery; + } else { + const dynamicFragmentTypeField = fragmentType( + safeVariableName, + derivedType + ); + typeMapProjection = `${safeVariableName} { ${[ + dynamicFragmentTypeField, + fragmentedQuery + ].join(', ')} }`; + // set param for dynamic fragment field + const fragmentTypeParams = derivedTypesParams({ + isInterfaceType: isInterfaceTypeFragment, + schema: resolveInfo.schema, + schemaTypeName: derivedType + }); + queryParams = { ...queryParams, ...fragmentTypeParams }; + } + } + const labelFilteringPredicate = `WHERE "${derivedType}" IN labels(${safeVariableName})`; + const typeSpecificListComprehension = `[${safeVariableName} IN [${safeVariableName}] ${labelFilteringPredicate} | ${typeMapProjection}]`; + return [typeSpecificListComprehension, queryParams]; }; // See: https://neo4j.com/docs/cypher-manual/current/syntax/operators/#syntax-concatenating-two-lists const concatenateComposedTypeLists = ({ fragmentedQuery }) => - `head(${fragmentedQuery.join(` + `)})`; + fragmentedQuery.length ? `head(${fragmentedQuery.join(` + `)})` : ''; diff --git a/src/translate.js b/src/translate.js index 4d981bc3..a807ac24 100644 --- a/src/translate.js +++ b/src/translate.js @@ -39,12 +39,13 @@ import { isSpatialDistanceInputType, isGraphqlScalarType, isGraphqlInterfaceType, + isGraphqlUnionType, innerType, relationDirective, typeIdentifiers, decideNeo4jTypeConstructor, getAdditionalLabels, - getDerivedTypeNames, + getInterfaceDerivedTypeNames, getPayloadSelections, isGraphqlObjectType } from './utils'; @@ -60,33 +61,43 @@ import { import { buildCypherSelection, isFragmentedSelection, - getComposedTypes, + getDerivedTypes, + getUnionDerivedTypes, mergeSelectionFragments } from './selections'; import _ from 'lodash'; import neo4j from 'neo4j-driver'; +import { isUnionTypeDefinition } from './augment/types/types'; -const derivedTypesParamName = interfaceName => `${interfaceName}_derivedTypes`; +const derivedTypesParamName = schemaTypeName => + `${schemaTypeName}_derivedTypes`; -export const fragmentType = (varName, interfaceName) => +export const fragmentType = (varName, schemaTypeName) => `FRAGMENT_TYPE: head( [ label IN labels(${varName}) WHERE label IN $${derivedTypesParamName( - interfaceName + schemaTypeName )} ] )`; export const derivedTypesParams = ({ isInterfaceType, + isUnionType, schema, - interfaceName, + schemaTypeName, usesFragments }) => { - const res = {}; - if (isInterfaceType && !usesFragments) { - res[derivedTypesParamName(interfaceName)] = getDerivedTypeNames( - schema, - interfaceName - ); + const params = {}; + if (!usesFragments) { + if (isInterfaceType) { + const paramName = derivedTypesParamName(schemaTypeName); + params[paramName] = getInterfaceDerivedTypeNames(schema, schemaTypeName); + } else if (isUnionType) { + const paramName = derivedTypesParamName(schemaTypeName); + const typeMap = schema.getTypeMap(); + const schemaType = typeMap[schemaTypeName]; + const types = schemaType.getTypes(); + params[paramName] = types.map(type => type.name); + } } - return res; + return params; }; export const customCypherField = ({ @@ -94,10 +105,12 @@ export const customCypherField = ({ cypherParams, paramIndex, schemaTypeRelation, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, initial, fieldName, fieldType, @@ -115,12 +128,14 @@ export const customCypherField = ({ const [mapProjection, labelPredicate] = buildMapProjection({ isComputedField: true, schemaType: innerSchemaType, + isObjectType: isObjectTypeField, isInterfaceType: isInterfaceTypeField, + isUnionType: isUnionTypeField, usesFragments, safeVariableName: nestedVariable, subQuery: subSelection[0], schemaTypeFields, - composedTypeMap, + derivedTypeMap, resolveInfo }); const headListWrapperPrefix = `${!isArrayType(fieldType) ? 'head(' : ''}`; @@ -159,8 +174,10 @@ export const relationFieldOnNodeType = ({ relType, nestedVariable, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, usesFragments, innerSchemaType, paramIndex, @@ -216,14 +233,18 @@ export const relationFieldOnNodeType = ({ }_${key}`; }); + const subQuery = subSelection[0]; + const [mapProjection, labelPredicate] = buildMapProjection({ schemaType: innerSchemaType, + isObjectType: isObjectTypeField, isInterfaceType: isInterfaceTypeField, + isUnionType: isUnionTypeField, usesFragments, safeVariableName, - subQuery: subSelection[0], + subQuery, schemaTypeFields, - composedTypeMap, + derivedTypeMap, resolveInfo }); @@ -250,9 +271,13 @@ export const relationFieldOnNodeType = ({ : `apoc.coll.sortMulti(` : '' }[(${safeVar(variableName)})${ - relDirection === 'in' || relDirection === 'IN' ? '<' : '' - }-[:${safeLabel([relType])}]-${ - relDirection === 'out' || relDirection === 'OUT' ? '>' : '' + isUnionTypeField + ? `--` + : `${ + relDirection === 'in' || relDirection === 'IN' ? '<' : '' + }-[:${safeLabel([relType])}]-${ + relDirection === 'out' || relDirection === 'OUT' ? '>' : '' + }` }(${safeVariableName}${`:${safeLabel([ innerSchemaType.name, ...getAdditionalLabels( @@ -379,8 +404,10 @@ export const nodeTypeFieldOnRelationType = ({ schemaTypeRelation, innerSchemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, usesFragments, paramIndex, parentSelectionInfo, @@ -399,12 +426,14 @@ export const nodeTypeFieldOnRelationType = ({ ) { const [mapProjection, labelPredicate] = buildMapProjection({ schemaType: innerSchemaType, + isObjectType: isObjectTypeField, isInterfaceType: isInterfaceTypeField, + isUnionType: isUnionTypeField, usesFragments, safeVariableName, subQuery: subSelection[0], schemaTypeFields, - composedTypeMap, + derivedTypeMap, resolveInfo }); const translationParams = relationTypeMutationPayloadField({ @@ -729,9 +758,10 @@ export const translateQuery = ({ const { typeName, variableName } = typeIdentifiers(resolveInfo.returnType); const schemaType = resolveInfo.schema.getType(typeName); - + const typeMap = resolveInfo.schema.getTypeMap(); const selections = getPayloadSelections(resolveInfo); const isInterfaceType = isGraphqlInterfaceType(schemaType); + const isUnionType = isGraphqlUnionType(schemaType); const isObjectType = isGraphqlObjectType(schemaType); const [nullParams, nonNullParams] = filterNullParams({ @@ -766,15 +796,16 @@ export const translateQuery = ({ let usesFragments = isFragmentedSelection({ selections }); const isFragmentedInterfaceType = isInterfaceType && usesFragments; const isFragmentedObjectType = isObjectType && usesFragments; - - const [schemaTypeFields, composedTypeMap] = mergeSelectionFragments({ + const [schemaTypeFields, derivedTypeMap] = mergeSelectionFragments({ schemaType, selections, isFragmentedObjectType, + isUnionType, + typeMap, resolveInfo }); const hasOnlySchemaTypeFragments = - schemaTypeFields.length > 0 && Object.keys(composedTypeMap).length === 0; + schemaTypeFields.length > 0 && Object.keys(derivedTypeMap).length === 0; // TODO refactor if (hasOnlySchemaTypeFragments) usesFragments = false; if (queryTypeCypherDirective) { @@ -786,11 +817,13 @@ export const translateQuery = ({ selections, variableName, safeVariableName, + isObjectType, isInterfaceType, + isUnionType, isFragmentedInterfaceType, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, orderByValue, outerSkipLimit, queryTypeCypherDirective, @@ -806,12 +839,14 @@ export const translateQuery = ({ selections, variableName, typeName, + isObjectType, isInterfaceType, + isUnionType, isFragmentedInterfaceType, isFragmentedObjectType, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, additionalLabels, neo4jTypeClauses, orderByValue, @@ -826,43 +861,63 @@ export const translateQuery = ({ }; const buildTypeCompositionPredicate = ({ - schemaTypeFields, - composedTypeMap, schemaType, + schemaTypeFields, + listVariable = 'x', + derivedTypeMap, safeVariableName, isInterfaceType, + isUnionType, + isComputedQuery, + isComputedMutation, + isComputedField, usesFragments, resolveInfo }) => { const schemaTypeName = schemaType.name; - const schemaTypeAstNode = schemaType.astNode; const isFragmentedInterfaceType = isInterfaceType && usesFragments; let labelPredicate = ''; - if (isFragmentedInterfaceType) { - let composedTypes = []; + if (isFragmentedInterfaceType || isUnionType) { + let derivedTypes = []; // If shared fields are selected then the translation builds // a type specific list comprehension for each interface implementing // type. Because of this, the type selecting predicate applied to // the interface type path pattern should allow for all possible // implementing types if (schemaTypeFields.length) { - composedTypes = getComposedTypes({ + derivedTypes = getDerivedTypes({ schemaTypeName, - schemaTypeAstNode, + derivedTypeMap, + isUnionType, isFragmentedInterfaceType, resolveInfo }); + } else if (isUnionType) { + derivedTypes = getUnionDerivedTypes({ + derivedTypeMap, + resolveInfo + }); } else { // Otherwise, use only those types provided in fragments - composedTypes = Object.keys(composedTypeMap); + derivedTypes = Object.keys(derivedTypeMap); } - const typeSelectionPredicates = composedTypes.map(selectedType => { + // TODO refactor above branch now that more specific branching was needed + const typeSelectionPredicates = derivedTypes.map(selectedType => { return `"${selectedType}" IN labels(${safeVariableName})`; }); if (typeSelectionPredicates.length) { labelPredicate = `(${typeSelectionPredicates.join(' OR ')})`; } } + if (labelPredicate) { + if (isComputedQuery) { + labelPredicate = `WITH [${safeVariableName} IN ${listVariable} WHERE ${labelPredicate} | ${safeVariableName}] AS ${listVariable} `; + } else if (isComputedMutation) { + labelPredicate = `UNWIND [${safeVariableName} IN ${listVariable} WHERE ${labelPredicate} | ${safeVariableName}] `; + } else if (isComputedField) { + labelPredicate = `WHERE ${labelPredicate} `; + } + } return labelPredicate; }; @@ -883,10 +938,12 @@ const customQuery = ({ argString, selections, variableName, + isObjectType, isInterfaceType, + isUnionType, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, orderByValue, outerSkipLimit, queryTypeCypherDirective, @@ -916,8 +973,9 @@ const customQuery = ({ const isScalarPayload = isNeo4jTypeOutput || isScalarType; const fragmentTypeParams = derivedTypesParams({ isInterfaceType, + isUnionType, schema: resolveInfo.schema, - interfaceName: schemaType.name, + schemaTypeName: schemaType.name, usesFragments }); @@ -925,8 +983,10 @@ const customQuery = ({ isComputedQuery: true, schemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, isScalarPayload, usesFragments, safeVariableName, @@ -954,10 +1014,12 @@ const nodeQuery = ({ selections, variableName, typeName, + isObjectType, isInterfaceType, + isUnionType, usesFragments, schemaTypeFields, - composedTypeMap, + derivedTypeMap, additionalLabels = [], neo4jTypeClauses, orderByValue, @@ -1016,16 +1078,19 @@ const nodeQuery = ({ const fragmentTypeParams = derivedTypesParams({ isInterfaceType, + isUnionType, schema: resolveInfo.schema, - interfaceName: schemaType.name, + schemaTypeName: schemaType.name, usesFragments }); const [mapProjection, labelPredicate] = buildMapProjection({ schemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, usesFragments, safeVariableName, subQuery, @@ -1060,52 +1125,72 @@ const nodeQuery = ({ const buildMapProjection = ({ schemaType, schemaTypeFields, - composedTypeMap, + listVariable, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, isScalarPayload, isComputedQuery, isComputedMutation, isComputedField, usesFragments, safeVariableName, - listVariable = 'x', subQuery, resolveInfo }) => { - let labelPredicate = buildTypeCompositionPredicate({ + const labelPredicate = buildTypeCompositionPredicate({ schemaType, schemaTypeFields, - composedTypeMap, + listVariable, + derivedTypeMap, safeVariableName, isInterfaceType, + isUnionType, + isComputedQuery, + isComputedMutation, + isComputedField, usesFragments, resolveInfo }); - let mapProjection = `${safeVariableName} {${subQuery}}`; - if (isInterfaceType) { - if (usesFragments) { - mapProjection = subQuery; - } else { - mapProjection = `${safeVariableName} {${fragmentType( - safeVariableName, - schemaType.name - )}${subQuery ? `,${subQuery}` : ''}}`; - } - } else if (isScalarPayload) { + const isFragmentedInterfaceType = usesFragments && isInterfaceType; + const isFragmentedUnionType = usesFragments && isUnionType; + let mapProjection = ''; + if (isScalarPayload) { + // A scalar type payload has no map projection mapProjection = safeVariableName; - } - if (labelPredicate) { - if (isComputedQuery) { - labelPredicate = `WITH [${safeVariableName} IN ${listVariable} WHERE ${labelPredicate} | ${safeVariableName}] AS ${listVariable} `; - } else if (isComputedMutation) { - labelPredicate = `UNWIND [${safeVariableName} IN ${listVariable} WHERE ${labelPredicate} | ${safeVariableName}] `; - } else if (isComputedField) { - labelPredicate = `WHERE ${labelPredicate} `; - } + } else if (isObjectType) { + mapProjection = `${safeVariableName} {${subQuery}}`; + } else if (isFragmentedInterfaceType || isFragmentedUnionType) { + // An interface type possibly uses fragments and a + // union type necessarily uses fragments + mapProjection = subQuery; + } else if (isInterfaceType || isUnionType) { + // If no fragments are used, then this is an interface type + // with only interface fields selected + mapProjection = `${safeVariableName} {${fragmentType( + safeVariableName, + schemaType.name + )}${subQuery ? `,${subQuery}` : ''}}`; } return [mapProjection, labelPredicate]; }; +const getUnionLabels = ({ typeName = '', typeMap = {} }) => { + const unionLabels = []; + Object.keys(typeMap).map(key => { + const definition = typeMap[key]; + const astNode = definition.astNode; + if (isUnionTypeDefinition({ definition: astNode })) { + const unionTypeName = astNode.name.value; + if (astNode.types.find(type => type.name.value === typeName)) { + unionLabels.push(unionTypeName); + } + } + }); + return unionLabels; +}; + // Mutation API root operation branch export const translateMutation = ({ resolveInfo, @@ -1114,23 +1199,16 @@ export const translateMutation = ({ offset, otherParams }) => { + const typeMap = resolveInfo.schema.getTypeMap(); const { typeName, variableName } = typeIdentifiers(resolveInfo.returnType); - const schemaType = resolveInfo.schema.getType(typeName); - const selections = getPayloadSelections(resolveInfo); - const outerSkipLimit = getOuterSkipLimit(first, offset); const orderByValue = computeOrderBy(resolveInfo, schemaType); const additionalNodeLabels = getAdditionalLabels( schemaType, getCypherParams(context) ); - const interfaceLabels = - typeof schemaType.getInterfaces === 'function' - ? schemaType.getInterfaces().map(i => i.name) - : []; - const mutationTypeCypherDirective = getMutationCypherDirective(resolveInfo); const mutationMeta = resolveInfo.schema .getMutationType() @@ -1149,23 +1227,39 @@ export const translateMutation = ({ const isInterfaceType = isGraphqlInterfaceType(schemaType); const isObjectType = isGraphqlObjectType(schemaType); + const isUnionType = isGraphqlUnionType(schemaType); + const usesFragments = isFragmentedSelection({ selections }); const isFragmentedObjectType = isObjectType && usesFragments; - const [schemaTypeFields, composedTypeMap] = mergeSelectionFragments({ + const interfaceLabels = + typeof schemaType.getInterfaces === 'function' + ? schemaType.getInterfaces().map(i => i.name) + : []; + const unionLabels = getUnionLabels({ typeName, typeMap }); + const additionalLabels = [ + ...additionalNodeLabels, + ...interfaceLabels, + ...unionLabels + ]; + + const [schemaTypeFields, derivedTypeMap] = mergeSelectionFragments({ schemaType, selections, isFragmentedObjectType, + isUnionType, + typeMap, resolveInfo }); - if (mutationTypeCypherDirective) { return customMutation({ resolveInfo, schemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, usesFragments, selections, params, @@ -1183,7 +1277,7 @@ export const translateMutation = ({ params, variableName, typeName, - additionalLabels: additionalNodeLabels.concat(interfaceLabels) + additionalLabels }); } else if (isDeleteMutation(resolveInfo)) { return nodeDelete({ @@ -1192,8 +1286,7 @@ export const translateMutation = ({ selections, params, variableName, - typeName, - additionalLabels: additionalNodeLabels + typeName }); } else if (isAddMutation(resolveInfo)) { return relationshipCreate({ @@ -1225,8 +1318,8 @@ export const translateMutation = ({ typeName, selections, schemaType, - params, - additionalLabels: additionalNodeLabels.concat(interfaceLabels) + additionalLabels, + params }); } } else if (isRemoveMutation(resolveInfo)) { @@ -1254,8 +1347,10 @@ const customMutation = ({ variableName, schemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, usesFragments, resolveInfo, orderByValue, @@ -1293,8 +1388,10 @@ const customMutation = ({ listVariable, schemaType, schemaTypeFields, - composedTypeMap, + derivedTypeMap, + isObjectType, isInterfaceType, + isUnionType, usesFragments, safeVariableName, subQuery, @@ -1329,8 +1426,9 @@ const customMutation = ({ } const fragmentTypeParams = derivedTypesParams({ isInterfaceType, + isUnionType, schema: resolveInfo.schema, - interfaceName: schemaType.name, + schemaTypeName: schemaType.name, usesFragments }); params = { ...params, ...subParams, ...fragmentTypeParams }; @@ -1389,11 +1487,10 @@ const nodeDelete = ({ variableName, typeName, schemaType, - additionalLabels, params }) => { const safeVariableName = safeVar(variableName); - const safeLabelName = safeLabel([typeName, ...additionalLabels]); + const safeLabelName = safeLabel(typeName); const args = getMutationArguments(resolveInfo); const primaryKeyArg = args[0]; const primaryKeyArgName = primaryKeyArg.name.value; @@ -1835,7 +1932,6 @@ const nodeMergeOrUpdate = ({ params }) => { const safeVariableName = safeVar(variableName); - const safeLabelName = safeLabel([typeName, ...additionalLabels]); const args = getMutationArguments(resolveInfo); const primaryKeyArg = args[0]; const primaryKeyArgName = primaryKeyArg.name.value; @@ -1862,7 +1958,9 @@ const nodeMergeOrUpdate = ({ resolveInfo }); let cypherOperation = ''; + let safeLabelName = safeLabel(typeName); if (isMergeMutation(resolveInfo)) { + safeLabelName = safeLabel([typeName, ...additionalLabels]); cypherOperation = 'MERGE'; } else if (isUpdateMutation(resolveInfo)) { cypherOperation = 'MATCH'; diff --git a/src/utils.js b/src/utils.js index 59ab421e..7c6bb71a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,9 +1,10 @@ -import { isObjectType, parse, GraphQLInt, Kind } from 'graphql'; +import { parse, GraphQLInt, Kind } from 'graphql'; import neo4j from 'neo4j-driver'; import _ from 'lodash'; import { Neo4jTypeName } from './augment/types/types'; import { SpatialType } from './augment/types/spatial'; import { unwrapNamedType } from './augment/fields'; +import { Neo4jTypeFormatted } from './augment/types/types'; function parseArg(arg, variableValues) { switch (arg.value.kind) { @@ -984,7 +985,7 @@ export const neo4jTypePredicateClauses = ( const paramValue = neo4jTypeParam.value; // If it is, set and use its .value if (paramValue) neo4jTypeParam = paramValue; - if (neo4jTypeParam['formatted']) { + if (neo4jTypeParam[Neo4jTypeFormatted.FORMATTED]) { // Only the dedicated 'formatted' arg is used if it is provided const type = t ? _getNamedType(t.type).name.value : ''; acc.push( @@ -1028,6 +1029,7 @@ export const getNeo4jTypeArguments = args => { : []; }; +// TODO rename and add logic for @skip and @include directives? export const removeIgnoredFields = (schemaType, selections) => { if (!isGraphqlScalarType(schemaType) && selections && selections.length) { const schemaTypeFields = schemaType.getFields(); @@ -1058,9 +1060,12 @@ const _getNamedType = type => { return type; }; -export const getDerivedTypeNames = (schema, interfaceName) => { - return Object.values(schema.getTypeMap()) - .filter(t => isObjectType(t)) - .filter(t => t.getInterfaces().some(i => i.name === interfaceName)) - .map(t => t.name); +export const getInterfaceDerivedTypeNames = (schema, interfaceName) => { + const implementingTypeMap = schema._implementations + ? schema._implementations[interfaceName] + : {}; + const implementingTypes = Object.values(implementingTypeMap).map( + type => type.name + ); + return implementingTypes.sort(); }; diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 156054db..7568ec8e 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -21,7 +21,7 @@ export function cypherTestRunner( ` type Mutation { CreateGenre(name: String): Genre @cypher(statement: "CREATE (g:Genre) SET g.name = $name RETURN g") - CreateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie + CreateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, released: DateTime): Movie CreateState(name: String!): State UpdateMovie(movieId: ID!, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie DeleteMovie(movieId: ID!): Movie @@ -34,8 +34,11 @@ type Mutation { customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") CustomCamera: Camera @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera") CustomCameras: [Camera] @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]") + CreateNewCamera(id: ID, type: String, make: String, weight: Int, features: [String]): NewCamera + CreateActor(userId: ID, name: String): Actor + computedMovieSearch: [MovieSearch] @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") } -`; + `; const checkCypherQuery = (object, params, ctx, resolveInfo) => { const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); @@ -73,11 +76,14 @@ type Mutation { computedObjectWithCypherParams: checkCypherQuery, computedStringList: checkCypherQuery, computedIntList: checkCypherQuery, - customWithArguments: checkCypherQuery + customWithArguments: checkCypherQuery, + MovieSearch: checkCypherQuery, + computedMovieSearch: checkCypherQuery }, Mutation: { CreateGenre: checkCypherMutation, CreateMovie: checkCypherMutation, + CreateActor: checkCypherMutation, CreateState: checkCypherMutation, UpdateMovie: checkCypherMutation, DeleteMovie: checkCypherMutation, @@ -89,7 +95,9 @@ type Mutation { computedSpatial: checkCypherMutation, customWithArguments: checkCypherMutation, CustomCamera: checkCypherMutation, - CustomCameras: checkCypherMutation + CustomCameras: checkCypherMutation, + CreateNewCamera: checkCypherMutation, + computedMovieSearch: checkCypherMutation } }; let augmentedTypeDefs = augmentTypeDefs(testMovieSchema, { auth: true }); @@ -193,10 +201,13 @@ export function augmentedSchemaCypherTestRunner( computedObjectWithCypherParams: checkCypherQuery, computedStringList: checkCypherQuery, computedIntList: checkCypherQuery, - customWithArguments: checkCypherQuery + customWithArguments: checkCypherQuery, + MovieSearch: checkCypherQuery, + computedMovieSearch: checkCypherQuery }, Mutation: { CreateMovie: checkCypherMutation, + CreateActor: checkCypherMutation, CreateState: checkCypherMutation, CreateTemporalNode: checkCypherMutation, UpdateTemporalNode: checkCypherMutation, @@ -229,7 +240,9 @@ export function augmentedSchemaCypherTestRunner( computedSpatial: checkCypherMutation, customWithArguments: checkCypherMutation, CustomCamera: checkCypherMutation, - CustomCameras: checkCypherMutation + CustomCameras: checkCypherMutation, + CreateNewCamera: checkCypherMutation, + computedMovieSearch: checkCypherMutation } }; diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index b8e01846..3cf8e660 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -8,7 +8,7 @@ export const testSchema = /* GraphQL */ ` title: String @isAuthenticated someprefix_title_with_underscores: String year: Int - released: DateTime! + released: DateTime plot: String poster: String imdbRating: Float @@ -148,6 +148,9 @@ export const testSchema = /* GraphQL */ ` location: Point ): [FriendOf] favorites: [Movie] @relation(name: "FAVORITED", direction: "OUT") + movieSearch: [MovieSearch] + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") } type FriendOf @relation { @@ -261,6 +264,9 @@ export const testSchema = /* GraphQL */ ` ): [InterfaceNoScalars] CustomCameras: [Camera] @cypher(statement: "MATCH (c:Camera) RETURN c") CustomCamera: Camera @cypher(statement: "MATCH (c:Camera) RETURN c") + MovieSearch: [MovieSearch] + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") } type Mutation { @@ -291,6 +297,8 @@ export const testSchema = /* GraphQL */ ` @cypher( statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]" ) + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") } type currentUserId { @@ -464,6 +472,8 @@ export const testSchema = /* GraphQL */ ` @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") } + union MovieSearch = Movie | Genre | Book | Actor | OldCamera + type CameraMan implements Person { userId: ID! name: String diff --git a/test/integration/integration.test.js b/test/integration/integration.test.js index 1841d4b0..002b0ab8 100644 --- a/test/integration/integration.test.js +++ b/test/integration/integration.test.js @@ -1272,6 +1272,107 @@ test.serial( } ); +test.serial('query union type using complex fragments', async t => { + t.plan(1); + + await client.mutate({ + mutation: gql` + mutation { + CreateOldCamera(id: "cam009", type: "macro", weight: 99) { + id + } + CreateUser(userId: "man009", name: "Johnnie Zoooooooom") { + userId + } + } + ` + }); + + let expected = { + data: { + MovieSearch: [ + { + __typename: 'Movie', + movieId: '12dd334d5zaaaa', + title: 'My Super Awesome Movie' + }, + { + __typename: 'OldCamera', + id: 'cam009', + type: 'macro' + }, + { + userId: 'man009', + name: 'Johnnie Zoooooooom', + __typename: 'User' + } + ] + } + }; + + await client + .query({ + query: gql` + query { + MovieSearch { + ... on Movie { + movieId + } + ... on User { + userId + } + ... on Camera { + __typename + ... on OldCamera { + id + type + } + } + ...MovieFragment + ... on Person { + ... on CameraMan { + _id + } + } + ...PersonFragment + } + } + + fragment MovieFragment on Movie { + title + } + + fragment PersonFragment on Person { + ... on User { + userId + name + } + __typename + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }) + .finally(async () => { + await client.mutate({ + mutation: gql` + mutation { + DeleteOldCamera(id: "cam009") { + id + } + DeleteUser(userId: "man009") { + userId + } + } + ` + }); + }); +}); + /* * Temporal type tests */ diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 20905eab..253333a1 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -151,6 +151,9 @@ test.cb('Test augmented schema', t => { orderBy: [_CameraOrdering] ): [Camera] @cypher(statement: "MATCH (c:Camera) RETURN c") CustomCamera: Camera @cypher(statement: "MATCH (c:Camera) RETURN c") + MovieSearch: [MovieSearch] + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") Genre( _id: String name: String @@ -798,7 +801,7 @@ test.cb('Test augmented schema', t => { title: String @isAuthenticated someprefix_title_with_underscores: String year: Int - released: _Neo4jDateTime! + released: _Neo4jDateTime plot: String poster: String imdbRating: Float @@ -1022,6 +1025,9 @@ test.cb('Test augmented schema', t => { orderBy: [_MovieOrdering] filter: _MovieFilter ): [Movie] @relation(name: "FAVORITED", direction: "OUT") + movieSearch: [MovieSearch] + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") _id: String } @@ -1656,6 +1662,8 @@ test.cb('Test augmented schema', t => { cameraBuddy_not_in: [_PersonFilter!] } + union MovieSearch = Movie | Genre | Book | Actor | OldCamera + type CameraMan implements Person { userId: ID! name: String @@ -1738,6 +1746,8 @@ test.cb('Test augmented schema', t => { @cypher( statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]" ) + computedMovieSearch: [MovieSearch] + @cypher(statement: "MATCH (ms:MovieSearch) RETURN ms") AddMovieGenres( from: _MovieInput! to: _GenreInput! @@ -1824,7 +1834,7 @@ test.cb('Test augmented schema', t => { title: String someprefix_title_with_underscores: String year: Int - released: _Neo4jDateTimeInput! + released: _Neo4jDateTimeInput plot: String poster: String imdbRating: Float diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index fc780b9d..f22bd573 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -653,7 +653,7 @@ test.cb('Create node mutation', t => { } }`, expectedCypherQuery = ` - CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId:$params.movieId,title:$params.title,year:$params.year,plot:$params.plot,poster:$params.poster,imdbRating:$params.imdbRating}) + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}:\`MovieSearch\` {movieId:$params.movieId,title:$params.title,year:$params.year,plot:$params.plot,poster:$params.poster,imdbRating:$params.imdbRating}) RETURN \`movie\` {_id: ID(\`movie\`), .title ,genres: [(\`movie\`)-[:\`IN_GENRE\`]->(\`movie_genres\`:\`Genre\`) | \`movie_genres\` { .name }] } AS \`movie\` `; @@ -704,7 +704,7 @@ test.cb('Update node mutation', t => { year } }`, - expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}{movieId: $params.movieId}) + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`{movieId: $params.movieId}) SET \`movie\` += {year:$params.year} RETURN \`movie\` {_id: ID(\`movie\`), .title , .year } AS \`movie\``; t.plan(2); @@ -725,7 +725,7 @@ test.cb('Delete node mutation', t => { movieId } }`, - expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: $movieId}) + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {movieId: $movieId}) WITH \`movie\` AS \`movie_toDelete\`, \`movie\` {_id: ID(\`movie\`), .movieId } AS \`movie\` DETACH DELETE \`movie_toDelete\` RETURN \`movie\``; @@ -2210,7 +2210,7 @@ test('query interfaced relation using inline fragment', t => { year } }`, - expectedCypherQuery = `MATCH (\`actor\`:\`Actor\`) RETURN \`actor\` { .name ,knows: [(\`actor\`)-[:\`KNOWS\`]->(\`actor_knows\`:\`Person\`) WHERE ("User" IN labels(\`actor_knows\`)) | head([\`actor_knows\` IN [\`actor_knows\`] WHERE [label IN labels(\`actor_knows\`) WHERE label = "User" | TRUE] | \`actor_knows\` { FRAGMENT_TYPE: "User", .name ,favorites: [(\`actor_knows\`)-[:\`FAVORITED\`]->(\`actor_knows_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`actor_knows_favorites\` { .movieId , .title , .year }] }])] } AS \`actor\``; + expectedCypherQuery = `MATCH (\`actor\`:\`Actor\`) RETURN \`actor\` { .name ,knows: [(\`actor\`)-[:\`KNOWS\`]->(\`actor_knows\`:\`Person\`) WHERE ("User" IN labels(\`actor_knows\`)) | head([\`actor_knows\` IN [\`actor_knows\`] WHERE "User" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "User", .name ,favorites: [(\`actor_knows\`)-[:\`FAVORITED\`]->(\`actor_knows_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`actor_knows_favorites\` { .movieId , .title , .year }] }])] } AS \`actor\``; t.plan(1); @@ -4463,7 +4463,7 @@ test('UUID value generated if no id value provided', t => { } }`, expectedCypherQuery = ` - CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released)}) + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}:\`MovieSearch\` {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released)}) RETURN \`movie\` { .title } AS \`movie\` `; @@ -4535,7 +4535,7 @@ test('Create node with list arguments', t => { } }`, expectedCypherQuery = ` - CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released),locations: [value IN $params.locations | point(value)],years:$params.years,titles:$params.titles,imdbRatings:$params.imdbRatings,releases: [value IN $params.releases | datetime(value)]}) + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}:\`MovieSearch\` {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released),locations: [value IN $params.locations | point(value)],years:$params.years,titles:$params.titles,imdbRatings:$params.imdbRatings,releases: [value IN $params.releases | datetime(value)]}) RETURN \`movie\` { .movieId , .title , .titles , .imdbRatings , .years ,releases: reduce(a = [], INSTANCE IN movie.releases | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , second: INSTANCE.second , formatted: toString(INSTANCE) }),locations: reduce(a = [], INSTANCE IN movie.locations | a + { x: INSTANCE.x , y: INSTANCE.y , z: INSTANCE.z })} AS \`movie\` `; @@ -5367,7 +5367,7 @@ test('query only an interface field', t => { first: -1, offset: 0, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5387,7 +5387,7 @@ test('query only __typename field on interface type', t => { first: -1, offset: 0, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5410,7 +5410,7 @@ test('query only interface fields', t => { offset: 0, first: -1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5435,7 +5435,7 @@ test('query only interface fields using untyped inline fragment', t => { offset: 0, first: -1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5458,7 +5458,7 @@ test('query only computed interface fields', t => { offset: 0, first: -1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5485,9 +5485,9 @@ test('query interface type relationship field', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5509,9 +5509,9 @@ test('query only __typename field on interface type relationship field', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5538,9 +5538,9 @@ test('query computed interface type relationship field', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5561,7 +5561,7 @@ test('query computed interface type relationship field using only an inline frag } } }`, - expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`camera\` RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,computedOperators: [camera_computedOperators IN [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams}, true) WHERE ("CameraMan" IN labels(camera_computedOperators)) | camera_computedOperators] | head([\`camera_computedOperators\` IN [\`camera_computedOperators\`] WHERE [label IN labels(\`camera_computedOperators\`) WHERE label = "CameraMan" | TRUE] | \`camera_computedOperators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] } AS \`camera\``; + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`camera\` RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,computedOperators: [camera_computedOperators IN [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams}, true) WHERE ("CameraMan" IN labels(camera_computedOperators)) | camera_computedOperators] | head([\`camera_computedOperators\` IN [\`camera_computedOperators\`] WHERE "CameraMan" IN labels(\`camera_computedOperators\`) | \`camera_computedOperators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] } AS \`camera\``; t.plan(3); return Promise.all([ @@ -5569,7 +5569,7 @@ test('query computed interface type relationship field using only an inline frag offset: 0, first: -1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5584,7 +5584,7 @@ test('query only fields on an implementing type using an inline fragment', t => } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5612,7 +5612,7 @@ test('pagination used on root and nested interface type field', t => { } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "Actor" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "User" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "User", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])][1..2] } AS \`camera\` SKIP toInteger($offset) LIMIT toInteger($first)`; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "Actor" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "User" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "User", .name }])][1..2] } AS \`camera\` SKIP toInteger($offset) LIMIT toInteger($first)`; t.plan(3); return Promise.all([ @@ -5622,7 +5622,7 @@ test('pagination used on root and nested interface type field', t => { '1_first': 1, '1_offset': 1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5643,7 +5643,7 @@ test('ordering used on root and nested interface type field', t => { } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WITH \`camera\` ORDER BY camera.type ASC RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,operators: apoc.coll.sortMulti([(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "Actor" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "User" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "User", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])], ['name']) } AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WITH \`camera\` ORDER BY camera.type ASC RETURN \`camera\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .make , .weight ,operators: apoc.coll.sortMulti([(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "Actor" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "User" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "User", .name }])], ['name']) } AS \`camera\``; t.plan(3); return Promise.all([ @@ -5652,7 +5652,7 @@ test('ordering used on root and nested interface type field', t => { first: -1, '1_orderBy': 'name_desc', cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -5678,7 +5678,7 @@ test('filtering used on root and nested interface type field with only fragments } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`)) AND ($filter._operators_not_null = TRUE AND EXISTS((\`camera\`)<-[:cameras]-(:Person))) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) AND ($1_filter._name_not_null = TRUE AND EXISTS(\`camera_operators\`.name)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .name }])] }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`)) AND ($filter._operators_not_null = TRUE AND EXISTS((\`camera\`)<-[:cameras]-(:Person))) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) AND ($1_filter._name_not_null = TRUE AND EXISTS(\`camera_operators\`.name)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .name }])] }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5721,7 +5721,7 @@ test('filtering used on root and nested interface using fragments and query vari } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\` {type:$type}) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE (\`camera_operators\`.userId = $1_filter.userId) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .id , .type ,computedOperators: [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams, name: $3_name}, true) | camera_computedOperators {FRAGMENT_TYPE: head( [ label IN labels(camera_computedOperators) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE (\`camera_operators\`.userId = $1_filter.userId) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .id , .type ,computedOperators: [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams, name: $3_name}, true) | camera_computedOperators {FRAGMENT_TYPE: head( [ label IN labels(camera_computedOperators) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\` {type:$type}) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE (\`camera_operators\`.userId = $1_filter.userId) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .id , .type ,computedOperators: [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams, name: $3_name}, true) | camera_computedOperators {FRAGMENT_TYPE: head( [ label IN labels(camera_computedOperators) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE (\`camera_operators\`.userId = $1_filter.userId) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .id , .type ,computedOperators: [ camera_computedOperators IN apoc.cypher.runFirstColumn("MATCH (this)<-[:cameras]-(p:Person) RETURN p", {this: camera, cypherParams: $cypherParams, name: $3_name}, true) | camera_computedOperators {FRAGMENT_TYPE: head( [ label IN labels(camera_computedOperators) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5744,7 +5744,7 @@ test('filtering used on root and nested interface using fragments and query vari userId: 'man001' }, '3_name': 'Johnnie Zoom', - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS } ), @@ -5772,7 +5772,7 @@ test('query only computed fields on an implementing type using an inline fragmen } } }`, - expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x WITH [\`camera\` IN x WHERE ("OldCamera" IN labels(\`camera\`)) | \`camera\`] AS x UNWIND x AS \`camera\` RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }]) AS \`camera\``; + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x WITH [\`camera\` IN x WHERE ("OldCamera" IN labels(\`camera\`)) | \`camera\`] AS x UNWIND x AS \`camera\` RETURN head([\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5801,7 +5801,7 @@ test('query computed interface fields using fragments on implementing types', t type weight }`, - expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x WITH [\`camera\` IN x WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) | \`camera\`] AS x UNWIND x AS \`camera\` RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .type , .id }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .type , .weight , .id }]) AS \`camera\``; + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (c:Camera) RETURN c", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x WITH [\`camera\` IN x WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) | \`camera\`] AS x UNWIND x AS \`camera\` RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .type , .weight , .id }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .type , .id }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5829,7 +5829,7 @@ test('query interface type relationship field using inline fragment on implement } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "Actor" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .userId }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "User" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "User", .userId }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .name , .userId }])] }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("Actor" IN labels(\`camera_operators\`) OR "CameraMan" IN labels(\`camera_operators\`) OR "User" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "Actor" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "Actor", .userId }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .name , .userId }] + [\`camera_operators\` IN [\`camera_operators\`] WHERE "User" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "User", .userId }])] }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5857,7 +5857,7 @@ test('query interface type relationship field using only inline fragment', t => } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5887,7 +5887,7 @@ test('query interface __typename as only field not within fragments on implement } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE [label IN labels(\`camera_operators\`) WHERE label = "CameraMan" | TRUE] | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) WHERE ("CameraMan" IN labels(\`camera_operators\`)) | head([\`camera_operators\` IN [\`camera_operators\`] WHERE "CameraMan" IN labels(\`camera_operators\`) | \`camera_operators\` { FRAGMENT_TYPE: "CameraMan", .userId , .name }])] }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5909,7 +5909,7 @@ test('query same field on implementing type using inline fragment', t => { } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5932,7 +5932,7 @@ test('query interface and implementing type using inline fragment', t => { } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id , .type }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -5961,14 +5961,14 @@ test('query interface type relationship fields within inline fragment', t => { } } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] , .id , .type , .make , .weight }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type , .make , .weight }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type , .make , .weight }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] , .id , .type , .make , .weight }]) AS \`camera\``; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -5987,7 +5987,7 @@ test('query interface and implementing type using fragment spread', t => { id type }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -6017,7 +6017,7 @@ test('query interface and implementing types using inline fragment and fragment type features }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell , .id , .make }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type , .features , .make }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id , .type , .features , .make }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell , .id , .make }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -6053,14 +6053,14 @@ test('query interface type relationship field on implementing types using inline userId } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] , .type , .weight }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .type , .weight }]) AS \`camera\``; + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId }] , .type , .weight }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name }] , .type , .weight }]) AS \`camera\``; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'], + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], cypherParams: CYPHER_PARAMS }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -6084,7 +6084,7 @@ test('query interface type payload of @cypher mutation field', t => { offset: 0, first: -1, cypherParams: CYPHER_PARAMS, - Camera_derivedTypes: ['OldCamera', 'NewCamera'] + Camera_derivedTypes: ['NewCamera', 'OldCamera'] }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -6097,8 +6097,8 @@ test('query interface type list payload of @cypher mutation field using fragment ... on NewCamera { features } - ...OldCameraFragment ...CameraFragment + ...OldCameraFragment } } fragment CameraFragment on Camera { @@ -6108,8 +6108,8 @@ test('query interface type list payload of @cypher mutation field using fragment smell }`, expectedCypherQuery = `CALL apoc.cypher.doIt("CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value - UNWIND [\`camera\` IN apoc.map.values(value, [keys(value)[0]])[0] WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) | \`camera\`] AS \`camera\` - RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell , .id , .type }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .features , .id , .type }]) AS \`camera\``; + UNWIND [\`camera\` IN apoc.map.values(value, [keys(value)[0]])[0] WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) | \`camera\`] AS \`camera\` + RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .features , .id , .type }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell , .id , .type }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -6137,7 +6137,7 @@ test('query interface type list payload of @cypher mutation field using only fra }`, expectedCypherQuery = `CALL apoc.cypher.doIt("CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value UNWIND [\`camera\` IN apoc.map.values(value, [keys(value)[0]])[0] WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) | \`camera\`] AS \`camera\` - RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .features }]) AS \`camera\``; + RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .features }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .smell }]) AS \`camera\``; t.plan(3); return Promise.all([ @@ -6168,7 +6168,7 @@ test('query interfaced relationship mutation payload using fragments', t => { MATCH (\`actor_from\`:\`Actor\` {userId: $from.userId}) MATCH (\`person_to\`:\`Person\` {userId: $to.userId}) CREATE (\`actor_from\`)-[\`knows_relation\`:\`KNOWS\`]->(\`person_to\`) - RETURN \`knows_relation\` { from: \`actor_from\` { .name } ,to: head([\`person_to\` IN [\`person_to\`] WHERE [label IN labels(\`person_to\`) WHERE label = "Actor" | TRUE] | \`person_to\` { FRAGMENT_TYPE: "Actor", .name }] + [\`person_to\` IN [\`person_to\`] WHERE [label IN labels(\`person_to\`) WHERE label = "User" | TRUE] | \`person_to\` { FRAGMENT_TYPE: "User", .userId , .name }] + [\`person_to\` IN [\`person_to\`] WHERE [label IN labels(\`person_to\`) WHERE label = "CameraMan" | TRUE] | \`person_to\` { FRAGMENT_TYPE: "CameraMan", .name }]) } AS \`_AddActorKnowsPayload\`; + RETURN \`knows_relation\` { from: \`actor_from\` { .name } ,to: head([\`person_to\` IN [\`person_to\`] WHERE "Actor" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "Actor", .name }] + [\`person_to\` IN [\`person_to\`] WHERE "CameraMan" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "CameraMan", .name }] + [\`person_to\` IN [\`person_to\`] WHERE "User" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "User", .userId , .name }]) } AS \`_AddActorKnowsPayload\`; `; t.plan(1); @@ -6203,7 +6203,7 @@ test('query interfaced relationship mutation payload using only fragments', t => MATCH (\`actor_from\`:\`Actor\` {userId: $from.userId}) MATCH (\`person_to\`:\`Person\` {userId: $to.userId}) CREATE (\`actor_from\`)-[\`knows_relation\`:\`KNOWS\`]->(\`person_to\`) - RETURN \`knows_relation\` { from: \`actor_from\` { .name } ,to: head([\`person_to\` IN [\`person_to\`] WHERE [label IN labels(\`person_to\`) WHERE label = "User" | TRUE] | \`person_to\` { FRAGMENT_TYPE: "User", .userId }]) } AS \`_AddActorKnowsPayload\`; + RETURN \`knows_relation\` { from: \`actor_from\` { .name } ,to: head([\`person_to\` IN [\`person_to\`] WHERE "User" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "User", .userId }]) } AS \`_AddActorKnowsPayload\`; `; t.plan(1); @@ -6221,11 +6221,6 @@ test('query interfaced relationship mutation payload using only fragments', t => ); }); -// FIXME Update we are merging the selection sets of multiple fragments on the same type -// Currently, such selection sets are not deeply merged. This results in only the first -// selection set for the 'operators' field being used; the 'userId' field selected for -// 'operators' from within the NewCameraFragment fragment spread is not used. -// See: mergeFragmentedSelections in selections.js test('query interface using multiple fragments on the same implementing type', t => { const graphQLQuery = `query { Camera { @@ -6237,24 +6232,422 @@ test('query interface using multiple fragments on the same implementing type', t } } ...NewCameraFragment + ... on Camera { + ... on NewCamera { + type + operators { + userId + } + } + } } } fragment NewCameraFragment on NewCamera { type operators { + __typename + } + }`, + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .name , .userId }] , .type , .weight }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .weight }]) AS \`camera\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Create object type node with additional union label', t => { + const graphQLQuery = `mutation { + CreateMovie( + title: "Searchable Movie" + ) { + movieId + title + } + }`, + expectedCypherQuery = ` + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}:\`MovieSearch\` {movieId: apoc.create.uuid(),title:$params.title}) + RETURN \`movie\` { .movieId , .title } AS \`movie\` + `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + params: { + title: 'Searchable Movie' + } + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Create interfaced object type node with additional union label', t => { + const graphQLQuery = `mutation { + CreateActor( + name: "John" + ) { userId + name } }`, - expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera\`) OR "NewCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "OldCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "OldCamera", .weight }] + [\`camera\` IN [\`camera\`] WHERE [label IN labels(\`camera\`) WHERE label = "NewCamera" | TRUE] | \`camera\` { FRAGMENT_TYPE: "NewCamera", .id ,operators: [(\`camera\`)<-[:\`cameras\`]-(\`camera_operators\`:\`Person\`) | \`camera_operators\` {FRAGMENT_TYPE: head( [ label IN labels(\`camera_operators\`) WHERE label IN $Person_derivedTypes ] ), .name }] , .type , .weight }]) AS \`camera\``; + expectedCypherQuery = ` + CREATE (\`actor\`:\`Actor\`:\`Person\`:\`MovieSearch\` {userId: apoc.create.uuid(),name:$params.name}) + RETURN \`actor\` { .userId , .name } AS \`actor\` + `; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { offset: 0, first: -1, + params: { + name: 'John' + } + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query only __typename field on union type', t => { + const graphQLQuery = `query { + MovieSearch { + __typename + } + }`, + expectedCypherQuery = `MATCH (\`movieSearch\`:\`MovieSearch\`) RETURN \`movieSearch\` {FRAGMENT_TYPE: head( [ label IN labels(\`movieSearch\`) WHERE label IN $MovieSearch_derivedTypes ] )} AS \`movieSearch\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + offset: 0, cypherParams: CYPHER_PARAMS, - Person_derivedTypes: ['Actor', 'User', 'CameraMan'] + MovieSearch_derivedTypes: ['Movie', 'Genre', 'Book', 'Actor', 'OldCamera'] + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query union type using fragments', t => { + const graphQLQuery = `query { + MovieSearch { + ... on Movie { + movieId + title + } + ...MovieSearchGenre + } + } + + fragment MovieSearchGenre on Genre { + name + }`, + expectedCypherQuery = `MATCH (\`movieSearch\`:\`MovieSearch\`) WHERE ("Genre" IN labels(\`movieSearch\`) OR "Movie" IN labels(\`movieSearch\`)) RETURN head([\`movieSearch\` IN [\`movieSearch\`] WHERE "Genre" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "Movie" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Movie", .movieId , .title }]) AS \`movieSearch\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query computed union type using fragments', t => { + const graphQLQuery = `query { + computedMovieSearch { + ... on Movie { + movieId + title + } + ...MovieSearchGenre + } + } + + fragment MovieSearchGenre on Genre { + name + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (ms:MovieSearch) RETURN ms", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x WITH [\`movieSearch\` IN x WHERE ("Genre" IN labels(\`movieSearch\`) OR "Movie" IN labels(\`movieSearch\`)) | \`movieSearch\`] AS x UNWIND x AS \`movieSearch\` RETURN head([\`movieSearch\` IN [\`movieSearch\`] WHERE "Genre" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "Movie" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Movie", .movieId , .title }]) AS \`movieSearch\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query union type relationship using fragments', t => { + const graphQLQuery = `query { + User { + movieSearch { + ... on Movie { + title + _id + } + ...MovieSearchGenre + } + } + } + + fragment MovieSearchGenre on Genre { + name + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {movieSearch: [(\`user\`)--(\`user_movieSearch\`:\`MovieSearch\`) WHERE ("Genre" IN labels(\`user_movieSearch\`) OR "Movie" IN labels(\`user_movieSearch\`)) | head([\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Genre" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Movie" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: "Movie", .title ,_id: ID(\`user_movieSearch\`) }])] } AS \`user\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query only __typename field on union type relationship', t => { + const graphQLQuery = `query { + User { + userId + name + movieSearch { + __typename + } + favorites { + movieId + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,movieSearch: [(\`user\`)--(\`user_movieSearch\`:\`MovieSearch\`) | \`user_movieSearch\` {FRAGMENT_TYPE: head( [ label IN labels(\`user_movieSearch\`) WHERE label IN $MovieSearch_derivedTypes ] )}] ,favorites: [(\`user\`)-[:\`FAVORITED\`]->(\`user_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`user_favorites\` { .movieId }] } AS \`user\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + MovieSearch_derivedTypes: [ + 'Movie', + 'Genre', + 'Book', + 'Actor', + 'OldCamera' + ], + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query only __typename field on computed union type relationship', t => { + const graphQLQuery = `query { + User { + computedMovieSearch { + __typename + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {computedMovieSearch: [ user_computedMovieSearch IN apoc.cypher.runFirstColumn("MATCH (ms:MovieSearch) RETURN ms", {this: user, cypherParams: $cypherParams}, true) | user_computedMovieSearch {FRAGMENT_TYPE: head( [ label IN labels(user_computedMovieSearch) WHERE label IN $MovieSearch_derivedTypes ] )}] } AS \`user\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + MovieSearch_derivedTypes: [ + 'Movie', + 'Genre', + 'Book', + 'Actor', + 'OldCamera' + ], + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query computed union type relationship using fragments', t => { + const graphQLQuery = `query { + User { + computedMovieSearch { + ... on Movie { + movieId + title + } + ...MovieSearchGenre + } + } + } + + fragment MovieSearchGenre on Genre { + name + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {computedMovieSearch: [user_computedMovieSearch IN [ user_computedMovieSearch IN apoc.cypher.runFirstColumn("MATCH (ms:MovieSearch) RETURN ms", {this: user, cypherParams: $cypherParams}, true) WHERE ("Genre" IN labels(user_computedMovieSearch) OR "Movie" IN labels(user_computedMovieSearch)) | user_computedMovieSearch] | head([\`user_computedMovieSearch\` IN [\`user_computedMovieSearch\`] WHERE "Genre" IN labels(\`user_computedMovieSearch\`) | \`user_computedMovieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`user_computedMovieSearch\` IN [\`user_computedMovieSearch\`] WHERE "Movie" IN labels(\`user_computedMovieSearch\`) | \`user_computedMovieSearch\` { FRAGMENT_TYPE: "Movie", .movieId , .title }])] } AS \`user\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query union type payload of computed mutation field', t => { + const graphQLQuery = `mutation { + computedMovieSearch { + ... on Movie { + title + } + } + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("MATCH (ms:MovieSearch) RETURN ms", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + UNWIND [\`movieSearch\` IN apoc.map.values(value, [keys(value)[0]])[0] WHERE ("Movie" IN labels(\`movieSearch\`)) | \`movieSearch\`] AS \`movieSearch\` + RETURN head([\`movieSearch\` IN [\`movieSearch\`] WHERE "Movie" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Movie", .title }]) AS \`movieSearch\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query union type using multiple fragments on the same interfaced object type', t => { + const graphQLQuery = `query { + MovieSearch { + __typename + ... on Movie { + movieId + title + } + ...MovieSearchGenre + ... on Person { + name + ... on Actor { + userId + movies { + movieId + genres { + _id + } + } + } + } + ...MovieSearchActor + } + } + + fragment MovieSearchGenre on Genre { + name + } + + fragment MovieSearchActor on Actor { + userId + movies { + movieId + title + genres { + name + } + } + }`, + expectedCypherQuery = `MATCH (\`movieSearch\`:\`MovieSearch\`) WHERE ("Genre" IN labels(\`movieSearch\`) OR "Movie" IN labels(\`movieSearch\`) OR "Person" IN labels(\`movieSearch\`)) RETURN head([\`movieSearch\` IN [\`movieSearch\`] WHERE "Genre" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "Movie" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Movie", .movieId , .title }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "Person" IN labels(\`movieSearch\`) | head([\`movieSearch\` IN [\`movieSearch\`] WHERE "Actor" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "Actor", .userId ,movies: [(\`movieSearch\`)-[:\`ACTED_IN\`]->(\`movieSearch_movies\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`movieSearch_movies\` { .movieId ,genres: [(\`movieSearch_movies\`)-[:\`IN_GENRE\`]->(\`movieSearch_movies_genres\`:\`Genre\`) | \`movieSearch_movies_genres\` {_id: ID(\`movieSearch_movies_genres\`), .name }] , .title }] , .name }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "CameraMan" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "CameraMan", .name }] + [\`movieSearch\` IN [\`movieSearch\`] WHERE "User" IN labels(\`movieSearch\`) | \`movieSearch\` { FRAGMENT_TYPE: "User", .name }])]) AS \`movieSearch\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query union type relationship using multiple fragments and interfaced object types', t => { + const graphQLQuery = `query { + User { + userId + name + movieSearch { + __typename + ... on Movie { + movieId + title + } + ... on Movie { + released { + year + } + } + ...MovieSearchGenre + ... on Person { + ... on Actor { + userId + name + movies { + movieId + genres { + _id + } + } + } + } + ...MovieSearchActor + ... on Camera { + id + type + } + } + favorites { + movieId + } + } + } + + fragment MovieSearchGenre on Genre { + name + } + + fragment MovieSearchActor on Actor { + userId + movies { + title + genres { + name + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,movieSearch: [(\`user\`)--(\`user_movieSearch\`:\`MovieSearch\`) WHERE ("Actor" IN labels(\`user_movieSearch\`) OR "Camera" IN labels(\`user_movieSearch\`) OR "Genre" IN labels(\`user_movieSearch\`) OR "Movie" IN labels(\`user_movieSearch\`)) | head([\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Camera" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: head( [ label IN labels(\`user_movieSearch\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type }] + [\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Genre" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: "Genre", .name }] + [\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Movie" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: "Movie", .movieId , .title ,released: { year: \`user_movieSearch\`.released.year } }] + [\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Person" IN labels(\`user_movieSearch\`) | head([\`user_movieSearch\` IN [\`user_movieSearch\`] WHERE "Actor" IN labels(\`user_movieSearch\`) | \`user_movieSearch\` { FRAGMENT_TYPE: "Actor", .userId , .name ,movies: [(\`user_movieSearch\`)-[:\`ACTED_IN\`]->(\`user_movieSearch_movies\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`user_movieSearch_movies\` { .movieId ,genres: [(\`user_movieSearch_movies\`)-[:\`IN_GENRE\`]->(\`user_movieSearch_movies_genres\`:\`Genre\`) | \`user_movieSearch_movies_genres\` {_id: ID(\`user_movieSearch_movies_genres\`), .name }] , .title }] }])])] ,favorites: [(\`user\`)-[:\`FAVORITED\`]->(\`user_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`user_favorites\` { .movieId }] } AS \`user\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + Camera_derivedTypes: ['NewCamera', 'OldCamera'], + cypherParams: CYPHER_PARAMS }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]);