diff --git a/src/augment.js b/src/augment.js index 90267108..237e702e 100644 --- a/src/augment.js +++ b/src/augment.js @@ -84,18 +84,18 @@ const augmentQueryArguments = typeMap => { }; export const augmentResolvers = ( - queryResolvers, - mutationResolvers, - typeMap + augmentedTypeMap, + resolvers ) => { - let resolvers = {}; - const queryMap = createOperationMap(typeMap.Query); - queryResolvers = possiblyAddResolvers(queryMap, queryResolvers); + let queryResolvers = resolvers && resolvers.Query ? resolvers.Query : {}; + let mutationResolvers = resolvers && resolvers.Mutation ? resolvers.Mutation : {}; + const generatedQueryMap = createOperationMap(augmentedTypeMap.Query); + queryResolvers = possiblyAddResolvers(generatedQueryMap, queryResolvers); if (Object.keys(queryResolvers).length > 0) { resolvers.Query = queryResolvers; } - const mutationMap = createOperationMap(typeMap.Mutation); - mutationResolvers = possiblyAddResolvers(mutationMap, mutationResolvers); + const generatedMutationMap = createOperationMap(augmentedTypeMap.Mutation); + mutationResolvers = possiblyAddResolvers(generatedMutationMap, mutationResolvers); if (Object.keys(mutationResolvers).length > 0) { resolvers.Mutation = mutationResolvers; } @@ -103,8 +103,8 @@ export const augmentResolvers = ( // must implement __resolveInfo for every Interface type // we use "FRAGMENT_TYPE" key to identify the Interface implementation // type at runtime, so grab this value - const interfaceTypes = Object.keys(typeMap).filter( - e => typeMap[e].kind === 'InterfaceTypeDefinition' + const interfaceTypes = Object.keys(augmentedTypeMap).filter( + e => augmentedTypeMap[e].kind === 'InterfaceTypeDefinition' ); interfaceTypes.map(e => { resolvers[e] = {}; @@ -357,13 +357,19 @@ const possiblyAddRelationMutations = ( relatedAstNode, true ); + // TODO refactor the getRelationTypeDirectiveArgs stuff in here, + // TODO out of it, and make it use the above, already obtained values... typeMap = possiblyAddNonSymmetricRelationshipType( relatedAstNode, capitalizedFieldName, typeName, - typeMap + typeMap, + field ); + // TODO probably put replaceRelationTypeValue above into possiblyAddNonSymmetricRelationshipType, after you refactor it fields[fieldIndex] = replaceRelationTypeValue( + fromName, + toName, field, capitalizedFieldName, typeName @@ -629,16 +635,19 @@ const possiblyAddTypeMutation = (namePrefix, astNode, typeMap, mutationMap) => { return typeMap; }; -const replaceRelationTypeValue = (field, capitalizedFieldName, typeName) => { +const replaceRelationTypeValue = (fromName, toName, field, capitalizedFieldName, typeName) => { const isList = isListType(field); + // TODO persist a required inner type, and required list type let type = { kind: 'NamedType', name: { kind: 'Name', - value: `_${typeName}${capitalizedFieldName}` + value: `_${typeName}${capitalizedFieldName}${ + fromName === toName ? 'Directions' : '' + }` } }; - if (isList) { + if (isList && fromName !== toName) { type = { kind: 'ListType', type: type @@ -652,7 +661,8 @@ const possiblyAddNonSymmetricRelationshipType = ( relationAstNode, capitalizedFieldName, typeName, - typeMap + typeMap, + field ) => { const fieldTypeName = `_${typeName}${capitalizedFieldName}`; if (!typeMap[fieldTypeName]) { @@ -660,8 +670,10 @@ const possiblyAddNonSymmetricRelationshipType = ( let fieldValueName = ''; let fromField = {}; let toField = {}; - let fromValue = ''; - let toValue = ''; + let _fromField = {}; + let _toField = {}; + let fromValue = undefined; + let toValue = undefined; let fields = relationAstNode.fields; const relationTypeDirective = getRelationTypeDirectiveArgs(relationAstNode); if (relationTypeDirective) { @@ -683,53 +695,53 @@ const possiblyAddNonSymmetricRelationshipType = ( return acc; }, []) .join('\n'); + if(fromValue && fromValue === toValue) { + // If field is a list type, then make .from and .to list types + const fieldIsList = isListType(field); - typeMap[fieldTypeName] = parse(` - type ${fieldTypeName} ${print(relationAstNode.directives)} { - ${relationPropertyFields} - ${getRelatedTypeSelectionFields( - typeName, - fromValue, - fromField, - toValue, - toField - )} + typeMap[`${fieldTypeName}Directions`] = parse(` + type ${fieldTypeName}Directions ${print(relationAstNode.directives)} { + from${getFieldArgumentsFromAst( + field, + typeName, + )}: ${fieldIsList ? '[' : ''}${fieldTypeName}${fieldIsList ? ']' : ''} + to${getFieldArgumentsFromAst( + field, + typeName, + )}: ${fieldIsList ? '[' : ''}${fieldTypeName}${fieldIsList ? ']' : ''} + }`); + + typeMap[fieldTypeName] = parse(` + type ${fieldTypeName} ${print(relationAstNode.directives)} { + ${relationPropertyFields} + ${fromValue}: ${fromValue} + } + `); + + // remove arguments on field + field.arguments = []; } - `); - } + else { + // Non-reflexive case, (User)-[RATED]->(Movie) + typeMap[fieldTypeName] = parse(` + type ${fieldTypeName} ${print(relationAstNode.directives)} { + ${relationPropertyFields} + ${ + typeName === toValue + ? // If this is the from, the allow selecting the to + `${fromValue}: ${fromValue}` + : // else this is the to, so allow selecting the from + typeName === fromValue + ? `${toValue}: ${toValue}` + : ''} + } + `); + } + } } return typeMap; }; -const getRelatedTypeSelectionFields = ( - typeName, - fromValue, - fromField, - toValue, - toField -) => { - // TODO identify and handle ambiguity of relation type symmetry, Person FRIEND_OF Person, etc. - // if(typeName === fromValue && typeName === toValue) { - // return ` - // from${fromField.arguments.length > 0 - // ? `(${getFieldArgumentsFromAst(fromField)})` - // : ''}: ${fromValue} - // to${toField.arguments.length > 0 - // ? `(${getFieldArgumentsFromAst(toField)})` - // : ''}: ${toValue}`; - // } - return typeName === fromValue - ? // If this is the from, the allow selecting the to - `${toValue}(${getFieldArgumentsFromAst(toField, toValue)}): ${toValue}` - : // else this is the to, so allow selecting the from - typeName === toValue - ? `${fromValue}(${getFieldArgumentsFromAst( - fromField, - fromValue - )}): ${fromValue}` - : ''; -}; - const addOrReplaceNodeIdField = (astNode, valueType) => { const fields = astNode ? astNode.fields : []; const index = fields.findIndex(e => e.name.value === '_id'); diff --git a/src/augmentSchema.js b/src/augmentSchema.js index 6403fe9a..3e6061b7 100644 --- a/src/augmentSchema.js +++ b/src/augmentSchema.js @@ -8,9 +8,9 @@ import { augmentResolvers } from "./augment"; -export const augmentedSchema = (typeMap, queryResolvers, mutationResolvers) => { +export const augmentedSchema = (typeMap, resolvers) => { const augmentedTypeMap = augmentTypeMap(typeMap); - const augmentedResolvers = augmentResolvers(queryResolvers, mutationResolvers, augmentedTypeMap); + const augmentedResolvers = augmentResolvers(augmentedTypeMap, resolvers); // TODO extract and persist logger and schemaDirectives, at least return makeExecutableSchema({ typeDefs: printTypeMap(augmentedTypeMap), @@ -34,9 +34,7 @@ export const makeAugmentedExecutableSchema = ({ }) => { const typeMap = extractTypeMapFromTypeDefs(typeDefs); const augmentedTypeMap = augmentTypeMap(typeMap); - const queryResolvers = resolvers && resolvers.Query ? resolvers.Query : {}; - const mutationResolvers = resolvers && resolvers.Mutation ? resolvers.Mutation : {}; - const augmentedResolvers = augmentResolvers(queryResolvers, mutationResolvers, augmentedTypeMap); + const augmentedResolvers = augmentResolvers(augmentedTypeMap, resolvers); resolverValidationOptions.requireResolversForResolveType = false; return makeExecutableSchema({ typeDefs: printTypeMap(augmentedTypeMap), @@ -73,15 +71,46 @@ export const extractTypeMapFromSchema = (schema) => { }, {}); } -export const extractResolvers = (operationType) => { - const operationTypeFields = operationType ? operationType.getFields() : {}; - const operations = Object.keys(operationTypeFields); - let resolver = {}; - return operations.length > 0 - ? operations.reduce((acc, t) => { - resolver = operationTypeFields[t].resolve; - if(resolver !== undefined) acc[t] = resolver; +export const extractResolversFromSchema = (schema) => { + const _typeMap = schema && schema._typeMap ? schema._typeMap : {}; + const types = Object.keys(_typeMap); + let type = {}; + let schemaTypeResolvers = {}; + return types.reduce( (acc, t) => { + // prevent extraction from schema introspection system keys + if(t !== "__Schema" + && t !== "__Type" + && t !== "__TypeKind" + && t !== "__Field" + && t !== "__InputValue" + && t !== "__EnumValue" + && t !== "__Directive") { + type = _typeMap[t]; + // resolvers are stored on the field level at a .resolve key + schemaTypeResolvers = extractFieldResolversFromSchemaType(type); + // do not add unless there exists at least one field resolver for type + if(schemaTypeResolvers) { + acc[t] = schemaTypeResolvers; + } + } + return acc; + }, {}) +} + +const extractFieldResolversFromSchemaType = (type) => { + const fields = type._fields; + const fieldKeys = fields ? Object.keys(fields) : []; + const fieldResolvers = fieldKeys.length > 0 + ? fieldKeys.reduce( (acc, t) => { + // do not add entry for this field unless it has resolver + if(fields[t].resolve !== undefined) { + acc[t] = fields[t].resolve; + } return acc; - }, {}) - : {}; + }, {}) + : undefined; + // do not return value unless there exists at least 1 field resolver + return fieldResolvers && Object.keys(fieldResolvers).length > 0 + ? fieldResolvers + : undefined; } diff --git a/src/index.js b/src/index.js index f15daf92..89416f96 100644 --- a/src/index.js +++ b/src/index.js @@ -13,12 +13,13 @@ import { isMutation, lowFirstLetter, typeIdentifiers, - parameterizeRelationFields + parameterizeRelationFields, + getFieldValueType } from './utils'; import { buildCypherSelection } from './selections'; import { extractTypeMapFromSchema, - extractResolvers, + extractResolversFromSchema, augmentedSchema, makeAugmentedExecutableSchema } from './augmentSchema'; @@ -146,8 +147,8 @@ export function cypherQuery( query = `MATCH (${variableName}:${typeName} ${argString}) ${predicate}` + - // ${variableName} { ${selection} } as ${variableName}`; `RETURN ${variableName} {${subQuery}} AS ${variableName}${orderByValue} ${outerSkipLimit}`; + } return [query, { ...nonNullParams, ...subParams }]; @@ -243,11 +244,11 @@ export function cypherMutation( resolveInfo.fieldName ].astNode.arguments; - const firstIdArg = args.find(e => getNamedType(e).type.name.value); + const firstIdArg = args.find(e => getFieldValueType(e) === "ID"); if (firstIdArg) { - const argName = firstIdArg.name.value; - if (params.params[argName] === undefined) { - query += `SET ${variableName}.${argName} = apoc.create.uuid() `; + const firstIdArgFieldName = firstIdArg.name.value; + if (params.params[firstIdArgFieldName] === undefined) { + query += `SET ${variableName}.${firstIdArgFieldName} = apoc.create.uuid() `; } } @@ -323,11 +324,14 @@ export function cypherMutation( initial: '', selections, variableName: lowercased, - fromVar, - toVar, schemaType, resolveInfo, - paramIndex: 1 + paramIndex: 1, + rootVariableNames: { + from: `${fromVar}`, + to: `${toVar}`, + }, + variableName: schemaType.name === fromType ? `${toVar}` : `${fromVar}` }); params = { ...params, ...subParams }; query = ` @@ -449,11 +453,11 @@ RETURN ${variableName}`; schemaType, resolveInfo, paramIndex: 1, - rootNodes: { + rootVariableNames: { from: `_${fromVar}`, to: `_${toVar}` }, - variableName: schemaType.name === fromType ? `_${toVar}` : `_${fromVar}` + variableName: schemaType.name === fromType ? `_${toVar}` : `_${fromVar}`, }); params = { ...params, ...subParams }; @@ -468,7 +472,7 @@ RETURN ${variableName}`; OPTIONAL MATCH (${fromVar})-[${fromVar + toVar}:${relationshipName}]->(${toVar}) DELETE ${fromVar + toVar} - WITH COUNT(*) AS scope, ${fromVar} AS _${fromVar}_from, ${toVar} AS _${toVar}_to + WITH COUNT(*) AS scope, ${fromVar} AS _${fromVar}, ${toVar} AS _${toVar} RETURN {${subQuery}} AS ${schemaType}; `; } else { @@ -480,11 +484,10 @@ RETURN ${variableName}`; return [query, params]; } -export const augmentSchema = schema => { - let typeMap = extractTypeMapFromSchema(schema); - let queryResolvers = extractResolvers(schema.getQueryType()); - let mutationResolvers = extractResolvers(schema.getMutationType()); - return augmentedSchema(typeMap, queryResolvers, mutationResolvers); +export const augmentSchema = (schema) => { + const typeMap = extractTypeMapFromSchema(schema); + const resolvers = extractResolversFromSchema(schema); + return augmentedSchema(typeMap, resolvers); }; export const makeAugmentedSchema = ({ diff --git a/src/selections.js b/src/selections.js index 2742fc8c..f7bc5d0a 100644 --- a/src/selections.js +++ b/src/selections.js @@ -6,13 +6,20 @@ import { innerFilterParams, getFilterParams, innerType, - isArrayType, isGraphqlScalarType, extractSelections, relationDirective, - getRelationTypeDirectiveArgs + getRelationTypeDirectiveArgs, + decideNestedVariableName, } from './utils'; +import { + customCypherField, + relationFieldOnNodeType, + relationTypeFieldOnNodeType, + nodeTypeFieldOnRelationType, +} from './translate'; + export function buildCypherSelection({ initial, selections, @@ -20,7 +27,7 @@ export function buildCypherSelection({ schemaType, resolveInfo, paramIndex = 1, - rootNodes + rootVariableNames, }) { if (!selections.length) { return [initial, {}]; @@ -44,7 +51,7 @@ export function buildCypherSelection({ selections: tailSelections, variableName, schemaType, - resolveInfo + resolveInfo, }; const recurse = args => { @@ -69,7 +76,7 @@ export function buildCypherSelection({ selections: fragmentSelections, variableName, schemaType, - resolveInfo + resolveInfo, }; return recurse({ initial: fragmentSelections.length @@ -157,9 +164,23 @@ export function buildCypherSelection({ ...tailParams }); } - // We have a graphql object type - const nestedVariable = variableName + '_' + fieldName; + const innerSchemaTypeAstNode = typeMap[innerSchemaType].astNode; + const innerSchemaTypeRelation = getRelationTypeDirectiveArgs(innerSchemaTypeAstNode); + const schemaTypeRelation = getRelationTypeDirectiveArgs(schemaTypeAstNode); + const { name: relType, direction: relDirection } = relationDirective( + schemaType, + fieldName + ); + + const nestedVariable = decideNestedVariableName({ + schemaTypeRelation, + innerSchemaTypeRelation, + variableName, + fieldName, + rootVariableNames + }); + const skipLimit = computeSkipLimit(headSelection, resolveInfo.variableValues); const subSelections = extractSelections( @@ -172,147 +193,64 @@ export function buildCypherSelection({ selections: subSelections, variableName: nestedVariable, schemaType: innerSchemaType, - resolveInfo + resolveInfo, }); let selection; - - // Object type field with cypher directive + const queryParams = innerFilterParams(filterParams); + const fieldInfo = { + initial, + fieldName, + fieldType, + variableName, + nestedVariable, + queryParams, + subSelection, + skipLimit, + commaIfTail, + tailParams, + }; if (customCypher) { - if (getRelationTypeDirectiveArgs(schemaTypeAstNode)) { - variableName = `${variableName}_relation`; - } - // similar: [ x IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie}, true) |x {.title}][1..2]) - const fieldIsList = !!fieldType.ofType; - selection = recurse({ - initial: `${initial}${fieldName}: ${ - fieldIsList ? '' : 'head(' - }[ ${nestedVariable} IN apoc.cypher.runFirstColumn("${customCypher}", ${cypherDirectiveArgs( - variableName, - headSelection, - schemaType, - resolveInfo - )}, true) | ${nestedVariable} {${subSelection[0]}}]${ - fieldIsList ? '' : ')' - }${skipLimit} ${commaIfTail}`, - ...tailParams - }); - } else { - // graphql object type, no custom cypher - const queryParams = innerFilterParams(filterParams); - const relationDirectiveData = getRelationTypeDirectiveArgs( - schemaTypeAstNode - ); - if (relationDirectiveData) { - const fromTypeName = relationDirectiveData.from; - const toTypeName = relationDirectiveData.to; - const isFromField = fieldName === fromTypeName || fieldName === 'from'; - const isToField = fieldName === toTypeName || fieldName === 'to'; - if (isFromField || isToField) { - if (rootNodes && (fieldName === 'from' || fieldName === 'to')) { - // Branch currenlty needed to be explicit about handling the .to and .from - // keys involved with the relation removal mutation, using rootNodes - selection = recurse({ - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[${ - isFromField ? `${rootNodes.from}_from` : `${rootNodes.to}_to` - } {${subSelection[0]}}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, - ...tailParams, - rootNodes, - variableName: isFromField ? rootNodes.to : rootNodes.from - }); - } else { - selection = recurse({ - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(:${ - fieldName === fromTypeName || fieldName === 'from' - ? toTypeName - : fromTypeName - })${ - fieldName === fromTypeName || fieldName === 'from' ? '<' : '' - }-[${variableName}_relation]-${ - fieldName === toTypeName || fieldName === 'to' ? '>' : '' - }(${nestedVariable}:${ - isInlineFragment ? interfaceLabel : innerSchemaType.name - }${queryParams}) | ${nestedVariable} {${ - isInlineFragment - ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] - : subSelection[0] - }}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, - ...tailParams - }); - } - } - } else { - let { name: relType, direction: relDirection } = relationDirective( - schemaType, - fieldName - ); - if (relType && relDirection) { - selection = recurse({ - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})${ - relDirection === 'in' || relDirection === 'IN' ? '<' : '' - }-[:${relType}]-${ - relDirection === 'out' || relDirection === 'OUT' ? '>' : '' - }(${nestedVariable}:${ - isInlineFragment ? interfaceLabel : innerSchemaType.name - }${queryParams}) | ${nestedVariable} {${ - isInlineFragment - ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] - : subSelection[0] - }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, - ...tailParams - }); - } else { - const innerSchemaTypeAstNode = typeMap[innerSchemaType].astNode; - const relationDirectiveData = getRelationTypeDirectiveArgs( - innerSchemaTypeAstNode - ); - if (relationDirectiveData) { - const relType = relationDirectiveData.name; - const fromTypeName = relationDirectiveData.from; - const toTypeName = relationDirectiveData.to; - const nestedRelationshipVariable = `${nestedVariable}_relation`; - const schemaTypeName = schemaType.name; - if (fromTypeName !== toTypeName) { - selection = recurse({ - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})${ - schemaTypeName === toTypeName ? '<' : '' - }-[${nestedRelationshipVariable}:${relType}${queryParams}]-${ - schemaTypeName === fromTypeName ? '>' : '' - }(:${ - schemaTypeName === fromTypeName ? toTypeName : fromTypeName - }) | ${nestedRelationshipVariable} {${subSelection[0]}}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, - ...tailParams - }); - } else { - // Type symmetry limitation, Person FRIEND_OF Person, assume OUT for now - selection = recurse({ - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(${variableName})-[${nestedRelationshipVariable}:${relType}${queryParams}]->(:${ - schemaTypeName === fromTypeName ? toTypeName : fromTypeName - }) | ${nestedRelationshipVariable} {${subSelection[0]}}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, - ...tailParams - }); - } - } - } - } + // Object type field with cypher directive + 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) { + // Object type field on relation type + // (from, to, renamed, relation mutation payloads...) + 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, + })); } return [selection[0], { ...selection[1], ...subSelection[1] }]; } diff --git a/src/translate.js b/src/translate.js new file mode 100644 index 00000000..75db2672 --- /dev/null +++ b/src/translate.js @@ -0,0 +1,235 @@ +import { + isArrayType, + cypherDirectiveArgs +} from './utils'; + +export const customCypherField = ({ + customCypher, + schemaTypeRelation, + initial, + fieldName, + fieldType, + nestedVariable, + variableName, + headSelection, + schemaType, + resolveInfo, + subSelection, + skipLimit, + commaIfTail, + tailParams +}) => { + if (schemaTypeRelation) { + variableName = `${variableName}_relation`; + } + const fieldIsList = !!fieldType.ofType; + // similar: [ x IN apoc.cypher.runFirstColumn("WITH {this} AS this MATCH (this)--(:Genre)--(o:Movie) RETURN o", {this: movie}, true) |x {.title}][1..2]) + return { + initial: `${initial}${fieldName}: ${ + fieldIsList ? '' : 'head(' + }[ ${nestedVariable} IN apoc.cypher.runFirstColumn("${customCypher}", ${cypherDirectiveArgs( + variableName, + headSelection, + schemaType, + resolveInfo + )}, true) | ${nestedVariable} {${subSelection[0]}}]${ + fieldIsList ? '' : ')' + }${skipLimit} ${commaIfTail}`, + ...tailParams, + }; +} + +export const relationFieldOnNodeType = ({ + initial, + fieldName, + fieldType, + variableName, + relDirection, + relType, + nestedVariable, + isInlineFragment, + interfaceLabel, + innerSchemaType, + queryParams, + subSelection, + skipLimit, + commaIfTail, + tailParams +}) => { + return { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(${variableName})${ + relDirection === 'in' || relDirection === 'IN' ? '<' : '' + }-[:${relType}]-${ + relDirection === 'out' || relDirection === 'OUT' ? '>' : '' + }(${nestedVariable}:${ + isInlineFragment ? interfaceLabel : innerSchemaType.name + }${queryParams}) | ${nestedVariable} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, + ...tailParams + }; +} + +export const relationTypeFieldOnNodeType = ({ + innerSchemaTypeRelation, + initial, + fieldName, + subSelection, + skipLimit, + commaIfTail, + tailParams, + fieldType, + variableName, + schemaType, + nestedVariable, + queryParams +}) => { + if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { + return { + initial: `${initial}${fieldName}: {${subSelection[0]}}${skipLimit} ${commaIfTail}`, + ...tailParams, + } + } + return { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(${variableName})${ + schemaType.name === innerSchemaTypeRelation.to ? '<' : '' + }-[${nestedVariable}_relation:${innerSchemaTypeRelation.name}${queryParams}]-${ + schemaType.name === innerSchemaTypeRelation.from ? '>' : '' + }(:${ + schemaType.name === innerSchemaTypeRelation.from ? innerSchemaTypeRelation.to : innerSchemaTypeRelation.from + }) | ${nestedVariable}_relation {${subSelection[0]}}]${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`, + ...tailParams + } +} + +export const nodeTypeFieldOnRelationType = ({ + fieldInfo, + rootVariableNames, + schemaTypeRelation, + innerSchemaType, + isInlineFragment, + interfaceLabel, +}) => { + if (rootVariableNames) { + // Special case used by relation mutation payloads + // rootVariableNames is persisted for sibling directed fields + return relationTypeMutationPayloadField({ + ...fieldInfo, + rootVariableNames + }); + } + else { + // Normal case of schemaType with a relationship directive + return directedFieldOnReflexiveRelationType({ + ...fieldInfo, + schemaTypeRelation, + innerSchemaType, + isInlineFragment, + interfaceLabel, + }); + } +} + +const relationTypeMutationPayloadField = ({ + initial, + fieldName, + variableName, + subSelection, + skipLimit, + commaIfTail, + tailParams, + rootVariableNames +}) => { + return { + initial: `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`, + ...tailParams, + rootVariableNames, + variableName: fieldName === 'from' ? rootVariableNames.to : rootVariableNames.from + } +} + +const directedFieldOnReflexiveRelationType = ({ + initial, + fieldName, + fieldType, + variableName, + queryParams, + nestedVariable, + subSelection, + skipLimit, + commaIfTail, + tailParams, + schemaTypeRelation, + innerSchemaType, + isInlineFragment, + interfaceLabel +}) => { + const relType = schemaTypeRelation.name; + const fromTypeName = schemaTypeRelation.from; + 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, + // we first check whether the relationship is reflexive + if(fromTypeName === toTypeName) { + if(fieldName === "from" || fieldName === "to") { + return { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(${variableName})${ + isFromField ? '<' : '' + }-[${relationshipVariableName}:${relType}${queryParams}]-${ + isToField ? '>' : '' + }(${nestedVariable}:${ + isInlineFragment + ? interfaceLabel + : fromTypeName + }) | ${relationshipVariableName} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`, + ...tailParams + }; + } + else { + // Case of a renamed directed field + return { + initial: `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`, + ...tailParams + }; + } + } + // Related node types are different + return { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(:${ + isFromField ? toTypeName : fromTypeName + })${ + isFromField ? '<' : '' + }-[${variableName}_relation]-${ + isToField ? '>' : '' + }(${nestedVariable}:${ + isInlineFragment ? interfaceLabel : innerSchemaType.name + }${queryParams}) | ${nestedVariable} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`, + ...tailParams + } +} diff --git a/src/utils.js b/src/utils.js index 73025101..e611a850 100644 --- a/src/utils.js +++ b/src/utils.js @@ -391,15 +391,19 @@ export const getRelationTypeDirectiveArgs = (relationshipType) => { } : undefined; } -export const getFieldArgumentsFromAst = (field, typeName) => { - const args = field.arguments; - field.arguments = possiblyAddArgument(args, "first", "Int"); - field.arguments = possiblyAddArgument(args, "offset", "Int"); - field.arguments = possiblyAddArgument(args, "orderBy", `_${typeName}Ordering`); - return field.arguments.reduce( (acc, t) => { +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`); + } + const args = augmentedArgs.reduce( (acc, t) => { acc.push(print(t)); return acc; }, []).join('\n'); + return args.length > 0 ? `(${args})` : ''; } export const getRelationMutationPayloadFieldsFromAst = (relatedAstNode) => { @@ -416,6 +420,13 @@ export const getRelationMutationPayloadFieldsFromAst = (relatedAstNode) => { }, []).join('\n'); } +export const getFieldValueType = (type) => { + if(type.kind !== "NamedType") { + return getFieldValueType(type.type); + } + return type.name.value; +} + export const getNamedType = (type) => { if(type.kind !== "NamedType") { return getNamedType(type.type); @@ -477,7 +488,9 @@ export const getPrimaryKey = (astNode) => { } export const getTypeDirective = (relatedAstNode, name) => { - return relatedAstNode.directives.find(e => e.name.value === name); + return relatedAstNode.directives + ? relatedAstNode.directives.find(e => e.name.value === name) + : undefined; } export const getFieldDirective = (field, directive) => { @@ -570,3 +583,45 @@ export const printTypeMap = (typeMap) => { }); } +export const decideNestedVariableName = ({ + schemaTypeRelation, + innerSchemaTypeRelation, + variableName, + fieldName, + rootVariableNames +}) => { + if(rootVariableNames) { + // Only show up for relation mutations + return rootVariableNames[fieldName]; + } + if(schemaTypeRelation) { + const fromTypeName = schemaTypeRelation.from; + const toTypeName = schemaTypeRelation.to; + if(fromTypeName === toTypeName) { + if(fieldName === "from" || fieldName === "to") { + return variableName + '_' + fieldName; + } + 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 { + // Types without @relation directives are assumed to be node types + // and only node types can have fields whose values are relation types + if (innerSchemaTypeRelation) { + // innerSchemaType is a field payload type using a @relation directive + if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { + return variableName; + } + } + else { + // related types are different + return variableName + '_' + fieldName; + } + } + return variableName + '_' + fieldName; +} diff --git a/test/augmentSchemaTest.js b/test/augmentSchemaTest.js index bc94bb74..e51a3207 100644 --- a/test/augmentSchemaTest.js +++ b/test/augmentSchemaTest.js @@ -49,6 +49,12 @@ type _AddMovieRatingsPayload { rating: Int } +type _AddUserFriendsPayload { + from: User + to: User + since: Int +} + type _AddUserRatedPayload { from: User to: Movie @@ -64,6 +70,10 @@ enum _BookOrdering { _id_desc } +input _FriendOfInput { + since: Int +} + input _GenreInput { name: String! } @@ -84,7 +94,7 @@ enum _MovieOrdering { type _MovieRatings { rating: Int - User(first: Int, offset: Int, orderBy: _UserOrdering): User + User: User } input _RatedInput { @@ -121,6 +131,11 @@ type _RemoveMovieRatingsPayload { to: Movie } +type _RemoveUserFriendsPayload { + from: User + to: User +} + type _RemoveUserRatedPayload { from: User to: Movie @@ -137,6 +152,16 @@ enum _StateOrdering { _id_desc } +type _UserFriends { + since: Int + User: User +} + +type _UserFriendsDirections { + from(since: Int): [_UserFriends] + to(since: Int): [_UserFriends] +} + input _UserInput { userId: ID! } @@ -152,7 +177,7 @@ enum _UserOrdering { type _UserRated { rating: Int - Movie(first: Int, offset: Int, orderBy: _MovieOrdering): Movie + Movie: Movie } type Actor implements Person { @@ -175,6 +200,12 @@ enum BookGenre { scalar DateTime +type FriendOf { + from: User + since: Int + to: User +} + type Genre { _id: Int name: String @@ -232,6 +263,8 @@ type Mutation { DeleteUser(userId: ID!): User AddUserRated(from: _UserInput!, to: _MovieInput!, data: _RatedInput!): _AddUserRatedPayload RemoveUserRated(from: _UserInput!, to: _MovieInput!): _RemoveUserRatedPayload + AddUserFriends(from: _UserInput!, to: _UserInput!, data: _FriendOfInput!): _AddUserFriendsPayload + RemoveUserFriends(from: _UserInput!, to: _UserInput!): _RemoveUserFriendsPayload CreateBook(genre: BookGenre): Book DeleteBook(genre: BookGenre!): Book } @@ -256,9 +289,9 @@ type Query { } type Rated { - from(first: Int, offset: Int, orderBy: _UserOrdering): User + from: User rating: Int - to(first: Int, offset: Int, orderBy: _MovieOrdering): Movie + to: Movie } type State { @@ -269,7 +302,8 @@ type State { type User implements Person { userId: ID! name: String - rated: [_UserRated] + rated(rating: Int): [_UserRated] + friends: _UserFriendsDirections _id: Int } `; diff --git a/test/cypherTest.js b/test/cypherTest.js index c34c706f..364d4601 100644 --- a/test/cypherTest.js +++ b/test/cypherTest.js @@ -668,7 +668,7 @@ test('Add relationship mutation', t => { 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: head([(:Genre)<-[in_genre_relation]-(in_genre_from:Movie) | in_genre_from { .movieId ,genres: [(in_genre_from)-[:IN_GENRE]->(in_genre_from_genres:Genre) | in_genre_from_genres {_id: ID(in_genre_from_genres), .name }] }]) ,to: head([(:Movie)-[in_genre_relation]->(in_genre_to:Genre) | in_genre_to { .name }]) } AS _AddMovieGenresPayload; + 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); @@ -707,7 +707,7 @@ test('Add relationship mutation with GraphQL variables', t => { 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: head([(:Genre)<-[in_genre_relation]-(in_genre_from:Movie) | in_genre_from { .movieId ,genres: [(in_genre_from)-[:IN_GENRE]->(in_genre_from_genres:Genre) | in_genre_from_genres {_id: ID(in_genre_from_genres), .name }] }]) ,to: head([(:Movie)-[in_genre_relation]->(in_genre_to:Genre) | in_genre_to { .name }]) } AS _AddMovieGenresPayload; + 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); @@ -724,6 +724,166 @@ test('Add relationship mutation with GraphQL variables', t => { ); }); +test('Add relationship mutation with relationship property', t => { + const graphQLQuery = `mutation someMutation { + AddUserRated( + from: { + userId: "123" + }, + to: { + movieId: "456" + }, + data: { + rating: 5 + } + ) { + from { + _id + userId + name + rated { + rating + Movie { + _id + movieId + title + } + } + } + to { + _id + movieId + title + ratings { + rating + User { + _id + userId + name + } + } + } + rating + } + }`, + expectedCypherQuery = ` + MATCH (user_from:User {userId: $from.userId}) + MATCH (movie_to:Movie {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); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { movieId: '456' }, + data: { rating: 5 }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + +test('Add relationship mutation with relationship property (reflexive)', t => { + const graphQLQuery = `mutation { + AddUserFriends( + from: { + userId: "123" + }, + to: { + userId: "456" + }, + data: { + since: 7 + } + ) { + from { + _id + userId + name + friends { + from { + since + User { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + } + to { + since + User { + _id + name + } + } + } + } + to { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + since + } + } + `, + expectedCypherQuery = ` + MATCH (user_from:User {userId: $from.userId}) + MATCH (user_to:User {userId: $to.userId}) + 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); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { userId: '456' }, + data: { since: 7 }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + test('Remove relationship mutation', t => { const graphQLQuery = `mutation someMutation { RemoveMovieGenres( @@ -735,6 +895,7 @@ test('Remove relationship mutation', t => { title } to { + _id name } } @@ -744,8 +905,8 @@ test('Remove relationship mutation', t => { 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_from, genre_to AS _genre_to_to - RETURN {from: head([_movie_from_from {_id: ID(_movie_from_from), .title }]) ,to: head([_genre_to_to { .name }]) } AS _RemoveMovieGenresPayload; + 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); @@ -762,6 +923,82 @@ test('Remove relationship mutation', t => { ); }); +test('Remove relationship mutation (reflexive)', t => { + const graphQLQuery = `mutation { + RemoveUserFriends( + from: { + userId: "123" + }, + to: { + userId: "456" + }, + ) { + from { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + to { + _id + name + friends { + from { + since + User { + _id + name + } + } + to { + since + User { + _id + name + } + } + } + } + } + } + `, + 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; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { userId: '123' }, + to: { userId: '456' }, + first: -1, + offset: 0 + }, + expectedCypherQuery + ); +}); + test('Handle GraphQL variables in nested selection - first/offset', t => { const graphQLQuery = `query ($year: Int!, $first: Int!) { @@ -946,46 +1183,6 @@ query getMovie { ]); }); -// // test augmented schema: -// test.cb('Add relationship mutation on augmented schema',t => { -// const graphQLQuery = ` -// mutation { -// AddMovieGenre(movieId: "123", name: "Boring") { -// title -// genres { -// name -// } -// } -// } -// `, -// expectedCypherQuery = `MATCH (movie:Movie {movieId: $movieId}) -// MATCH (genre:Genre {name: $name}) -// CREATE (movie)-[:IN_GENRE]->(genre) -// RETURN movie { .title ,genres: [(movie)-[:IN_GENRE]->(movie_genres:Genre) | movie_genres { .name }] } AS movie;`; -// -// t.plan (1); -// // FIXME: not testing Cypher params -// // { movieId: '123', name: 'Boring' } -// augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery); -// -// }); -// -// test.cb('Create node mutation on augmented schema', t=> { -// const graphQLQuery = ` -// mutation { -// CreateGenre(name: "Boring") { -// name -// } -// }`, -// expectedCypherQuery = `CREATE (genre:Genre) SET genre = $params RETURN genre { .name } AS genre`; -// t.plan(2); -// // FIXME: not testing Cypher params -// // { params: { name: 'Boring' } } -// -// augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery); -// -// }); - test('nested fragments', t => { const graphQLQuery = ` query movieItems { @@ -1126,6 +1323,204 @@ test('query for relationship properties', t => { ); }); +test('query reflexive relation nested in non-reflexive relation', t => { + const graphQLQuery = `query { + Movie { + movieId + title + ratings { + rating + User { + userId + name + friends { + from { + since + User { + name + friends { + from { + since + User { + name + } + } + to { + since + User { + name + } + } + } + } + } + to { + since + User { + name + friends { + from { + since + User { + name + } + } + to { + since + User { + name + } + } + } + } + } + } + } + } + } + }`, + 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); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query non-reflexive relation nested in reflexive relation', t => { + const graphQLQuery = `query { + User { + _id + name + friends { + from { + since + User { + _id + name + rated { + rating + Movie { + _id + ratings { + rating + User { + _id + friends { + from { + since + User { + _id + } + } + to { + since + User { + _id + } + } + } + } + } + } + } + } + } + to { + since + User { + _id + name + rated { + rating + Movie { + _id + } + } + } + } + } + } + }`, + 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); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query relation type with argument', t => { + const graphQLQuery = `query { + User { + _id + name + rated(rating: 5) { + rating + Movie { + title + } + } + } + }`, + 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); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query reflexive relation type with arguments', t => { + const graphQLQuery = `query { + User { + userId + name + friends { + from(since: 3) { + since + User { + name + } + } + to(since: 5) { + since + User { + name + } + } + } + } + } + `, + 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); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('query using inline fragment', t => { const graphQLQuery = ` { @@ -1155,3 +1550,4 @@ test('query using inline fragment', t => { {} ); }); + diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 321bbae4..9d99d412 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -102,6 +102,11 @@ export function augmentedSchemaCypherTestRunner( //t.plan(1); const resolvers = { Query: { + User(object, params, ctx, resolveInfo) { + let [query, queryParams] = cypherQuery(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + }, Movie(object, params, ctx, resolveInfo) { let [query, queryParams] = cypherQuery(params, ctx, resolveInfo); t.is(query, expectedCypherQuery); @@ -145,9 +150,27 @@ export function augmentedSchemaCypherTestRunner( t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); t.end(); + }, + AddUserRated(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + AddUserFriends(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); + }, + RemoveUserFriends(object, params, ctx, resolveInfo) { + const [query, queryParams] = cypherMutation(params, ctx, resolveInfo); + t.is(query, expectedCypherQuery); + t.deepEqual(queryParams, expectedCypherParams); + t.end(); } } - }; + } const augmentedSchema = makeAugmentedSchema({ typeDefs: testSchema, @@ -160,6 +183,9 @@ export function augmentedSchemaCypherTestRunner( return graphql(augmentedSchema, graphqlQuery, null, null, graphqlParams); } + + + export function augmentedSchema() { const schema = makeExecutableSchema({ typeDefs: testSchema, diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 837d79f7..28de9ee5 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -45,7 +45,14 @@ type Actor implements Person { type User implements Person { userId: ID! name: String - rated: [Rated] + rated(rating: Int): [Rated] + friends(since: Int): [FriendOf] +} + +type FriendOf { + from: User + since: Int + to: User } type Rated {