From 3f293bcd7ab77b9ec1fae522a842109828f9a156 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:00:31 -0800 Subject: [PATCH 01/11] removed Point primary key --- example/apollo-server/movies-schema.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 From 9bca89a3812965f7fbc59dfee30106df36010c15 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:01:09 -0800 Subject: [PATCH 02/11] Added merge node mutation --- src/augment/types/node/mutation.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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 From cc487e414500e9b63ca46fe0bc530500bfab6bf9 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:01:39 -0800 Subject: [PATCH 03/11] Added merge relationship mutation --- src/augment/types/relationship/mutation.js | 55 +++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) 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, From e20de8fde7edb78973a1362489d3287ba74e96d7 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:05:56 -0800 Subject: [PATCH 04/11] Merge operation support This was implemented by unifying the merge logic with the update logic, with some minor branching used to capture translation differences. --- src/translate.js | 349 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 259 insertions(+), 90 deletions(-) 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, From a8b944f1f7e6996c6563e8bca5e3d5a7d0d1b58b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:11:02 -0800 Subject: [PATCH 05/11] Merge helpers, Point input serialization fix Adds predicate functions for merge operations. Fixes improper use of neo4j.int on whole number values provided to Float type fields on the Point type, in buildCypherParameters. For example, while a { height: 3.5 } param would be kept as { height: 3.5 }, if one were to provide { height: 3 }, then it would be inappropriately serialized to { height: { low: 3, high: 0 } }. --- src/utils.js | 231 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 81 deletions(-) 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 } }; From 65b56d92c93e9ac93bb8b7f863ffc00acf08958d Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:11:50 -0800 Subject: [PATCH 06/11] Merge mutation test resolvers --- test/helpers/cypherTestHelpers.js | 7 +++++++ 1 file changed, 7 insertions(+) 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, From 5b30a2d2c21b2d8d966260ffafe1a70f5fe5f49e Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:12:14 -0800 Subject: [PATCH 07/11] Removal of Point type primary key --- test/helpers/testSchema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) } From cf0f133391ddf5ea7663e8342c3d4d7f3b39e9b8 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:15:43 -0800 Subject: [PATCH 08/11] Merge mutation tests Also changes the use of the pointKey Point type primary key on SpatialNode to use the non-pk 'point' field, since SpatialNode now has an 'id: ID' primary key. --- test/integration/integration.test.js | 261 +++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 11 deletions(-) diff --git a/test/integration/integration.test.js b/test/integration/integration.test.js index b9eb30d0..ab768a4c 100644 --- a/test/integration/integration.test.js +++ b/test/integration/integration.test.js @@ -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('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 From 3e9cd59e433380a610af4f90bff32423628656fc Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:17:55 -0800 Subject: [PATCH 09/11] Merge mutation fields Also updates the SpatialNode type to have an 'id: ID' primary key, given that the Point type should not be used --- test/unit/augmentSchemaTest.test.js | 152 ++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 10 deletions(-) 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 From dde835267f45a7fac358a27b23b326282bb228d1 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:18:26 -0800 Subject: [PATCH 10/11] Merge mutation translation tests --- test/unit/cypherTest.test.js | 666 ++++++++++++++++++++++++----------- 1 file changed, 458 insertions(+), 208 deletions(-) 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( From e1ac087724a0bcca00d53e9a5c2ffc0899b07eaa Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:33:39 -0800 Subject: [PATCH 11/11] Create and Merge node should use test.serial --- test/integration/integration.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/integration.test.js b/test/integration/integration.test.js index ab768a4c..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 = { @@ -273,7 +273,7 @@ test('Create node mutation (not-isolated)', async t => { }); }); -test('Merge node mutation (not-isolated)', async t => { +test.serial('Merge node mutation (not-isolated)', async t => { t.plan(1); let expected = {