diff --git a/example/apollo-server/movies-schema.js b/example/apollo-server/movies-schema.js index 08a5bcb0..3b65d763 100644 --- a/example/apollo-server/movies-schema.js +++ b/example/apollo-server/movies-schema.js @@ -71,9 +71,9 @@ type OnlyDate { } type SpatialNode { - pointKey: Point + id: ID! point: Point - spatialNodes(pointKey: Point): [SpatialNode] + spatialNodes(point: Point): [SpatialNode] @relation(name: "SPATIAL", direction: OUT) } @@ -115,7 +115,8 @@ type Query { MovieById(movieId: ID!): Movie GenresBySubstring(substring: String): [Genre] @cypher(statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g") Books: [Book] -}`; +} +`; export const resolvers = { // root entry point to GraphQL service diff --git a/src/augment/types/node/mutation.js b/src/augment/types/node/mutation.js index 4c7d63be..24c6c643 100644 --- a/src/augment/types/node/mutation.js +++ b/src/augment/types/node/mutation.js @@ -19,11 +19,11 @@ import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields'; /** * An enum describing the names of node type mutations */ -export const NodeMutation = { +const NodeMutation = { CREATE: 'Create', UPDATE: 'Update', - DELETE: 'Delete' - // MERGE: 'Merge' + DELETE: 'Delete', + MERGE: 'Merge' }; /** @@ -103,7 +103,10 @@ const buildNodeMutationField = ({ }; if (mutationAction === NodeMutation.CREATE) { mutationFields.push(buildField(mutationField)); - } else if (mutationAction === NodeMutation.UPDATE) { + } else if ( + mutationAction === NodeMutation.UPDATE || + mutationAction === NodeMutation.MERGE + ) { if (primaryKey && mutationField.args.length > 1) { mutationFields.push(buildField(mutationField)); } @@ -134,8 +137,8 @@ const buildNodeMutationArguments = ({ const type = field.type; if (operationName === NodeMutation.CREATE) { // Uses primary key and any other property field - if (primaryKeyName === field.name) { - if (field.type.name === GraphQLID.name) { + if (primaryKeyName === name) { + if (type.name === GraphQLID.name) { // Create auto-generates ID primary keys args.push({ name, @@ -158,7 +161,10 @@ const buildNodeMutationArguments = ({ type }); } - } else if (operationName === NodeMutation.UPDATE) { + } else if ( + operationName === NodeMutation.UPDATE || + operationName === NodeMutation.MERGE + ) { // Uses primary key and any other property field if (primaryKeyName === name) { // Require primary key otherwise diff --git a/src/augment/types/relationship/mutation.js b/src/augment/types/relationship/mutation.js index 46660b0a..a9061811 100644 --- a/src/augment/types/relationship/mutation.js +++ b/src/augment/types/relationship/mutation.js @@ -2,7 +2,11 @@ import { RelationshipDirectionField } from './relationship'; import { buildNodeOutputFields } from './query'; import { shouldAugmentRelationshipField } from '../../augment'; import { OperationType } from '../../types/types'; -import { TypeWrappers, getFieldDefinition } from '../../fields'; +import { + TypeWrappers, + getFieldDefinition, + unwrapNamedType +} from '../../fields'; import { DirectiveDefinition, buildAuthScopeDirective, @@ -20,15 +24,18 @@ import { buildObjectType, buildInputObjectType } from '../../ast'; +import { getPrimaryKey } from '../../../utils'; /** * An enum describing the names of relationship mutations, * for node and relationship type fields (field and type * relation directive) */ -export const RelationshipMutation = { +const RelationshipMutation = { CREATE: 'Add', - DELETE: 'Remove' + DELETE: 'Remove', + UPDATE: 'Update', + MERGE: 'Merge' }; /** @@ -68,11 +75,19 @@ export const augmentRelationshipMutationAPI = ({ typeName, fieldName }); + const fromTypeDefinition = typeDefinitionMap[fromType]; + const toTypeDefinition = typeDefinitionMap[toType]; + const fromTypePk = getPrimaryKey(fromTypeDefinition); + const toTypePk = getPrimaryKey(toTypeDefinition); if ( !getFieldDefinition({ fields: mutationType.fields, name: mutationName - }) + }) && + // Only generate mutation API for given relationship if both related + // nodes have a primary key + fromTypePk && + toTypePk ) { [operationTypeMap, generatedTypeMap] = buildRelationshipMutationAPI({ mutationAction, @@ -149,6 +164,7 @@ const buildRelationshipMutationAPI = ({ relationshipName, fromType, toType, + propertyInputValues, propertyOutputFields, mutationOutputType, outputType, @@ -183,6 +199,7 @@ const buildRelationshipMutationField = ({ relationshipName, fromType, toType, + propertyInputValues, propertyOutputFields, mutationOutputType, outputType, @@ -191,7 +208,10 @@ const buildRelationshipMutationField = ({ }) => { if ( mutationAction === RelationshipMutation.CREATE || - mutationAction === RelationshipMutation.DELETE + mutationAction === RelationshipMutation.DELETE || + (mutationAction === RelationshipMutation.UPDATE && + propertyInputValues.length) || + mutationAction === RelationshipMutation.MERGE ) { operationTypeMap[OperationType.MUTATION].fields.push( buildField({ @@ -213,7 +233,6 @@ const buildRelationshipMutationField = ({ relationshipName, fromType, toType, - propertyOutputFields, config }) }) @@ -252,7 +271,9 @@ const buildRelationshipMutationPropertyInputType = ({ generatedTypeMap }) => { if ( - mutationAction === RelationshipMutation.CREATE && + (mutationAction === RelationshipMutation.CREATE || + mutationAction === RelationshipMutation.UPDATE || + mutationAction === RelationshipMutation.MERGE) && propertyInputValues.length ) { let nonComputedPropertyInputFields = propertyInputValues.filter(field => { @@ -289,7 +310,9 @@ const buildRelationshipMutationArguments = ({ }) => { const fieldArguments = buildNodeSelectionArguments({ fromType, toType }); if ( - mutationAction === RelationshipMutation.CREATE && + (mutationAction === RelationshipMutation.CREATE || + mutationAction === RelationshipMutation.UPDATE || + mutationAction === RelationshipMutation.MERGE) && propertyOutputFields.length ) { fieldArguments.push( @@ -311,7 +334,6 @@ const buildRelationshipMutationDirectives = ({ relationshipName, fromType, toType, - propertyOutputFields, config }) => { const mutationMetaDirective = buildMutationMetaDirective({ @@ -326,6 +348,10 @@ const buildRelationshipMutationDirectives = ({ authAction = 'Create'; } else if (mutationAction === RelationshipMutation.DELETE) { authAction = 'Delete'; + } else if (mutationAction === RelationshipMutation.UPDATE) { + authAction = 'Update'; + } else if (mutationAction === RelationshipMutation.MERGE) { + authAction = 'Merge'; } if (authAction) { directives.push( @@ -362,7 +388,9 @@ const buildRelationshipMutationOutputType = ({ }) => { if ( mutationAction === RelationshipMutation.CREATE || - mutationAction === RelationshipMutation.DELETE + mutationAction === RelationshipMutation.DELETE || + mutationAction === RelationshipMutation.MERGE || + mutationAction === RelationshipMutation.UPDATE ) { const relationTypeDirective = buildRelationDirective({ relationshipName, @@ -370,7 +398,11 @@ const buildRelationshipMutationOutputType = ({ toType }); let fields = buildNodeOutputFields({ fromType, toType }); - if (mutationAction === RelationshipMutation.CREATE) { + if ( + mutationAction === RelationshipMutation.CREATE || + mutationAction === RelationshipMutation.UPDATE || + mutationAction === RelationshipMutation.MERGE + ) { // TODO temporary block on cypher field arguments - needs translation test const mutationOutputFields = propertyOutputFields.map(field => { if (isCypherField({ directives: field.directives })) { @@ -382,6 +414,7 @@ const buildRelationshipMutationOutputType = ({ }); fields.push(...mutationOutputFields); } + // Overwrite generatedTypeMap[mutationOutputType] = buildObjectType({ name: buildName({ name: mutationOutputType }), fields, diff --git a/src/translate.js b/src/translate.js index 467fb5c3..bd7c6003 100644 --- a/src/translate.js +++ b/src/translate.js @@ -9,6 +9,7 @@ import { isCreateMutation, isUpdateMutation, isRemoveMutation, + isMergeMutation, isDeleteMutation, computeOrderBy, innerFilterParams, @@ -955,22 +956,26 @@ export const translateMutation = ({ ? schemaType.getInterfaces().map(i => i.name) : []; const mutationTypeCypherDirective = getMutationCypherDirective(resolveInfo); + const mutationMeta = resolveInfo.schema + .getMutationType() + .getFields() + [resolveInfo.fieldName].astNode.directives.find(x => { + return x.name.value === 'MutationMeta'; + }); const params = initializeMutationParams({ + mutationMeta, resolveInfo, mutationTypeCypherDirective, first, otherParams, offset }); - const mutationInfo = { - params, - selections, - schemaType, - resolveInfo - }; if (mutationTypeCypherDirective) { return customMutation({ - ...mutationInfo, + resolveInfo, + schemaType, + selections, + params, context, mutationTypeCypherDirective, variableName, @@ -979,34 +984,64 @@ export const translateMutation = ({ }); } else if (isCreateMutation(resolveInfo)) { return nodeCreate({ - ...mutationInfo, + resolveInfo, + schemaType, + selections, + params, variableName, typeName, additionalLabels: additionalNodeLabels.concat(interfaceLabels) }); - } else if (isUpdateMutation(resolveInfo)) { - return nodeUpdate({ - ...mutationInfo, - variableName, - typeName, - additionalLabels: additionalNodeLabels - }); } else if (isDeleteMutation(resolveInfo)) { return nodeDelete({ - ...mutationInfo, + resolveInfo, + schemaType, + selections, + params, variableName, typeName, additionalLabels: additionalNodeLabels }); } else if (isAddMutation(resolveInfo)) { return relationshipCreate({ - ...mutationInfo, + resolveInfo, + schemaType, + selections, + params, context }); + } else if (isUpdateMutation(resolveInfo) || isMergeMutation(resolveInfo)) { + /** + * TODO: Once we are no longer using the @MutationMeta directive + * on relationship mutations, we will need to more directly identify + * whether this Merge mutation if for a node or relationship + */ + if (mutationMeta) { + return relationshipMergeOrUpdate({ + mutationMeta, + resolveInfo, + selections, + schemaType, + params, + context + }); + } else { + return nodeMergeOrUpdate({ + resolveInfo, + variableName, + typeName, + selections, + schemaType, + params, + additionalLabels: additionalNodeLabels + }); + } } else if (isRemoveMutation(resolveInfo)) { return relationshipDelete({ - ...mutationInfo, - variableName, + resolveInfo, + schemaType, + selections, + params, context }); } else { @@ -1102,7 +1137,8 @@ const nodeCreate = ({ args, statements, params, - paramKey: 'params' + paramKey: 'params', + resolveInfo }); const [subQuery, subParams] = buildCypherSelection({ selections, @@ -1118,63 +1154,6 @@ const nodeCreate = ({ return [query, params]; }; -const nodeUpdate = ({ - resolveInfo, - variableName, - typeName, - selections, - schemaType, - additionalLabels, - params -}) => { - const safeVariableName = safeVar(variableName); - const safeLabelName = safeLabel([typeName, ...additionalLabels]); - - const args = getMutationArguments(resolveInfo); - const primaryKeyArg = args[0]; - const primaryKeyArgName = primaryKeyArg.name.value; - const neo4jTypeArgs = getNeo4jTypeArguments(args); - const [primaryKeyParam, updateParams] = splitSelectionParameters( - params, - primaryKeyArgName, - 'params' - ); - const neo4jTypeClauses = neo4jTypePredicateClauses( - primaryKeyParam, - safeVariableName, - neo4jTypeArgs, - 'params' - ); - const predicateClauses = [...neo4jTypeClauses] - .filter(predicate => !!predicate) - .join(' AND '); - const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : ''; - let [preparedParams, paramUpdateStatements] = buildCypherParameters({ - args, - params: updateParams, - paramKey: 'params' - }); - let query = `MATCH (${safeVariableName}:${safeLabelName}${ - predicate !== '' - ? `) ${predicate} ` - : `{${primaryKeyArgName}: $params.${primaryKeyArgName}})` - } - `; - if (paramUpdateStatements.length > 0) { - query += `SET ${safeVariableName} += {${paramUpdateStatements.join(',')}} `; - } - const [subQuery, subParams] = buildCypherSelection({ - selections, - variableName, - schemaType, - resolveInfo - }); - preparedParams.params[primaryKeyArgName] = primaryKeyParam[primaryKeyArgName]; - params = { ...preparedParams, ...subParams }; - query += `RETURN ${safeVariableName} {${subQuery}} AS ${safeVariableName}`; - return [query, params]; -}; - const nodeDelete = ({ resolveInfo, selections, @@ -1196,7 +1175,7 @@ const nodeDelete = ({ safeVariableName, neo4jTypeArgs ); - let [preparedParams] = buildCypherParameters({ args, params }); + let [preparedParams] = buildCypherParameters({ args, params, resolveInfo }); let query = `MATCH (${safeVariableName}:${safeLabelName}${ neo4jTypeClauses.length > 0 ? `) WHERE ${neo4jTypeClauses.join(' AND ')}` @@ -1289,7 +1268,8 @@ const relationshipCreate = ({ const [preparedParams, paramStatements] = buildCypherParameters({ args: dataFields, params, - paramKey: 'data' + paramKey: 'data', + resolveInfo }); const schemaTypeName = safeVar(schemaType); const fromVariable = safeVar(fromVar); @@ -1357,7 +1337,6 @@ const relationshipCreate = ({ const relationshipDelete = ({ resolveInfo, selections, - variableName, schemaType, params, context @@ -1480,6 +1459,206 @@ const relationshipDelete = ({ return [query, params]; }; +const relationshipMergeOrUpdate = ({ + mutationMeta, + resolveInfo, + selections, + schemaType, + params, + context +}) => { + let query = ''; + let relationshipNameArg = undefined; + let fromTypeArg = undefined; + let toTypeArg = undefined; + try { + relationshipNameArg = mutationMeta.arguments.find(x => { + return x.name.value === 'relationship'; + }); + fromTypeArg = mutationMeta.arguments.find(x => { + return x.name.value === 'from'; + }); + toTypeArg = mutationMeta.arguments.find(x => { + return x.name.value === 'to'; + }); + } catch (e) { + throw new Error( + 'Missing required argument in MutationMeta directive (relationship, from, or to)' + ); + } + if (relationshipNameArg && fromTypeArg && toTypeArg) { + //TODO: need to handle one-to-one and one-to-many + const args = getMutationArguments(resolveInfo); + const typeMap = resolveInfo.schema.getTypeMap(); + const cypherParams = getCypherParams(context); + const fromType = fromTypeArg.value.value; + const fromVar = `${lowFirstLetter(fromType)}_from`; + const fromInputArg = args.find(e => e.name.value === 'from').type; + const fromInputAst = + typeMap[getNamedType(fromInputArg).type.name.value].astNode; + const fromFields = fromInputAst.fields; + const fromParam = fromFields[0].name.value; + const fromNodeNeo4jTypeArgs = getNeo4jTypeArguments(fromFields); + + const toType = toTypeArg.value.value; + const toVar = `${lowFirstLetter(toType)}_to`; + const toInputArg = args.find(e => e.name.value === 'to').type; + const toInputAst = + typeMap[getNamedType(toInputArg).type.name.value].astNode; + const toFields = toInputAst.fields; + const toParam = toFields[0].name.value; + const toNodeNeo4jTypeArgs = getNeo4jTypeArguments(toFields); + + const relationshipName = relationshipNameArg.value.value; + const lowercased = relationshipName.toLowerCase(); + const dataInputArg = args.find(e => e.name.value === 'data'); + const dataInputAst = dataInputArg + ? typeMap[getNamedType(dataInputArg.type).type.name.value].astNode + : undefined; + const dataFields = dataInputAst ? dataInputAst.fields : []; + + const [preparedParams, paramStatements] = buildCypherParameters({ + args: dataFields, + params, + paramKey: 'data', + resolveInfo + }); + const schemaTypeName = safeVar(schemaType); + const fromVariable = safeVar(fromVar); + const fromAdditionalLabels = getAdditionalLabels( + resolveInfo.schema.getType(fromType), + cypherParams + ); + const fromLabel = safeLabel([fromType, ...fromAdditionalLabels]); + const toVariable = safeVar(toVar); + const toAdditionalLabels = getAdditionalLabels( + resolveInfo.schema.getType(toType), + cypherParams + ); + const toLabel = safeLabel([toType, ...toAdditionalLabels]); + const relationshipVariable = safeVar(lowercased + '_relation'); + const relationshipLabel = safeLabel(relationshipName); + const fromNodeNeo4jTypeClauses = neo4jTypePredicateClauses( + preparedParams.from, + fromVariable, + fromNodeNeo4jTypeArgs, + 'from' + ); + const toNodeNeo4jTypeClauses = neo4jTypePredicateClauses( + preparedParams.to, + toVariable, + toNodeNeo4jTypeArgs, + 'to' + ); + const [subQuery, subParams] = buildCypherSelection({ + selections, + schemaType, + resolveInfo, + parentSelectionInfo: { + rootType: 'relationship', + from: fromVar, + to: toVar, + variableName: lowercased + }, + variableName: schemaType.name === fromType ? `${toVar}` : `${fromVar}`, + cypherParams: getCypherParams(context) + }); + let cypherOperation = ''; + if (isMergeMutation(resolveInfo)) { + cypherOperation = 'MERGE'; + } else if (isUpdateMutation(resolveInfo)) { + cypherOperation = 'MATCH'; + } + params = { ...preparedParams, ...subParams }; + query = ` + MATCH (${fromVariable}:${fromLabel}${ + fromNodeNeo4jTypeClauses && fromNodeNeo4jTypeClauses.length > 0 + ? // uses either a WHERE clause for managed type primary keys (temporal, etc.) + `) WHERE ${fromNodeNeo4jTypeClauses.join(' AND ')} ` + : // or a an internal matching clause for normal, scalar property primary keys + // NOTE this will need to change if we at some point allow for multi field node selection + ` {${fromParam}: $from.${fromParam}})` + } + MATCH (${toVariable}:${toLabel}${ + toNodeNeo4jTypeClauses && toNodeNeo4jTypeClauses.length > 0 + ? `) WHERE ${toNodeNeo4jTypeClauses.join(' AND ')} ` + : ` {${toParam}: $to.${toParam}})` + } + ${cypherOperation} (${fromVariable})-[${relationshipVariable}:${relationshipLabel}]->(${toVariable})${ + paramStatements.length > 0 + ? ` + SET ${relationshipVariable} += {${paramStatements.join(',')}} ` + : '' + } + RETURN ${relationshipVariable} { ${subQuery} } AS ${schemaTypeName}; + `; + } + return [query, params]; +}; + +const nodeMergeOrUpdate = ({ + resolveInfo, + variableName, + typeName, + selections, + schemaType, + additionalLabels, + params +}) => { + const safeVariableName = safeVar(variableName); + const safeLabelName = safeLabel([typeName, ...additionalLabels]); + const args = getMutationArguments(resolveInfo); + const primaryKeyArg = args[0]; + const primaryKeyArgName = primaryKeyArg.name.value; + const neo4jTypeArgs = getNeo4jTypeArguments(args); + const [primaryKeyParam, updateParams] = splitSelectionParameters( + params, + primaryKeyArgName, + 'params' + ); + const neo4jTypeClauses = neo4jTypePredicateClauses( + primaryKeyParam, + safeVariableName, + neo4jTypeArgs, + 'params' + ); + const predicateClauses = [...neo4jTypeClauses] + .filter(predicate => !!predicate) + .join(' AND '); + const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : ''; + let [preparedParams, paramUpdateStatements] = buildCypherParameters({ + args, + params: updateParams, + paramKey: 'params', + resolveInfo + }); + let cypherOperation = ''; + if (isMergeMutation(resolveInfo)) { + cypherOperation = 'MERGE'; + } else if (isUpdateMutation(resolveInfo)) { + cypherOperation = 'MATCH'; + } + let query = `${cypherOperation} (${safeVariableName}:${safeLabelName}${ + predicate !== '' + ? `) ${predicate} ` + : `{${primaryKeyArgName}: $params.${primaryKeyArgName}})` + } + `; + if (paramUpdateStatements.length > 0) { + query += `SET ${safeVariableName} += {${paramUpdateStatements.join(',')}} `; + } + const [subQuery, subParams] = buildCypherSelection({ + selections, + variableName, + schemaType, + resolveInfo + }); + preparedParams.params[primaryKeyArgName] = primaryKeyParam[primaryKeyArgName]; + params = { ...preparedParams, ...subParams }; + query += `RETURN ${safeVariableName} {${subQuery}} AS ${safeVariableName}`; + return [query, params]; +}; + const neo4jTypeOrderingClauses = (selections, innerSchemaType) => { // TODO use extractSelections instead? const selectedTypes = @@ -2049,16 +2228,6 @@ const translateNullFilter = ({ return `${nullFieldPredicate}${predicate}`; }; -//! case 1 -// filterOperationType, -// propertyPath - -//! case 2 -// filterOperationType, -// propertyPath, -// isListFilterArgument, -// parameterPath - const buildOperatorExpression = ({ filterOperationType, propertyPath, diff --git a/src/utils.js b/src/utils.js index ab5fcca0..8e79d55a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,9 +1,10 @@ -import { isObjectType, parse } from 'graphql'; +import { isObjectType, parse, GraphQLInt } from 'graphql'; import { v1 as neo4j } from 'neo4j-driver'; import _ from 'lodash'; import filter from 'lodash/filter'; import { Neo4jTypeName } from './augment/types/types'; import { SpatialType } from './augment/types/spatial'; +import { unwrapNamedType } from './augment/fields'; function parseArg(arg, variableValues) { switch (arg.value.kind) { @@ -165,6 +166,14 @@ export const isDeleteMutation = _isNamedMutation('delete'); export const isRemoveMutation = _isNamedMutation('remove'); +export const isMergeMutation = _isNamedMutation('merge'); + +const isRelationshipUpdateMutation = ({ resolveInfo, mutationMeta }) => + isUpdateMutation(resolveInfo) && !mutationMeta; + +const isRelationshipMergeMutation = ({ resolveInfo, mutationMeta }) => + isMergeMutation(resolveInfo) && !mutationMeta; + export function isMutation(resolveInfo) { return resolveInfo.operation.operation === 'mutation'; } @@ -356,7 +365,7 @@ export const computeOrderBy = (resolveInfo, schemaType) => { }; }; -export const possiblySetFirstId = ({ args, statements, params }) => { +export const possiblySetFirstId = ({ args, statements = [], params }) => { const arg = args.find(e => _getNamedType(e).name.value === 'ID'); // arg is the first ID field if it exists, and we set the value // if no value is provided for the field name (arg.name.value) in params @@ -392,17 +401,17 @@ export const getAdditionalLabels = (schemaType, cypherParams) => { return parsedLabels; }; -// TODO refactor export const buildCypherParameters = ({ args, statements = [], params, - paramKey + paramKey, + resolveInfo }) => { const dataParams = paramKey ? params[paramKey] : params; const paramKeys = dataParams ? Object.keys(dataParams) : []; if (args) { - statements = paramKeys.reduce((acc, paramName) => { + statements = paramKeys.reduce((paramStatements, paramName) => { const param = paramKey ? params[paramKey][paramName] : params[paramName]; // Get the AST definition for the argument matching this param name const fieldAst = args.find(arg => arg.name.value === paramName); @@ -410,87 +419,23 @@ export const buildCypherParameters = ({ const fieldType = _getNamedType(fieldAst.type); const fieldTypeName = fieldType.name.value; if (isNeo4jTypeInput(fieldTypeName)) { - const formatted = param.formatted; - const neo4jTypeConstructor = decideNeo4jTypeConstructor( - fieldTypeName - ); - if (neo4jTypeConstructor) { - // Prefer only using formatted, if provided - if (formatted) { - if (paramKey) params[paramKey][paramName] = formatted; - else params[paramName] = formatted; - acc.push( - `${paramName}: ${neo4jTypeConstructor}($${ - paramKey ? `${paramKey}.` : '' - }${paramName})` - ); - } else { - let neo4jTypeParam = {}; - if (Array.isArray(param)) { - const count = param.length; - let i = 0; - for (; i < count; ++i) { - neo4jTypeParam = param[i]; - const formatted = neo4jTypeParam.formatted; - if (neo4jTypeParam.formatted) { - paramKey - ? (params[paramKey][paramName] = formatted) - : (params[paramName] = formatted); - } else { - Object.keys(neo4jTypeParam).forEach(e => { - if (Number.isInteger(neo4jTypeParam[e])) { - paramKey - ? (params[paramKey][paramName][i][e] = neo4j.int( - neo4jTypeParam[e] - )) - : (params[paramName][i][e] = neo4j.int( - neo4jTypeParam[e] - )); - } - }); - } - } - acc.push( - `${paramName}: [value IN $${ - paramKey ? `${paramKey}.` : '' - }${paramName} | ${neo4jTypeConstructor}(value)]` - ); - } else { - neo4jTypeParam = paramKey - ? params[paramKey][paramName] - : params[paramName]; - const formatted = neo4jTypeParam.formatted; - if (neo4jTypeParam.formatted) { - paramKey - ? (params[paramKey][paramName] = formatted) - : (params[paramName] = formatted); - } else { - Object.keys(neo4jTypeParam).forEach(e => { - if (Number.isInteger(neo4jTypeParam[e])) { - paramKey - ? (params[paramKey][paramName][e] = neo4j.int( - neo4jTypeParam[e] - )) - : (params[paramName][e] = neo4j.int(neo4jTypeParam[e])); - } - }); - } - acc.push( - `${paramName}: ${neo4jTypeConstructor}($${ - paramKey ? `${paramKey}.` : '' - }${paramName})` - ); - } - } - } + paramStatements = buildNeo4jTypeCypherParameters({ + paramStatements, + params, + param, + paramKey, + paramName, + fieldTypeName, + resolveInfo + }); } else { // normal case - acc.push( + paramStatements.push( `${paramName}:$${paramKey ? `${paramKey}.` : ''}${paramName}` ); } } - return acc; + return paramStatements; }, statements); } if (paramKey) { @@ -499,6 +444,118 @@ export const buildCypherParameters = ({ return [params, statements]; }; +const buildNeo4jTypeCypherParameters = ({ + paramStatements, + params, + param, + paramKey, + paramName, + fieldTypeName, + resolveInfo +}) => { + const formatted = param.formatted; + const neo4jTypeConstructor = decideNeo4jTypeConstructor(fieldTypeName); + if (neo4jTypeConstructor) { + // Prefer only using formatted, if provided + if (formatted) { + if (paramKey) params[paramKey][paramName] = formatted; + else params[paramName] = formatted; + paramStatements.push( + `${paramName}: ${neo4jTypeConstructor}($${ + paramKey ? `${paramKey}.` : '' + }${paramName})` + ); + } else { + let neo4jTypeParam = {}; + if (Array.isArray(param)) { + const count = param.length; + let paramIndex = 0; + for (; paramIndex < count; ++paramIndex) { + neo4jTypeParam = param[paramIndex]; + if (neo4jTypeParam.formatted) { + const formatted = neo4jTypeParam.formatted; + if (paramKey) params[paramKey][paramName] = formatted; + else params[paramName] = formatted; + } else { + params = serializeNeo4jIntegers({ + paramKey, + fieldTypeName, + paramName, + paramIndex: paramIndex, + neo4jTypeParam, + params, + resolveInfo + }); + } + } + paramStatements.push( + `${paramName}: [value IN $${ + paramKey ? `${paramKey}.` : '' + }${paramName} | ${neo4jTypeConstructor}(value)]` + ); + } else { + if (paramKey) neo4jTypeParam = params[paramKey][paramName]; + else neo4jTypeParam = params[paramName]; + const formatted = neo4jTypeParam.formatted; + if (neo4jTypeParam.formatted) { + if (paramKey) params[paramKey][paramName] = formatted; + else params[paramName] = formatted; + } else { + params = serializeNeo4jIntegers({ + paramKey, + fieldTypeName, + paramName, + neo4jTypeParam, + params, + resolveInfo + }); + } + paramStatements.push( + `${paramName}: ${neo4jTypeConstructor}($${ + paramKey ? `${paramKey}.` : '' + }${paramName})` + ); + } + } + } + return paramStatements; +}; + +const serializeNeo4jIntegers = ({ + paramKey, + fieldTypeName, + paramName, + paramIndex, + neo4jTypeParam = {}, + params = {}, + resolveInfo +}) => { + const schema = resolveInfo.schema; + const neo4jTypeDefinition = schema.getType(fieldTypeName); + const neo4jTypeAst = neo4jTypeDefinition.astNode; + const neo4jTypeFields = neo4jTypeAst.fields; + Object.keys(neo4jTypeParam).forEach(paramFieldName => { + const fieldAst = neo4jTypeFields.find( + field => field.name.value === paramFieldName + ); + const unwrappedFieldType = unwrapNamedType({ type: fieldAst.type }); + const fieldTypeName = unwrappedFieldType.name; + if ( + fieldTypeName === GraphQLInt.name && + Number.isInteger(neo4jTypeParam[paramFieldName]) + ) { + const serialized = neo4j.int(neo4jTypeParam[paramFieldName]); + let param = params[paramName]; + if (paramIndex >= 0) { + if (paramKey) param = params[paramKey][paramName][paramIndex]; + else param = param[paramIndex]; + } else if (paramKey) param = params[paramKey][paramName]; + param[paramFieldName] = serialized; + } + }); + return params; +}; + // TODO refactor to handle Query/Mutation type schema directives const directiveWithArgs = (directiveName, args) => (schemaType, fieldName) => { function fieldDirective(schemaType, fieldName, directiveName) { @@ -647,6 +704,15 @@ export const getPrimaryKey = astNode => { if (!pk) { pk = firstField(fields); } + // Do not allow Point primary key + if (pk) { + const type = pk.type; + const unwrappedType = unwrapNamedType({ type }); + const typeName = unwrappedType.name; + if (isSpatialType(typeName) || typeName === SpatialType.POINT) { + pk = undefined; + } + } return pk; }; @@ -727,13 +793,16 @@ export const decideNestedVariableName = ({ }; export const initializeMutationParams = ({ + mutationMeta, resolveInfo, mutationTypeCypherDirective, otherParams, first, offset }) => { - return (isCreateMutation(resolveInfo) || isUpdateMutation(resolveInfo)) && + return (isCreateMutation(resolveInfo) || + isRelationshipUpdateMutation({ resolveInfo, mutationMeta }) || + isRelationshipMergeMutation({ resolveInfo, mutationMeta })) && !mutationTypeCypherDirective ? { params: otherParams, ...{ first, offset } } : { ...otherParams, ...{ first, offset } }; diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index f339e831..f0049b83 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -25,6 +25,7 @@ type Mutation { CreateState(name: String!): State UpdateMovie(movieId: ID!, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie DeleteMovie(movieId: ID!): Movie + MergeUser(userId: ID!, name: String): User currentUserId: String @cypher(statement: "RETURN $cypherParams.currentUserId") computedObjectWithCypherParams: currentUserId @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") computedStringList: [String] @cypher(statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList") @@ -75,6 +76,7 @@ type Mutation { CreateState: checkCypherMutation, UpdateMovie: checkCypherMutation, DeleteMovie: checkCypherMutation, + MergeUser: checkCypherMutation, currentUserId: checkCypherMutation, computedObjectWithCypherParams: checkCypherMutation, computedStringList: checkCypherMutation, @@ -196,10 +198,15 @@ export function augmentedSchemaCypherTestRunner( AddSpatialNodeSpatialNodes: checkCypherMutation, RemoveSpatialNodeSpatialNodes: checkCypherMutation, AddMovieGenres: checkCypherMutation, + MergeMovieGenres: checkCypherMutation, RemoveMovieGenres: checkCypherMutation, AddUserRated: checkCypherMutation, + MergeUserRated: checkCypherMutation, + UpdateUserRated: checkCypherMutation, RemoveUserRated: checkCypherMutation, AddUserFriends: checkCypherMutation, + MergeUserFriends: checkCypherMutation, + UpdateUserFriends: checkCypherMutation, RemoveUserFriends: checkCypherMutation, currentUserId: checkCypherMutation, computedObjectWithCypherParams: checkCypherMutation, diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 6c9c585f..e0a0419d 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -265,9 +265,9 @@ export const testSchema = /* GraphQL */ ` } type SpatialNode { - pointKey: Point + id: ID! point: Point - spatialNodes(pointKey: Point): [SpatialNode] + spatialNodes(point: Point): [SpatialNode] @relation(name: "SPATIAL", direction: OUT) } diff --git a/test/integration/integration.test.js b/test/integration/integration.test.js index b9eb30d0..2804f8fe 100644 --- a/test/integration/integration.test.js +++ b/test/integration/integration.test.js @@ -212,7 +212,7 @@ test('Mutation with @cypher directive (not-isolated)', async t => { }); }); -test('Create node mutation (not-isolated)', async t => { +test.serial('Create node mutation (not-isolated)', async t => { t.plan(1); let expected = { @@ -223,7 +223,13 @@ test('Create node mutation (not-isolated)', async t => { year: 2018, plot: 'An unending saga', poster: 'www.movieposter.com/img.png', - imdbRating: 1 + imdbRating: 1, + location: { + __typename: '_Neo4jPoint', + longitude: 46.870035, + latitude: -113.990976, + height: 12.3 + } } } }; @@ -239,12 +245,83 @@ test('Create node mutation (not-isolated)', async t => { plot: "An unending saga" poster: "www.movieposter.com/img.png" imdbRating: 1.0 + location: { + longitude: 46.870035 + latitude: -113.990976 + height: 12.3 + } ) { title year plot poster imdbRating + location { + longitude + latitude + height + } + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); +}); + +test.serial('Merge node mutation (not-isolated)', async t => { + t.plan(1); + + let expected = { + data: { + MergeMovie: { + __typename: 'Movie', + title: 'My Super Awesome Movie', + year: 2018, + plot: 'An unending saga', + poster: 'www.movieposter.com/img.png', + imdbRating: 1, + location: { + __typename: '_Neo4jPoint', + longitude: 46.870035, + latitude: -113.990976, + height: 12.3 + } + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation someMutation { + MergeMovie( + movieId: "12dd334d5zaaaa" + title: "My Super Awesome Movie" + year: 2018 + plot: "An unending saga" + poster: "www.movieposter.com/img.png" + imdbRating: 1.0 + location: { + longitude: 46.870035 + latitude: -113.990976 + height: 12.3 + } + ) { + title + year + plot + poster + imdbRating + location { + longitude + latitude + height + } } } ` @@ -358,6 +435,75 @@ test.serial('Add relationship mutation (not-isolated)', async t => { }); }); +test.serial('Merge relationship mutation (not-isolated)', async t => { + t.plan(1); + + let expected = { + data: { + MergeMovieGenre: { + __typename: 'Movie', + title: 'Chungking Express (Chung Hing sam lam)', + genres: [ + { + name: 'Mystery', + __typename: 'Genre' + }, + { + name: 'Drama', + __typename: 'Genre' + }, + { + name: 'Romance', + __typename: 'Genre' + }, + { + name: 'Action', + __typename: 'Genre' + } + ] + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation mergeGenreRelationToMovie( + $from: _MovieInput! + $to: _GenreInput! + ) { + MergeMovieGenres(from: $from, to: $to) { + from { + title + genres { + name + } + } + to { + name + } + } + } + `, + variables: { + from: { + movieId: '123' + }, + to: { + name: 'Action' + } + } + }) + .then(data => { + t.is(data.data.MergeMovieGenres.from.genres.length, 4); + // FIXME: Check length of genres array instead of exact response until ordering is implemented + //t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); +}); + test.serial('Remove relationship mutation (not-isolated)', async t => { t.plan(1); @@ -1334,6 +1480,94 @@ test.serial( } ); +test.serial( + 'Merge relationship with temporal property (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + MergeMovieRatings: { + __typename: '_MergeMovieRatingsPayload', + date: { + __typename: '_Neo4jDate', + formatted: '2018-12-18' + }, + rating: 9 + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation { + MergeMovieRatings( + from: { userId: 18 } + to: { movieId: 6683 } + data: { rating: 9, date: { year: 2018, month: 12, day: 18 } } + ) { + date { + formatted + } + rating + } + } + ` + }) + .then(data => { + t.deepEqual(data, expected); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Update relationship with temporal property (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + UpdateMovieRatings: { + __typename: '_UpdateMovieRatingsPayload', + date: { + __typename: '_Neo4jDate', + formatted: '2018-12-18' + }, + rating: 7 + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation { + UpdateMovieRatings( + from: { userId: 18 } + to: { movieId: 6683 } + data: { rating: 7, date: { year: 2018, month: 12, day: 18 } } + ) { + date { + formatted + } + rating + } + } + ` + }) + .then(data => { + t.deepEqual(data, expected); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + test.serial( 'Query for temporal property on relationship (not-isolated)', async t => { @@ -1352,7 +1586,7 @@ test.serial( __typename: '_Neo4jDate', formatted: '2018-12-18' }, - rating: 5 + rating: 7 } ] } @@ -1547,7 +1781,8 @@ test.serial( data: { CreateSpatialNode: { __typename: 'SpatialNode', - pointKey: { + id: 'xyz', + point: { __typename: '_Neo4jPoint', crs: 'wgs-84-3d', latitude: 20, @@ -1563,9 +1798,11 @@ test.serial( mutation: gql` mutation { CreateSpatialNode( - pointKey: { longitude: 10, latitude: 20, height: 30 } + id: "xyz" + point: { longitude: 10, latitude: 20, height: 30 } ) { - pointKey { + id + point { longitude latitude height @@ -1594,7 +1831,8 @@ test.serial( SpatialNode: [ { __typename: 'SpatialNode', - pointKey: { + id: 'xyz', + point: { __typename: '_Neo4jPoint', crs: 'wgs-84-3d', latitude: 20, @@ -1610,8 +1848,9 @@ test.serial( .query({ query: gql` { - SpatialNode(pointKey: { longitude: 10, latitude: 20, height: 30 }) { - pointKey { + SpatialNode(point: { longitude: 10, latitude: 20, height: 30 }) { + id + point { longitude latitude height @@ -1637,7 +1876,7 @@ test.serial('Spatial - filtering - field equal to given value', async t => { SpatialNode: [ { __typename: 'SpatialNode', - pointKey: { + point: { __typename: '_Neo4jPoint', crs: 'wgs-84-3d', latitude: 20, @@ -1653,9 +1892,9 @@ test.serial('Spatial - filtering - field equal to given value', async t => { query: gql` { SpatialNode( - filter: { pointKey: { longitude: 10, latitude: 20, height: 30 } } + filter: { point: { longitude: 10, latitude: 20, height: 30 } } ) { - pointKey { + point { longitude latitude height diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 97d18cac..f16595e7 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -182,7 +182,7 @@ test.cb('Test augmented schema', t => { filter: _TemporalNodeFilter ): [TemporalNode] @hasScope(scopes: ["TemporalNode: Read"]) SpatialNode( - pointKey: _Neo4jPointInput + id: ID point: _Neo4jPointInput _id: String first: Int @@ -1187,6 +1187,12 @@ test.cb('Test augmented schema', t => { ): _RemoveMovieGenresPayload @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + MergeMovieGenres( + from: _MovieInput! + to: _GenreInput! + ): _MergeMovieGenresPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Merge", "Genre: Merge"]) AddMovieActors( from: _ActorInput! to: _MovieInput! @@ -1199,6 +1205,12 @@ test.cb('Test augmented schema', t => { ): _RemoveMovieActorsPayload @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + MergeMovieActors( + from: _ActorInput! + to: _MovieInput! + ): _MergeMovieActorsPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Merge", "Movie: Merge"]) AddMovieFilmedIn( from: _MovieInput! to: _StateInput! @@ -1211,6 +1223,12 @@ test.cb('Test augmented schema', t => { ): _RemoveMovieFilmedInPayload @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") @hasScope(scopes: ["Movie: Delete", "State: Delete"]) + MergeMovieFilmedIn( + from: _MovieInput! + to: _StateInput! + ): _MergeMovieFilmedInPayload + @MutationMeta(relationship: "FILMED_IN", from: "Movie", to: "State") + @hasScope(scopes: ["Movie: Merge", "State: Merge"]) AddMovieRatings( from: _UserInput! to: _MovieInput! @@ -1224,6 +1242,20 @@ test.cb('Test augmented schema', t => { ): _RemoveMovieRatingsPayload @MutationMeta(relationship: "RATED", from: "User", to: "Movie") @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + UpdateMovieRatings( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _UpdateMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Update", "Movie: Update"]) + MergeMovieRatings( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _MergeMovieRatingsPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Merge", "Movie: Merge"]) CreateMovie( movieId: ID title: String @@ -1259,6 +1291,23 @@ test.cb('Test augmented schema', t => { releases: [_Neo4jDateTimeInput] ): Movie @hasScope(scopes: ["Movie: Update"]) DeleteMovie(movieId: ID!): Movie @hasScope(scopes: ["Movie: Delete"]) + MergeMovie( + movieId: ID! + title: String + someprefix_title_with_underscores: String + year: Int + released: _Neo4jDateTimeInput + plot: String + poster: String + imdbRating: Float + avgStars: Float + location: _Neo4jPointInput + locations: [_Neo4jPointInput] + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [_Neo4jDateTimeInput] + ): Movie @hasScope(scopes: ["Movie: Merge"]) AddGenreMovies( from: _MovieInput! to: _GenreInput! @@ -1271,6 +1320,12 @@ test.cb('Test augmented schema', t => { ): _RemoveGenreMoviesPayload @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") @hasScope(scopes: ["Movie: Delete", "Genre: Delete"]) + MergeGenreMovies( + from: _MovieInput! + to: _GenreInput! + ): _MergeGenreMoviesPayload + @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") + @hasScope(scopes: ["Movie: Merge", "Genre: Merge"]) CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) @@ -1287,11 +1342,19 @@ test.cb('Test augmented schema', t => { ): _RemoveActorMoviesPayload @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") @hasScope(scopes: ["Actor: Delete", "Movie: Delete"]) + MergeActorMovies( + from: _ActorInput! + to: _MovieInput! + ): _MergeActorMoviesPayload + @MutationMeta(relationship: "ACTED_IN", from: "Actor", to: "Movie") + @hasScope(scopes: ["Actor: Merge", "Movie: Merge"]) CreateActor(userId: ID, name: String): Actor @hasScope(scopes: ["Actor: Create"]) UpdateActor(userId: ID!, name: String): Actor @hasScope(scopes: ["Actor: Update"]) DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) + MergeActor(userId: ID!, name: String): Actor + @hasScope(scopes: ["Actor: Merge"]) AddUserRated( from: _UserInput! to: _MovieInput! @@ -1305,6 +1368,20 @@ test.cb('Test augmented schema', t => { ): _RemoveUserRatedPayload @MutationMeta(relationship: "RATED", from: "User", to: "Movie") @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + UpdateUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _UpdateUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Update", "Movie: Update"]) + MergeUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _MergeUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Merge", "Movie: Merge"]) AddUserFriends( from: _UserInput! to: _UserInput! @@ -1318,6 +1395,20 @@ test.cb('Test augmented schema', t => { ): _RemoveUserFriendsPayload @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") @hasScope(scopes: ["User: Delete", "User: Delete"]) + UpdateUserFriends( + from: _UserInput! + to: _UserInput! + data: _FriendOfInput! + ): _UpdateUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Update", "User: Update"]) + MergeUserFriends( + from: _UserInput! + to: _UserInput! + data: _FriendOfInput! + ): _MergeUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Merge", "User: Merge"]) AddUserFavorites( from: _UserInput! to: _MovieInput! @@ -1330,11 +1421,19 @@ test.cb('Test augmented schema', t => { ): _RemoveUserFavoritesPayload @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + MergeUserFavorites( + from: _UserInput! + to: _MovieInput! + ): _MergeUserFavoritesPayload + @MutationMeta(relationship: "FAVORITED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Merge", "Movie: Merge"]) CreateUser(userId: ID, name: String): User @hasScope(scopes: ["User: Create"]) UpdateUser(userId: ID!, name: String): User @hasScope(scopes: ["User: Update"]) DeleteUser(userId: ID!): User @hasScope(scopes: ["User: Delete"]) + MergeUser(userId: ID!, name: String): User + @hasScope(scopes: ["User: Merge"]) CreateBook(genre: BookGenre): Book @hasScope(scopes: ["Book: Create"]) DeleteBook(genre: BookGenre!): Book @hasScope(scopes: ["Book: Delete"]) CreatecurrentUserId(userId: String): currentUserId @@ -1361,6 +1460,16 @@ test.cb('Test augmented schema', t => { to: "TemporalNode" ) @hasScope(scopes: ["TemporalNode: Delete", "TemporalNode: Delete"]) + MergeTemporalNodeTemporalNodes( + from: _TemporalNodeInput! + to: _TemporalNodeInput! + ): _MergeTemporalNodeTemporalNodesPayload + @MutationMeta( + relationship: "TEMPORAL" + from: "TemporalNode" + to: "TemporalNode" + ) + @hasScope(scopes: ["TemporalNode: Merge", "TemporalNode: Merge"]) CreateTemporalNode( datetime: _Neo4jDateTimeInput name: String @@ -1381,6 +1490,15 @@ test.cb('Test augmented schema', t => { ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode @hasScope(scopes: ["TemporalNode: Delete"]) + MergeTemporalNode( + datetime: _Neo4jDateTimeInput! + name: String + time: _Neo4jTimeInput + date: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetimes: [_Neo4jLocalDateTimeInput] + ): TemporalNode @hasScope(scopes: ["TemporalNode: Merge"]) AddSpatialNodeSpatialNodes( from: _SpatialNodeInput! to: _SpatialNodeInput! @@ -1401,16 +1519,24 @@ test.cb('Test augmented schema', t => { to: "SpatialNode" ) @hasScope(scopes: ["SpatialNode: Delete", "SpatialNode: Delete"]) - CreateSpatialNode( - pointKey: _Neo4jPointInput - point: _Neo4jPointInput - ): SpatialNode @hasScope(scopes: ["SpatialNode: Create"]) - UpdateSpatialNode( - pointKey: _Neo4jPointInput! - point: _Neo4jPointInput - ): SpatialNode @hasScope(scopes: ["SpatialNode: Update"]) - DeleteSpatialNode(pointKey: _Neo4jPointInput!): SpatialNode + MergeSpatialNodeSpatialNodes( + from: _SpatialNodeInput! + to: _SpatialNodeInput! + ): _MergeSpatialNodeSpatialNodesPayload + @MutationMeta( + relationship: "SPATIAL" + from: "SpatialNode" + to: "SpatialNode" + ) + @hasScope(scopes: ["SpatialNode: Merge", "SpatialNode: Merge"]) + CreateSpatialNode(id: ID, point: _Neo4jPointInput): SpatialNode + @hasScope(scopes: ["SpatialNode: Create"]) + UpdateSpatialNode(id: ID!, point: _Neo4jPointInput): SpatialNode + @hasScope(scopes: ["SpatialNode: Update"]) + DeleteSpatialNode(id: ID!): SpatialNode @hasScope(scopes: ["SpatialNode: Delete"]) + MergeSpatialNode(id: ID!, point: _Neo4jPointInput): SpatialNode + @hasScope(scopes: ["SpatialNode: Merge"]) AddCasedTypeState( from: _CasedTypeInput! to: _StateInput! @@ -1423,6 +1549,12 @@ test.cb('Test augmented schema', t => { ): _RemoveCasedTypeStatePayload @MutationMeta(relationship: "FILMED_IN", from: "CasedType", to: "State") @hasScope(scopes: ["CasedType: Delete", "State: Delete"]) + MergeCasedTypeState( + from: _CasedTypeInput! + to: _StateInput! + ): _MergeCasedTypeStatePayload + @MutationMeta(relationship: "FILMED_IN", from: "CasedType", to: "State") + @hasScope(scopes: ["CasedType: Merge", "State: Merge"]) CreateCasedType(name: String): CasedType @hasScope(scopes: ["CasedType: Create"]) DeleteCasedType(name: String!): CasedType diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index 20fe6446..9552b212 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -672,6 +672,30 @@ test.cb('Create node mutation', t => { }); }); +test.cb('Merge node mutation', t => { + const graphQLQuery = `mutation { + MergeUser( + userId: "883c6b3e-3863-49a1-b190-1cee083c98b1", + name: "Michael" + ) { + userId + name + } + }`, + expectedCypherQuery = `MERGE (\`user\`:\`User\`{userId: $params.userId}) + SET \`user\` += {name:$params.name} RETURN \`user\` { .userId , .name } AS \`user\``; + + t.plan(2); + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + userId: '883c6b3e-3863-49a1-b190-1cee083c98b1', + name: 'Michael' + }, + first: -1, + offset: 0 + }); +}); + test.cb('Update node mutation', t => { const graphQLQuery = `mutation updateMutation { UpdateMovie(movieId: "12dd334d5", year: 2010) { @@ -754,6 +778,46 @@ test('Add relationship mutation', t => { ); }); +test('Merge relationship mutation', t => { + const graphQLQuery = `mutation someMutation { + MergeMovieGenres( + from: { movieId: "123" }, + to: { name: "Action" } + ) { + from { + movieId + genres { + _id + name + } + } + to { + name + } + } + }`, + expectedCypherQuery = ` + MATCH (\`movie_from\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: $from.movieId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + MERGE (\`movie_from\`)-[\`in_genre_relation\`:\`IN_GENRE\`]->(\`genre_to\`) + RETURN \`in_genre_relation\` { from: \`movie_from\` { .movieId ,genres: [(\`movie_from\`)-[:\`IN_GENRE\`]->(\`movie_from_genres\`:\`Genre\`) | movie_from_genres {_id: ID(\`movie_from_genres\`), .name }] } ,to: \`genre_to\` { .name } } AS \`_MergeMovieGenresPayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { movieId: '123' }, + to: { name: 'Action' }, + first: -1, + offset: 0 + }, + expectedCypherQuery, + {} + ); +}); + test('Add relationship mutation with GraphQL variables', t => { const graphQLQuery = `mutation someMutation($from: _MovieInput!) { AddMovieGenres( @@ -857,6 +921,138 @@ test('Add relationship mutation with relationship property', t => { ); }); +test('Merge relationship mutation with relationship property', t => { + const graphQLQuery = `mutation someMutation { + MergeUserRated( + from: { userId: "123" } + to: { movieId: "8" } + data: { rating: 9 } + ) { + from { + _id + userId + name + rated { + rating + Movie { + _id + movieId + title + } + } + } + to { + _id + movieId + title + ratings { + rating + User { + _id + userId + name + } + } + } + rating + } + }`, + expectedCypherQuery = ` + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`movie_to\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: $to.movieId}) + MERGE (\`user_from\`)-[\`rated_relation\`:\`RATED\`]->(\`movie_to\`) + SET \`rated_relation\` += {rating:$data.rating} + RETURN \`rated_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_Movie {_id: ID(\`user_from_rated_Movie\`), .movieId , .title }]) }] } ,to: \`movie_to\` {_id: ID(\`movie_to\`), .movieId , .title ,ratings: [(\`movie_to\`)<-[\`movie_to_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_to_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_to_ratings_relation\`]-(\`movie_to_ratings_User\`:\`User\`) | movie_to_ratings_User {_id: ID(\`movie_to_ratings_User\`), .userId , .name }]) }] } , .rating } AS \`_MergeUserRatedPayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { movieId: '456' }, + data: { rating: 9 }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + +test('Update relationship mutation with relationship property', t => { + const graphQLQuery = `mutation someMutation { + UpdateUserRated( + from: { userId: "123" } + to: { movieId: "2kljghd" } + data: { + rating: 1, + location: { + longitude: 3.0, + latitude: 4.5, + height: 12.5 + } + } + ) { + from { + _id + userId + name + rated { + rating + Movie { + _id + movieId + title + } + } + } + to { + _id + movieId + title + ratings { + rating + User { + _id + userId + name + } + } + } + rating + } + }`, + expectedCypherQuery = ` + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`movie_to\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: $to.movieId}) + MATCH (\`user_from\`)-[\`rated_relation\`:\`RATED\`]->(\`movie_to\`) + SET \`rated_relation\` += {rating:$data.rating,location: point($data.location)} + RETURN \`rated_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_Movie {_id: ID(\`user_from_rated_Movie\`), .movieId , .title }]) }] } ,to: \`movie_to\` {_id: ID(\`movie_to\`), .movieId , .title ,ratings: [(\`movie_to\`)<-[\`movie_to_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_to_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_to_ratings_relation\`]-(\`movie_to_ratings_User\`:\`User\`) | movie_to_ratings_User {_id: ID(\`movie_to_ratings_User\`), .userId , .name }]) }] } , .rating } AS \`_UpdateUserRatedPayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { movieId: '2kljghd' }, + data: { + rating: 1, + location: { + longitude: 3.0, + latitude: 4.5, + height: 12.5 + } + }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + test('Add reflexive relationship mutation with relationship property', t => { const graphQLQuery = `mutation { AddUserFriends( @@ -953,6 +1149,200 @@ test('Add reflexive relationship mutation with relationship property', t => { ); }); +test('Merge reflexive relationship mutation with relationship property', t => { + const graphQLQuery = `mutation { + MergeUserFriends( + from: { + userId: "123" + }, + to: { + userId: "456" + }, + data: { + since: 8 + } + ) { + from { + _id + userId + name + friends { + from { + since + User { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + } + to { + since + User { + _id + name + } + } + } + } + to { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + since + } + } + `, + expectedCypherQuery = ` + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`user_to\`:\`User\` {userId: $to.userId}) + MERGE (\`user_from\`)-[\`friend_of_relation\`:\`FRIEND_OF\`]->(\`user_to\`) + SET \`friend_of_relation\` += {since:$data.since} + RETURN \`friend_of_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,friends: {from: [(\`user_from\`)<-[\`user_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from\`:\`User\`) | user_from_from_relation { .since ,User: user_from_from {_id: ID(\`user_from_from\`), .name ,friends: {from: [(\`user_from_from\`)<-[\`user_from_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from_from\`:\`User\`) | user_from_from_from_relation { .since ,User: user_from_from_from {_id: ID(\`user_from_from_from\`), .name } }] ,to: [(\`user_from_from\`)-[\`user_from_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_from_to\`:\`User\`) | user_from_from_to_relation { .since ,User: user_from_from_to {_id: ID(\`user_from_from_to\`), .name } }] } } }] ,to: [(\`user_from\`)-[\`user_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_to\`:\`User\`) | user_from_to_relation { .since ,User: user_from_to {_id: ID(\`user_from_to\`), .name } }] } } ,to: \`user_to\` {_id: ID(\`user_to\`), .name ,friends: {from: [(\`user_to\`)<-[\`user_to_from_relation\`:\`FRIEND_OF\`]-(\`user_to_from\`:\`User\`) | user_to_from_relation { .since ,User: user_to_from {_id: ID(\`user_to_from\`), .name } }] ,to: [(\`user_to\`)-[\`user_to_to_relation\`:\`FRIEND_OF\`]->(\`user_to_to\`:\`User\`) | user_to_to_relation { .since ,User: user_to_to {_id: ID(\`user_to_to\`), .name } }] } } , .since } AS \`_MergeUserFriendsPayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { userId: '456' }, + data: { since: 8 }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + +test('Update reflexive relationship mutation with relationship property', t => { + const graphQLQuery = `mutation { + UpdateUserFriends( + from: { + userId: "123" + }, + to: { + userId: "456" + }, + data: { + since: 8 + } + ) { + from { + _id + userId + name + friends { + from { + since + User { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + } + to { + since + User { + _id + name + } + } + } + } + to { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + since + } + } + `, + expectedCypherQuery = ` + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`user_to\`:\`User\` {userId: $to.userId}) + MATCH (\`user_from\`)-[\`friend_of_relation\`:\`FRIEND_OF\`]->(\`user_to\`) + SET \`friend_of_relation\` += {since:$data.since} + RETURN \`friend_of_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,friends: {from: [(\`user_from\`)<-[\`user_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from\`:\`User\`) | user_from_from_relation { .since ,User: user_from_from {_id: ID(\`user_from_from\`), .name ,friends: {from: [(\`user_from_from\`)<-[\`user_from_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from_from\`:\`User\`) | user_from_from_from_relation { .since ,User: user_from_from_from {_id: ID(\`user_from_from_from\`), .name } }] ,to: [(\`user_from_from\`)-[\`user_from_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_from_to\`:\`User\`) | user_from_from_to_relation { .since ,User: user_from_from_to {_id: ID(\`user_from_from_to\`), .name } }] } } }] ,to: [(\`user_from\`)-[\`user_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_to\`:\`User\`) | user_from_to_relation { .since ,User: user_from_to {_id: ID(\`user_from_to\`), .name } }] } } ,to: \`user_to\` {_id: ID(\`user_to\`), .name ,friends: {from: [(\`user_to\`)<-[\`user_to_from_relation\`:\`FRIEND_OF\`]-(\`user_to_from\`:\`User\`) | user_to_from_relation { .since ,User: user_to_from {_id: ID(\`user_to_from\`), .name } }] ,to: [(\`user_to\`)-[\`user_to_to_relation\`:\`FRIEND_OF\`]->(\`user_to_to\`:\`User\`) | user_to_to_relation { .since ,User: user_to_to {_id: ID(\`user_to_to\`), .name } }] } } , .since } AS \`_UpdateUserFriendsPayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { userId: '456' }, + data: { since: 8 }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + test('Remove relationship mutation', t => { const graphQLQuery = `mutation someMutation { RemoveMovieGenres( @@ -1751,23 +2141,10 @@ test('Create node with temporal properties', t => { test('Create node with spatial properties', t => { const graphQLQuery = `mutation { CreateSpatialNode( - pointKey: { - x: 10, - y: 20, - z: 30 - }, - point: { - longitude: 40, - latitude: 50, - height: 60 - } + id: "xyz" + point: { longitude: 40, latitude: 50, height: 60 } ) { - pointKey { - x - y - z - crs - } + id point { longitude latitude @@ -1777,8 +2154,8 @@ test('Create node with spatial properties', t => { } }`, expectedCypherQuery = ` - CREATE (\`spatialNode\`:\`SpatialNode\` {pointKey: point($params.pointKey),point: point($params.point)}) - RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z , crs: \`spatialNode\`.pointKey.crs },point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\` + CREATE (\`spatialNode\`:\`SpatialNode\` {id:$params.id,point: point($params.point)}) + RETURN \`spatialNode\` { .id ,point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\` `; t.plan(1); @@ -1909,22 +2286,14 @@ test('Query node with temporal properties using temporal arguments', t => { ); }); -test('Query node with spatial properties using spatial arguments', t => { +test('Query node with spatial properties', t => { const graphQLQuery = `query { - SpatialNode( - pointKey: { - x: 10 - }, + SpatialNode( point: { - longitude: 40 + longitude: 40, latitude: 50, height: 60 } ) { - pointKey { - x - y - z - crs - } + id point { longitude latitude @@ -1933,7 +2302,7 @@ test('Query node with spatial properties using spatial arguments', t => { } } }`, - expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x AND \`spatialNode\`.point.longitude = $point.longitude RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z , crs: \`spatialNode\`.pointKey.crs },point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\``; + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.point.longitude = $point.longitude AND \`spatialNode\`.point.latitude = $point.latitude AND \`spatialNode\`.point.height = $point.height RETURN \`spatialNode\` { .id ,point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\``; t.plan(1); @@ -2108,30 +2477,22 @@ test('Nested Query with temporal property arguments', t => { test('Nested Query with spatial property arguments', t => { const graphQLQuery = `query { - SpatialNode( - pointKey: { - x: 50 - } - ) { - pointKey { - x - y - z + SpatialNode(point: { longitude: 1.5 }) { + point { + longitude + latitude + height } - spatialNodes( - pointKey: { - y: 20 - } - ) { - pointKey { - x - y - z + spatialNodes(point: { longitude: 40 }) { + point { + longitude + latitude + height } } } }`, - expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z },spatialNodes: [(\`spatialNode\`)-[:\`SPATIAL\`]->(\`spatialNode_spatialNodes\`:\`SpatialNode\`) WHERE spatialNode_spatialNodes.pointKey.y = $1_pointKey.y | spatialNode_spatialNodes {pointKey: { x: \`spatialNode_spatialNodes\`.pointKey.x , y: \`spatialNode_spatialNodes\`.pointKey.y , z: \`spatialNode_spatialNodes\`.pointKey.z }}] } AS \`spatialNode\``; + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.point.longitude = $point.longitude RETURN \`spatialNode\` {point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height },spatialNodes: [(\`spatialNode\`)-[:\`SPATIAL\`]->(\`spatialNode_spatialNodes\`:\`SpatialNode\`) WHERE spatialNode_spatialNodes.point.longitude = $1_point.longitude | spatialNode_spatialNodes {point: { longitude: \`spatialNode_spatialNodes\`.point.longitude , latitude: \`spatialNode_spatialNodes\`.point.latitude , height: \`spatialNode_spatialNodes\`.point.height }}] } AS \`spatialNode\``; t.plan(1); @@ -2232,27 +2593,25 @@ test('Update temporal and non-temporal properties on node using temporal propert ); }); -test('Update node spatial property using spatial property node selection', t => { +test('Update node spatial property', t => { const graphQLQuery = `mutation { UpdateSpatialNode( - pointKey: { - y: 60 - } + id: "xyz", point: { - x: 100, - y: 200, - z: 300 + longitude: 100, + latitude: 200, + height: 300 } ) { point { - x - y - z + longitude + latitude + height } } }`, - expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.y = $params.pointKey.y - SET \`spatialNode\` += {point: point($params.point)} RETURN \`spatialNode\` {point: { x: \`spatialNode\`.point.x , y: \`spatialNode\`.point.y , z: \`spatialNode\`.point.z }} AS \`spatialNode\``; + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`{id: $params.id}) + SET \`spatialNode\` += {point: point($params.point)} RETURN \`spatialNode\` {point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height }} AS \`spatialNode\``; t.plan(1); @@ -2405,37 +2764,6 @@ RETURN \`temporalNode\``; ); }); -test('Delete node using spatial property node selection', t => { - const graphQLQuery = `mutation { - DeleteSpatialNode( - pointKey: { - x: 50 - } - ) { - _id - pointKey { - x - y - z - } - } - }`, - expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x -WITH \`spatialNode\` AS \`spatialNode_toDelete\`, \`spatialNode\` {_id: ID(\`spatialNode\`),pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z }} AS \`spatialNode\` -DETACH DELETE \`spatialNode_toDelete\` -RETURN \`spatialNode\``; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - test('Add relationship mutation using temporal property node selection', t => { const graphQLQuery = `mutation { AddTemporalNodeTemporalNodes( @@ -2594,42 +2922,6 @@ test('Add relationship mutation using temporal property node selection', t => { ); }); -test('Add relationship mutation using spatial property node selection', t => { - const graphQLQuery = `mutation { - AddSpatialNodeSpatialNodes( - from: { pointKey: { x: 50 } } - to: { pointKey: { y: 20 } } - ) { - from { - pointKey { - x - } - } - to { - pointKey { - y - } - } - } - }`, - expectedCypherQuery = ` - MATCH (\`spatialNode_from\`:\`SpatialNode\`) WHERE \`spatialNode_from\`.pointKey.x = $from.pointKey.x - MATCH (\`spatialNode_to\`:\`SpatialNode\`) WHERE \`spatialNode_to\`.pointKey.y = $to.pointKey.y - CREATE (\`spatialNode_from\`)-[\`spatial_relation\`:\`SPATIAL\`]->(\`spatialNode_to\`) - RETURN \`spatial_relation\` { from: \`spatialNode_from\` {pointKey: { x: \`spatialNode_from\`.pointKey.x }} ,to: \`spatialNode_to\` {pointKey: { y: \`spatialNode_to\`.pointKey.y }} } AS \`_AddSpatialNodeSpatialNodesPayload\`; - `; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - test('Remove relationship mutation using temporal property node selection', t => { const graphQLQuery = `mutation { RemoveTemporalNodeTemporalNodes( @@ -2790,44 +3082,6 @@ test('Remove relationship mutation using temporal property node selection', t => ); }); -test('Remove relationship mutation using spatial property node selection', t => { - const graphQLQuery = `mutation { - RemoveSpatialNodeSpatialNodes( - from: { pointKey: { x: 50 } } - to: { pointKey: { y: 20 } } - ) { - from { - pointKey { - x - } - } - to { - pointKey { - y - } - } - } - }`, - expectedCypherQuery = ` - MATCH (\`spatialNode_from\`:\`SpatialNode\`) WHERE \`spatialNode_from\`.pointKey.x = $from.pointKey.x - MATCH (\`spatialNode_to\`:\`SpatialNode\`) WHERE \`spatialNode_to\`.pointKey.y = $to.pointKey.y - OPTIONAL MATCH (\`spatialNode_from\`)-[\`spatialNode_fromspatialNode_to\`:\`SPATIAL\`]->(\`spatialNode_to\`) - DELETE \`spatialNode_fromspatialNode_to\` - WITH COUNT(*) AS scope, \`spatialNode_from\` AS \`_spatialNode_from\`, \`spatialNode_to\` AS \`_spatialNode_to\` - RETURN {from: \`_spatialNode_from\` {pointKey: { x: \`_spatialNode_from\`.pointKey.x }} ,to: \`_spatialNode_to\` {pointKey: { y: \`_spatialNode_to\`.pointKey.y }} } AS \`_RemoveSpatialNodeSpatialNodesPayload\`; - `; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - test('Query relationship with temporal properties', t => { const graphQLQuery = `query { Movie { @@ -2858,32 +3112,6 @@ test('Query relationship with temporal properties', t => { ); }); -test('Query relationship with spatial properties', t => { - const graphQLQuery = `query { - User { - rated { - location { - x - y - z - srid - } - } - } - }`, - expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`]->(:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | user_rated_relation {location: { x: \`user_rated_relation\`.location.x , y: \`user_rated_relation\`.location.y , z: \`user_rated_relation\`.location.z , srid: \`user_rated_relation\`.location.srid }}] } AS \`user\``; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - test('Add relationship mutation with temporal properties', t => { const graphQLQuery = `mutation { AddUserRated( @@ -3037,25 +3265,21 @@ test('Add relationship mutation with temporal properties', t => { test('Add relationship mutation with spatial properties', t => { const graphQLQuery = `mutation { AddUserRated( - from: { - userId: "6973aff4-3113-45b0-9ce4-9879f0077b46" - }, - to: { - movieId: "6f565c2a-cf1b-4969-951e-d0adade1e48c" - }, - data: { - rating: 10, + from: { userId: "123" } + to: { movieId: "2kljghd" } + data: { + rating: 10, location: { - x: 10, - y: 20, - z: 30 + longitude: 10, + latitude: 20.3, + height: 30.2 } } ) { location { - x - y - z + longitude + latitude + height } from { _id @@ -3069,7 +3293,7 @@ test('Add relationship mutation with spatial properties', t => { MATCH (\`user_from\`:\`User\` {userId: $from.userId}) MATCH (\`movie_to\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\` {movieId: $to.movieId}) CREATE (\`user_from\`)-[\`rated_relation\`:\`RATED\` {rating:$data.rating,location: point($data.location)}]->(\`movie_to\`) - RETURN \`rated_relation\` { location: { x: \`rated_relation\`.location.x , y: \`rated_relation\`.location.y , z: \`rated_relation\`.location.z },from: \`user_from\` {_id: ID(\`user_from\`)} ,to: \`movie_to\` {_id: ID(\`movie_to\`)} } AS \`_AddUserRatedPayload\`; + RETURN \`rated_relation\` { location: { longitude: \`rated_relation\`.location.longitude , latitude: \`rated_relation\`.location.latitude , height: \`rated_relation\`.location.height },from: \`user_from\` {_id: ID(\`user_from\`)} ,to: \`movie_to\` {_id: ID(\`movie_to\`)} } AS \`_AddUserRatedPayload\`; `; t.plan(1); @@ -3083,6 +3307,32 @@ test('Add relationship mutation with spatial properties', t => { ); }); +test('Query relationship with spatial properties', t => { + const graphQLQuery = `query { + User { + rated { + location { + longitude + latitude + height + srid + } + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`]->(:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | user_rated_relation {location: { longitude: \`user_rated_relation\`.location.longitude , latitude: \`user_rated_relation\`.location.latitude , height: \`user_rated_relation\`.location.height , srid: \`user_rated_relation\`.location.srid }}] } AS \`user\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Add relationship mutation with list properties', t => { const graphQLQuery = `mutation { AddUserRated(