From bba9bfc297bffc5af81f69199a7b754cefecb295 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Thu, 7 Feb 2019 14:50:40 -0800 Subject: [PATCH 01/13] block neo4j_ignore from root type fields --- src/augment.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/augment.js b/src/augment.js index 9d75374b..64b5544d 100644 --- a/src/augment.js +++ b/src/augment.js @@ -249,9 +249,11 @@ export const possiblyAddArgument = (args, fieldName, fieldType) => { }; const augmentType = (astNode, typeMap, resolvers, rootTypes, config) => { + const typeName = astNode.name.value; const queryType = rootTypes.query; + const mutationType = rootTypes.mutation; if (isNodeType(astNode)) { - if (shouldAugmentType(config, 'query', astNode.name.value)) { + if (shouldAugmentType(config, 'query', typeName)) { // Only add _id field to type if query API is generated for type astNode.fields = addOrReplaceNodeIdField(astNode, resolvers); } @@ -263,12 +265,19 @@ const augmentType = (astNode, typeMap, resolvers, rootTypes, config) => { queryType ); } - astNode.fields = possiblyAddIgnoreDirective( - astNode, - typeMap, - resolvers, - config - ); + // Should only ignore fields on non-root types + if ( + config.ignore === true && + typeName !== queryType && + typeName !== mutationType + ) { + astNode.fields = possiblyAddIgnoreDirective( + astNode, + typeMap, + resolvers, + config + ); + } return astNode; }; From 70b0502976a62dd2a3ece9acf58114e26453d044 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Thu, 7 Feb 2019 14:52:16 -0800 Subject: [PATCH 02/13] sets default true for ignore config --- src/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 77340f41..1bbad781 100644 --- a/src/index.js +++ b/src/index.js @@ -108,7 +108,8 @@ export const augmentSchema = ( query: true, mutation: true, temporal: true, - debug: true + debug: true, + ignore: true } ) => { const typeMap = extractTypeMapFromSchema(schema); @@ -131,7 +132,8 @@ export const makeAugmentedSchema = ({ query: true, mutation: true, temporal: true, - debug: true + debug: true, + ignore: true } }) => { if (schema) { From 58f77b6a01d5bbbb9c4a02991d5407494c76df3b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Fri, 8 Feb 2019 11:52:27 -0800 Subject: [PATCH 03/13] removal of ignore config --- src/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 1bbad781..77340f41 100644 --- a/src/index.js +++ b/src/index.js @@ -108,8 +108,7 @@ export const augmentSchema = ( query: true, mutation: true, temporal: true, - debug: true, - ignore: true + debug: true } ) => { const typeMap = extractTypeMapFromSchema(schema); @@ -132,8 +131,7 @@ export const makeAugmentedSchema = ({ query: true, mutation: true, temporal: true, - debug: true, - ignore: true + debug: true } }) => { if (schema) { From b2e71d90bca5ffb07ce5aaba582ec1a63c15d90c Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Fri, 8 Feb 2019 12:05:39 -0800 Subject: [PATCH 04/13] reversion --- src/augment.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/augment.js b/src/augment.js index 64b5544d..1183ca59 100644 --- a/src/augment.js +++ b/src/augment.js @@ -249,11 +249,9 @@ export const possiblyAddArgument = (args, fieldName, fieldType) => { }; const augmentType = (astNode, typeMap, resolvers, rootTypes, config) => { - const typeName = astNode.name.value; const queryType = rootTypes.query; - const mutationType = rootTypes.mutation; if (isNodeType(astNode)) { - if (shouldAugmentType(config, 'query', typeName)) { + if (shouldAugmentType(config, 'query', astNode.name.value)) { // Only add _id field to type if query API is generated for type astNode.fields = addOrReplaceNodeIdField(astNode, resolvers); } @@ -265,19 +263,16 @@ const augmentType = (astNode, typeMap, resolvers, rootTypes, config) => { queryType ); } - // Should only ignore fields on non-root types - if ( - config.ignore === true && - typeName !== queryType && - typeName !== mutationType - ) { - astNode.fields = possiblyAddIgnoreDirective( - astNode, - typeMap, - resolvers, - config - ); - } + // FIXME: inferring where to add @neo4j_ignore directive improperly causes + // fields to be ignored when logger is added, so remove functionality + // until we refactor how to infer when @neo4j_ignore directive is needed + // see https://github.com/neo4j-graphql/neo4j-graphql-js/issues/189 + // astNode.fields = possiblyAddIgnoreDirective( + // astNode, + // typeMap, + // resolvers, + // config + // ); return astNode; }; From 14e5d2d41b38d1302964d7b04f35f0491b460754 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:40:07 -0800 Subject: [PATCH 05/13] Update augment.js --- src/augment.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/augment.js b/src/augment.js index 1183ca59..02dee6ce 100644 --- a/src/augment.js +++ b/src/augment.js @@ -206,7 +206,6 @@ const augmentResolvers = (augmentedTypeMap, resolvers, config) => { const possiblyAddOrderingArgument = (args, fieldName) => { const orderingType = `_${fieldName}Ordering`; if (args.findIndex(e => e.name.value === orderingType) === -1) { - // TODO refactor args.push({ kind: 'InputValueDefinition', name: { @@ -1370,10 +1369,10 @@ const shouldAugmentRelationField = (config, rootType, fromName, toName) => shouldAugmentType(config, rootType, toName); const fieldIsNotIgnored = (astNode, field, resolvers) => { - return ( - !getFieldDirective(field, 'neo4j_ignore') && - !getCustomFieldResolver(astNode, field, resolvers) - ); + return !getFieldDirective(field, 'neo4j_ignore'); + // FIXME: issue related to inferences on AST field .resolve + // See: possiblyAddIgnoreDirective + // !getCustomFieldResolver(astNode, field, resolvers) }; const isNotSystemField = name => { From e952ffd6726a8b23abdfe292e61e8124e0322275 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:43:01 -0800 Subject: [PATCH 06/13] Update index.js replaced getQuerySelections and getMutationSelections with getPayloadSelections because, for scalar payload mutations, getMutationSelections would return a selection set equal to the mutation's arguments --- src/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 77340f41..b1f0eb7b 100644 --- a/src/index.js +++ b/src/index.js @@ -5,8 +5,7 @@ import { extractTypeMapFromTypeDefs, addDirectiveDeclarations, printTypeMap, - getQuerySelections, - getMutationSelections + getPayloadSelections } from './utils'; import { extractTypeMapFromSchema, @@ -67,9 +66,10 @@ export function cypherQuery( ) { const { typeName, variableName } = typeIdentifiers(resolveInfo.returnType); const schemaType = resolveInfo.schema.getType(typeName); - const selections = getQuerySelections(resolveInfo); + const selections = getPayloadSelections(resolveInfo); return translateQuery({ resolveInfo, + context, schemaType, selections, variableName, @@ -89,9 +89,10 @@ export function cypherMutation( ) { const { typeName, variableName } = typeIdentifiers(resolveInfo.returnType); const schemaType = resolveInfo.schema.getType(typeName); - const selections = getMutationSelections(resolveInfo); + const selections = getPayloadSelections(resolveInfo); return translateMutation({ resolveInfo, + context, schemaType, selections, variableName, From fef3098e1539dddae0174cd4f6dc637ba3b56cfd Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:44:28 -0800 Subject: [PATCH 07/13] Handles cases involving cypherParams refactoring introduced isScalarSchemaType and schemaTypeField variables --- src/selections.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/selections.js b/src/selections.js index 71149379..3e0cb323 100644 --- a/src/selections.js +++ b/src/selections.js @@ -31,6 +31,7 @@ import { export function buildCypherSelection({ initial, + cypherParams, selections, variableName, schemaType, @@ -60,6 +61,7 @@ export function buildCypherSelection({ let tailParams = { selections: tailSelections, + cypherParams, variableName, schemaType, resolveInfo, @@ -104,9 +106,12 @@ export function buildCypherSelection({ } const commaIfTail = tailSelections.length > 0 ? ',' : ''; - + const isScalarSchemaType = isGraphqlScalarType(schemaType); + const schemaTypeField = !isScalarSchemaType + ? schemaType.getFields()[fieldName] + : {}; // Schema meta fields(__schema, __typename, etc) - if (!schemaType.getFields()[fieldName]) { + if (!isScalarSchemaType && !schemaTypeField) { return recurse({ initial: tailSelections.length ? initial @@ -115,7 +120,8 @@ export function buildCypherSelection({ }); } - const fieldType = schemaType.getFields()[fieldName].type; + const fieldType = + schemaTypeField && schemaTypeField.type ? schemaTypeField.type : {}; const innerSchemaType = innerType(fieldType); // for target "type" aka label if ( @@ -166,6 +172,7 @@ export function buildCypherSelection({ return recurse({ initial: `${initial}${fieldName}: apoc.cypher.runFirstColumn("${customCypher}", ${cypherDirectiveArgs( variableName, + cypherParams, headSelection, schemaType, resolveInfo @@ -192,7 +199,10 @@ export function buildCypherSelection({ }); } // We have a graphql object type - const innerSchemaTypeAstNode = typeMap[innerSchemaType].astNode; + const innerSchemaTypeAstNode = + innerSchemaType && typeMap[innerSchemaType] + ? typeMap[innerSchemaType].astNode + : {}; const innerSchemaTypeRelation = getRelationTypeDirectiveArgs( innerSchemaTypeAstNode ); @@ -213,7 +223,7 @@ export function buildCypherSelection({ const skipLimit = computeSkipLimit(headSelection, resolveInfo.variableValues); const subSelections = extractSelections( - headSelection.selectionSet.selections, + headSelection.selectionSet ? headSelection.selectionSet.selections : [], resolveInfo.fragments ); @@ -223,6 +233,7 @@ export function buildCypherSelection({ variableName: nestedVariable, schemaType: innerSchemaType, resolveInfo, + cypherParams, parentSelectionInfo: { fieldName, schemaType, @@ -235,7 +246,10 @@ export function buildCypherSelection({ }); let selection; - const fieldArgs = schemaType.getFields()[fieldName].args.map(e => e.astNode); + const fieldArgs = + !isScalarSchemaType && schemaTypeField && schemaTypeField.args + ? schemaTypeField.args.map(e => e.astNode) + : []; const temporalArgs = getTemporalArguments(fieldArgs); const queryParams = paramsToString( innerFilterParams(filterParams, temporalArgs) @@ -259,6 +273,7 @@ export function buildCypherSelection({ selection = recurse( customCypherField({ ...fieldInfo, + cypherParams, schemaType, schemaTypeRelation, customCypher, From 5d0e4537a108cbb626e74de74dfe628946c18250 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:45:50 -0800 Subject: [PATCH 08/13] Handles for cypherParams includes some defensive branches from implementing support for scalar payload mutations --- src/translate.js | 64 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/src/translate.js b/src/translate.js index 53e67ca8..5057f540 100644 --- a/src/translate.js +++ b/src/translate.js @@ -30,7 +30,8 @@ import { splitSelectionParameters, getTemporalArguments, temporalPredicateClauses, - isTemporalType + isTemporalType, + isGraphqlScalarType } from './utils'; import { getNamedType } from 'graphql'; import { buildCypherSelection } from './selections'; @@ -38,6 +39,7 @@ import _ from 'lodash'; export const customCypherField = ({ customCypher, + cypherParams, schemaTypeRelation, initial, fieldName, @@ -62,6 +64,7 @@ export const customCypherField = ({ fieldIsList ? '' : 'head(' }[ ${nestedVariable} IN apoc.cypher.runFirstColumn("${customCypher}", ${cypherDirectiveArgs( variableName, + cypherParams, headSelection, schemaType, resolveInfo @@ -368,7 +371,7 @@ export const temporalField = ({ // containing this temporal field was a node let variableName = parentVariableName; let fieldIsArray = isArrayType(parentFieldType); - if (!isNodeType(parentSchemaType.astNode)) { + if (parentSchemaType && !isNodeType(parentSchemaType.astNode)) { // initial assumption wrong, build appropriate relationship variable if ( isRootSelection({ @@ -474,6 +477,7 @@ export const temporalType = ({ // Query API root operation branch export const translateQuery = ({ resolveInfo, + context, selections, variableName, typeName, @@ -493,13 +497,15 @@ export const translateQuery = ({ const queryArgs = getQueryArguments(resolveInfo); const temporalArgs = getTemporalArguments(queryArgs); const queryTypeCypherDirective = getQueryCypherDirective(resolveInfo); + const cypherParams = getCypherParams(context); const queryParams = paramsToString( innerFilterParams( filterParams, temporalArgs, null, queryTypeCypherDirective ? true : false - ) + ), + cypherParams ); const safeVariableName = safeVar(variableName); const temporalClauses = temporalPredicateClauses( @@ -509,9 +515,11 @@ export const translateQuery = ({ ); const outerSkipLimit = getOuterSkipLimit(first); const orderByValue = computeOrderBy(resolveInfo, selections); + if (queryTypeCypherDirective) { return customQuery({ resolveInfo, + cypherParams, schemaType, argString: queryParams, selections, @@ -525,6 +533,7 @@ export const translateQuery = ({ } else { return nodeQuery({ resolveInfo, + cypherParams, schemaType, argString: queryParams, selections, @@ -542,9 +551,19 @@ export const translateQuery = ({ } }; +const getCypherParams = context => { + return context && + context.cypherParams && + context.cypherParams instanceof Object && + Object.keys(context.cypherParams).length > 0 + ? context.cypherParams + : undefined; +}; + // Custom read operation const customQuery = ({ resolveInfo, + cypherParams, schemaType, argString, selections, @@ -558,6 +577,7 @@ const customQuery = ({ const safeVariableName = safeVar(variableName); const [subQuery, subParams] = buildCypherSelection({ initial: '', + cypherParams, selections, variableName, schemaType, @@ -565,20 +585,32 @@ const customQuery = ({ paramIndex: 1 }); const params = { ...nonNullParams, ...subParams }; + if (cypherParams) { + params['cypherParams'] = cypherParams; + } // QueryType with a @cypher directive const cypherQueryArg = queryTypeCypherDirective.arguments.find(x => { return x.name.value === 'statement'; }); + const isScalarType = isGraphqlScalarType(schemaType); + const temporalType = isTemporalType(schemaType.name); const query = `WITH apoc.cypher.runFirstColumn("${ cypherQueryArg.value.value - }", ${argString || 'null'}, True) AS x UNWIND x AS ${safeVariableName} - RETURN ${safeVariableName} {${subQuery}} AS ${safeVariableName}${orderByValue} ${outerSkipLimit}`; + }", ${argString || + 'null'}, True) AS x UNWIND x AS ${safeVariableName} RETURN ${safeVariableName} ${ + // Don't add subQuery for scalar type payloads + // FIXME: fix subselection translation for temporal type payload + !temporalType && !isScalarType + ? `{${subQuery}} AS ${safeVariableName}${orderByValue}` + : '' + } ${outerSkipLimit}`; return [query, params]; }; // Generated API const nodeQuery = ({ resolveInfo, + cypherParams, schemaType, selections, variableName, @@ -596,6 +628,7 @@ const nodeQuery = ({ const safeLabelName = safeLabel(typeName); const [subQuery, subParams] = buildCypherSelection({ initial: '', + cypherParams, selections, variableName, schemaType, @@ -603,6 +636,9 @@ const nodeQuery = ({ paramIndex: 1 }); const params = { ...nonNullParams, ...subParams }; + if (cypherParams) { + params['cypherParams'] = cypherParams; + } const arrayParams = _.pickBy(filterParams, Array.isArray); const args = innerFilterParams(filterParams, temporalArgs); @@ -642,6 +678,7 @@ const nodeQuery = ({ // Mutation API root operation branch export const translateMutation = ({ resolveInfo, + context, schemaType, selections, variableName, @@ -669,6 +706,7 @@ export const translateMutation = ({ if (mutationTypeCypherDirective) { return customMutation({ ...mutationInfo, + context, mutationTypeCypherDirective, variableName, orderByValue, @@ -712,6 +750,7 @@ export const translateMutation = ({ // Custom write operation const customMutation = ({ params, + context, mutationTypeCypherDirective, selections, variableName, @@ -720,6 +759,7 @@ const customMutation = ({ orderByValue, outerSkipLimit }) => { + const cypherParams = getCypherParams(context); const safeVariableName = safeVar(variableName); // FIXME: support IN for multiple values -> WHERE const argString = paramsToString( @@ -728,7 +768,8 @@ const customMutation = ({ null, null, true - ) + ), + cypherParams ); const cypherQueryArg = mutationTypeCypherDirective.arguments.find(x => { return x.name.value === 'statement'; @@ -741,12 +782,21 @@ const customMutation = ({ resolveInfo, paramIndex: 1 }); + const isScalarType = isGraphqlScalarType(schemaType); + const temporalType = isTemporalType(schemaType.name); params = { ...params, ...subParams }; + if (cypherParams) { + params['cypherParams'] = cypherParams; + } const query = `CALL apoc.cypher.doIt("${ cypherQueryArg.value.value }", ${argString}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS ${safeVariableName} - RETURN ${safeVariableName} {${subQuery}} AS ${safeVariableName}${orderByValue} ${outerSkipLimit}`; + RETURN ${safeVariableName} ${ + !temporalType && !isScalarType + ? `{${subQuery}} AS ${safeVariableName}${orderByValue} ${outerSkipLimit}` + : '' + }`; return [query, params]; }; From 394822674c27e124206ef25f1e167c81d2f996bf Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:46:40 -0800 Subject: [PATCH 09/13] scalar payload mutation support, other fixes --- src/utils.js | 90 +++++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/utils.js b/src/utils.js index 331f57f5..27cc837f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,6 +6,9 @@ import filter from 'lodash/filter'; function parseArg(arg, variableValues) { switch (arg.value.kind) { + case 'Variable': { + return variableValues[arg.value.name.value]; + } case 'IntValue': { return parseInt(arg.value.value); } @@ -44,15 +47,17 @@ export const parseFieldSdl = sdl => { }; export const parseInputFieldsSdl = fields => { + let arr = []; if (Array.isArray(fields)) { fields = fields.join('\n'); - return fields - ? parse(`input Type { ${fields} }`).definitions[0].fields - : {}; + arr = fields ? parse(`type Type { ${fields} }`).definitions[0].fields : []; + arr = arr.map(e => ({ + kind: 'InputValueDefinition', + name: e.name, + type: e.type + })); } - return fields - ? parse(`input Type { ${fields} }`).definitions[0].fields[0] - : {}; + return arr; }; export const parseDirectiveSdl = sdl => { @@ -96,13 +101,14 @@ export function extractSelections(selections, fragments) { export function extractQueryResult({ records }, returnType) { const { variableName } = typeIdentifiers(returnType); - - let result = isArrayType(returnType) - ? records.map(record => record.get(variableName)) - : records.length - ? records[0].get(variableName) - : null; - + let result = null; + if (isArrayType(returnType)) { + result = records.map(record => record.get(variableName)); + } else if (records.length) { + // could be object or scalar + result = records[0].get(variableName); + result = Array.isArray(result) ? result[0] : result; + } // handle Integer fields result = _.cloneDeepWith(result, field => { if (neo4j.isInt(field)) { @@ -110,7 +116,6 @@ export function extractQueryResult({ records }, returnType) { return field.inSafeRange() ? field.toNumber() : field.toString(); } }); - return result; } @@ -136,6 +141,7 @@ function getDefaultArguments(fieldName, schemaType) { export function cypherDirectiveArgs( variable, + cypherParams, headSelection, schemaType, resolveInfo @@ -146,14 +152,18 @@ export function cypherDirectiveArgs( resolveInfo.variableValues ); - let args = JSON.stringify(Object.assign(defaultArgs, queryArgs)).replace( + const args = JSON.stringify(Object.assign(defaultArgs, queryArgs)).replace( /\"([^(\")"]+)\":/g, ' $1: ' ); return args === '{}' - ? `{this: ${variable}${args.substring(1)}` - : `{this: ${variable},${args.substring(1)}`; + ? `{this: ${variable}, ${ + cypherParams ? `cypherParams: $cypherParams` : '' + }${args.substring(1)}` + : `{this: ${variable},${ + cypherParams ? ` cypherParams: $cypherParams,` : '' + }${args.substring(1)}`; } export function _isNamedMutation(name) { @@ -188,7 +198,7 @@ export function isGraphqlScalarType(type) { } export function isArrayType(type) { - return type.toString().startsWith('['); + return type ? type.toString().startsWith('[') : false; } export const isRelationTypeDirectedField = fieldName => { @@ -321,7 +331,7 @@ export function innerFilterParams( : []; } -export function paramsToString(params) { +export function paramsToString(params, cypherParams) { if (params.length > 0) { const strings = _.map(params, param => { return `${param.key}:${param.paramKey ? `$${param.paramKey}.` : '$'}${ @@ -330,7 +340,9 @@ export function paramsToString(params) { : `${param.value.index}_${param.key}` }`; }); - return `{${strings.join(', ')}}`; + return `{${strings.join(', ')}${ + cypherParams ? `, cypherParams: $cypherParams}` : '}' + }`; } return ''; } @@ -499,13 +511,19 @@ export const buildCypherParameters = ({ // TODO refactor to handle Query/Mutation type schema directives const directiveWithArgs = (directiveName, args) => (schemaType, fieldName) => { function fieldDirective(schemaType, fieldName, directiveName) { - return schemaType - .getFields() - [fieldName].astNode.directives.find(e => e.name.value === directiveName); + return !isGraphqlScalarType(schemaType) + ? schemaType + .getFields() + [fieldName].astNode.directives.find( + e => e.name.value === directiveName + ) + : {}; } function directiveArgument(directive, name) { - return directive.arguments.find(e => e.name.value === name).value.value; + return directive && directive.arguments + ? directive.arguments.find(e => e.name.value === name).value.value + : []; } const directive = fieldDirective(schemaType, fieldName, directiveName); @@ -803,28 +821,20 @@ export const initializeMutationParams = ({ export const getOuterSkipLimit = first => `SKIP $offset${first > -1 ? ' LIMIT $first' : ''}`; -export const getQuerySelections = resolveInfo => { +export const getPayloadSelections = resolveInfo => { const filteredFieldNodes = filter( resolveInfo.fieldNodes, n => n.name.value === resolveInfo.fieldName ); - // FIXME: how to handle multiple fieldNode matches - return extractSelections( - filteredFieldNodes[0].selectionSet.selections, - resolveInfo.fragments - ); -}; - -export const getMutationSelections = resolveInfo => { - let selections = getQuerySelections(resolveInfo); - if (selections.length === 0) { - // FIXME: why aren't the selections found in the filteredFieldNode? - selections = extractSelections( - resolveInfo.operation.selectionSet.selections, + if (filteredFieldNodes[0] && filteredFieldNodes[0].selectionSet) { + // FIXME: how to handle multiple fieldNode matches + const x = extractSelections( + filteredFieldNodes[0].selectionSet.selections, resolveInfo.fragments ); + return x; } - return selections; + return []; }; export const filterNullParams = ({ offset, first, otherParams }) => { @@ -1089,7 +1099,7 @@ export const getCustomFieldResolver = (astNode, field, resolvers) => { }; export const removeIgnoredFields = (schemaType, selections) => { - if (schemaType && selections && selections.length) { + if (!isGraphqlScalarType(schemaType) && selections && selections.length) { let schemaTypeField = ''; selections = selections.filter(e => { if (e.kind === 'Field') { From 5449751a98b7b72d3606a859497b6376a26c75ea Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:47:18 -0800 Subject: [PATCH 10/13] Update testSchema.js --- test/helpers/testSchema.js | 392 +++++++++++++++++++++++-------------- 1 file changed, 244 insertions(+), 148 deletions(-) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 3fcf0d9c..4b2001dd 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -1,161 +1,257 @@ -export const testSchema = `type Movie { - _id: String - movieId: ID! - title: String - year: Int - released: DateTime! - plot: String - poster: String - imdbRating: Float - genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT") - similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o") - mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this") - degree: Int @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())") - actors(first: Int = 3, offset: Int = 0, name: String, names: [String]): [Actor] @relation(name: "ACTED_IN", direction:"IN") - avgStars: Float - filmedIn: State @relation(name: "FILMED_IN", direction:"OUT") - scaleRating(scale: Int = 3): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") - scaleRatingFloat(scale: Float = 1.5): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") - actorMovies: [Movie] @cypher(statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other") - ratings( - rating: Int +export const testSchema = /* GraphQL */ ` + type Movie { + _id: String + movieId: ID! + title: String + year: Int + released: DateTime! + plot: String + poster: String + imdbRating: Float + genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT") + similar(first: Int = 3, offset: Int = 0): [Movie] + @cypher( + statement: "WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o" + ) + mostSimilar: Movie @cypher(statement: "WITH {this} AS this RETURN this") + degree: Int + @cypher(statement: "WITH {this} AS this RETURN SIZE((this)--())") + actors( + first: Int = 3 + offset: Int = 0 + name: String + names: [String] + ): [Actor] @relation(name: "ACTED_IN", direction: "IN") + avgStars: Float + filmedIn: State @relation(name: "FILMED_IN", direction: "OUT") + scaleRating(scale: Int = 3): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + scaleRatingFloat(scale: Float = 1.5): Float + @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") + actorMovies: [Movie] + @cypher( + statement: "MATCH (this)-[:ACTED_IN*2]-(other:Movie) RETURN other" + ) + ratings( + rating: Int + time: Time + date: Date + datetime: DateTime + localtime: LocalTime + localdatetime: LocalDateTime + ): [Rated] + years: [Int] + titles: [String] + imdbRatings: [Float] + releases: [DateTime] + customField: String @neo4j_ignore + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + } + + type Genre { + _id: String! + name: String + movies(first: Int = 3, offset: Int = 0): [Movie] + @relation(name: "IN_GENRE", direction: "IN") + highestRatedMovie: Movie + @cypher( + statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" + ) + } + + type State { + customField: String @neo4j_ignore + name: String! + } + + interface Person { + userId: ID! + name: String + } + + type Actor implements Person { + userId: ID! + name: String + movies: [Movie] @relation(name: "ACTED_IN", direction: "OUT") + } + + type User implements Person { + userId: ID! + name: String + currentUserId(strArg: String, strInputArg: strInput): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + rated( + rating: Int + time: Time + date: Date + datetime: DateTime + localtime: LocalTime + localdatetime: LocalDateTime + ): [Rated] + friends( + since: Int + time: Time + date: Date + datetime: DateTime + localtime: LocalTime + localdatetime: LocalDateTime + ): [FriendOf] + favorites: [Movie] @relation(name: "FAVORITED", direction: "OUT") + } + + type FriendOf { + from: User + currentUserId: String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) + since: Int time: Time date: Date datetime: DateTime + datetimes: [DateTime] localtime: LocalTime localdatetime: LocalDateTime - ): [Rated] - years: [Int] - titles: [String] - imdbRatings: [Float] - releases: [DateTime] - customField: String -} - -type Genre { - _id: String! - name: String - movies(first: Int = 3, offset: Int = 0): [Movie] @relation(name: "IN_GENRE", direction: "IN") - highestRatedMovie: Movie @cypher(statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1") -} - -type State { - customField: String @neo4j_ignore - name: String! -} - -interface Person { - userId: ID! - name: String -} - -type Actor implements Person { - userId: ID! - name: String - movies: [Movie] @relation(name: "ACTED_IN", direction:"OUT") -} - -type User implements Person { - userId: ID! - name: String - rated( + to: User + } + + type Rated { + from: User + currentUserId(strArg: String): String + @cypher( + statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" + ) rating: Int + ratings: [Int] time: Time date: Date datetime: DateTime localtime: LocalTime localdatetime: LocalDateTime - ): [Rated] - friends( - since: Int, - time: Time, - date: Date, - datetime: DateTime, - localtime: LocalTime, - localdatetime: LocalDateTime - ): [FriendOf] -} - -type FriendOf { - from: User - since: Int - time: Time - date: Date - datetime: DateTime - datetimes: [DateTime] - localtime: LocalTime - localdatetime: LocalDateTime - to: User -} - -type Rated { - from: User - rating: Int - ratings: [Int] - time: Time - date: Date - datetime: DateTime - localtime: LocalTime - localdatetime: LocalDateTime - datetimes: [DateTime] - to: Movie -} - -enum BookGenre { - Mystery, - Science, - Math -} - -type Book { - genre: BookGenre -} - -enum _MovieOrdering { - title_desc, - title_asc -} - -enum _GenreOrdering { - name_desc, - name_asc -} - -type Query { - Movie(_id: String, movieId: ID, title: String, year: Int, released: DateTime, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie] - MoviesByYear(year: Int): [Movie] - MoviesByYears(year: [Int]): [Movie] - MovieById(movieId: ID!): Movie - MovieBy_Id(_id: String!): Movie - GenresBySubstring(substring: String): [Genre] @cypher(statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g") - State: [State] - Books: [Book] -} - -type TemporalNode { - datetime: DateTime - name: String - time: Time - date: Date - localtime: LocalTime - localdatetime: LocalDateTime - localdatetimes: [LocalDateTime] - temporalNodes( - time: Time, - date: Date, - datetime: DateTime, - localtime: LocalTime, + datetimes: [DateTime] + to: Movie + } + + enum BookGenre { + Mystery + Science + Math + } + + type Book { + genre: BookGenre + } + + enum _MovieOrdering { + title_desc + title_asc + } + + enum _GenreOrdering { + name_desc + name_asc + } + + type Query { + Movie( + _id: String + movieId: ID + title: String + year: Int + released: DateTime + plot: String + poster: String + imdbRating: Float + first: Int + offset: Int + orderBy: _MovieOrdering + ): [Movie] + MoviesByYear(year: Int): [Movie] + MoviesByYears(year: [Int]): [Movie] + MovieById(movieId: ID!): Movie + MovieBy_Id(_id: String!): Movie + GenresBySubstring(substring: String): [Genre] + @cypher( + statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g" + ) + State: [State] + User(userId: ID, name: String, _id: String): [User] + Books: [Book] + currentUserId: String + @cypher(statement: "RETURN $cypherParams.currentUserId AS currentUserId") + computedBoolean: Boolean @cypher(statement: "RETURN true") + computedFloat: Float @cypher(statement: "RETURN 3.14") + computedInt: Int @cypher(statement: "RETURN 1") + computedIntList: [Int] + @cypher(statement: "UNWIND [1, 2, 3] AS intList RETURN intList") + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + computedTemporal: DateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + } + + type Mutation { + currentUserId: String + @cypher(statement: "RETURN $cypherParams.currentUserId") + computedObjectWithCypherParams: currentUserId + @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") + computedTemporal: DateTime + @cypher( + statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" + ) + computedStringList: [String] + @cypher( + statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" + ) + customWithArguments(strArg: String, strInputArg: strInput): String + @cypher(statement: "RETURN $strInputArg.strArg") + } + + type currentUserId { + userId: String + } + + type TemporalNode { + datetime: DateTime + name: String + time: Time + date: Date + localtime: LocalTime localdatetime: LocalDateTime - ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) -} - -type ignoredType { - ignoredField: String @neo4j_ignore -} - -scalar Time -scalar Date -scalar DateTime -scalar LocalTime -scalar LocalDateTime + localdatetimes: [LocalDateTime] + temporalNodes( + time: Time + date: Date + datetime: DateTime + localtime: LocalTime + localdatetime: LocalDateTime + ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) + } + + type ignoredType { + ignoredField: String @neo4j_ignore + } + + scalar Time + scalar Date + scalar DateTime + scalar LocalTime + scalar LocalDateTime + + input strInput { + strArg: String + } `; From 704afa9162a5658106c61937d15d2fd4c37d726e Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:48:07 -0800 Subject: [PATCH 11/13] Update cypherTest.js --- test/cypherTest.js | 496 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 481 insertions(+), 15 deletions(-) diff --git a/test/cypherTest.js b/test/cypherTest.js index 8fef59dc..f8f43bdc 100644 --- a/test/cypherTest.js +++ b/test/cypherTest.js @@ -4,6 +4,10 @@ import { augmentedSchemaCypherTestRunner } from './helpers/cypherTestHelpers'; +const CYPHER_PARAMS = { + userId: 'user-id' +}; + test('simple Cypher query', t => { const graphQLQuery = `{ Movie(title: "River Runs Through It, A") { @@ -17,6 +21,7 @@ test('simple Cypher query', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -39,6 +44,7 @@ test('Simple skip limit', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: 1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -58,7 +64,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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ @@ -66,6 +72,7 @@ test('Cypher projection skip limit', t => { title: 'River Runs Through It, A', '1_first': 3, first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -87,6 +94,7 @@ test('Handle Query with name not aligning to type', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { year: 2010, first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -106,6 +114,7 @@ test('Query without arguments, non-null type', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -127,6 +136,7 @@ test('Query single object', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { movieId: '18', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -152,6 +162,7 @@ test('Query single object relation', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { movieId: '3100', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -179,6 +190,7 @@ test('Query single object and array of objects relations', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { movieId: '3100', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -209,7 +221,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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_actors_movies_actors_movies_similar { .title , .year }][..3] }] }] }] }] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -218,6 +230,7 @@ test('Deeply nested object query', t => { '1_name': 'Tom Hanks', '2_first': 3, first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -239,6 +252,7 @@ test('Handle meta field at beginning of selection set', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -261,6 +275,7 @@ test('Handle meta field at end of selection set', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -284,6 +299,7 @@ test('Handle meta field in middle of selection set', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -300,13 +316,14 @@ 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, cypherParams: $cypherParams}, true) | movie_mostSimilar { .title , .year }]) } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -320,12 +337,13 @@ 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, cypherParams: $cypherParams, scale: 3}, false)} AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0, title: 'River Runs Through It, A' }), @@ -340,12 +358,13 @@ 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, cypherParams: $cypherParams, scale: 10}, false)} AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0, title: 'River Runs Through It, A', '1_scale': 10 @@ -368,6 +387,7 @@ test('Query for Neo4js internal _id', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -389,6 +409,7 @@ test('Query for Neo4js internal _id and another param before _id', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -409,6 +430,7 @@ test('Query for Neo4js internal _id and another param after _id', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0, year: 2010 }), @@ -430,6 +452,7 @@ test('Query for Neo4js internal _id by dedicated Query MovieBy_Id(_id: String!)' return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -449,6 +472,7 @@ test(`Query for null value translates to 'IS NULL' WHERE clause`, t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -469,6 +493,7 @@ test(`Query for null value combined with internal ID and another param`, t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { year: 2010, first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -489,13 +514,14 @@ 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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0, '1_name': 'Tom Hanks', '3_first': 3 @@ -518,12 +544,13 @@ 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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS `movie` SKIP $offset'; t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { title: 'River Runs Through It, A', + cypherParams: CYPHER_PARAMS, first: -1, offset: 0, '1_first': 3, @@ -545,8 +572,7 @@ 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", {offset:$offset, first:$first, 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", {offset:$offset, first:$first, substring:$substring, cypherParams: $cypherParams}, 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([ @@ -554,7 +580,8 @@ test('Handle @cypher directive on Query Type', t => { substring: 'Action', first: -1, offset: 0, - '1_first': 3 + '1_first': 3, + cypherParams: CYPHER_PARAMS }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -566,7 +593,7 @@ test.cb('Handle @cypher directive on Mutation type', t => { name } }`, - expectedCypherQuery = `CALL apoc.cypher.doIt("CREATE (g:Genre) SET g.name = $name RETURN g", {name:$name, first:$first, offset:$offset}) YIELD value + expectedCypherQuery = `CALL apoc.cypher.doIt("CREATE (g:Genre) SET g.name = $name RETURN g", {name:$name, first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`genre\` RETURN \`genre\` { .name } AS \`genre\` SKIP $offset`; @@ -574,6 +601,7 @@ test.cb('Handle @cypher directive on Mutation type', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { name: 'Wildlife Documentary', first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }); }); @@ -1015,7 +1043,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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_similar { .title }][..3] } AS \`movie\` SKIP $offset`; t.plan(3); @@ -1027,6 +1055,7 @@ test('Handle GraphQL variables in nested selection - first/offset', t => { expectedCypherQuery, { '1_first': 3, + cypherParams: CYPHER_PARAMS, year: 2016, first: -1, offset: 0 @@ -1054,7 +1083,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, cypherParams: $cypherParams, first: 3, offset: 0}, true) | movie_similar { .title ,scaleRating: apoc.cypher.runFirstColumn("WITH $this AS this RETURN $scale * this.imdbRating", {this: movie_similar, cypherParams: $cypherParams, scale: 5}, false)}][..3] } AS \`movie\` SKIP $offset`; t.plan(3); return Promise.all([ @@ -1068,7 +1097,8 @@ test('Handle GraphQL variables in nest selection - @cypher param (not first/offs first: -1, offset: 0, '1_first': 3, - '2_scale': 5 + '2_scale': 5, + cypherParams: CYPHER_PARAMS } ), augmentedSchemaCypherTestRunner( @@ -1100,6 +1130,7 @@ test('Return internal node id for _id field', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { year: 2016, + cypherParams: CYPHER_PARAMS, first: -1, offset: 0 }), @@ -1120,6 +1151,7 @@ test('Treat enum as a scalar', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, first: -1, offset: 0 }), @@ -1147,6 +1179,7 @@ query getMovie { t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, title: 'River Runs Through It, A', first: -1, offset: 0 @@ -1180,6 +1213,7 @@ query getMovie { t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, title: 'River Runs Through It, A', first: -1, offset: 0 @@ -1209,6 +1243,7 @@ test('nested fragments', t => { t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, year: 2010, first: -1, offset: 0 @@ -1237,6 +1272,7 @@ test('fragments on relations', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { year: 2010, + cypherParams: CYPHER_PARAMS, first: -1, offset: 0 }), @@ -1267,6 +1303,7 @@ test('nested fragments on relations', t => { t.plan(3); return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, year: 2010, first: -1, offset: 0 @@ -3687,6 +3724,7 @@ test('Cypher array queries', t => { cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { year: [1999], first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -3712,7 +3750,8 @@ test('Cypher array sub queries', t => { year: [1998], '1_names': ['Jeff Bridges', 'John Goodman'], first: -1, - offset: 0 + offset: 0, + cypherParams: CYPHER_PARAMS }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); @@ -3757,6 +3796,7 @@ test('Query node with ignored field', t => { return Promise.all([ cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { first: -1, + cypherParams: CYPHER_PARAMS, offset: 0 }), augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) @@ -3895,3 +3935,429 @@ test('Deeply nested query using temporal orderBy', t => { augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) ]); }); + +test('Handle @cypher field with String payload using cypherParams', t => { + const graphQLQuery = `query { + User { + userId + currentUserId + name + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\` ) RETURN \`user\` { .userId ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user, cypherParams: $cypherParams}, false), .name } AS \`user\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle nested @cypher fields that use cypherParams', t => { + const graphQLQuery = `query { + User { + userId + currentUserId + name + friends { + to { + since + currentUserId + User { + name + currentUserId + } + } + from { + since + currentUserId + } + } + rated { + rating + currentUserId + } + favorites { + movieId + currentUserId + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\` ) RETURN \`user\` { .userId ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user, cypherParams: $cypherParams}, false), .name ,friends: {to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) | user_to_relation { .since ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user_to_relation, cypherParams: $cypherParams}, false),User: user_to { .name ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user_to, cypherParams: $cypherParams}, false)} }] ,from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) | user_from_relation { .since ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user_from_relation, cypherParams: $cypherParams}, false)}] } ,rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`]->(:\`Movie\`) | user_rated_relation { .rating ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user_rated_relation, cypherParams: $cypherParams}, false)}] ,favorites: [(\`user\`)-[:\`FAVORITED\`]->(\`user_favorites\`:\`Movie\`) | user_favorites { .movieId ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: user_favorites, cypherParams: $cypherParams}, false)}] } AS \`user\` SKIP $offset`; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query using cypherParams with String payload', t => { + const graphQLQuery = `query { + currentUserId + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS currentUserId", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`string\` RETURN \`string\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query using cypherParams with Object payload', t => { + const graphQLQuery = `query { + computedObjectWithCypherParams { + userId + } + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN { userId: $cypherParams.currentUserId }", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`currentUserId\` RETURN \`currentUserId\` { .userId } AS \`currentUserId\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with Boolean payload', t => { + const graphQLQuery = `query { + computedBoolean + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN true", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`boolean\` RETURN \`boolean\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with Int payload', t => { + const graphQLQuery = `query { + computedInt + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN 1", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`int\` RETURN \`int\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with Float payload', t => { + const graphQLQuery = `query { + computedFloat + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN 3.14", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`float\` RETURN \`float\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with String list payload', t => { + const graphQLQuery = `query { + computedStringList + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("UNWIND ['hello', 'world'] AS stringList RETURN stringList", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`string\` RETURN \`string\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with Int list payload', t => { + const graphQLQuery = `query { + computedIntList + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("UNWIND [1, 2, 3] AS intList RETURN intList", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`int\` RETURN \`int\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher query with Temporal payload', t => { + const graphQLQuery = `query { + computedTemporal { + year + month + day + hour + minute + second + microsecond + millisecond + nanosecond + timezone + formatted + } + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`_Neo4jDateTime\` RETURN \`_Neo4jDateTime\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher mutation using cypherParams with String payload', t => { + const graphQLQuery = `mutation { + currentUserId + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("RETURN $cypherParams.currentUserId", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`string\` + RETURN \`string\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + cypherParams: CYPHER_PARAMS, + first: -1, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher mutation using cypherParams with Object payload', t => { + const graphQLQuery = `mutation { + computedObjectWithCypherParams { + userId + } + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("RETURN { userId: $cypherParams.currentUserId }", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`currentUserId\` + RETURN \`currentUserId\` { .userId } AS \`currentUserId\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher mutation with String list payload', t => { + const graphQLQuery = `mutation { + computedStringList + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("UNWIND ['hello', 'world'] AS stringList RETURN stringList", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`string\` + RETURN \`string\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle @cypher mutation with Temporal payload', t => { + const graphQLQuery = `mutation { + computedTemporal { + year + month + day + hour + minute + second + microsecond + millisecond + nanosecond + timezone + formatted + } + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`_Neo4jDateTime\` + RETURN \`_Neo4jDateTime\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('Handle nested @cypher fields using parameterized arguments and cypherParams', t => { + const graphQLQuery = `query someQuery( + $strArg1: String + $strArg2: String + $strArg3: String + $strInputArg: strInput + ) { + Movie { + _id + currentUserId(strArg: $strArg1) + ratings { + currentUserId(strArg: $strArg2) + User { + name + currentUserId(strArg: $strArg3, strInputArg: $strInputArg) + } + } + } + }`, + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\` ) RETURN \`movie\` {_id: ID(\`movie\`),currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: movie, cypherParams: $cypherParams, strArg: "Yo Dawg"}, false),ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation {currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: movie_ratings_relation, cypherParams: $cypherParams, strArg: "Yoo Dawg"}, false),User: head([(:\`Movie\`)<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .name ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: movie_ratings_User, cypherParams: $cypherParams, strArg: "Yooo Dawg", strInputArg: { strArg: "Yoooo Dawg"}}, false)}]) }] } AS \`movie\` SKIP $offset`; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + strArg1: 'Yo Dawg', + strArg2: 'Yoo Dawg', + strArg3: 'Yooo Dawg', + strInputArg: { + strArg: 'Yoooo Dawg' + }, + cypherParams: CYPHER_PARAMS + }, + expectedCypherQuery + ) + ]); +}); + +test('Handle @cypher mutation with input type argument', t => { + const graphQLQuery = `mutation someMutation($strArg: String, $strInputArg: strInput) { + customWithArguments(strArg: $strArg, strInputArg: $strInputArg ) + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("RETURN $strInputArg.strArg", {strArg:$strArg, strInputArg:$strInputArg, first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`string\` + RETURN \`string\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner( + t, + graphQLQuery, + { + strArg: 'Hello', + strInputArg: { + strArg: 'World' + } + }, + expectedCypherQuery, + { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0, + strArg: 'Hello', + strInputArg: { + strArg: 'World' + } + } + ), + augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + strArg: 'Hello', + strInputArg: { + strArg: 'World' + } + }, + expectedCypherQuery + ) + ]); +}); + +test('Handle @cypher query with parameterized input type argument', t => { + const graphQLQuery = `query someQuery ($strArg: String, $strInputArg: strInput) { + customWithArguments(strArg: $strArg, strInputArg: $strInputArg ) + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("RETURN $strInputArg.strArg", {offset:$offset, first:$first, strArg:$strArg, strInputArg:$strInputArg, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`string\` RETURN \`string\` SKIP $offset`; + + t.plan(3); + return Promise.all([ + cypherTestRunner( + t, + graphQLQuery, + { + strArg: 'Hello', + strInputArg: { + strArg: 'World' + } + }, + expectedCypherQuery, + { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0, + strArg: 'Hello', + strInputArg: { + strArg: 'World' + }, + cypherParams: CYPHER_PARAMS + } + ), + augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + strArg: 'Hello', + strInputArg: { + strArg: 'World' + } + }, + expectedCypherQuery + ) + ]); +}); From e5f947030562c93d0a84d93713ea3ae18cc63fc7 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:48:42 -0800 Subject: [PATCH 12/13] Update augmentSchemaTest.js --- test/augmentSchemaTest.js | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/test/augmentSchemaTest.js b/test/augmentSchemaTest.js index 5b1ea863..ceecc22b 100644 --- a/test/augmentSchemaTest.js +++ b/test/augmentSchemaTest.js @@ -4,7 +4,6 @@ import { printSchema } from 'graphql'; test.cb('Test augmented schema', t => { let schema = augmentedSchema(); - let expectedSchema = `directive @cypher(statement: String) on FIELD_DEFINITION directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT @@ -54,6 +53,7 @@ type _AddMovieGenresPayload { type _AddMovieRatingsPayload { from: User to: Movie + currentUserId: String rating: Int ratings: [Int] time: _Neo4jTime @@ -69,9 +69,15 @@ type _AddTemporalNodeTemporalNodesPayload { to: TemporalNode } +type _AddUserFavoritesPayload { + from: User + to: Movie +} + type _AddUserFriendsPayload { from: User to: User + currentUserId: String since: Int time: _Neo4jTime date: _Neo4jDate @@ -84,6 +90,7 @@ type _AddUserFriendsPayload { type _AddUserRatedPayload { from: User to: Movie + currentUserId: String rating: Int ratings: [Int] time: _Neo4jTime @@ -105,6 +112,17 @@ enum _BookOrdering { _id_desc } +input _currentUserIdInput { + userId: String! +} + +enum _currentUserIdOrdering { + userId_asc + userId_desc + _id_asc + _id_desc +} + input _FriendOfInput { since: Int time: _Neo4jTimeInput @@ -134,6 +152,7 @@ enum _MovieOrdering { } type _MovieRatings { + currentUserId(strArg: String): String rating: Int ratings: [Int] time: _Neo4jTime @@ -306,6 +325,11 @@ type _RemoveTemporalNodeTemporalNodesPayload { to: TemporalNode } +type _RemoveUserFavoritesPayload { + from: User + to: Movie +} + type _RemoveUserFriendsPayload { from: User to: User @@ -349,6 +373,7 @@ enum _TemporalNodeOrdering { } type _UserFriends { + currentUserId: String since: Int time: _Neo4jTime date: _Neo4jDate @@ -373,11 +398,14 @@ enum _UserOrdering { userId_desc name_asc name_desc + currentUserId_asc + currentUserId_desc _id_asc _id_desc } type _UserRated { + currentUserId(strArg: String): String rating: Int ratings: [Int] time: _Neo4jTime @@ -407,12 +435,18 @@ enum BookGenre { Math } +type currentUserId { + userId: String + _id: String +} + scalar Date scalar DateTime type FriendOf { from: User + currentUserId: String since: Int time: _Neo4jTime date: _Neo4jDate @@ -463,9 +497,15 @@ type Movie { imdbRatings: [Float] releases: [_Neo4jDateTime] customField: String + currentUserId(strArg: String): String } type Mutation { + currentUserId: String + computedObjectWithCypherParams: currentUserId + computedTemporal: _Neo4jDateTime + computedStringList: [String] + customWithArguments(strArg: String, strInputArg: strInput): String CreateMovie(movieId: ID, title: String, year: Int, released: _Neo4jDateTimeInput!, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie UpdateMovie(movieId: ID!, title: String, year: Int, released: _Neo4jDateTimeInput, plot: String, poster: String, imdbRating: Float, avgStars: Float, years: [Int], titles: [String], imdbRatings: [Float], releases: [_Neo4jDateTimeInput]): Movie DeleteMovie(movieId: ID!): Movie @@ -495,8 +535,12 @@ type Mutation { RemoveUserRated(from: _UserInput!, to: _MovieInput!): _RemoveUserRatedPayload AddUserFriends(from: _UserInput!, to: _UserInput!, data: _FriendOfInput!): _AddUserFriendsPayload RemoveUserFriends(from: _UserInput!, to: _UserInput!): _RemoveUserFriendsPayload + AddUserFavorites(from: _UserInput!, to: _MovieInput!): _AddUserFavoritesPayload + RemoveUserFavorites(from: _UserInput!, to: _MovieInput!): _RemoveUserFavoritesPayload CreateBook(genre: BookGenre): Book DeleteBook(genre: BookGenre!): Book + CreatecurrentUserId(userId: String): currentUserId + DeletecurrentUserId(userId: String!): currentUserId CreateTemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode UpdateTemporalNode(datetime: _Neo4jDateTimeInput!, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: [_Neo4jLocalDateTimeInput]): TemporalNode DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode @@ -517,16 +561,26 @@ type Query { MovieBy_Id(_id: String!): Movie GenresBySubstring(substring: String, first: Int, offset: Int, orderBy: [_GenreOrdering]): [Genre] State(first: Int, offset: Int, orderBy: [_StateOrdering]): [State] + User(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_UserOrdering]): [User] Books(first: Int, offset: Int, orderBy: [_BookOrdering]): [Book] + currentUserId: String + computedBoolean: Boolean + computedFloat: Float + computedInt: Int + computedIntList: [Int] + computedStringList: [String] + computedTemporal: _Neo4jDateTime + computedObjectWithCypherParams: currentUserId + customWithArguments(strArg: String, strInputArg: strInput): String Genre(_id: String, name: String, first: Int, offset: Int, orderBy: [_GenreOrdering]): [Genre] Actor(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_ActorOrdering]): [Actor] - User(userId: ID, name: String, _id: String, first: Int, offset: Int, orderBy: [_UserOrdering]): [User] Book(genre: BookGenre, _id: String, first: Int, offset: Int, orderBy: [_BookOrdering]): [Book] TemporalNode(datetime: _Neo4jDateTimeInput, name: String, time: _Neo4jTimeInput, date: _Neo4jDateInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, localdatetimes: _Neo4jLocalDateTimeInput, _id: String, first: Int, offset: Int, orderBy: [_TemporalNodeOrdering]): [TemporalNode] } type Rated { from: User + currentUserId(strArg: String): String rating: Int ratings: [Int] time: _Neo4jTime @@ -544,6 +598,10 @@ type State { _id: String } +input strInput { + strArg: String +} + type TemporalNode { datetime: _Neo4jDateTime name: String @@ -561,8 +619,10 @@ scalar Time type User implements Person { userId: ID! name: String + currentUserId(strArg: String, strInputArg: strInput): String rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput): [_UserRated] friends: _UserFriendsDirections + favorites(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] _id: String } `; From b371eb317c82386e9bc4f0acbce1528cc80b863b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Sat, 16 Feb 2019 18:49:07 -0800 Subject: [PATCH 13/13] Update cypherTestHelpers.js --- test/helpers/cypherTestHelpers.js | 186 +++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 3 deletions(-) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index c53877dc..a5f7e5ff 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -26,11 +26,21 @@ type Mutation { CreateState(name: String!): State UpdateMovie(movieId: ID!, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie DeleteMovie(movieId: ID!): Movie -} + 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") + computedTemporal: DateTime @cypher(statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }") + customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") + } `; const resolvers = { Query: { + User(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, Movie(object, params, ctx, resolveInfo) { const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); t.is(query, expectedCypherQuery); @@ -70,6 +80,51 @@ type Mutation { const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); + }, + computedBoolean(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedInt(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedFloat(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + currentUserId(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedTemporal(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedObjectWithCypherParams(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedStringList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedIntList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + customWithArguments(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); } }, Mutation: { @@ -102,6 +157,36 @@ type Mutation { t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); t.end(); + }, + currentUserId(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedObjectWithCypherParams(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedStringList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedTemporal(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + customWithArguments(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); } } }; @@ -115,7 +200,17 @@ type Mutation { }); // query the test schema with the test query, assertion is in the resolver - return graphql(schema, graphqlQuery, null, null, graphqlParams); + return graphql( + schema, + graphqlQuery, + null, + { + cypherParams: { + userId: 'user-id' + } + }, + graphqlParams + ); } // Optimization to prevent schema augmentation from running for every test @@ -198,6 +293,51 @@ export function augmentedSchemaCypherTestRunner( const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); + }, + computedBoolean(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedInt(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedFloat(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + currentUserId(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedTemporal(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedObjectWithCypherParams(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedStringList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + computedIntList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, + customWithArguments(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); } }, Mutation: { @@ -278,6 +418,36 @@ export function augmentedSchemaCypherTestRunner( t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); t.end(); + }, + currentUserId(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedObjectWithCypherParams(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedStringList(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + computedTemporal(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + customWithArguments(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); } } }; @@ -290,7 +460,17 @@ export function augmentedSchemaCypherTestRunner( } }); - return graphql(augmentedSchema, graphqlQuery, null, null, graphqlParams); + return graphql( + augmentedSchema, + graphqlQuery, + null, + { + cypherParams: { + userId: 'user-id' + } + }, + graphqlParams + ); } const augmentedSchemaTypeDefs = augmentTypeDefs(testSchema);