diff --git a/src/index.js b/src/index.js index f6604cd4..d581853c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,9 @@ import { getFieldValueType, extractTypeMapFromTypeDefs, addDirectiveDeclarations, - printTypeMap + printTypeMap, + safeLabel, + safeVar } from './utils'; import { buildCypherSelection } from './selections'; import { @@ -132,14 +134,16 @@ export function cypherQuery( query = `WITH apoc.cypher.runFirstColumn("${ cypherQueryArg.value.value - }", ${argString}, True) AS x UNWIND x AS ${variableName} - RETURN ${variableName} {${subQuery}} AS ${variableName}${orderByValue} ${outerSkipLimit}`; + }", ${argString}, True) AS x UNWIND x AS ${safeVar(variableName)} + RETURN ${safeVar(variableName)} {${subQuery}} AS ${safeVar( + variableName + )}${orderByValue} ${outerSkipLimit}`; } else { // No @cypher directive on QueryType // FIXME: support IN for multiple values -> WHERE const idWherePredicate = - typeof _id !== 'undefined' ? `ID(${variableName})=${_id}` : ''; + typeof _id !== 'undefined' ? `ID(${safeVar(variableName)})=${_id}` : ''; const nullFieldPredicates = Object.keys(nullParams).map( key => `${variableName}.${key} IS NULL` ); @@ -149,9 +153,12 @@ export function cypherQuery( const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : ''; query = - `MATCH (${variableName}:${typeName} ${argString}) ${predicate}` + - `RETURN ${variableName} {${subQuery}} AS ${variableName}${orderByValue} ${outerSkipLimit}`; - + `MATCH (${safeVar(variableName)}:${safeLabel( + typeName + )} ${argString}) ${predicate}` + + `RETURN ${safeVar(variableName)} {${subQuery}} AS ${safeVar( + variableName + )}${orderByValue} ${outerSkipLimit}`; } return [query, { ...nonNullParams, ...subParams }]; @@ -227,10 +234,12 @@ export function cypherMutation( cypherQueryArg.value.value }", ${argString}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS ${variableName} - RETURN ${variableName} {${subQuery}} AS ${variableName}${orderByValue} ${outerSkipLimit}`; + RETURN ${safeVar(variableName)} {${subQuery}} AS ${safeVar( + variableName + )}${orderByValue} ${outerSkipLimit}`; } else if (isCreateMutation(resolveInfo)) { - query = `CREATE (${variableName}:${typeName}) `; - query += `SET ${variableName} = $params `; + query = `CREATE (${safeVar(variableName)}:${safeLabel(typeName)}) `; + query += `SET ${safeVar(variableName)} = $params `; //query += `RETURN ${variable}`; const [subQuery, subParams] = buildCypherSelection({ @@ -247,7 +256,7 @@ export function cypherMutation( resolveInfo.fieldName ].astNode.arguments; - const firstIdArg = args.find(e => getFieldValueType(e) === "ID"); + const firstIdArg = args.find(e => getFieldValueType(e) === 'ID'); if (firstIdArg) { const firstIdArgFieldName = firstIdArg.name.value; if (params.params[firstIdArgFieldName] === undefined) { @@ -255,7 +264,9 @@ export function cypherMutation( } } - query += `RETURN ${variableName} {${subQuery}} AS ${variableName}`; + query += `RETURN ${safeVar(variableName)} {${subQuery}} AS ${safeVar( + variableName + )}`; } else if (isAddMutation(resolveInfo)) { let mutationMeta, relationshipNameArg, fromTypeArg, toTypeArg; @@ -332,25 +343,35 @@ export function cypherMutation( paramIndex: 1, rootVariableNames: { from: `${fromVar}`, - to: `${toVar}`, + to: `${toVar}` }, variableName: schemaType.name === fromType ? `${toVar}` : `${fromVar}` }); params = { ...params, ...subParams }; query = ` - MATCH (${fromVar}:${fromType} {${fromParam}: $from.${fromParam}}) - MATCH (${toVar}:${toType} {${toParam}: $to.${toParam}}) - CREATE (${fromVar})-[${lowercased}_relation:${relationshipName}${ + MATCH (${safeVar(fromVar)}:${safeLabel( + fromType + )} {${fromParam}: $from.${fromParam}}) + MATCH (${safeVar(toVar)}:${safeLabel( + toType + )} {${toParam}: $to.${toParam}}) + CREATE (${safeVar(fromVar)})-[${safeVar( + lowercased + '_relation' + )}:${safeLabel(relationshipName)}${ relationPropertyArguments ? ` {${relationPropertyArguments}}` : '' - }]->(${toVar}) - RETURN ${lowercased}_relation { ${subQuery} } AS ${schemaType}; + }]->(${safeVar(toVar)}) + RETURN ${safeVar(lowercased + '_relation')} { ${subQuery} } AS ${safeVar( + schemaType + )}; `; } else if (isUpdateMutation(resolveInfo)) { const idParam = resolveInfo.schema.getMutationType().getFields()[ resolveInfo.fieldName ].astNode.arguments[0].name.value; - query = `MATCH (${variableName}:${typeName} {${idParam}: $params.${ + query = `MATCH (${safeVar(variableName)}:${safeLabel( + typeName + )} {${idParam}: $params.${ resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName] .astNode.arguments[0].name.value }}) `; @@ -366,7 +387,9 @@ export function cypherMutation( }); params = { ...params, ...subParams }; - query += `RETURN ${variableName} {${subQuery}} AS ${variableName}`; + query += `RETURN ${safeVar(variableName)} {${subQuery}} AS ${safeVar( + variableName + )}`; } else if (isDeleteMutation(resolveInfo)) { const idParam = resolveInfo.schema.getMutationType().getFields()[ resolveInfo.fieldName @@ -384,14 +407,17 @@ export function cypherMutation( // Cannot execute a map projection on a deleted node in Neo4j // so the projection is executed and aliased before the delete - query = `MATCH (${variableName}:${typeName} {${idParam}: $${ + query = `MATCH (${safeVar(variableName)}:${safeLabel( + typeName + )} {${idParam}: $${ resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName] .astNode.arguments[0].name.value }}) -WITH ${variableName} AS ${variableName + - '_toDelete'}, ${variableName} {${subQuery}} AS ${variableName} -DETACH DELETE ${variableName + '_toDelete'} -RETURN ${variableName}`; +WITH ${safeVar(variableName)} AS ${safeVar( + variableName + '_toDelete' + )}, ${safeVar(variableName)} {${subQuery}} AS ${safeVar(variableName)} +DETACH DELETE ${safeVar(variableName + '_toDelete')} +RETURN ${safeVar(variableName)}`; } else if (isRemoveMutation(resolveInfo)) { let mutationMeta, relationshipNameArg, fromTypeArg, toTypeArg; @@ -460,7 +486,7 @@ RETURN ${variableName}`; from: `_${fromVar}`, to: `_${toVar}` }, - variableName: schemaType.name === fromType ? `_${toVar}` : `_${fromVar}`, + variableName: schemaType.name === fromType ? `_${toVar}` : `_${fromVar}` }); params = { ...params, ...subParams }; @@ -470,13 +496,20 @@ RETURN ${variableName}`; // object construction into a WITH statement above the DELETE, then return it // the delete query = ` - MATCH (${fromVar}:${fromType} {${fromParam}: $from.${fromParam}}) - MATCH (${toVar}:${toType} {${toParam}: $to.${toParam}}) - OPTIONAL MATCH (${fromVar})-[${fromVar + - toVar}:${relationshipName}]->(${toVar}) - DELETE ${fromVar + toVar} - WITH COUNT(*) AS scope, ${fromVar} AS _${fromVar}, ${toVar} AS _${toVar} - RETURN {${subQuery}} AS ${schemaType}; + MATCH (${safeVar(fromVar)}:${safeLabel( + fromType + )} {${fromParam}: $from.${fromParam}}) + MATCH (${safeVar(toVar)}:${safeLabel( + toType + )} {${toParam}: $to.${toParam}}) + OPTIONAL MATCH (${safeVar(fromVar)})-[${safeVar( + fromVar + toVar + )}:${safeLabel(relationshipName)}]->(${safeVar(toVar)}) + DELETE ${safeVar(fromVar + toVar)} + WITH COUNT(*) AS scope, ${safeVar(fromVar)} AS ${safeVar( + '_' + fromVar + )}, ${safeVar(toVar)} AS ${safeVar('_' + toVar)} + RETURN {${subQuery}} AS ${safeVar(schemaType)}; `; } else { // throw error - don't know how to handle this type of mutation @@ -527,9 +560,9 @@ export const makeAugmentedSchema = ({ }); }; -export const augmentTypeDefs = (typeDefs) => { +export const augmentTypeDefs = typeDefs => { const typeMap = extractTypeMapFromTypeDefs(typeDefs); // overwrites any provided declarations of system directives const augmented = addDirectiveDeclarations(typeMap); return printTypeMap(augmented); -} +}; diff --git a/src/selections.js b/src/selections.js index f7bc5d0a..ace85524 100644 --- a/src/selections.js +++ b/src/selections.js @@ -11,13 +11,15 @@ import { relationDirective, getRelationTypeDirectiveArgs, decideNestedVariableName, + safeLabel, + safeVar } from './utils'; import { customCypherField, relationFieldOnNodeType, relationTypeFieldOnNodeType, - nodeTypeFieldOnRelationType, + nodeTypeFieldOnRelationType } from './translate'; export function buildCypherSelection({ @@ -27,7 +29,7 @@ export function buildCypherSelection({ schemaType, resolveInfo, paramIndex = 1, - rootVariableNames, + rootVariableNames }) { if (!selections.length) { return [initial, {}]; @@ -51,7 +53,7 @@ export function buildCypherSelection({ selections: tailSelections, variableName, schemaType, - resolveInfo, + resolveInfo }; const recurse = args => { @@ -76,7 +78,7 @@ export function buildCypherSelection({ selections: fragmentSelections, variableName, schemaType, - resolveInfo, + resolveInfo }; return recurse({ initial: fragmentSelections.length @@ -136,7 +138,9 @@ export function buildCypherSelection({ // Database meta fields(_id) if (fieldName === '_id') { return recurse({ - initial: `${initial}${fieldName}: ID(${variableName})${commaIfTail}`, + initial: `${initial}${fieldName}: ID(${safeVar( + variableName + )})${commaIfTail}`, ...tailParams }); } @@ -166,7 +170,9 @@ export function buildCypherSelection({ } // We have a graphql object type const innerSchemaTypeAstNode = typeMap[innerSchemaType].astNode; - const innerSchemaTypeRelation = getRelationTypeDirectiveArgs(innerSchemaTypeAstNode); + const innerSchemaTypeRelation = getRelationTypeDirectiveArgs( + innerSchemaTypeAstNode + ); const schemaTypeRelation = getRelationTypeDirectiveArgs(schemaTypeAstNode); const { name: relType, direction: relDirection } = relationDirective( schemaType, @@ -175,12 +181,12 @@ export function buildCypherSelection({ const nestedVariable = decideNestedVariableName({ schemaTypeRelation, - innerSchemaTypeRelation, + innerSchemaTypeRelation, variableName, fieldName, rootVariableNames }); - + const skipLimit = computeSkipLimit(headSelection, resolveInfo.variableValues); const subSelections = extractSelections( @@ -193,64 +199,69 @@ export function buildCypherSelection({ selections: subSelections, variableName: nestedVariable, schemaType: innerSchemaType, - resolveInfo, + resolveInfo }); let selection; const queryParams = innerFilterParams(filterParams); const fieldInfo = { initial, - fieldName, - fieldType, - variableName, - nestedVariable, - queryParams, - subSelection, - skipLimit, + fieldName, + fieldType, + variableName, + nestedVariable, + queryParams, + subSelection, + skipLimit, commaIfTail, - tailParams, + tailParams }; if (customCypher) { // Object type field with cypher directive - selection = recurse(customCypherField({ - ...fieldInfo, - schemaType, - schemaTypeRelation, - customCypher, - headSelection, - resolveInfo, - })); - } - else if(relType && relDirection) { + selection = recurse( + customCypherField({ + ...fieldInfo, + schemaType, + schemaTypeRelation, + customCypher, + headSelection, + resolveInfo + }) + ); + } else if (relType && relDirection) { // Object type field with relation directive - selection = recurse(relationFieldOnNodeType({ - ...fieldInfo, - relDirection, - relType, - isInlineFragment, - interfaceLabel, - innerSchemaType, - })); - } - else if (schemaTypeRelation) { + selection = recurse( + relationFieldOnNodeType({ + ...fieldInfo, + relDirection, + relType, + isInlineFragment, + interfaceLabel, + innerSchemaType + }) + ); + } else if (schemaTypeRelation) { // Object type field on relation type // (from, to, renamed, relation mutation payloads...) - selection = recurse(nodeTypeFieldOnRelationType({ - fieldInfo, - rootVariableNames, - schemaTypeRelation, - innerSchemaType, - isInlineFragment, - interfaceLabel, - })); - } - else if(innerSchemaTypeRelation) { + selection = recurse( + nodeTypeFieldOnRelationType({ + fieldInfo, + rootVariableNames, + schemaTypeRelation, + innerSchemaType, + isInlineFragment, + interfaceLabel + }) + ); + } else if (innerSchemaTypeRelation) { // Relation type field on node type (field payload types...) - selection = recurse(relationTypeFieldOnNodeType({ - ...fieldInfo, - innerSchemaTypeRelation, - schemaType, - })); + selection = recurse( + relationTypeFieldOnNodeType({ + ...fieldInfo, + innerSchemaTypeRelation, + schemaType + }) + ); } return [selection[0], { ...selection[1], ...subSelection[1] }]; } diff --git a/src/translate.js b/src/translate.js index 75db2672..fcd03e85 100644 --- a/src/translate.js +++ b/src/translate.js @@ -1,7 +1,4 @@ -import { - isArrayType, - cypherDirectiveArgs -} from './utils'; +import { isArrayType, cypherDirectiveArgs, safeLabel, safeVar } from './utils'; export const customCypherField = ({ customCypher, @@ -35,44 +32,44 @@ export const customCypherField = ({ )}, true) | ${nestedVariable} {${subSelection[0]}}]${ fieldIsList ? '' : ')' }${skipLimit} ${commaIfTail}`, - ...tailParams, + ...tailParams }; -} +}; export const relationFieldOnNodeType = ({ - initial, - fieldName, - fieldType, - variableName, - relDirection, + initial, + fieldName, + fieldType, + variableName, + relDirection, relType, - nestedVariable, - isInlineFragment, - interfaceLabel, - innerSchemaType, - queryParams, - subSelection, - skipLimit, + nestedVariable, + isInlineFragment, + interfaceLabel, + innerSchemaType, + queryParams, + subSelection, + skipLimit, commaIfTail, tailParams }) => { return { initial: `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})${ + }[(${safeVar(variableName)})${ relDirection === 'in' || relDirection === 'IN' ? '<' : '' - }-[:${relType}]-${ + }-[:${safeLabel(relType)}]-${ relDirection === 'out' || relDirection === 'OUT' ? '>' : '' - }(${nestedVariable}:${ + }(${safeVar(nestedVariable)}:${safeLabel( isInlineFragment ? interfaceLabel : innerSchemaType.name - }${queryParams}) | ${nestedVariable} {${ + )}${queryParams}) | ${nestedVariable} {${ isInlineFragment ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] : subSelection[0] }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, ...tailParams }; -} +}; export const relationTypeFieldOnNodeType = ({ innerSchemaTypeRelation, @@ -81,7 +78,7 @@ export const relationTypeFieldOnNodeType = ({ subSelection, skipLimit, commaIfTail, - tailParams, + tailParams, fieldType, variableName, schemaType, @@ -90,25 +87,31 @@ export const relationTypeFieldOnNodeType = ({ }) => { if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { return { - initial: `${initial}${fieldName}: {${subSelection[0]}}${skipLimit} ${commaIfTail}`, - ...tailParams, - } + initial: `${initial}${fieldName}: {${ + subSelection[0] + }}${skipLimit} ${commaIfTail}`, + ...tailParams + }; } return { initial: `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})${ + }[(${safeVar(variableName)})${ schemaType.name === innerSchemaTypeRelation.to ? '<' : '' - }-[${nestedVariable}_relation:${innerSchemaTypeRelation.name}${queryParams}]-${ + }-[${safeVar(nestedVariable + '_relation')}:${safeLabel( + innerSchemaTypeRelation.name + )}${queryParams}]-${ schemaType.name === innerSchemaTypeRelation.from ? '>' : '' - }(:${ - schemaType.name === innerSchemaTypeRelation.from ? innerSchemaTypeRelation.to : innerSchemaTypeRelation.from - }) | ${nestedVariable}_relation {${subSelection[0]}}]${ + }(:${safeLabel( + schemaType.name === innerSchemaTypeRelation.from + ? innerSchemaTypeRelation.to + : innerSchemaTypeRelation.from + )}) | ${nestedVariable}_relation {${subSelection[0]}}]${ !isArrayType(fieldType) ? ')' : '' }${skipLimit} ${commaIfTail}`, ...tailParams - } -} + }; +}; export const nodeTypeFieldOnRelationType = ({ fieldInfo, @@ -116,7 +119,7 @@ export const nodeTypeFieldOnRelationType = ({ schemaTypeRelation, innerSchemaType, isInlineFragment, - interfaceLabel, + interfaceLabel }) => { if (rootVariableNames) { // Special case used by relation mutation payloads @@ -125,18 +128,17 @@ export const nodeTypeFieldOnRelationType = ({ ...fieldInfo, rootVariableNames }); - } - else { + } else { // Normal case of schemaType with a relationship directive return directedFieldOnReflexiveRelationType({ ...fieldInfo, schemaTypeRelation, innerSchemaType, isInlineFragment, - interfaceLabel, + interfaceLabel }); } -} +}; const relationTypeMutationPayloadField = ({ initial, @@ -149,12 +151,15 @@ const relationTypeMutationPayloadField = ({ rootVariableNames }) => { return { - initial: `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`, + initial: `${initial}${fieldName}: ${variableName} {${ + subSelection[0] + }}${skipLimit} ${commaIfTail}`, ...tailParams, rootVariableNames, - variableName: fieldName === 'from' ? rootVariableNames.to : rootVariableNames.from - } -} + variableName: + fieldName === 'from' ? rootVariableNames.to : rootVariableNames.from + }; +}; const directedFieldOnReflexiveRelationType = ({ initial, @@ -177,36 +182,35 @@ const directedFieldOnReflexiveRelationType = ({ const toTypeName = schemaTypeRelation.to; const isFromField = fieldName === fromTypeName || fieldName === 'from'; const isToField = fieldName === toTypeName || fieldName === 'to'; - const relationshipVariableName = `${variableName}_${isFromField ? 'from' : 'to'}_relation`; - // Since the translations are significantly different, + const relationshipVariableName = `${variableName}_${ + isFromField ? 'from' : 'to' + }_relation`; + // Since the translations are significantly different, // we first check whether the relationship is reflexive - if(fromTypeName === toTypeName) { - if(fieldName === "from" || fieldName === "to") { + if (fromTypeName === toTypeName) { + if (fieldName === 'from' || fieldName === 'to') { return { initial: `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})${ - isFromField ? '<' : '' - }-[${relationshipVariableName}:${relType}${queryParams}]-${ + }[(${safeVar(variableName)})${isFromField ? '<' : ''}-[${safeVar( + relationshipVariableName + )}:${safeLabel(relType)}${queryParams}]-${ isToField ? '>' : '' - }(${nestedVariable}:${ - isInlineFragment - ? interfaceLabel - : fromTypeName - }) | ${relationshipVariableName} {${ + }(${safeVar(nestedVariable)}:${safeLabel( + isInlineFragment ? interfaceLabel : fromTypeName + )}) | ${relationshipVariableName} {${ isInlineFragment ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] : subSelection[0] - }}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, + }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, ...tailParams }; - } - else { + } else { // Case of a renamed directed field return { - initial: `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`, + initial: `${initial}${fieldName}: ${variableName} {${ + subSelection[0] + }}${skipLimit} ${commaIfTail}`, ...tailParams }; } @@ -215,21 +219,17 @@ const directedFieldOnReflexiveRelationType = ({ return { initial: `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' - }[(:${ - isFromField ? toTypeName : fromTypeName - })${ + }[(:${safeLabel(isFromField ? toTypeName : fromTypeName)})${ isFromField ? '<' : '' - }-[${variableName}_relation]-${ + }-[${safeVar(variableName + '_relation')}]-${ isToField ? '>' : '' - }(${nestedVariable}:${ + }(${safeVar(nestedVariable)}:${safeLabel( isInlineFragment ? interfaceLabel : innerSchemaType.name - }${queryParams}) | ${nestedVariable} {${ + )}${queryParams}) | ${nestedVariable} {${ isInlineFragment ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] : subSelection[0] - }}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, + }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, ...tailParams - } -} + }; +}; diff --git a/src/utils.js b/src/utils.js index b152f019..77988910 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ import { print, parse } from 'graphql'; import { possiblyAddArgument } from './augment'; import { v1 as neo4j } from 'neo4j-driver'; +import _ from 'lodash'; function parseArg(arg, variableValues) { switch (arg.value.kind) { @@ -243,30 +244,28 @@ export function extractQueryResult({ records }, returnType) { : null; result = convertIntegerFields(result); - return result; + return result; } -const convertIntegerFields = (result) => { +const convertIntegerFields = result => { const keys = result ? Object.keys(result) : []; let field = undefined; let num = undefined; keys.forEach(e => { field = result[e]; - if(neo4j.isInt(field)) { + if (neo4j.isInt(field)) { num = neo4j.int(field); - if(neo4j.integer.inSafeRange(num)) { + if (neo4j.integer.inSafeRange(num)) { result[e] = num.toString(); - } - else { + } else { result[e] = num.toString(); } - } - else if(typeof result[e] === "object") { + } else if (typeof result[e] === 'object') { return convertIntegerFields(result[e]); } }); return result; -} +}; export function computeSkipLimit(selection, variableValues) { let first = argumentValue(selection, 'first', variableValues); @@ -391,190 +390,220 @@ export function fixParamsForAddRelationshipMutation(params, resolveInfo) { export const isKind = (type, kind) => type && type.kind === kind; -export const isListType = (type, isList=false) => { - if(!isKind(type, "NamedType")) { - if(isKind(type, "ListType")) isList = true; +export const isListType = (type, isList = false) => { + if (!isKind(type, 'NamedType')) { + if (isKind(type, 'ListType')) isList = true; return isListType(type.type, isList); } return isList; -} +}; -export const parameterizeRelationFields = (fields) => { - let name = ""; - return Object.keys(fields).reduce( (acc, t) => { - name = fields[t].name.value; - acc.push(`${name}:$data.${name}`); - return acc; - }, []).join(','); -} +export const parameterizeRelationFields = fields => { + let name = ''; + return Object.keys(fields) + .reduce((acc, t) => { + name = fields[t].name.value; + acc.push(`${name}:$data.${name}`); + return acc; + }, []) + .join(','); +}; -export const getRelationTypeDirectiveArgs = (relationshipType) => { - const directive = relationshipType.directives.find(e => e.name.value === "relation"); - return directive ? { - name: directive.arguments.find(e => e.name.value === "name").value.value, - from: directive.arguments.find(e => e.name.value === "from").value.value, - to: directive.arguments.find(e => e.name.value === "to").value.value - } : undefined; -} +export const getRelationTypeDirectiveArgs = relationshipType => { + const directive = relationshipType.directives.find( + e => e.name.value === 'relation' + ); + return directive + ? { + name: directive.arguments.find(e => e.name.value === 'name').value + .value, + from: directive.arguments.find(e => e.name.value === 'from').value + .value, + to: directive.arguments.find(e => e.name.value === 'to').value.value + } + : undefined; +}; export const getFieldArgumentsFromAst = (field, typeName, fieldIsList) => { let fieldArgs = field.arguments ? field.arguments : []; let augmentedArgs = [...fieldArgs]; - if(fieldIsList) { - augmentedArgs = possiblyAddArgument(augmentedArgs, "first", "Int"); - augmentedArgs = possiblyAddArgument(augmentedArgs, "offset", "Int"); - augmentedArgs = possiblyAddArgument(augmentedArgs, "orderBy", `_${typeName}Ordering`); + if (fieldIsList) { + augmentedArgs = possiblyAddArgument(augmentedArgs, 'first', 'Int'); + augmentedArgs = possiblyAddArgument(augmentedArgs, 'offset', 'Int'); + augmentedArgs = possiblyAddArgument( + augmentedArgs, + 'orderBy', + `_${typeName}Ordering` + ); } - const args = augmentedArgs.reduce( (acc, t) => { - acc.push(print(t)); - return acc; - }, []).join('\n'); + const args = augmentedArgs + .reduce((acc, t) => { + acc.push(print(t)); + return acc; + }, []) + .join('\n'); return args.length > 0 ? `(${args})` : ''; -} +}; -export const getRelationMutationPayloadFieldsFromAst = (relatedAstNode) => { +export const getRelationMutationPayloadFieldsFromAst = relatedAstNode => { let isList = false; - let fieldName = ""; - return relatedAstNode.fields.reduce( (acc, t) => { - fieldName = t.name.value; - if(fieldName !== "to" && fieldName !== "from") { - isList = isListType(t); - // Use name directly in order to prevent requiring required fields on the payload type - acc.push(`${fieldName}: ${ isList ? '[' : '' }${getNamedType(t).name.value}${ isList ? `]` : '' }${print(t.directives)}`); - } - return acc; - }, []).join('\n'); -} + let fieldName = ''; + return relatedAstNode.fields + .reduce((acc, t) => { + fieldName = t.name.value; + if (fieldName !== 'to' && fieldName !== 'from') { + isList = isListType(t); + // Use name directly in order to prevent requiring required fields on the payload type + acc.push( + `${fieldName}: ${isList ? '[' : ''}${getNamedType(t).name.value}${ + isList ? `]` : '' + }${print(t.directives)}` + ); + } + return acc; + }, []) + .join('\n'); +}; -export const getFieldValueType = (type) => { - if(type.kind !== "NamedType") { +export const getFieldValueType = type => { + if (type.kind !== 'NamedType') { return getFieldValueType(type.type); } return type.name.value; -} +}; -export const getNamedType = (type) => { - if(type.kind !== "NamedType") { +export const getNamedType = type => { + if (type.kind !== 'NamedType') { return getNamedType(type.type); } return type; -} +}; -export const isBasicScalar = (name) => { - return name === "ID" || name === "String" - || name === "Float" || name === "Int" || name === "Boolean"; -} +export const isBasicScalar = name => { + return ( + name === 'ID' || + name === 'String' || + name === 'Float' || + name === 'Int' || + name === 'Boolean' + ); +}; -const firstNonNullAndIdField = (fields) => { - let valueTypeName = ""; +const firstNonNullAndIdField = fields => { + let valueTypeName = ''; return fields.find(e => { valueTypeName = getNamedType(e).name.value; - return e.name.value !== '_id' - && e.type.kind === 'NonNullType' - && valueTypeName === 'ID'; + return ( + e.name.value !== '_id' && + e.type.kind === 'NonNullType' && + valueTypeName === 'ID' + ); }); -} +}; -const firstIdField = (fields) => { - let valueTypeName = ""; +const firstIdField = fields => { + let valueTypeName = ''; return fields.find(e => { valueTypeName = getNamedType(e).name.value; - return e.name.value !== '_id' - && valueTypeName === 'ID'; + return e.name.value !== '_id' && valueTypeName === 'ID'; }); -} +}; -const firstNonNullField = (fields) => { - let valueTypeName = ""; +const firstNonNullField = fields => { + let valueTypeName = ''; return fields.find(e => { valueTypeName = getNamedType(e).name.value; return valueTypeName === 'NonNullType'; }); -} +}; -const firstField = (fields) => { +const firstField = fields => { return fields.find(e => { return e.name.value !== '_id'; }); -} +}; -export const getPrimaryKey = (astNode) => { +export const getPrimaryKey = astNode => { const fields = astNode.fields; let pk = firstNonNullAndIdField(fields); - if(!pk) { + if (!pk) { pk = firstIdField(fields); } - if(!pk) { + if (!pk) { pk = firstNonNullField(fields); } - if(!pk) { + if (!pk) { pk = firstField(fields); } return pk; -} +}; export const getTypeDirective = (relatedAstNode, name) => { - return relatedAstNode.directives + return relatedAstNode.directives ? relatedAstNode.directives.find(e => e.name.value === name) : undefined; -} +}; export const getFieldDirective = (field, directive) => { return field && field.directives.find(e => e.name.value === directive); }; -export const isNonNullType = (type, isRequired=false, parent={}) => { - if(!isKind(type, "NamedType")) { +export const isNonNullType = (type, isRequired = false, parent = {}) => { + if (!isKind(type, 'NamedType')) { return isNonNullType(type.type, isRequired, type); } - if(isKind(parent, "NonNullType")) { + if (isKind(parent, 'NonNullType')) { isRequired = true; } return isRequired; -} +}; -export const createOperationMap = (type) => { +export const createOperationMap = type => { const fields = type ? type.fields : []; - return fields.reduce( (acc, t) => { + return fields.reduce((acc, t) => { acc[t.name.value] = t; return acc; }, {}); -} +}; -export const isNodeType = (astNode) => { +export const isNodeType = astNode => { // TODO: check for @ignore and @model directives - return astNode + return ( + astNode && // must be graphql object type - && astNode.kind === "ObjectTypeDefinition" + astNode.kind === 'ObjectTypeDefinition' && // is not Query or Mutation type - && astNode.name.value !== "Query" - && astNode.name.value !== "Mutation" + astNode.name.value !== 'Query' && + astNode.name.value !== 'Mutation' && // does not have relation type directive - && getTypeDirective(astNode, "relation") === undefined + getTypeDirective(astNode, 'relation') === undefined && // does not have from and to fields; not relation type - && astNode.fields - && astNode.fields.find(e => e.name.value === "from") === undefined - && astNode.fields.find(e => e.name.value === "to") === undefined; -} + astNode.fields && + astNode.fields.find(e => e.name.value === 'from') === undefined && + astNode.fields.find(e => e.name.value === 'to') === undefined + ); +}; -export const parseFieldSdl = (sdl) => { +export const parseFieldSdl = sdl => { return sdl ? parse(`type fieldToParse { ${sdl} }`).definitions[0].fields[0] : {}; -} +}; -export const getRelationDirection = (relationDirective) => { +export const getRelationDirection = relationDirective => { let direction = {}; try { - direction = relationDirective.arguments.filter(a => a.name.value === 'direction')[0]; + direction = relationDirective.arguments.filter( + a => a.name.value === 'direction' + )[0]; return direction.value.value; } catch (e) { // FIXME: should we ignore this error to define default behavior? throw new Error('No direction argument specified on @relation directive'); } -} +}; -export const getRelationName = (relationDirective) => { +export const getRelationName = relationDirective => { let name = {}; try { name = relationDirective.arguments.filter(a => a.name.value === 'name')[0]; @@ -583,24 +612,26 @@ export const getRelationName = (relationDirective) => { // FIXME: should we ignore this error to define default behavior? throw new Error('No name argument specified on @relation directive'); } -} +}; -export const createRelationMap = (typeMap) => { +export const createRelationMap = typeMap => { let astNode = {}; - let name = ""; + let name = ''; let fields = []; - let fromTypeName = ""; - let toTypeName = ""; + let fromTypeName = ''; + let toTypeName = ''; let typeDirective = {}; - return Object.keys(typeMap).reduce( (acc, t) => { + return Object.keys(typeMap).reduce((acc, t) => { astNode = typeMap[t]; name = astNode.name.value; fields = astNode.fields; - typeDirective = getTypeDirective(astNode, "relation"); - if(typeDirective) { + typeDirective = getTypeDirective(astNode, 'relation'); + if (typeDirective) { // validate the other fields to make sure theyre not nodes or rel types - fromTypeName = typeDirective.arguments.find(e => e.name.value === "from").value.value; - toTypeName = typeDirective.arguments.find(e => e.name.value === "to").value.value; + fromTypeName = typeDirective.arguments.find(e => e.name.value === 'from') + .value.value; + toTypeName = typeDirective.arguments.find(e => e.name.value === 'to') + .value.value; acc[name] = { from: typeMap[fromTypeName], to: typeMap[toTypeName] @@ -608,42 +639,66 @@ export const createRelationMap = (typeMap) => { } return acc; }, {}); -} +}; + +/** + * Render safe a variable name according to cypher rules + * @param {String} i input variable name + * @returns {String} escaped text suitable for interpolation in cypher + */ +export const safeVar = i => { + // There are rare cases where the var input is an object and has to be stringified + // to produce the right output. + const asStr = `${i}`; + + // Rules: https://neo4j.com/docs/developer-manual/current/cypher/syntax/naming/ + return '`' + asStr.replace(/[-!$%^&*()_+|~=`{}\[\]:";'<>?,.\/]/g, '_') + '`'; +}; -export const printTypeMap = (typeMap) => { +/** + * Render safe a label name by enclosing it in backticks and escaping any + * existing backtick if present. + * @param {String} l a label name + * @returns {String} an escaped label name suitable for cypher concat + */ +export const safeLabel = l => { + const asStr = `${l}`; + const escapeInner = asStr.replace(/\`/g, '\\`'); + return '`' + escapeInner + '`'; +}; + +export const printTypeMap = typeMap => { return print({ - "kind": "Document", - "definitions": Object.values(typeMap) + kind: 'Document', + definitions: Object.values(typeMap) }); -} +}; -export const decideNestedVariableName = ({ - schemaTypeRelation, - innerSchemaTypeRelation, - variableName, +export const decideNestedVariableName = ({ + schemaTypeRelation, + innerSchemaTypeRelation, + variableName, fieldName, rootVariableNames }) => { - if(rootVariableNames) { + if (rootVariableNames) { // Only show up for relation mutations return rootVariableNames[fieldName]; } - if(schemaTypeRelation) { + if (schemaTypeRelation) { const fromTypeName = schemaTypeRelation.from; const toTypeName = schemaTypeRelation.to; - if(fromTypeName === toTypeName) { - if(fieldName === "from" || fieldName === "to") { + if (fromTypeName === toTypeName) { + if (fieldName === 'from' || fieldName === 'to') { return variableName + '_' + fieldName; - } - else { + } else { // Case of a reflexive relationship type's directed field // being renamed to its node type value // ex: from: User -> User: User return variableName; } } - } - else { + } else { // Types without @relation directives are assumed to be node types // and only node types can have fields whose values are relation types if (innerSchemaTypeRelation) { @@ -651,30 +706,35 @@ export const decideNestedVariableName = ({ if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { return variableName; } - } - else { + } else { // related types are different return variableName + '_' + fieldName; } } return variableName + '_' + fieldName; -} +}; -export const extractTypeMapFromTypeDefs = (typeDefs) => { +export const extractTypeMapFromTypeDefs = typeDefs => { // TODO: accept alternative typeDefs formats (arr of strings, ast, etc.) // into a single string for parse, add validatation const astNodes = parse(typeDefs).definitions; - return astNodes.reduce( (acc, t) => { + return astNodes.reduce((acc, t) => { acc[t.name.value] = t; return acc; }, {}); -} +}; -export const addDirectiveDeclarations = (typeMap) => { +export const addDirectiveDeclarations = typeMap => { // overwrites any provided directive declarations for system directive names - typeMap['cypher'] = parse(`directive @cypher(statement: String) on FIELD_DEFINITION`); - typeMap['relation'] = parse(`directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT`); - typeMap['MutationMeta'] = parse(`directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION`); + typeMap['cypher'] = parse( + `directive @cypher(statement: String) on FIELD_DEFINITION` + ); + typeMap['relation'] = parse( + `directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT` + ); + typeMap['MutationMeta'] = parse( + `directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION` + ); typeMap['_RelationDirections'] = parse(`enum _RelationDirections { IN OUT }`); return typeMap; -} +}; diff --git a/test/cypherTest.js b/test/cypherTest.js index 55583f25..178d6ae6 100644 --- a/test/cypherTest.js +++ b/test/cypherTest.js @@ -10,7 +10,7 @@ test('simple Cypher query', t => { title } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -32,7 +32,7 @@ test('Simple skip limit', t => { } `, expectedCypherQuery = - 'MATCH (movie:Movie {title:$title}) RETURN movie { .title , .year } AS movie SKIP $offset LIMIT $first'; + 'MATCH (`movie`:`Movie` {title:$title}) RETURN `movie` { .title , .year } AS `movie` SKIP $offset LIMIT $first'; t.plan(3); return Promise.all([ @@ -58,7 +58,7 @@ test('Cypher projection skip limit', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {title:$title}) RETURN `movie` { .title ,actors: [(`movie`)<-[:`ACTED_IN`]-(`movie_actors`:`Actor`) | movie_actors { .name }] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -80,7 +80,7 @@ test('Handle Query with name not aligning to type', t => { } `, expectedCypherQuery = - 'MATCH (movie:Movie {year:$year}) RETURN movie { .title } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {year:$year}) RETURN `movie` { .title } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -100,7 +100,7 @@ test('Query without arguments, non-null type', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {}) RETURN movie { .movieId } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {}) RETURN `movie` { .movieId } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -120,7 +120,7 @@ test('Query single object', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {movieId:$movieId}) RETURN movie { .title } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {movieId:$movieId}) RETURN `movie` { .title } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -145,7 +145,7 @@ test('Query single object relation', t => { } `, expectedCypherQuery = - 'MATCH (movie:Movie {movieId:$movieId}) RETURN movie { .title ,filmedIn: head([(movie)-[:FILMED_IN]->(movie_filmedIn:State) | movie_filmedIn { .name }]) } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {movieId:$movieId}) RETURN `movie` { .title ,filmedIn: head([(`movie`)-[:`FILMED_IN`]->(`movie_filmedIn`:`State`) | movie_filmedIn { .name }]) } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -172,7 +172,7 @@ test('Query single object and array of objects relations', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {movieId:$movieId}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] ,filmedIn: head([(movie)-[:FILMED_IN]->(movie_filmedIn:State) | movie_filmedIn { .name }]) } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {movieId:$movieId}) RETURN `movie` { .title ,actors: [(`movie`)<-[:`ACTED_IN`]-(`movie_actors`:`Actor`) | movie_actors { .name }] ,filmedIn: head([(`movie`)-[:`FILMED_IN`]->(`movie_filmedIn`:`State`) | movie_filmedIn { .name }]) } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -209,7 +209,7 @@ test('Deeply nested object query', t => { } } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name ,movies: [(movie_actors)-[:ACTED_IN]->(movie_actors_movies:Movie) | movie_actors_movies { .title ,actors: [(movie_actors_movies)<-[:ACTED_IN]-(movie_actors_movies_actors:Actor{name:$1_name}) | movie_actors_movies_actors { .name ,movies: [(movie_actors_movies_actors)-[:ACTED_IN]->(movie_actors_movies_actors_movies:Movie) | movie_actors_movies_actors_movies { .title , .year ,similar: [ movie_actors_movies_actors_movies_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie_actors_movies_actors_movies, first: 3, offset: 0}, true) | movie_actors_movies_actors_movies_similar { .title , .year }][..3] }] }] }] }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`) | movie_actors { .name ,movies: [(\`movie_actors\`)-[:\`ACTED_IN\`]->(\`movie_actors_movies\`:\`Movie\`) | movie_actors_movies { .title ,actors: [(\`movie_actors_movies\`)<-[:\`ACTED_IN\`]-(\`movie_actors_movies_actors\`:\`Actor\`{name:$1_name}) | movie_actors_movies_actors { .name ,movies: [(\`movie_actors_movies_actors\`)-[:\`ACTED_IN\`]->(\`movie_actors_movies_actors_movies\`:\`Movie\`) | movie_actors_movies_actors_movies { .title , .year ,similar: [ movie_actors_movies_actors_movies_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie_actors_movies_actors_movies, first: 3, offset: 0}, true) | movie_actors_movies_actors_movies_similar { .title , .year }][..3] }] }] }] }] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -232,7 +232,7 @@ test('Handle meta field at beginning of selection set', t => { title } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -254,7 +254,7 @@ test('Handle meta field at end of selection set', t => { } } `, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -277,7 +277,7 @@ test('Handle meta field in middle of selection set', t => { } } `, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -300,7 +300,7 @@ test('Handle @cypher directive without any params for sub-query', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie {mostSimilar: head([ movie_mostSimilar IN apoc.cypher.runFirstColumn("WITH {this} AS this RETURN this", {this: movie}, true) | movie_mostSimilar { .title , .year }]) } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` {mostSimilar: head([ movie_mostSimilar IN apoc.cypher.runFirstColumn("WITH {this} AS this RETURN this", {this: movie}, true) | movie_mostSimilar { .title , .year }]) } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -320,7 +320,7 @@ test('Pass @cypher directive default params to sub-query', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie {scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie, scale: 3}, false)} AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` {scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie, scale: 3}, false)} AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -340,7 +340,7 @@ test('Pass @cypher directive params to sub-query', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie {scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie, scale: 10}, false)} AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` {scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie, scale: 10}, false)} AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -362,7 +362,7 @@ test('Query for Neo4js internal _id', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {}) WHERE ID(movie)=0 RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {}) WHERE ID(\`movie\`)=0 RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -382,7 +382,7 @@ test('Query for Neo4js internal _id and another param before _id', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) WHERE ID(movie)=0 RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) WHERE ID(\`movie\`)=0 RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -403,7 +403,7 @@ test('Query for Neo4js internal _id and another param after _id', t => { } }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) WHERE ID(movie)=0 RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) WHERE ID(\`movie\`)=0 RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -424,7 +424,7 @@ test('Query for Neo4js internal _id by dedicated Query MovieBy_Id(_id: String!)' } }`, - expectedCypherQuery = `MATCH (movie:Movie {}) WHERE ID(movie)=0 RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {}) WHERE ID(\`movie\`)=0 RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -443,7 +443,7 @@ test(`Query for null value translates to 'IS NULL' WHERE clause`, t => { year } }`, - expectedCypherQuery = `MATCH (movie:Movie {}) WHERE movie.poster IS NULL RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {}) WHERE movie.poster IS NULL RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -462,7 +462,7 @@ test(`Query for null value combined with internal ID and another param`, t => { year } }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) WHERE ID(movie)=0 AND movie.poster IS NULL RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) WHERE ID(\`movie\`)=0 AND movie.poster IS NULL RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -489,7 +489,7 @@ test('Cypher subquery filters', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor{name:$1_name}) | movie_actors { .name }] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {title:$title}) RETURN `movie` { .title ,actors: [(`movie`)<-[:`ACTED_IN`]-(`movie_actors`:`Actor`{name:$1_name}) | movie_actors { .name }] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -518,7 +518,7 @@ test('Cypher subquery filters with paging', t => { } }`, expectedCypherQuery = - 'MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor{name:$1_name}) | movie_actors { .name }][..3] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS movie SKIP $offset'; + 'MATCH (`movie`:`Movie` {title:$title}) RETURN `movie` { .title ,actors: [(`movie`)<-[:`ACTED_IN`]-(`movie_actors`:`Actor`{name:$1_name}) | movie_actors { .name }][..3] ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -545,8 +545,8 @@ test('Handle @cypher directive on Query Type', t => { } } `, - expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g", {substring:$substring}, True) AS x UNWIND x AS genre - RETURN genre { .name ,movies: [(genre)<-[:IN_GENRE]-(genre_movies:Movie{}) | genre_movies { .title }][..3] } AS genre SKIP $offset`; + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g", {substring:$substring}, True) AS x UNWIND x AS \`genre\` + RETURN \`genre\` { .name ,movies: [(\`genre\`)<-[:\`IN_GENRE\`]-(\`genre_movies\`:\`Movie\`{}) | genre_movies { .title }][..3] } AS \`genre\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -568,7 +568,7 @@ test.cb('Handle @cypher directive on Mutation type', t => { }`, expectedCypherQuery = `CALL apoc.cypher.doIt("CREATE (g:Genre) SET g.name = $name RETURN g", {name:$name}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS genre - RETURN genre { .name } AS genre SKIP $offset`; + RETURN \`genre\` { .name } AS \`genre\` SKIP $offset`; t.plan(2); cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { @@ -588,7 +588,7 @@ test.cb('Create node mutation', t => { } } }`, - expectedCypherQuery = `CREATE (movie:Movie) SET movie = $params RETURN movie {_id: ID(movie), .title ,genres: [(movie)-[:IN_GENRE]->(movie_genres:Genre) | movie_genres { .name }] } AS movie`; + expectedCypherQuery = `CREATE (\`movie\`:\`Movie\`) SET \`movie\` = $params RETURN \`movie\` {_id: ID(\`movie\`), .title ,genres: [(\`movie\`)-[:\`IN_GENRE\`]->(\`movie_genres\`:\`Genre\`) | movie_genres { .name }] } AS \`movie\``; t.plan(2); cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { @@ -613,7 +613,7 @@ test.cb('Update node mutation', t => { year } }`, - expectedCypherQuery = `MATCH (movie:Movie {movieId: $params.movieId}) SET movie += $params RETURN movie {_id: ID(movie), .title , .year } AS movie`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {movieId: $params.movieId}) SET movie += $params RETURN \`movie\` {_id: ID(\`movie\`), .title , .year } AS \`movie\``; t.plan(2); cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { @@ -633,10 +633,10 @@ test.cb('Delete node mutation', t => { movieId } }`, - expectedCypherQuery = `MATCH (movie:Movie {movieId: $movieId}) -WITH movie AS movie_toDelete, movie {_id: ID(movie), .movieId } AS movie -DETACH DELETE movie_toDelete -RETURN movie`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {movieId: $movieId}) +WITH \`movie\` AS \`movie_toDelete\`, \`movie\` {_id: ID(\`movie\`), .movieId } AS \`movie\` +DETACH DELETE \`movie_toDelete\` +RETURN \`movie\``; t.plan(2); cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { @@ -665,10 +665,10 @@ test('Add relationship mutation', t => { } }`, expectedCypherQuery = ` - MATCH (movie_from:Movie {movieId: $from.movieId}) - MATCH (genre_to:Genre {name: $to.name}) - CREATE (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 _AddMovieGenresPayload; + MATCH (\`movie_from\`:\`Movie\` {movieId: $from.movieId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + CREATE (\`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 \`_AddMovieGenresPayload\`; `; t.plan(1); @@ -681,7 +681,8 @@ test('Add relationship mutation', t => { first: -1, offset: 0 }, - expectedCypherQuery + expectedCypherQuery, + {} ); }); @@ -704,10 +705,10 @@ test('Add relationship mutation with GraphQL variables', t => { } }`, expectedCypherQuery = ` - MATCH (movie_from:Movie {movieId: $from.movieId}) - MATCH (genre_to:Genre {name: $to.name}) - CREATE (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 _AddMovieGenresPayload; + MATCH (\`movie_from\`:\`Movie\` {movieId: $from.movieId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + CREATE (\`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 \`_AddMovieGenresPayload\`; `; t.plan(1); @@ -767,10 +768,10 @@ test('Add relationship mutation with relationship property', t => { } }`, expectedCypherQuery = ` - MATCH (user_from:User {userId: $from.userId}) - MATCH (movie_to:Movie {movieId: $to.movieId}) - CREATE (user_from)-[rated_relation:RATED {rating:$data.rating}]->(movie_to) - RETURN rated_relation { from: user_from {_id: ID(user_from), .userId , .name ,rated: [(user_from)-[user_from_rated_relation:RATED]->(:Movie) | user_from_rated_relation { .rating ,Movie: head([(:User)-[user_from_rated_relation]->(user_from_rated_Movie:Movie) | 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)<-[movie_to_ratings_relation]-(movie_to_ratings_User:User) | movie_to_ratings_User {_id: ID(movie_to_ratings_User), .userId , .name }]) }] } , .rating } AS _AddUserRatedPayload; + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`movie_to\`:\`Movie\` {movieId: $to.movieId}) + CREATE (\`user_from\`)-[\`rated_relation\`:\`RATED\` {rating:$data.rating}]->(\`movie_to\`) + RETURN \`rated_relation\` { from: user_from {_id: ID(\`user_from\`), .userId , .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`) | 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\`)<-[\`movie_to_ratings_relation\`]-(\`movie_to_ratings_User\`:\`User\`) | movie_to_ratings_User {_id: ID(\`movie_to_ratings_User\`), .userId , .name }]) }] } , .rating } AS \`_AddUserRatedPayload\`; `; t.plan(1); @@ -863,10 +864,10 @@ test('Add relationship mutation with relationship property (reflexive)', t => { } `, expectedCypherQuery = ` - MATCH (user_from:User {userId: $from.userId}) - MATCH (user_to:User {userId: $to.userId}) - CREATE (user_from)-[friend_of_relation:FRIEND_OF {since:$data.since}]->(user_to) - 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 _AddUserFriendsPayload; + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`user_to\`:\`User\` {userId: $to.userId}) + CREATE (\`user_from\`)-[\`friend_of_relation\`:\`FRIEND_OF\` {since:$data.since}]->(\`user_to\`) + 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 \`_AddUserFriendsPayload\`; `; t.plan(1); @@ -901,12 +902,12 @@ test('Remove relationship mutation', t => { } }`, expectedCypherQuery = ` - MATCH (movie_from:Movie {movieId: $from.movieId}) - MATCH (genre_to:Genre {name: $to.name}) - OPTIONAL MATCH (movie_from)-[movie_fromgenre_to:IN_GENRE]->(genre_to) - DELETE movie_fromgenre_to - WITH COUNT(*) AS scope, movie_from AS _movie_from, genre_to AS _genre_to - RETURN {from: _movie_from {_id: ID(_movie_from), .title } ,to: _genre_to {_id: ID(_genre_to), .name } } AS _RemoveMovieGenresPayload; + MATCH (\`movie_from\`:\`Movie\` {movieId: $from.movieId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + OPTIONAL MATCH (\`movie_from\`)-[\`movie_fromgenre_to\`:\`IN_GENRE\`]->(\`genre_to\`) + DELETE \`movie_fromgenre_to\` + WITH COUNT(*) AS scope, \`movie_from\` AS \`_movie_from\`, \`genre_to\` AS \`_genre_to\` + RETURN {from: _movie_from {_id: ID(\`_movie_from\`), .title } ,to: _genre_to {_id: ID(\`_genre_to\`), .name } } AS \`_RemoveMovieGenresPayload\`; `; t.plan(1); @@ -977,12 +978,12 @@ test('Remove relationship mutation (reflexive)', t => { } `, expectedCypherQuery = ` - MATCH (user_from:User {userId: $from.userId}) - MATCH (user_to:User {userId: $to.userId}) - OPTIONAL MATCH (user_from)-[user_fromuser_to:FRIEND_OF]->(user_to) - DELETE user_fromuser_to - WITH COUNT(*) AS scope, user_from AS _user_from, user_to AS _user_to - RETURN {from: _user_from {_id: ID(_user_from), .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 } }] ,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 } }] } } } AS _RemoveUserFriendsPayload; + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`user_to\`:\`User\` {userId: $to.userId}) + OPTIONAL MATCH (\`user_from\`)-[\`user_fromuser_to\`:\`FRIEND_OF\`]->(\`user_to\`) + DELETE \`user_fromuser_to\` + WITH COUNT(*) AS scope, \`user_from\` AS \`_user_from\`, \`user_to\` AS \`_user_to\` + RETURN {from: _user_from {_id: ID(\`_user_from\`), .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 } }] ,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 } }] } } } AS \`_RemoveUserFriendsPayload\`; `; t.plan(1); @@ -1010,7 +1011,7 @@ test('Handle GraphQL variables in nested selection - first/offset', t => { } } }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title , .year ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title , .year ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS \`movie\` SKIP $offset`; t.plan(3); @@ -1049,7 +1050,7 @@ test('Handle GraphQL variables in nest selection - @cypher param (not first/offs } }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title , .year ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title ,scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie_similar, scale: 5}, false)}][..3] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title , .year ,similar: [ movie_similar IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie, first: 3, offset: 0}, true) | movie_similar { .title ,scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie_similar, scale: 5}, false)}][..3] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1088,7 +1089,7 @@ test('Return internal node id for _id field', t => { } } `, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie {_id: ID(movie), .title , .year ,genres: [(movie)-[:IN_GENRE]->(movie_genres:Genre) | movie_genres {_id: ID(movie_genres), .name }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` {_id: ID(\`movie\`), .title , .year ,genres: [(\`movie\`)-[:\`IN_GENRE\`]->(\`movie_genres\`:\`Genre\`) | movie_genres {_id: ID(\`movie_genres\`), .name }] } AS \`movie\` SKIP $offset`; t.plan(3); @@ -1109,7 +1110,7 @@ test('Treat enum as a scalar', t => { genre } }`, - expectedCypherQuery = `MATCH (book:Book {}) RETURN book { .genre } AS book SKIP $offset`; + expectedCypherQuery = `MATCH (\`book\`:\`Book\` {}) RETURN \`book\` { .genre } AS \`book\` SKIP $offset`; t.plan(3); @@ -1137,7 +1138,7 @@ query getMovie { year } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`) | movie_actors { .name }] , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1170,7 +1171,7 @@ query getMovie { } } `, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`) | movie_actors { .name }] , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1199,7 +1200,7 @@ test('nested fragments', t => { fragment Bar on Movie { year }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title , .year } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title , .year } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1226,7 +1227,7 @@ test('fragments on relations', t => { fragment Foo on Actor { name }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`) | movie_actors { .name }] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1257,7 +1258,7 @@ test('nested fragments on relations', t => { fragment Bar on Actor { name }`, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`) | movie_actors { .name }] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1280,7 +1281,7 @@ test('orderBy test - descending, top level - augmented schema', t => { } } `, - expectedCypherQuery = `MATCH (movie:Movie {year:$year}) RETURN movie { .title ,actors: [(movie)<-[:ACTED_IN]-(movie_actors:Actor{}) | movie_actors { .name }][..3] } AS movie ORDER BY movie.title DESC SKIP $offset LIMIT $first`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {year:$year}) RETURN \`movie\` { .title ,actors: [(\`movie\`)<-[:\`ACTED_IN\`]-(\`movie_actors\`:\`Actor\`{}) | movie_actors { .name }][..3] } AS \`movie\` ORDER BY movie.title DESC SKIP $offset LIMIT $first`; t.plan(1); @@ -1310,7 +1311,7 @@ test('query for relationship properties', t => { } } }`, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title ,ratings: [(movie)<-[movie_ratings_relation:RATED]-(:User) | movie_ratings_relation { .rating ,User: head([(:Movie)<-[movie_ratings_relation]-(movie_ratings_User:User) | movie_ratings_User { .name }]) }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`)<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .name }]) }] } AS \`movie\` SKIP $offset`; t.plan(1); @@ -1379,7 +1380,7 @@ test('query reflexive relation nested in non-reflexive relation', t => { } } }`, - expectedCypherQuery = `MATCH (movie:Movie {}) RETURN movie { .movieId , .title ,ratings: [(movie)<-[movie_ratings_relation:RATED]-(:User) | movie_ratings_relation { .rating ,User: head([(:Movie)<-[movie_ratings_relation]-(movie_ratings_User:User) | movie_ratings_User { .userId , .name ,friends: {from: [(movie_ratings_User)<-[movie_ratings_User_from_relation:FRIEND_OF]-(movie_ratings_User_from:User) | movie_ratings_User_from_relation { .since ,User: movie_ratings_User_from { .name ,friends: {from: [(movie_ratings_User_from)<-[movie_ratings_User_from_from_relation:FRIEND_OF]-(movie_ratings_User_from_from:User) | movie_ratings_User_from_from_relation { .since ,User: movie_ratings_User_from_from { .name } }] ,to: [(movie_ratings_User_from)-[movie_ratings_User_from_to_relation:FRIEND_OF]->(movie_ratings_User_from_to:User) | movie_ratings_User_from_to_relation { .since ,User: movie_ratings_User_from_to { .name } }] } } }] ,to: [(movie_ratings_User)-[movie_ratings_User_to_relation:FRIEND_OF]->(movie_ratings_User_to:User) | movie_ratings_User_to_relation { .since ,User: movie_ratings_User_to { .name ,friends: {from: [(movie_ratings_User_to)<-[movie_ratings_User_to_from_relation:FRIEND_OF]-(movie_ratings_User_to_from:User) | movie_ratings_User_to_from_relation { .since ,User: movie_ratings_User_to_from { .name } }] ,to: [(movie_ratings_User_to)-[movie_ratings_User_to_to_relation:FRIEND_OF]->(movie_ratings_User_to_to:User) | movie_ratings_User_to_to_relation { .since ,User: movie_ratings_User_to_to { .name } }] } } }] } }]) }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {}) RETURN \`movie\` { .movieId , .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`)<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .userId , .name ,friends: {from: [(\`movie_ratings_User\`)<-[\`movie_ratings_User_from_relation\`:\`FRIEND_OF\`]-(\`movie_ratings_User_from\`:\`User\`) | movie_ratings_User_from_relation { .since ,User: movie_ratings_User_from { .name ,friends: {from: [(\`movie_ratings_User_from\`)<-[\`movie_ratings_User_from_from_relation\`:\`FRIEND_OF\`]-(\`movie_ratings_User_from_from\`:\`User\`) | movie_ratings_User_from_from_relation { .since ,User: movie_ratings_User_from_from { .name } }] ,to: [(\`movie_ratings_User_from\`)-[\`movie_ratings_User_from_to_relation\`:\`FRIEND_OF\`]->(\`movie_ratings_User_from_to\`:\`User\`) | movie_ratings_User_from_to_relation { .since ,User: movie_ratings_User_from_to { .name } }] } } }] ,to: [(\`movie_ratings_User\`)-[\`movie_ratings_User_to_relation\`:\`FRIEND_OF\`]->(\`movie_ratings_User_to\`:\`User\`) | movie_ratings_User_to_relation { .since ,User: movie_ratings_User_to { .name ,friends: {from: [(\`movie_ratings_User_to\`)<-[\`movie_ratings_User_to_from_relation\`:\`FRIEND_OF\`]-(\`movie_ratings_User_to_from\`:\`User\`) | movie_ratings_User_to_from_relation { .since ,User: movie_ratings_User_to_from { .name } }] ,to: [(\`movie_ratings_User_to\`)-[\`movie_ratings_User_to_to_relation\`:\`FRIEND_OF\`]->(\`movie_ratings_User_to_to\`:\`User\`) | movie_ratings_User_to_to_relation { .since ,User: movie_ratings_User_to_to { .name } }] } } }] } }]) }] } AS \`movie\` SKIP $offset`; t.plan(1); @@ -1447,7 +1448,7 @@ test('query non-reflexive relation nested in reflexive relation', t => { } } }`, - expectedCypherQuery = `MATCH (user:User {}) RETURN user {_id: ID(user), .name ,friends: {from: [(user)<-[user_from_relation:FRIEND_OF]-(user_from:User) | user_from_relation { .since ,User: user_from {_id: ID(user_from), .name ,rated: [(user_from)-[user_from_rated_relation:RATED]->(:Movie) | user_from_rated_relation { .rating ,Movie: head([(:User)-[user_from_rated_relation]->(user_from_rated_Movie:Movie) | user_from_rated_Movie {_id: ID(user_from_rated_Movie),ratings: [(user_from_rated_Movie)<-[user_from_rated_Movie_ratings_relation:RATED]-(:User) | user_from_rated_Movie_ratings_relation { .rating ,User: head([(:Movie)<-[user_from_rated_Movie_ratings_relation]-(user_from_rated_Movie_ratings_User:User) | user_from_rated_Movie_ratings_User {_id: ID(user_from_rated_Movie_ratings_User),friends: {from: [(user_from_rated_Movie_ratings_User)<-[user_from_rated_Movie_ratings_User_from_relation:FRIEND_OF]-(user_from_rated_Movie_ratings_User_from:User) | user_from_rated_Movie_ratings_User_from_relation { .since ,User: user_from_rated_Movie_ratings_User_from {_id: ID(user_from_rated_Movie_ratings_User_from)} }] ,to: [(user_from_rated_Movie_ratings_User)-[user_from_rated_Movie_ratings_User_to_relation:FRIEND_OF]->(user_from_rated_Movie_ratings_User_to:User) | user_from_rated_Movie_ratings_User_to_relation { .since ,User: user_from_rated_Movie_ratings_User_to {_id: ID(user_from_rated_Movie_ratings_User_to)} }] } }]) }] }]) }] } }] ,to: [(user)-[user_to_relation:FRIEND_OF]->(user_to:User) | user_to_relation { .since ,User: user_to {_id: ID(user_to), .name ,rated: [(user_to)-[user_to_rated_relation:RATED]->(:Movie) | user_to_rated_relation { .rating ,Movie: head([(:User)-[user_to_rated_relation]->(user_to_rated_Movie:Movie) | user_to_rated_Movie {_id: ID(user_to_rated_Movie)}]) }] } }] } } AS user SKIP $offset`; + expectedCypherQuery = `MATCH (\`user\`:\`User\` {}) RETURN \`user\` {_id: ID(\`user\`), .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from {_id: ID(\`user_from\`), .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`) | user_from_rated_Movie {_id: ID(\`user_from_rated_Movie\`),ratings: [(\`user_from_rated_Movie\`)<-[\`user_from_rated_Movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | user_from_rated_Movie_ratings_relation { .rating ,User: head([(:\`Movie\`)<-[\`user_from_rated_Movie_ratings_relation\`]-(\`user_from_rated_Movie_ratings_User\`:\`User\`) | user_from_rated_Movie_ratings_User {_id: ID(\`user_from_rated_Movie_ratings_User\`),friends: {from: [(\`user_from_rated_Movie_ratings_User\`)<-[\`user_from_rated_Movie_ratings_User_from_relation\`:\`FRIEND_OF\`]-(\`user_from_rated_Movie_ratings_User_from\`:\`User\`) | user_from_rated_Movie_ratings_User_from_relation { .since ,User: user_from_rated_Movie_ratings_User_from {_id: ID(\`user_from_rated_Movie_ratings_User_from\`)} }] ,to: [(\`user_from_rated_Movie_ratings_User\`)-[\`user_from_rated_Movie_ratings_User_to_relation\`:\`FRIEND_OF\`]->(\`user_from_rated_Movie_ratings_User_to\`:\`User\`) | user_from_rated_Movie_ratings_User_to_relation { .since ,User: user_from_rated_Movie_ratings_User_to {_id: ID(\`user_from_rated_Movie_ratings_User_to\`)} }] } }]) }] }]) }] } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to {_id: ID(\`user_to\`), .name ,rated: [(\`user_to\`)-[\`user_to_rated_relation\`:\`RATED\`]->(:\`Movie\`) | user_to_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_to_rated_relation\`]->(\`user_to_rated_Movie\`:\`Movie\`) | user_to_rated_Movie {_id: ID(\`user_to_rated_Movie\`)}]) }] } }] } } AS \`user\` SKIP $offset`; t.plan(1); @@ -1473,7 +1474,7 @@ test('query relation type with argument', t => { } } }`, - expectedCypherQuery = `MATCH (user:User {}) RETURN user {_id: ID(user), .name ,rated: [(user)-[user_rated_relation:RATED{rating:$1_rating}]->(:Movie) | user_rated_relation { .rating ,Movie: head([(:User)-[user_rated_relation]->(user_rated_Movie:Movie) | user_rated_Movie { .title }]) }] } AS user SKIP $offset`; + expectedCypherQuery = `MATCH (\`user\`:\`User\` {}) RETURN \`user\` {_id: ID(\`user\`), .name ,rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`{rating:$1_rating}]->(:\`Movie\`) | user_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_rated_relation\`]->(\`user_rated_Movie\`:\`Movie\`) | user_rated_Movie { .title }]) }] } AS \`user\` SKIP $offset`; t.plan(1); @@ -1508,7 +1509,7 @@ test('query reflexive relation type with arguments', t => { } } `, - expectedCypherQuery = `MATCH (user:User {}) RETURN user { .userId , .name ,friends: {from: [(user)<-[user_from_relation:FRIEND_OF{since:$1_since}]-(user_from:User) | user_from_relation { .since ,User: user_from { .name } }] ,to: [(user)-[user_to_relation:FRIEND_OF{since:$3_since}]->(user_to:User) | user_to_relation { .since ,User: user_to { .name } }] } } AS user SKIP $offset`; + expectedCypherQuery = `MATCH (\`user\`:\`User\` {}) RETURN \`user\` { .userId , .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`{since:$1_since}]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from { .name } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`{since:$3_since}]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to { .name } }] } } AS \`user\` SKIP $offset`; t.plan(1); @@ -1538,7 +1539,7 @@ test('query using inline fragment', t => { } } `, - expectedCypherQuery = `MATCH (movie:Movie {title:$title}) RETURN movie { .title ,ratings: [(movie)<-[movie_ratings_relation:RATED]-(:User) | movie_ratings_relation { .rating ,User: head([(:Movie)<-[movie_ratings_relation]-(movie_ratings_User:User) | movie_ratings_User { .name , .userId }]) }] } AS movie SKIP $offset`; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` {title:$title}) RETURN \`movie\` { .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`)<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .name , .userId }]) }] } AS \`movie\` SKIP $offset`; t.plan(1); @@ -1550,4 +1551,3 @@ test('query using inline fragment', t => { {} ); }); -