diff --git a/src/augment.js b/src/augment.js index 5f829d1e..e3d8d6fb 100644 --- a/src/augment.js +++ b/src/augment.js @@ -9,7 +9,7 @@ import { _getNamedType, getPrimaryKey, getFieldDirective, - getRelationTypeDirectiveArgs, + getRelationTypeDirective, getRelationMutationPayloadFieldsFromAst, getRelationDirection, getRelationName, @@ -441,36 +441,206 @@ const possiblyAddQuery = (astNode, typeMap, resolvers, rootTypes, config) => { const possiblyAddFilterInput = (astNode, typeMap, resolvers, config) => { const typeName = astNode.name.value; - if (isNodeType(astNode) && shouldAugmentType(config, 'query', typeName)) { - const name = `_${astNode.name.value}Filter`; - const filterFields = buildFilterFields( - name, - astNode, - typeMap, - resolvers, - config - ); - if (typeMap[name] === undefined && filterFields.length) { - typeMap[name] = parse(`input ${name} {${filterFields.join('')}}`); + let filterType = `_${typeName}Filter`; + const filterFields = []; + if (shouldAugmentType(config, 'query', typeName)) { + if (isNodeType(astNode)) { + astNode.fields.forEach(t => { + const fieldName = t.name.value; + const isList = _isListType(t); + let valueTypeName = _getNamedType(t).name.value; + const valueType = typeMap[valueTypeName]; + if ( + fieldIsNotIgnored(astNode, t, resolvers) && + isNotSystemField(fieldName) && + !getFieldDirective(t, 'cypher') + ) { + const relatedType = typeMap[valueTypeName]; + const relationTypeDirective = getRelationTypeDirective(relatedType); + let isRelationType = false; + let isReflexiveRelationType = false; + let relationFilterName = `_${typeName}${valueTypeName}Filter`; + const reflexiveFilterName = `_${valueTypeName}DirectionsFilter`; + if (relationTypeDirective) { + isRelationType = true; + let fromType = ''; + let toType = ''; + fromType = relationTypeDirective.from; + toType = relationTypeDirective.to; + if (fromType === toType) { + isReflexiveRelationType = true; + if (typeMap[reflexiveFilterName] === undefined) { + relationFilterName = `_${valueTypeName}Filter`; + typeMap[reflexiveFilterName] = parse(` + input ${reflexiveFilterName} { + from: ${relationFilterName} + to: ${relationFilterName} + } + `); + const relationTypeFilters = buildFilterFields({ + filterType: relationFilterName, + astNode: relatedType, + typeMap, + resolvers, + config + }); + relationTypeFilters.push(` + ${toType}: _${toType}Filter + `); + typeMap[relationFilterName] = parse( + `input ${relationFilterName} {${relationTypeFilters.join( + '' + )}}` + ); + } + } else { + if (typeMap[relationFilterName] === undefined) { + const relationTypeFilters = buildFilterFields({ + filterType: relationFilterName, + astNode: relatedType, + typeMap, + resolvers, + config + }); + let relatedTypeName = toType; + if (typeName === toType) { + relatedTypeName = fromType; + } + relationTypeFilters.push(` + ${relatedTypeName}: _${relatedTypeName}Filter + `); + typeMap[relationFilterName] = parse( + `input ${relationFilterName} {${relationTypeFilters.join( + '' + )}}` + ); + } + } + } + if (!isList) { + if (valueTypeName === 'ID' || valueTypeName == 'String') { + filterFields.push(`${fieldName}: ${valueTypeName} + ${fieldName}_not: ${valueTypeName} + ${fieldName}_in: [${valueTypeName}!] + ${fieldName}_not_in: [${valueTypeName}!] + ${fieldName}_contains: ${valueTypeName} + ${fieldName}_not_contains: ${valueTypeName} + ${fieldName}_starts_with: ${valueTypeName} + ${fieldName}_not_starts_with: ${valueTypeName} + ${fieldName}_ends_with: ${valueTypeName} + ${fieldName}_not_ends_with: ${valueTypeName} + `); + } else if ( + valueTypeName === 'Int' || + valueTypeName === 'Float' || + isTemporalType(valueTypeName) + ) { + if (isTemporalType(valueTypeName)) { + valueTypeName = `${valueTypeName}Input`; + } + filterFields.push(` + ${fieldName}: ${valueTypeName} + ${fieldName}_not: ${valueTypeName} + ${fieldName}_in: [${valueTypeName}!] + ${fieldName}_not_in: [${valueTypeName}!] + ${fieldName}_lt: ${valueTypeName} + ${fieldName}_lte: ${valueTypeName} + ${fieldName}_gt: ${valueTypeName} + ${fieldName}_gte: ${valueTypeName} + `); + } else if (valueTypeName === 'Boolean') { + filterFields.push(` + ${fieldName}: ${valueTypeName} + ${fieldName}_not: ${valueTypeName} + `); + } else if (isKind(valueType, 'EnumTypeDefinition')) { + filterFields.push(` + ${fieldName}: ${valueTypeName} + ${fieldName}_not: ${valueTypeName} + ${fieldName}_in: [${valueTypeName}!] + ${fieldName}_not_in: [${valueTypeName}!] + `); + } else if ( + isKind(valueType, 'ObjectTypeDefinition') && + shouldAugmentType(config, 'query', valueTypeName) + ) { + let relationFilterType = ''; + if (getFieldDirective(t, 'relation')) { + relationFilterType = `_${valueTypeName}Filter`; + } else if (isReflexiveRelationType) { + relationFilterType = reflexiveFilterName; + } else if (isRelationType) { + relationFilterType = relationFilterName; + } + if (relationFilterType) { + filterFields.push(` + ${fieldName}: ${relationFilterType} + ${fieldName}_not: ${relationFilterType} + ${fieldName}_in: [${relationFilterType}!] + ${fieldName}_not_in: [${relationFilterType}!] + `); + } + } + } else if ( + isKind(valueType, 'ObjectTypeDefinition') && + shouldAugmentType(config, 'query', valueTypeName) + ) { + let relationFilterType = ''; + if (getFieldDirective(t, 'relation')) { + relationFilterType = `_${valueTypeName}Filter`; + } else if (isReflexiveRelationType) { + relationFilterType = reflexiveFilterName; + } else if (isRelationType) { + relationFilterType = relationFilterName; + } + if (relationFilterType) { + filterFields.push(` + ${fieldName}: ${relationFilterType} + ${fieldName}_not: ${relationFilterType} + ${fieldName}_in: [${relationFilterType}!] + ${fieldName}_not_in: [${relationFilterType}!] + ${fieldName}_some: ${relationFilterType} + ${fieldName}_none: ${relationFilterType} + ${fieldName}_single: ${relationFilterType} + ${fieldName}_every: ${relationFilterType} + `); + } + } + } + }); } - // if existent, we could merge with provided custom filter here + } + if (filterFields.length) { + filterFields.unshift(` + AND: [${filterType}!] + OR: [${filterType}!] + `); + } + if (typeMap[filterType] === undefined && filterFields.length) { + typeMap[filterType] = parse( + `input ${filterType} {${filterFields.join('')}}` + ); } return typeMap; }; -const buildFilterFields = (filterType, astNode, typeMap, resolvers, config) => { - const fields = astNode.fields; - const filterFields = fields.reduce((acc, t) => { +const buildFilterFields = ({ + filterType, + astNode, + typeMap, + resolvers, + config +}) => { + const filterFields = astNode.fields.reduce((filters, t) => { const fieldName = t.name.value; - const valueTypeName = _getNamedType(t).name.value; const isList = _isListType(t); const valueType = typeMap[valueTypeName]; + let valueTypeName = _getNamedType(t).name.value; if ( fieldIsNotIgnored(astNode, t, resolvers) && isNotSystemField(fieldName) && !getFieldDirective(t, 'cypher') ) { - const filters = []; if (!isList) { if (valueTypeName === 'ID' || valueTypeName == 'String') { filters.push(`${fieldName}: ${valueTypeName} @@ -484,7 +654,14 @@ const buildFilterFields = (filterType, astNode, typeMap, resolvers, config) => { ${fieldName}_ends_with: ${valueTypeName} ${fieldName}_not_ends_with: ${valueTypeName} `); - } else if (valueTypeName === 'Int' || valueTypeName === 'Float') { + } else if ( + valueTypeName === 'Int' || + valueTypeName === 'Float' || + isTemporalType(valueTypeName) + ) { + if (isTemporalType(valueTypeName)) { + valueTypeName = `${valueTypeName}Input`; + } filters.push(` ${fieldName}: ${valueTypeName} ${fieldName}_not: ${valueTypeName} @@ -494,7 +671,7 @@ const buildFilterFields = (filterType, astNode, typeMap, resolvers, config) => { ${fieldName}_lte: ${valueTypeName} ${fieldName}_gt: ${valueTypeName} ${fieldName}_gte: ${valueTypeName} - `); + `); } else if (valueTypeName === 'Boolean') { filters.push(` ${fieldName}: ${valueTypeName} @@ -509,45 +686,43 @@ const buildFilterFields = (filterType, astNode, typeMap, resolvers, config) => { `); } else if ( isKind(valueType, 'ObjectTypeDefinition') && - getFieldDirective(t, 'relation') && shouldAugmentType(config, 'query', valueTypeName) ) { - // one-to-one @relation field - filters.push(` - ${fieldName}: _${valueTypeName}Filter - ${fieldName}_not: _${valueTypeName}Filter - ${fieldName}_in: [_${valueTypeName}Filter!] - ${fieldName}_not_in: [_${valueTypeName}Filter!] - `); + if (getFieldDirective(t, 'relation')) { + // one-to-one @relation field + filters.push(` + ${fieldName}: _${valueTypeName}Filter + ${fieldName}_not: _${valueTypeName}Filter + ${fieldName}_in: [_${valueTypeName}Filter!] + ${fieldName}_not_in: [_${valueTypeName}Filter!] + `); + } } } else if ( isKind(valueType, 'ObjectTypeDefinition') && - getFieldDirective(t, 'relation') && shouldAugmentType(config, 'query', valueTypeName) ) { - // one-to-many @relation field - filters.push(` - ${fieldName}: _${valueTypeName}Filter - ${fieldName}_not: _${valueTypeName}Filter - ${fieldName}_in: [_${valueTypeName}Filter!] - ${fieldName}_not_in: [_${valueTypeName}Filter!] - ${fieldName}_some: _${valueTypeName}Filter - ${fieldName}_none: _${valueTypeName}Filter - ${fieldName}_single: _${valueTypeName}Filter - ${fieldName}_every: _${valueTypeName}Filter + if (getFieldDirective(t, 'relation')) { + filters.push(` + ${fieldName}: _${valueTypeName}Filter + ${fieldName}_not: _${valueTypeName}Filter + ${fieldName}_in: [_${valueTypeName}Filter!] + ${fieldName}_not_in: [_${valueTypeName}Filter!] + ${fieldName}_some: _${valueTypeName}Filter + ${fieldName}_none: _${valueTypeName}Filter + ${fieldName}_single: _${valueTypeName}Filter + ${fieldName}_every: _${valueTypeName}Filter `); - } - if (filters.length) { - acc.push(...filters); + } } } - return acc; + return filters; }, []); - if (filterFields) { + if (filterFields.length) { filterFields.unshift(` - AND: [${filterType}] - OR: [${filterType}] - `); + AND: [${filterType}!] + OR: [${filterType}!] + `); } return filterFields; }; @@ -619,41 +794,49 @@ const possiblyAddTypeFieldArguments = ( queryType ) => { const fields = astNode.fields; - let relationTypeName = ''; - let relationType = {}; - let args = []; fields.forEach(field => { - relationTypeName = _getNamedType(field).name.value; - relationType = typeMap[relationTypeName]; + let fieldTypeName = _getNamedType(field).name.value; + let fieldType = typeMap[fieldTypeName]; + let args = field.arguments; if ( + fieldType && fieldIsNotIgnored(astNode, field, resolvers) && - // only adds args if node payload type has not been excluded - shouldAugmentType(config, 'query', relationTypeName) && - // we know astNode is a node type, so this field should be a node type - // as well, since the generated args are only for node type lists - isNodeType(relationType) && - (getFieldDirective(field, 'relation') || - getFieldDirective(field, 'cypher')) + shouldAugmentType(config, 'query', fieldTypeName) ) { - args = field.arguments; - if (_isListType(field)) { - // the args (first / offset / orderBy) are only generated for list fields - field.arguments = possiblyAddArgument(args, 'first', 'Int'); - field.arguments = possiblyAddArgument(args, 'offset', 'Int'); - field.arguments = possiblyAddOrderingArgument(args, relationTypeName); - } - if (!getFieldDirective(field, 'cypher')) { - field.arguments = possiblyAddArgument( - args, - 'filter', - `_${relationTypeName}Filter` - ); + const relationTypeDirective = getRelationTypeDirective(fieldType); + if (isNodeType(fieldType)) { + if (getFieldDirective(field, 'cypher')) { + if (_isListType(field)) { + args = addPaginationArgs(args, fieldTypeName); + } + } else if (getFieldDirective(field, 'relation')) { + if (_isListType(field)) { + args = addPaginationArgs(args, fieldTypeName); + } + args = possiblyAddArgument(args, 'filter', `_${fieldTypeName}Filter`); + } + } else if (relationTypeDirective) { + const fromType = relationTypeDirective.from; + const toType = relationTypeDirective.to; + let filterTypeName = `_${astNode.name.value}${fieldTypeName}Filter`; + if (fromType === toType) { + filterTypeName = `_${fieldTypeName}Filter`; + } + args = possiblyAddArgument(args, 'filter', filterTypeName); } + field.arguments = args; } }); return fields; }; +const addPaginationArgs = (args, fieldTypeName) => { + args = possiblyAddArgument(args, 'first', 'Int'); + args = possiblyAddArgument(args, 'offset', 'Int'); + args = possiblyAddOrderingArgument(args, fieldTypeName); + return args; +}; + const possiblyAddObjectType = (typeMap, name) => { if (typeMap[name] === undefined) { typeMap[name] = { @@ -735,7 +918,7 @@ const possiblyAddRelationTypeFieldPayload = ( let fromValue = undefined; let toValue = undefined; let fields = relationAstNode.fields; - const relationTypeDirective = getRelationTypeDirectiveArgs(relationAstNode); + const relationTypeDirective = getRelationTypeDirective(relationAstNode); if (relationTypeDirective) { // TODO refactor const relationTypePayloadFields = fields @@ -760,7 +943,12 @@ const possiblyAddRelationTypeFieldPayload = ( if (fromValue && fromValue === toValue) { // If field is a list type, then make .from and .to list types const fieldIsList = _isListType(field); - const fieldArgs = getFieldArgumentsFromAst(field, typeName); + const fieldArgs = getFieldArgumentsFromAst( + field, + typeName, + fieldIsList, + fieldTypeName + ); typeMap[`${fieldTypeName}Directions`] = parse(` type ${fieldTypeName}Directions ${print(relationAstNode.directives)} { from${fieldArgs}: ${fieldIsList ? '[' : ''}${fieldTypeName}${ @@ -853,7 +1041,7 @@ const possiblyAddRelationMutationField = ( // Prevents overwriting if (typeMap[payloadTypeName] === undefined) { typeMap[payloadTypeName] = parse(` - type ${payloadTypeName} @relation(name: "${relationName}", from: "${fromName}", to: "${toName}") { + type ${payloadTypeName} @relation(name: \"${relationName}\", from: \"${fromName}\", to: \"${toName}\") { from: ${fromName} to: ${toName} ${ @@ -1158,18 +1346,18 @@ const addRelationTypeDirectives = typeMap => { // replace it if it exists in order to force correct configuration astNode.directives[typeDirectiveIndex] = parseDirectiveSdl(` @relation( - name: ${relationName}, - from: ${fromTypeName}, - to: ${toTypeName} + name: \"${relationName}\", + from: \"${fromTypeName}\", + to: \"${toTypeName}\" ) `); } else { astNode.directives.push( parseDirectiveSdl(` @relation( - name: ${relationName}, - from: ${fromTypeName}, - to: ${toTypeName} + name: \"${relationName}\", + from: \"${fromTypeName}\", + to: \"${toTypeName}\" ) `) ); @@ -1562,19 +1750,25 @@ export const addTemporalTypes = (typeMap, config) => { return transformTemporalFields(typeMap, config); }; -const getFieldArgumentsFromAst = (field, typeName, fieldIsList) => { - let fieldArgs = field.arguments ? field.arguments : []; - let paginationArgs = []; +const getFieldArgumentsFromAst = ( + field, + typeName, + fieldIsList, + fieldTypeName +) => { + let args = field.arguments ? field.arguments : []; if (fieldIsList) { - paginationArgs = possiblyAddArgument(fieldArgs, 'first', 'Int'); - paginationArgs = possiblyAddArgument(fieldArgs, 'offset', 'Int'); - paginationArgs = possiblyAddArgument( - fieldArgs, - 'orderBy', - `_${typeName}Ordering` - ); + // TODO https://github.com/neo4j-graphql/neo4j-graphql-js/issues/232 + // args = possiblyAddArgument(args, 'first', 'Int'); + // args = possiblyAddArgument(args, 'offset', 'Int'); + // args = possiblyAddArgument( + // args, + // 'orderBy', + // `${fieldTypeName}Ordering` + // ); } - const args = [paginationArgs, ...fieldArgs] + args = possiblyAddArgument(args, 'filter', `${fieldTypeName}Filter`); + args = args .reduce((acc, t) => { acc.push(print(t)); return acc; diff --git a/src/selections.js b/src/selections.js index 542642d3..9b15e919 100644 --- a/src/selections.js +++ b/src/selections.js @@ -10,7 +10,7 @@ import { isGraphqlScalarType, extractSelections, relationDirective, - getRelationTypeDirectiveArgs, + getRelationTypeDirective, decideNestedVariableName, safeLabel, safeVar, @@ -26,8 +26,7 @@ import { relationTypeFieldOnNodeType, nodeTypeFieldOnRelationType, temporalType, - temporalField, - transformExistentialFilterParams + temporalField } from './translate'; export function buildCypherSelection({ @@ -45,7 +44,7 @@ export function buildCypherSelection({ return [initial, {}]; } selections = removeIgnoredFields(schemaType, selections); - const selectionFilters = filtersFromSelections( + let selectionFilters = filtersFromSelections( selections, resolveInfo.variableValues ); @@ -64,6 +63,7 @@ export function buildCypherSelection({ selections: tailSelections, cypherParams, variableName, + paramIndex, schemaType, resolveInfo, parentSelectionInfo, @@ -171,7 +171,7 @@ export function buildCypherSelection({ // Main control flow if (isGraphqlScalarType(innerSchemaType)) { if (customCypher) { - if (getRelationTypeDirectiveArgs(schemaTypeAstNode)) { + if (getRelationTypeDirective(schemaTypeAstNode)) { variableName = `${variableName}_relation`; } return recurse({ @@ -209,10 +209,10 @@ export function buildCypherSelection({ innerSchemaType && typeMap[innerSchemaType] ? typeMap[innerSchemaType].astNode : {}; - const innerSchemaTypeRelation = getRelationTypeDirectiveArgs( + const innerSchemaTypeRelation = getRelationTypeDirective( innerSchemaTypeAstNode ); - const schemaTypeRelation = getRelationTypeDirectiveArgs(schemaTypeAstNode); + const schemaTypeRelation = getRelationTypeDirective(schemaTypeAstNode); const { name: relType, direction: relDirection } = relationDirective( schemaType, fieldName @@ -233,7 +233,7 @@ export function buildCypherSelection({ resolveInfo.fragments ); - const subSelection = recurse({ + let subSelection = recurse({ initial: '', selections: subSelections, variableName: nestedVariable, @@ -305,64 +305,63 @@ export function buildCypherSelection({ nestedVariable, temporalArgs ); - selection = recurse( - relationFieldOnNodeType({ - ...fieldInfo, - schemaType, - selections, - selectionFilters, - relDirection, - relType, - isInlineFragment, - interfaceLabel, - innerSchemaType, - temporalClauses, - resolveInfo, - paramIndex, - fieldArgs - }) - ); - // post-processing of extracted argument parameter data for - // null filters used for existence predicates - const parentParamIndex = parentSelectionInfo.paramIndex; - const filterParamKey = `${parentParamIndex}_filter`; - // gets filter argument from subSelection because they - // overwrite those in selection[1] in the below root return - const fieldArgumentParams = subSelection[1]; - const filterParam = fieldArgumentParams[filterParamKey]; - if (filterParam) { - subSelection[1][filterParamKey] = transformExistentialFilterParams( - filterParam - ); - } + // translate field, arguments and argument params + const translation = relationFieldOnNodeType({ + ...fieldInfo, + schemaType, + selections, + selectionFilters, + relDirection, + relType, + isInlineFragment, + interfaceLabel, + innerSchemaType, + temporalClauses, + resolveInfo, + paramIndex, + fieldArgs + }); + selection = recurse(translation.selection); + // set subSelection to update field argument params + subSelection = translation.subSelection; } else if (schemaTypeRelation) { // Object type field on relation type // (from, to, renamed, relation mutation payloads...) - selection = recurse( - nodeTypeFieldOnRelationType({ - fieldInfo, - schemaTypeRelation, - innerSchemaType, - isInlineFragment, - interfaceLabel, - paramIndex, - schemaType, - filterParams, - temporalArgs, - parentSelectionInfo - }) - ); + const translation = nodeTypeFieldOnRelationType({ + fieldInfo, + schemaTypeRelation, + innerSchemaType, + isInlineFragment, + interfaceLabel, + paramIndex, + schemaType, + filterParams, + temporalArgs, + parentSelectionInfo, + resolveInfo, + selectionFilters, + fieldArgs + }); + selection = recurse(translation.selection); + // set subSelection to update field argument params + subSelection = translation.subSelection; } else if (innerSchemaTypeRelation) { // Relation type field on node type (field payload types...) - selection = recurse( - relationTypeFieldOnNodeType({ - ...fieldInfo, - innerSchemaTypeRelation, - schemaType, - filterParams, - temporalArgs - }) - ); + const translation = relationTypeFieldOnNodeType({ + ...fieldInfo, + innerSchemaTypeRelation, + schemaType, + innerSchemaType, + filterParams, + temporalArgs, + resolveInfo, + selectionFilters, + paramIndex, + fieldArgs + }); + selection = recurse(translation.selection); + // set subSelection to update field argument params + subSelection = translation.subSelection; } return [selection[0], { ...selection[1], ...subSelection[1] }]; } diff --git a/src/translate.js b/src/translate.js index af1c7eb5..0c61ac2d 100644 --- a/src/translate.js +++ b/src/translate.js @@ -23,7 +23,7 @@ import { initializeMutationParams, getMutationCypherDirective, isNodeType, - getRelationTypeDirectiveArgs, + getRelationTypeDirective, isRelationTypeDirectedField, isRelationTypePayload, isRootSelection, @@ -31,20 +31,24 @@ import { getTemporalArguments, temporalPredicateClauses, isTemporalType, + isTemporalInputType, isGraphqlScalarType, innerType, relationDirective, - typeIdentifiers + typeIdentifiers, + decideTemporalConstructor } from './utils'; import { getNamedType, isScalarType, isEnumType, + isObjectType, isInputType, isListType } from 'graphql'; import { buildCypherSelection } from './selections'; import _ from 'lodash'; +import { v1 as neo4j } from 'neo4j-driver'; export const customCypherField = ({ customCypher, @@ -122,15 +126,22 @@ export const relationFieldOnNodeType = ({ const queryParams = paramsToString( _.filter(allParams, param => !Array.isArray(param.value)) ); - // build predicates for filter argument if provided - const filterPredicates = buildFilterPredicates( + + const [filterPredicates, serializedFilterParam] = processFilterArgument({ fieldArgs, - innerSchemaType, - nestedVariable, + schemaType: innerSchemaType, + variableName: nestedVariable, resolveInfo, - selectionFilters, + params: selectionFilters, paramIndex - ); + }); + const filterParamKey = `${tailParams.paramIndex}_filter`; + const fieldArgumentParams = subSelection[1]; + const filterParam = fieldArgumentParams[filterParamKey]; + if (filterParam) { + subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; + } + const arrayFilterParams = _.pickBy( filterParams, (param, keyName) => Array.isArray(param.value) && !('orderBy' === keyName) @@ -152,39 +163,42 @@ export const relationFieldOnNodeType = ({ filterParams ); return { - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }${ - orderByParam - ? temporalOrdering - ? `[sortedElement IN apoc.coll.sortMulti(` - : `apoc.coll.sortMulti(` - : '' - }[(${safeVar(variableName)})${ - relDirection === 'in' || relDirection === 'IN' ? '<' : '' - }-[:${safeLabel(relType)}]-${ - relDirection === 'out' || relDirection === 'OUT' ? '>' : '' - }(${safeVariableName}:${safeLabel( - isInlineFragment ? interfaceLabel : innerSchemaType.name - )}${queryParams})${ - whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '' - } | ${nestedVariable} {${ - isInlineFragment - ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] - : subSelection[0] - }}]${ - orderByParam - ? `, [${buildSortMultiArgs(orderByParam)}])${ - temporalOrdering - ? ` | sortedElement { .*, ${temporalTypeSelections( - selections, - innerSchemaType - )}}]` - : `` - }` - : '' - }${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }${ + orderByParam + ? temporalOrdering + ? `[sortedElement IN apoc.coll.sortMulti(` + : `apoc.coll.sortMulti(` + : '' + }[(${safeVar(variableName)})${ + relDirection === 'in' || relDirection === 'IN' ? '<' : '' + }-[:${safeLabel(relType)}]-${ + relDirection === 'out' || relDirection === 'OUT' ? '>' : '' + }(${safeVariableName}:${safeLabel( + isInlineFragment ? interfaceLabel : innerSchemaType.name + )}${queryParams})${ + whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '' + } | ${nestedVariable} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${ + orderByParam + ? `, [${buildSortMultiArgs(orderByParam)}])${ + temporalOrdering + ? ` | sortedElement { .*, ${temporalTypeSelections( + selections, + innerSchemaType + )}}]` + : `` + }` + : '' + }${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; }; @@ -199,17 +213,25 @@ export const relationTypeFieldOnNodeType = ({ fieldType, variableName, schemaType, + innerSchemaType, nestedVariable, queryParams, filterParams, - temporalArgs + temporalArgs, + resolveInfo, + selectionFilters, + paramIndex, + fieldArgs }) => { if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { return { - initial: `${initial}${fieldName}: {${ - subSelection[0] - }}${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: {${ + subSelection[0] + }}${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; } const relationshipVariableName = `${nestedVariable}_relation`; @@ -218,27 +240,45 @@ export const relationTypeFieldOnNodeType = ({ relationshipVariableName, temporalArgs ); + const [filterPredicates, serializedFilterParam] = processFilterArgument({ + fieldArgs, + schemaType: innerSchemaType, + variableName: relationshipVariableName, + resolveInfo, + params: selectionFilters, + paramIndex, + rootIsRelationType: true + }); + const filterParamKey = `${tailParams.paramIndex}_filter`; + const fieldArgumentParams = subSelection[1]; + const filterParam = fieldArgumentParams[filterParamKey]; + if (filterParam) { + subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; + } + + const whereClauses = [...temporalClauses, ...filterPredicates]; return { - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(${safeVar(variableName)})${ - schemaType.name === innerSchemaTypeRelation.to ? '<' : '' - }-[${safeVar(relationshipVariableName)}:${safeLabel( - innerSchemaTypeRelation.name - )}${queryParams}]-${ - schemaType.name === innerSchemaTypeRelation.from ? '>' : '' - }(:${safeLabel( - schemaType.name === innerSchemaTypeRelation.from - ? innerSchemaTypeRelation.to - : innerSchemaTypeRelation.from - )}) ${ - temporalClauses.length > 0 - ? `WHERE ${temporalClauses.join(' AND ')} ` - : '' - }| ${relationshipVariableName} {${subSelection[0]}}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(${safeVar(variableName)})${ + schemaType.name === innerSchemaTypeRelation.to ? '<' : '' + }-[${safeVar(relationshipVariableName)}:${safeLabel( + innerSchemaTypeRelation.name + )}${queryParams}]-${ + schemaType.name === innerSchemaTypeRelation.from ? '>' : '' + }(:${safeLabel( + schemaType.name === innerSchemaTypeRelation.from + ? innerSchemaTypeRelation.to + : innerSchemaTypeRelation.from + )}) ${ + whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')} ` : '' + }| ${relationshipVariableName} {${subSelection[0]}}]${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; }; @@ -252,7 +292,10 @@ export const nodeTypeFieldOnRelationType = ({ schemaType, filterParams, temporalArgs, - parentSelectionInfo + parentSelectionInfo, + resolveInfo, + selectionFilters, + fieldArgs }) => { if ( isRootSelection({ @@ -261,10 +304,13 @@ export const nodeTypeFieldOnRelationType = ({ }) && isRelationTypeDirectedField(fieldInfo.fieldName) ) { - return relationTypeMutationPayloadField({ - ...fieldInfo, - parentSelectionInfo - }); + return { + selection: relationTypeMutationPayloadField({ + ...fieldInfo, + parentSelectionInfo + }), + subSelection: fieldInfo.subSelection + }; } // Normal case of schemaType with a relationship directive return directedNodeTypeFieldOnRelationType({ @@ -276,7 +322,10 @@ export const nodeTypeFieldOnRelationType = ({ paramIndex, schemaType, filterParams, - temporalArgs + temporalArgs, + resolveInfo, + selectionFilters, + fieldArgs }); }; @@ -317,7 +366,11 @@ const directedNodeTypeFieldOnRelationType = ({ isInlineFragment, interfaceLabel, filterParams, - temporalArgs + temporalArgs, + paramIndex, + resolveInfo, + selectionFilters, + fieldArgs }) => { const relType = schemaTypeRelation.name; const fromTypeName = schemaTypeRelation.from; @@ -337,53 +390,78 @@ const directedNodeTypeFieldOnRelationType = ({ temporalFieldRelationshipVariableName, temporalArgs ); + const [filterPredicates, serializedFilterParam] = processFilterArgument({ + fieldArgs, + schemaType: innerSchemaType, + variableName: relationshipVariableName, + resolveInfo, + params: selectionFilters, + paramIndex, + rootIsRelationType: true + }); + const filterParamKey = `${tailParams.paramIndex}_filter`; + const fieldArgumentParams = subSelection[1]; + const filterParam = fieldArgumentParams[filterParamKey]; + if (filterParam) { + subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; + } + const whereClauses = [...temporalClauses, ...filterPredicates]; return { - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(${safeVar(variableName)})${isFromField ? '<' : ''}-[${safeVar( - relationshipVariableName - )}:${safeLabel(relType)}${queryParams}]-${ - isToField ? '>' : '' - }(${safeVar(nestedVariable)}:${safeLabel( - isInlineFragment ? interfaceLabel : fromTypeName - )}) ${ - temporalClauses.length > 0 - ? `WHERE ${temporalClauses.join(' AND ')} ` - : '' - }| ${relationshipVariableName} {${ - isInlineFragment - ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] - : subSelection[0] - }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(${safeVar(variableName)})${isFromField ? '<' : ''}-[${safeVar( + relationshipVariableName + )}:${safeLabel(relType)}${queryParams}]-${ + isToField ? '>' : '' + }(${safeVar(nestedVariable)}:${safeLabel( + isInlineFragment ? interfaceLabel : fromTypeName + )}) ${ + whereClauses.length > 0 + ? `WHERE ${whereClauses.join(' AND ')} ` + : '' + }| ${relationshipVariableName} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; } else { // Case of a renamed directed field // e.g., 'from: Movie' -> 'Movie: Movie' return { - initial: `${initial}${fieldName}: ${variableName} {${ - subSelection[0] - }}${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: ${variableName} {${ + subSelection[0] + }}${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; } } else { variableName = variableName + '_relation'; return { - initial: `${initial}${fieldName}: ${ - !isArrayType(fieldType) ? 'head(' : '' - }[(:${safeLabel(isFromField ? toTypeName : fromTypeName)})${ - isFromField ? '<' : '' - }-[${safeVar(variableName)}]-${isToField ? '>' : ''}(${safeVar( - nestedVariable - )}:${safeLabel( - isInlineFragment ? interfaceLabel : innerSchemaType.name - )}${queryParams}) | ${nestedVariable} {${ - isInlineFragment - ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] - : subSelection[0] - }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, - ...tailParams + selection: { + initial: `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }[(:${safeLabel(isFromField ? toTypeName : fromTypeName)})${ + isFromField ? '<' : '' + }-[${safeVar(variableName)}]-${isToField ? '>' : ''}(${safeVar( + nestedVariable + )}:${safeLabel( + isInlineFragment ? interfaceLabel : innerSchemaType.name + )}${queryParams}) | ${nestedVariable} {${ + isInlineFragment + ? 'FRAGMENT_TYPE: "' + interfaceLabel + '",' + subSelection[0] + : subSelection[0] + }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`, + ...tailParams + }, + subSelection }; } }; @@ -417,7 +495,7 @@ export const temporalField = ({ // then we need to use the root variableName variableName = `${secondParentVariableName}_relation`; } else if (isRelationTypePayload(parentSchemaType)) { - const parentSchemaTypeRelation = getRelationTypeDirectiveArgs( + const parentSchemaTypeRelation = getRelationTypeDirective( parentSchemaType.astNode ); if (parentSchemaTypeRelation.from === parentSchemaTypeRelation.to) { @@ -670,16 +748,22 @@ const nodeQuery = ({ resolveInfo, paramIndex: rootParamIndex }); - const params = { ...nonNullParams, ...subParams }; + + const fieldArgs = getQueryArguments(resolveInfo); + const [filterPredicates, serializedFilter] = processFilterArgument({ + fieldArgs, + schemaType, + variableName, + resolveInfo, + params: nonNullParams, + paramIndex: rootParamIndex + }); + let params = { ...serializedFilter, ...subParams }; + if (cypherParams) { params['cypherParams'] = cypherParams; } - // transform null filters in root filter argument - const filterParam = params['filter']; - if (filterParam) - params['filter'] = transformExistentialFilterParams(filterParam); - const arrayParams = _.pickBy(filterParams, Array.isArray); const args = innerFilterParams(filterParams, temporalArgs); @@ -699,17 +783,6 @@ const nodeQuery = ({ (value, key) => `${safeVariableName}.${safeVar(key)} IN $${key}` ); - // build predicates for filter argument if provided - const fieldArgs = getQueryArguments(resolveInfo); - const filterPredicates = buildFilterPredicates( - fieldArgs, - schemaType, - variableName, - resolveInfo, - nonNullParams, - rootParamIndex - ); - const predicateClauses = [ idWherePredicate, ...filterPredicates, @@ -722,7 +795,7 @@ const nodeQuery = ({ const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : ''; - const query = + let query = `MATCH (${safeVariableName}:${safeLabelName}${ argString ? ` ${argString}` : '' }) ${predicate}` + @@ -1316,17 +1389,20 @@ const buildSortMultiArgs = param => { .join(','); }; -const buildFilterPredicates = ( +const processFilterArgument = ({ fieldArgs, schemaType, variableName, resolveInfo, params, - paramIndex -) => { + paramIndex, + rootIsRelationType = false +}) => { const filterArg = fieldArgs.find(e => e.name.value === 'filter'); const filterValue = Object.keys(params).length ? params['filter'] : undefined; - let filterPredicates = []; + const filterParamKey = paramIndex > 1 ? `${paramIndex - 1}_filter` : `filter`; + const filterCypherParam = `$${filterParamKey}`; + let translations = []; // if field has both a filter argument and argument data is provided if (filterArg && filterValue) { const schema = resolveInfo.schema; @@ -1334,57 +1410,295 @@ const buildFilterPredicates = ( const filterSchemaType = schema.getType(typeName); // get fields of filter type const typeFields = filterSchemaType.getFields(); - // align with naming scheme of extracted argument Cypher params - const filterParam = - paramIndex > 1 ? `$${paramIndex - 1}_filter` : `$filter`; - // recursively translate argument filterParam relative to schemaType - filterPredicates = translateFilterArguments( - schemaType, + const [filterFieldMap, serializedFilterParam] = analyzeFilterArguments({ + filterValue, + typeFields, variableName, + filterCypherParam, + schemaType, + schema + }); + translations = translateFilterArguments({ + filterFieldMap, typeFields, - filterParam, - schema, - filterValue - ); + filterCypherParam, + rootIsRelationType, + variableName, + schemaType, + schema + }); + params = { + ...params, + [filterParamKey]: serializedFilterParam + }; } - return filterPredicates; + return [translations, params]; }; -const translateFilterArguments = ( +const analyzeFilterArguments = ({ + filterValue, + typeFields, + variableName, + filterCypherParam, schemaType, + schema +}) => { + return Object.entries(filterValue).reduce( + ([filterFieldMap, serializedParams], [name, value]) => { + const [serializedValue, fieldMap] = analyzeFilterArgument({ + field: typeFields[name], + filterValue: value, + filterValues: filterValue, + fieldName: name, + filterParam: filterCypherParam, + variableName, + schemaType, + schema + }); + const filterParamName = serializeFilterFieldName(name, value); + filterFieldMap[filterParamName] = fieldMap; + serializedParams[filterParamName] = serializedValue; + return [filterFieldMap, serializedParams]; + }, + [{}, {}] + ); +}; + +const analyzeFilterArgument = ({ + parentFieldName, + field, + filterValue, + fieldName, variableName, - typeFields, filterParam, - schema, + parentSchemaType, + schemaType, + schema +}) => { + const fieldType = field.type; + const innerFieldType = innerType(fieldType); + const typeName = innerFieldType.name; + const parsedFilterName = parseFilterArgumentName(fieldName); + let filterOperationField = parsedFilterName.name; + let filterOperationType = parsedFilterName.type; + // defaults + let filterMapValue = true; + let serializedFilterParam = filterValue; + if (isScalarType(innerFieldType) || isEnumType(innerFieldType)) { + if (isExistentialFilter(filterOperationType, filterValue)) { + serializedFilterParam = true; + filterMapValue = null; + } + } else if (isInputType(innerFieldType)) { + // check when filterSchemaType the same as schemaTypeField + const filterSchemaType = schema.getType(typeName); + const typeFields = filterSchemaType.getFields(); + if (fieldName === 'AND' || fieldName === 'OR') { + // recursion + [serializedFilterParam, filterMapValue] = analyzeNestedFilterArgument({ + filterValue, + filterOperationType, + parentFieldName: fieldName, + parentSchemaType: schemaType, + schemaType, + variableName, + filterParam, + typeFields, + schema + }); + } else { + const schemaTypeField = schemaType.getFields()[filterOperationField]; + const innerSchemaType = innerType(schemaTypeField.type); + if (isObjectType(innerSchemaType)) { + const [ + thisType, + relatedType, + relationLabel, + relationDirection, + isRelation, + isRelationType, + isRelationTypeNode, + isReflexiveRelationType, + isReflexiveTypeDirectedField + ] = decideRelationFilterMetadata({ + fieldName, + parentSchemaType, + schemaType, + variableName, + innerSchemaType, + filterOperationField + }); + if (isReflexiveTypeDirectedField) { + // for the 'from' and 'to' fields on the payload of a reflexive + // relation type to use the parent field name, ex: 'knows_some' + // is used for 'from' and 'to' in 'knows_some: { from: {}, to: {} }' + const parsedFilterName = parseFilterArgumentName(parentFieldName); + filterOperationField = parsedFilterName.name; + filterOperationType = parsedFilterName.type; + } + if (isExistentialFilter(filterOperationType, filterValue)) { + serializedFilterParam = true; + filterMapValue = null; + } else if (isTemporalInputType(typeName)) { + serializedFilterParam = serializeTemporalParam(filterValue); + } else if (isRelation || isRelationType || isRelationTypeNode) { + // recursion + [serializedFilterParam, filterMapValue] = analyzeNestedFilterArgument( + { + filterValue, + filterOperationType, + isRelationType, + parentFieldName: fieldName, + parentSchemaType: schemaType, + schemaType: innerSchemaType, + variableName, + filterParam, + typeFields, + schema + } + ); + } + } + } + } + return [serializedFilterParam, filterMapValue]; +}; + +const analyzeNestedFilterArgument = ({ + parentSchemaType, + parentFieldName, + schemaType, + variableName, filterValue, - parentVariableName -) => { - // root call to translateFilterArgument, recursive calls in buildUniquePredicates - // translates each provided filter relative to its corresponding field in typeFields - return Object.entries(filterValue).reduce((predicates, [name, value]) => { - const predicate = translateFilterArgument({ - parentVariableName, - field: typeFields[name], - filterValue: value, - fieldName: name, - variableName, - filterParam, - schemaType, - schema + filterParam, + typeFields, + schema +}) => { + const isList = Array.isArray(filterValue); + // coersion to array for dynamic iteration of objects and arrays + if (!isList) filterValue = [filterValue]; + let serializedFilterValue = []; + let filterValueFieldMap = {}; + filterValue.forEach(filter => { + let serializedValues = {}; + let serializedValue = {}; + let valueFieldMap = {}; + Object.entries(filter).forEach(([fieldName, value]) => { + fieldName = deserializeFilterFieldName(fieldName); + [serializedValue, valueFieldMap] = analyzeFilterArgument({ + parentFieldName, + field: typeFields[fieldName], + filterValue: value, + filterValues: filter, + fieldName, + variableName, + filterParam, + parentSchemaType, + schemaType, + schema + }); + const filterParamName = serializeFilterFieldName(fieldName, value); + const filterMapEntry = filterValueFieldMap[filterParamName]; + if (!filterMapEntry) filterValueFieldMap[filterParamName] = valueFieldMap; + // deep merges in order to capture differences in objects within nested array filters + else + filterValueFieldMap[filterParamName] = _.merge( + filterMapEntry, + valueFieldMap + ); + serializedValues[filterParamName] = serializedValue; }); - if (predicate) predicates.push(`(${predicate})`); - return predicates; + serializedFilterValue.push(serializedValues); + }); + // undo array coersion + if (!isList) serializedFilterValue = serializedFilterValue[0]; + return [serializedFilterValue, filterValueFieldMap]; +}; + +const serializeFilterFieldName = (name, value) => { + if (value === null) { + const parsedFilterName = parseFilterArgumentName(name); + const filterOperationType = parsedFilterName.type; + if (!filterOperationType || filterOperationType === 'not') { + return `_${name}_null`; + } + } + return name; +}; + +const serializeTemporalParam = filterValue => { + const isList = Array.isArray(filterValue); + if (!isList) filterValue = [filterValue]; + let serializedValues = filterValue.reduce((serializedValues, filter) => { + let serializedValue = {}; + if (filter['formatted']) serializedValue = filter['formatted']; + else { + serializedValue = Object.entries(filter).reduce( + (serialized, [key, value]) => { + if (Number.isInteger(value)) value = neo4j.int(value); + serialized[key] = value; + return serialized; + }, + {} + ); + } + serializedValues.push(serializedValue); + return serializedValues; }, []); + if (!isList) serializedValues = serializedValues[0]; + return serializedValues; +}; + +const deserializeFilterFieldName = name => { + if (name.startsWith('_') && name.endsWith('_null')) { + name = name.substring(1, name.length - 5); + } + return name; +}; + +const translateFilterArguments = ({ + filterFieldMap, + typeFields, + filterCypherParam, + variableName, + rootIsRelationType, + schemaType, + schema +}) => { + return Object.entries(filterFieldMap).reduce( + (translations, [name, value]) => { + // the filter field map uses serialized field names to allow for both field: {} and field: null + name = deserializeFilterFieldName(name); + const translation = translateFilterArgument({ + field: typeFields[name], + filterParam: filterCypherParam, + fieldName: name, + filterValue: value, + rootIsRelationType, + variableName, + schemaType, + schema + }); + if (translation) { + translations.push(`(${translation})`); + } + return translations; + }, + [] + ); }; const translateFilterArgument = ({ - parentVariableName, + parentParamPath, + parentFieldName, isListFilterArgument, field, filterValue, fieldName, + rootIsRelationType, variableName, filterParam, + parentSchemaType, schemaType, schema }) => { @@ -1394,37 +1708,37 @@ const translateFilterArgument = ({ const typeName = innerFieldType.name; // build path for parameter data for current filter field const parameterPath = `${ - parentVariableName ? parentVariableName : filterParam + parentParamPath ? parentParamPath : filterParam }.${fieldName}`; // parse field name into prefix (ex: name, company) and // possible suffix identifying operation type (ex: _gt, _in) const parsedFilterName = parseFilterArgumentName(fieldName); - const filterOperationField = parsedFilterName.name; - const filterOperationType = parsedFilterName.type; + let filterOperationField = parsedFilterName.name; + let filterOperationType = parsedFilterName.type; // short-circuit evaluation: predicate used to skip a field // if processing a list of objects that possibly contain different arguments const nullFieldPredicate = decideNullSkippingPredicate({ parameterPath, isListFilterArgument, - parentVariableName + parentParamPath }); + let translation = ''; if (isScalarType(innerFieldType) || isEnumType(innerFieldType)) { - // translations of scalar type filters are simply relative - // to their field name suffix, filterOperationType - return translateScalarFilter({ + translation = translateScalarFilter({ isListFilterArgument, filterOperationField, filterOperationType, filterValue, + fieldName, variableName, parameterPath, - parentVariableName, + parentParamPath, filterParam, nullFieldPredicate }); } else if (isInputType(innerFieldType)) { - // translations of input type filters decide arguments for a call to buildPredicateFunction - return translateInputFilter({ + translation = translateInputFilter({ + rootIsRelationType, isListFilterArgument, filterOperationField, filterOperationType, @@ -1435,12 +1749,15 @@ const translateFilterArgument = ({ typeName, fieldType, schema, + parentSchemaType, schemaType, parameterPath, - parentVariableName, + parentParamPath, + parentFieldName, nullFieldPredicate }); } + return translation; }; const parseFilterArgumentName = fieldName => { @@ -1463,26 +1780,23 @@ const translateScalarFilter = ({ filterValue, variableName, parameterPath, - parentVariableName, + parentParamPath, filterParam, nullFieldPredicate }) => { - const safeVariableName = safeVar(variableName); // build path to node/relationship property - const propertyPath = `${safeVariableName}.${filterOperationField}`; + const propertyPath = `${safeVar(variableName)}.${filterOperationField}`; if (isExistentialFilter(filterOperationType, filterValue)) { return translateNullFilter({ - propertyPath, filterOperationField, filterOperationType, + propertyPath, filterParam, - parentVariableName, + parentParamPath, isListFilterArgument }); } - // some object arguments in an array filter may differ internally - // so skip the field predicate if a corresponding value is not provided - return `${nullFieldPredicate}${buildScalarFilterPredicate( + return `${nullFieldPredicate}${buildOperatorExpression( filterOperationType, propertyPath )} ${parameterPath}`; @@ -1494,24 +1808,22 @@ const isExistentialFilter = (type, value) => const decideNullSkippingPredicate = ({ parameterPath, isListFilterArgument, - parentVariableName + parentParamPath }) => - isListFilterArgument && parentVariableName - ? `${parameterPath} IS NULL OR ` - : ''; + isListFilterArgument && parentParamPath ? `${parameterPath} IS NULL OR ` : ''; const translateNullFilter = ({ filterOperationField, filterOperationType, filterParam, propertyPath, - parentVariableName, + parentParamPath, isListFilterArgument }) => { const isNegationFilter = filterOperationType === 'not'; // allign with modified parameter names for null filters const paramPath = `${ - parentVariableName ? parentVariableName : filterParam + parentParamPath ? parentParamPath : filterParam }._${filterOperationField}_${isNegationFilter ? `not_` : ''}null`; // build a predicate for checking the existence of a // property or relationship @@ -1523,12 +1835,17 @@ const translateNullFilter = ({ const nullFieldPredicate = decideNullSkippingPredicate({ parameterPath: paramPath, isListFilterArgument, - parentVariableName + parentParamPath }); return `${nullFieldPredicate}${predicate}`; }; -const buildScalarFilterPredicate = (filterOperationType, propertyPath) => { +const buildOperatorExpression = ( + filterOperationType, + propertyPath, + isListFilterArgument +) => { + if (isListFilterArgument) return `${propertyPath} =`; switch (filterOperationType) { case 'not': return `NOT ${propertyPath} = `; @@ -1556,12 +1873,14 @@ const buildScalarFilterPredicate = (filterOperationType, propertyPath) => { return `${propertyPath} >`; case 'gte': return `${propertyPath} >=`; - default: + default: { return `${propertyPath} =`; + } } }; const translateInputFilter = ({ + rootIsRelationType, isListFilterArgument, filterOperationField, filterOperationType, @@ -1572,17 +1891,21 @@ const translateInputFilter = ({ typeName, fieldType, schema, + parentSchemaType, schemaType, parameterPath, - parentVariableName, + parentParamPath, + parentFieldName, nullFieldPredicate }) => { + // check when filterSchemaType the same as schemaTypeField const filterSchemaType = schema.getType(typeName); const typeFields = filterSchemaType.getFields(); - if (filterOperationField === 'AND' || filterOperationField === 'OR') { + if (fieldName === 'AND' || fieldName === 'OR') { return translateLogicalFilter({ filterValue, variableName, + filterOperationType, filterOperationField, fieldName, filterParam, @@ -1590,34 +1913,77 @@ const translateInputFilter = ({ schema, schemaType, parameterPath, - parentVariableName, - isListFilterArgument, nullFieldPredicate }); } else { - const { name: relLabel, direction: relDirection } = relationDirective( - schemaType, - filterOperationField - ); - if (relLabel && relDirection) { - return translateRelationshipFilter({ - relLabel, - relDirection, - filterValue, - variableName, - filterOperationField, - filterOperationType, + const schemaTypeField = schemaType.getFields()[filterOperationField]; + const innerSchemaType = innerType(schemaTypeField.type); + if (isObjectType(innerSchemaType)) { + const [ + thisType, + relatedType, + relationLabel, + relationDirection, + isRelation, + isRelationType, + isRelationTypeNode, + isReflexiveRelationType, + isReflexiveTypeDirectedField + ] = decideRelationFilterMetadata({ fieldName, - filterParam, - typeFields, - fieldType, - schema, + parentSchemaType, schemaType, - parameterPath, - parentVariableName, - isListFilterArgument, - nullFieldPredicate + variableName, + innerSchemaType, + filterOperationField }); + if (isTemporalInputType(typeName)) { + const temporalFunction = decideTemporalConstructor(typeName); + return translateTemporalFilter({ + isRelationTypeNode, + filterValue, + variableName, + filterOperationField, + filterOperationType, + fieldName, + filterParam, + fieldType, + parameterPath, + parentParamPath, + isListFilterArgument, + nullFieldPredicate, + temporalFunction + }); + } else if (isRelation || isRelationType || isRelationTypeNode) { + return translateRelationFilter({ + rootIsRelationType, + thisType, + relatedType, + relationLabel, + relationDirection, + isRelationType, + isRelationTypeNode, + isReflexiveRelationType, + isReflexiveTypeDirectedField, + filterValue, + variableName, + filterOperationField, + filterOperationType, + fieldName, + filterParam, + typeFields, + fieldType, + schema, + schemaType, + innerSchemaType, + parameterPath, + parentParamPath, + isListFilterArgument, + nullFieldPredicate, + parentSchemaType, + parentFieldName + }); + } } } }; @@ -1625,6 +1991,7 @@ const translateInputFilter = ({ const translateLogicalFilter = ({ filterValue, variableName, + filterOperationType, filterOperationField, fieldName, filterParam, @@ -1632,44 +1999,50 @@ const translateLogicalFilter = ({ schema, schemaType, parameterPath, - parentVariableName, - isListFilterArgument, nullFieldPredicate }) => { const listElementVariable = `_${fieldName}`; - const predicateListVariable = parameterPath; // build predicate expressions for all unique arguments within filterValue // isListFilterArgument is true here so that nullFieldPredicate is used - const predicates = buildUniquePredicates({ + const predicates = buildFilterPredicates({ + filterOperationType, + parentFieldName: fieldName, + listVariable: listElementVariable, + parentSchemaType: schemaType, + isListFilterArgument: true, schemaType, variableName, - listVariable: listElementVariable, filterValue, filterParam, typeFields, - schema, - isListFilterArgument: true + schema }); + const predicateListVariable = parameterPath; // decide root predicate function const rootPredicateFunction = decidePredicateFunction({ filterOperationField }); // build root predicate expression - return buildPredicateFunction({ - listElementVariable, - parameterPath, - parentVariableName, - rootPredicateFunction, + const translation = buildPredicateFunction({ + nullFieldPredicate, predicateListVariable, + rootPredicateFunction, predicates, - isListFilterArgument, - nullFieldPredicate + listElementVariable }); + return translation; }; -const translateRelationshipFilter = ({ - relLabel, - relDirection, +const translateRelationFilter = ({ + rootIsRelationType, + thisType, + relatedType, + relationLabel, + relationDirection, + isRelationType, + isRelationTypeNode, + isReflexiveRelationType, + isReflexiveTypeDirectedField, filterValue, variableName, filterOperationField, @@ -1680,125 +2053,290 @@ const translateRelationshipFilter = ({ fieldType, schema, schemaType, + innerSchemaType, parameterPath, - parentVariableName, + parentParamPath, isListFilterArgument, - nullFieldPredicate + nullFieldPredicate, + parentSchemaType, + parentFieldName }) => { - // get related type for relationship variables and pattern - const innerSchemaType = innerType( - schemaType.getFields()[filterOperationField].type - ); - // build safe relationship variables - const { - typeName: relatedTypeName, - variableName: relatedTypeNameLow - } = typeIdentifiers(innerSchemaType); - // because ALL(n IN [] WHERE n) currently returns true - // an existence predicate is added to make sure a relationship exists - // otherwise a node returns when it has 0 such relationships, since the - // predicate function then evaluates an empty list - const pathExistencePredicate = buildRelationshipExistencePath( + if (isReflexiveTypeDirectedField) { + // when at the 'from' or 'to' fields of a reflexive relation type payload + // we need to use the name of the parent schema type, ex: 'person' for + // Person.knows gets used here for reflexive path patterns, rather than + // the normally set 'person_filter_person' variableName + variableName = parentSchemaType.name.toLowerCase(); + } + const pathExistencePredicate = buildRelationExistencePath( variableName, - relLabel, - relDirection, - relatedTypeName + relationLabel, + relationDirection, + relatedType, + isRelationTypeNode ); if (isExistentialFilter(filterOperationType, filterValue)) { return translateNullFilter({ - propertyPath: pathExistencePredicate, filterOperationField, filterOperationType, + propertyPath: pathExistencePredicate, filterParam, - parentVariableName, + parentParamPath, isListFilterArgument }); } - const schemaTypeNameLow = schemaType.name.toLowerCase(); - const safeRelVariableName = safeVar( - `${schemaTypeNameLow}_filter_${relatedTypeNameLow}` - ); - const safeRelatedTypeNameLow = safeVar(relatedTypeNameLow); + if (isReflexiveTypeDirectedField) { + // causes the 'from' and 'to' fields on the payload of a reflexive + // relation type to use the parent field name, ex: 'knows_some' + // is used for 'from' and 'to' in 'knows_some: { from: {}, to: {} }' + const parsedFilterName = parseFilterArgumentName(parentFieldName); + filterOperationField = parsedFilterName.name; + filterOperationType = parsedFilterName.type; + } // build a list comprehension containing path pattern for related type - const predicateListVariable = buildRelationshipListPattern({ - fromVar: schemaTypeNameLow, - relVar: safeRelVariableName, - relLabel: relLabel, - relDirection: relDirection, - toVar: relatedTypeNameLow, - toLabel: relatedTypeName, - fieldName + const predicateListVariable = buildRelatedTypeListComprehension({ + rootIsRelationType, + variableName, + thisType, + relatedType, + relationLabel, + relationDirection, + isRelationTypeNode, + isRelationType }); - // decide root predicate function - let rootPredicateFunction = decidePredicateFunction({ + + const rootPredicateFunction = decidePredicateFunction({ + isRelationTypeNode, filterOperationField, + filterOperationType + }); + return buildRelationPredicate({ + rootIsRelationType, + isRelationType, + isListFilterArgument, + isReflexiveRelationType, + isReflexiveTypeDirectedField, + thisType, + relatedType, + schemaType, + innerSchemaType, + fieldName, + fieldType, filterOperationType, - isRelation: true + filterValue, + filterParam, + typeFields, + schema, + parameterPath, + nullFieldPredicate, + pathExistencePredicate, + predicateListVariable, + rootPredicateFunction }); - let predicates = ''; - if (isListType(fieldType)) { - const listVariable = `_${fieldName}`; - predicates = buildUniquePredicates({ - isListFilterArgument: true, - schemaType: innerSchemaType, - variableName: relatedTypeNameLow, - listVariable, - filterValue, - filterParam, - typeFields, - schema +}; + +const decideRelationFilterMetadata = ({ + fieldName, + parentSchemaType, + schemaType, + variableName, + innerSchemaType, + filterOperationField +}) => { + let thisType = ''; + let relatedType = ''; + let isRelation = false; + let isRelationType = false; + let isRelationTypeNode = false; + let isReflexiveRelationType = false; + let isReflexiveTypeDirectedField = false; + // @relation field directive + let { name: relLabel, direction: relDirection } = relationDirective( + schemaType, + filterOperationField + ); + // @relation type directive on node type field + const innerRelationTypeDirective = getRelationTypeDirective( + innerSchemaType.astNode + ); + // @relation type directive on this type; node type field on relation type + // If there is no @relation directive on the schemaType, check the parentSchemaType + // for the same directive obtained above when the relation type is first seen + const relationTypeDirective = getRelationTypeDirective(schemaType.astNode); + if (relLabel && relDirection) { + isRelation = true; + const typeVariables = typeIdentifiers(innerSchemaType); + thisType = schemaType.name; + relatedType = typeVariables.typeName; + } else if (innerRelationTypeDirective) { + isRelationType = true; + [thisType, relatedType, relDirection] = decideRelationTypeDirection( + schemaType, + innerRelationTypeDirective + ); + if (thisType === relatedType) { + isReflexiveRelationType = true; + if (fieldName === 'from') { + isReflexiveTypeDirectedField = true; + relDirection = 'IN'; + } else if (fieldName === 'to') { + isReflexiveTypeDirectedField = true; + relDirection = 'OUT'; + } + } + relLabel = innerRelationTypeDirective.name; + } else if (relationTypeDirective) { + isRelationTypeNode = true; + [thisType, relatedType, relDirection] = decideRelationTypeDirection( + parentSchemaType, + relationTypeDirective + ); + relLabel = variableName; + } + return [ + thisType, + relatedType, + relLabel, + relDirection, + isRelation, + isRelationType, + isRelationTypeNode, + isReflexiveRelationType, + isReflexiveTypeDirectedField + ]; +}; + +const decideRelationTypeDirection = (schemaType, relationTypeDirective) => { + let fromType = relationTypeDirective.from; + let toType = relationTypeDirective.to; + let relDirection = 'OUT'; + if (fromType !== toType) { + if (schemaType && schemaType.name === toType) { + const temp = fromType; + fromType = toType; + toType = temp; + relDirection = 'IN'; + } + } + return [fromType, toType, relDirection]; +}; + +const buildRelationPredicate = ({ + rootIsRelationType, + isRelationType, + isReflexiveRelationType, + isReflexiveTypeDirectedField, + thisType, + isListFilterArgument, + relatedType, + schemaType, + innerSchemaType, + fieldName, + fieldType, + filterOperationType, + filterValue, + filterParam, + typeFields, + schema, + parameterPath, + nullFieldPredicate, + pathExistencePredicate, + predicateListVariable, + rootPredicateFunction +}) => { + let relationVariable = buildRelationVariable(thisType, relatedType); + const isRelationList = isListType(fieldType); + let variableName = relatedType.toLowerCase(); + let listVariable = parameterPath; + if (rootIsRelationType || isRelationType) { + // change the variable to be used in filtering + // to the appropriate relationship variable + // ex: project -> person_filter_project + variableName = relationVariable; + } + if (isRelationList) { + // set the base list comprehension variable + // to point at each array element instead + // ex: $filter.company_in -> _company_in + listVariable = `_${fieldName}`; + // set to list to enable null field + // skipping for all child filters + isListFilterArgument = true; + } + let predicates = buildFilterPredicates({ + parentFieldName: fieldName, + parentSchemaType: schemaType, + schemaType: innerSchemaType, + variableName, + isListFilterArgument, + listVariable, + filterOperationType, + isRelationType, + filterValue, + filterParam, + typeFields, + schema + }); + if (isRelationList) { + predicates = buildPredicateFunction({ + predicateListVariable: parameterPath, + listElementVariable: listVariable, + rootPredicateFunction, + predicates }); - // build root predicate to contain nested predicate - predicates = `${rootPredicateFunction}(${listVariable} IN ${parameterPath} WHERE (${predicates}))`; - // change root predicate to ALL to act as a boolean - // evaluation of the above nested rootPredicateFunction - rootPredicateFunction = 'ALL'; - } else { - predicates = buildUniquePredicates({ - schemaType: innerSchemaType, - variableName: relatedTypeNameLow, - listVariable: parameterPath, - filterValue, - filterParam, - typeFields, - schema + rootPredicateFunction = decidePredicateFunction({ + isRelationList }); } + if (isReflexiveRelationType && !isReflexiveTypeDirectedField) { + // At reflexive relation type fields, sufficient predicates and values are already + // obtained from the above call to the recursive buildFilterPredicates + // ex: Person.knows, Person.knows_in, etc. + // Note: Since only the internal 'from' and 'to' fields are translated for reflexive + // relation types, their translations will use the fieldName and schema type name + // of this field. See: the top of translateRelationFilter + return predicates; + } + const listElementVariable = safeVar(variableName); return buildPredicateFunction({ - listElementVariable: safeRelatedTypeNameLow, - parameterPath, - parentVariableName, - rootPredicateFunction, + nullFieldPredicate, + pathExistencePredicate, predicateListVariable, + rootPredicateFunction, predicates, - pathExistencePredicate, - isListFilterArgument, - nullFieldPredicate + listElementVariable }); }; const buildPredicateFunction = ({ - listElementVariable, - rootPredicateFunction, + nullFieldPredicate, + pathExistencePredicate, predicateListVariable, + rootPredicateFunction, predicates, - pathExistencePredicate, - nullFieldPredicate + listElementVariable }) => { // https://neo4j.com/docs/cypher-manual/current/functions/predicate/ - return `${nullFieldPredicate}${ + return `${nullFieldPredicate || ''}${ pathExistencePredicate ? `EXISTS(${pathExistencePredicate}) AND ` : '' }${rootPredicateFunction}(${listElementVariable} IN ${predicateListVariable} WHERE ${predicates})`; }; +const buildRelationVariable = (thisType, relatedType) => { + return `${thisType.toLowerCase()}_filter_${relatedType.toLowerCase()}`; +}; + const decidePredicateFunction = ({ filterOperationField, filterOperationType, - isRelation + isRelationTypeNode, + isRelationList }) => { if (filterOperationField === 'AND') return 'ALL'; else if (filterOperationField === 'OR') return 'ANY'; - else if (isRelation) { + else if (isRelationTypeNode) return 'ALL'; + else if (isRelationList) return 'ALL'; + else { switch (filterOperationType) { case 'not': return 'NONE'; @@ -1820,52 +2358,61 @@ const decidePredicateFunction = ({ } }; -const buildRelationshipListPattern = ({ - fromVar, - relVar, - relLabel, - relDirection, - toVar, - toLabel +const buildRelatedTypeListComprehension = ({ + rootIsRelationType, + variableName, + thisType, + relatedType, + relationLabel, + relationDirection, + isRelationTypeNode, + isRelationType }) => { + let relationVariable = buildRelationVariable(thisType, relatedType); + if (rootIsRelationType) { + relationVariable = variableName; + } + const thisTypeVariable = safeVar(thisType.toLowerCase()); // prevents related node variable from // conflicting with parent variables - toVar = `_${toVar}`; - const safeFromVar = safeVar(fromVar); - const safeToVar = safeVar(toVar); + const relatedTypeVariable = safeVar(`_${relatedType.toLowerCase()}`); // builds a path pattern within a list comprehension // that extracts related nodes - return `[(${safeFromVar})${ - relDirection === 'IN' ? '<' : '' - }-[${relVar}:${relLabel}]-${ - relDirection === 'OUT' ? '>' : '' - }(${safeToVar}:${toLabel}) | ${safeToVar}]`; + return `[(${thisTypeVariable})${relationDirection === 'IN' ? '<' : ''}-[${ + isRelationType + ? safeVar(`_${relationVariable}`) + : isRelationTypeNode + ? safeVar(relationVariable) + : '' + }${!isRelationTypeNode ? `:${relationLabel}` : ''}]-${ + relationDirection === 'OUT' ? '>' : '' + }(${isRelationType ? '' : relatedTypeVariable}:${relatedType}) | ${ + isRelationType ? safeVar(`_${relationVariable}`) : relatedTypeVariable + }]`; }; -const buildRelationshipExistencePath = ( +const buildRelationExistencePath = ( fromVar, relLabel, relDirection, - toType + toType, + isRelationTypeNode ) => { + // because ALL(n IN [] WHERE n) currently returns true + // an existence predicate is added to make sure a relationship exists + // otherwise a node returns when it has 0 such relationships, since the + // predicate function then evaluates an empty list const safeFromVar = safeVar(fromVar); - return `(${safeFromVar})${relDirection === 'IN' ? '<' : ''}-[:${relLabel}]-${ - relDirection === 'OUT' ? '>' : '' - }(:${toType})`; -}; - -const decideFilterParamName = (name, value) => { - if (value === null) { - const parsedFilterName = parseFilterArgumentName(name); - const filterOperationType = parsedFilterName.type; - if (!filterOperationType || filterOperationType === 'not') { - return `_${name}_null`; - } - } - return name; + return !isRelationTypeNode + ? `(${safeFromVar})${relDirection === 'IN' ? '<' : ''}-[:${relLabel}]-${ + relDirection === 'OUT' ? '>' : '' + }(:${toType})` + : ''; }; -const buildUniquePredicates = ({ +const buildFilterPredicates = ({ + parentSchemaType, + parentFieldName, schemaType, variableName, listVariable, @@ -1873,67 +2420,109 @@ const buildUniquePredicates = ({ filterParam, typeFields, schema, - isListFilterArgument = false + isListFilterArgument }) => { - // coercion of object argument to array for general use of reduce - if (!Array.isArray(filterValue)) filterValue = [filterValue]; - // used to prevent building a duplicate translation when - // the same filter field is provided in multiple objects - const translatedFilters = {}; - // recursion: calls translateFilterArgument for every field - return filterValue - .reduce((predicates, filter) => { - Object.entries(filter).forEach(([name, value]) => { - const filterParamName = decideFilterParamName(name, value); - if (!translatedFilters[filterParamName]) { - const predicate = translateFilterArgument({ - isListFilterArgument: isListFilterArgument, - parentVariableName: listVariable, - field: typeFields[name], - filterValue: value, - fieldName: name, - variableName, - filterParam, - schemaType, - schema - }); - if (predicate) { - translatedFilters[filterParamName] = true; - predicates.push(`(${predicate})`); - } - } + return Object.entries(filterValue) + .reduce((predicates, [name, value]) => { + name = deserializeFilterFieldName(name); + const predicate = translateFilterArgument({ + field: typeFields[name], + parentParamPath: listVariable, + fieldName: name, + filterValue: value, + parentFieldName, + parentSchemaType, + isListFilterArgument, + variableName, + filterParam, + schemaType, + schema }); + if (predicate) { + predicates.push(`(${predicate})`); + } return predicates; }, []) .join(' AND '); }; -export const transformExistentialFilterParams = filterParam => { - return Object.entries(filterParam).reduce((acc, [key, value]) => { - const parsed = parseFilterArgumentName(key); - const filterOperationType = parsed.type; - // align with parameter naming scheme used during translation - if (isExistentialFilter(filterOperationType, value)) { - // name: null -> _name_null: true - // company_not: null -> _company_not_null: true - key = decideFilterParamName(key, value); - value = true; - } else if (typeof value === 'object') { - // recurse: array filter - if (Array.isArray(value)) { - value = value.map(filter => { - // prevent recursing for scalar list filters - if (typeof filter === 'object') { - return transformExistentialFilterParams(filter); - } - return filter; - }); - } else { - // recurse: object filter - value = transformExistentialFilterParams(value); - } - } - acc[key] = value; - return acc; - }, {}); +const translateTemporalFilter = ({ + isRelationTypeNode, + filterValue, + variableName, + filterOperationField, + filterOperationType, + fieldName, + filterParam, + fieldType, + parameterPath, + parentParamPath, + isListFilterArgument, + nullFieldPredicate, + temporalFunction +}) => { + const safeVariableName = safeVar(variableName); + const propertyPath = `${safeVariableName}.${filterOperationField}`; + if (isExistentialFilter(filterOperationType, filterValue)) { + return translateNullFilter({ + filterOperationField, + filterOperationType, + propertyPath, + filterParam, + parentParamPath, + isListFilterArgument + }); + } + const rootPredicateFunction = decidePredicateFunction({ + isRelationTypeNode, + filterOperationField, + filterOperationType + }); + return buildTemporalPredicate({ + fieldName, + fieldType, + filterValue, + filterOperationField, + filterOperationType, + parameterPath, + variableName, + nullFieldPredicate, + rootPredicateFunction, + temporalFunction + }); +}; + +const buildTemporalPredicate = ({ + fieldName, + fieldType, + filterOperationField, + filterOperationType, + parameterPath, + variableName, + nullFieldPredicate, + rootPredicateFunction, + temporalFunction +}) => { + // ex: project -> person_filter_project + const isListFilterArgument = isListType(fieldType); + let listVariable = parameterPath; + // ex: $filter.datetime_in -> _datetime_in + if (isListFilterArgument) listVariable = `_${fieldName}`; + const safeVariableName = safeVar(variableName); + const propertyPath = `${safeVariableName}.${filterOperationField}`; + const operatorExpression = buildOperatorExpression( + filterOperationType, + propertyPath, + isListFilterArgument + ); + let translation = `(${nullFieldPredicate}${operatorExpression} ${temporalFunction}(${listVariable}))`; + if (isListFilterArgument) { + translation = buildPredicateFunction({ + predicateListVariable: parameterPath, + listElementVariable: listVariable, + rootPredicateFunction, + predicates: translation + }); + } + return translation; }; diff --git a/src/utils.js b/src/utils.js index e93e2deb..f1a40b6b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -271,7 +271,7 @@ export const isNodeType = astNode => { export const isRelationTypePayload = schemaType => { const astNode = schemaType ? schemaType.astNode : undefined; - const directive = astNode ? getRelationTypeDirectiveArgs(astNode) : undefined; + const directive = astNode ? getRelationTypeDirective(astNode) : undefined; return astNode && astNode.fields && directive ? astNode.fields.find(e => { return e.name.value === directive.from || e.name.value === directive.to; @@ -530,11 +530,13 @@ export const buildCypherParameters = ({ const directiveWithArgs = (directiveName, args) => (schemaType, fieldName) => { function fieldDirective(schemaType, fieldName, directiveName) { return !isGraphqlScalarType(schemaType) - ? schemaType - .getFields() - [fieldName].astNode.directives.find( - e => e.name.value === directiveName - ) + ? schemaType.getFields() && + schemaType.getFields()[fieldName] && + schemaType + .getFields() + [fieldName].astNode.directives.find( + e => e.name.value === directiveName + ) : {}; } @@ -651,7 +653,7 @@ function argumentValue(selection, name, variableValues) { } } -export const getRelationTypeDirectiveArgs = relationshipType => { +export const getRelationTypeDirective = relationshipType => { const directive = relationshipType && relationshipType.directives ? relationshipType.directives.find(e => e.name.value === 'relation') @@ -912,17 +914,18 @@ export const splitSelectionParameters = ( export const isTemporalField = (schemaType, name) => { const type = schemaType ? schemaType.name : ''; return ( - (isTemporalType(type) && name === 'year') || - name === 'month' || - name === 'day' || - name === 'hour' || - name === 'minute' || - name === 'second' || - name === 'microsecond' || - name === 'millisecond' || - name === 'nanosecond' || - name === 'timezone' || - name === 'formatted' + isTemporalType(type) && + (name === 'year' || + name === 'month' || + name === 'day' || + name === 'hour' || + name === 'minute' || + name === 'second' || + name === 'microsecond' || + name === 'millisecond' || + name === 'nanosecond' || + name === 'timezone' || + name === 'formatted') ); }; @@ -937,28 +940,25 @@ export const isTemporalType = name => { }; export const getTemporalCypherConstructor = fieldAst => { - let cypherFunction = undefined; const type = fieldAst ? _getNamedType(fieldAst.type).name.value : ''; - switch (type) { + return decideTemporalConstructor(type); +}; + +export const decideTemporalConstructor = typeName => { + switch (typeName) { case '_Neo4jTimeInput': - cypherFunction = 'time'; - break; + return 'time'; case '_Neo4jDateInput': - cypherFunction = 'date'; - break; + return 'date'; case '_Neo4jDateTimeInput': - cypherFunction = 'datetime'; - break; + return 'datetime'; case '_Neo4jLocalTimeInput': - cypherFunction = 'localtime'; - break; + return 'localtime'; case '_Neo4jLocalDateTimeInput': - cypherFunction = 'localdatetime'; - break; + return 'localdatetime'; default: - break; + return ''; } - return cypherFunction; }; export const getTemporalArguments = args => { diff --git a/test/augmentSchemaTest.js b/test/augmentSchemaTest.js index cfe67811..4b32e328 100644 --- a/test/augmentSchemaTest.js +++ b/test/augmentSchemaTest.js @@ -19,8 +19,8 @@ directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION directive @hasScope(scopes: [String]) on OBJECT | FIELD_DEFINITION input _ActorFilter { - AND: [_ActorFilter] - OR: [_ActorFilter] + AND: [_ActorFilter!] + OR: [_ActorFilter!] userId: ID userId_not: ID userId_in: [ID!] @@ -141,8 +141,8 @@ type _AddUserRatedPayload { } input _BookFilter { - AND: [_BookFilter] - OR: [_BookFilter] + AND: [_BookFilter!] + OR: [_BookFilter!] genre: BookGenre genre_not: BookGenre genre_in: [BookGenre!] @@ -161,8 +161,8 @@ enum _BookOrdering { } input _currentUserIdFilter { - AND: [_currentUserIdFilter] - OR: [_currentUserIdFilter] + AND: [_currentUserIdFilter!] + OR: [_currentUserIdFilter!] userId: String userId_not: String userId_in: [String!] @@ -186,6 +186,65 @@ enum _currentUserIdOrdering { _id_desc } +input _FriendOfDirectionsFilter { + from: _FriendOfFilter + to: _FriendOfFilter +} + +input _FriendOfFilter { + AND: [_FriendOfFilter!] + OR: [_FriendOfFilter!] + since: Int + since_not: Int + since_in: [Int!] + since_not_in: [Int!] + since_lt: Int + since_lte: Int + since_gt: Int + since_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter +} + input _FriendOfInput { since: Int time: _Neo4jTimeInput @@ -197,8 +256,8 @@ input _FriendOfInput { } input _GenreFilter { - AND: [_GenreFilter] - OR: [_GenreFilter] + AND: [_GenreFilter!] + OR: [_GenreFilter!] name: String name_not: String name_in: [String!] @@ -229,8 +288,8 @@ enum _GenreOrdering { } input _MovieFilter { - AND: [_MovieFilter] - OR: [_MovieFilter] + AND: [_MovieFilter!] + OR: [_MovieFilter!] movieId: ID movieId_not: ID movieId_in: [ID!] @@ -259,6 +318,14 @@ input _MovieFilter { year_lte: Int year_gt: Int year_gte: Int + released: _Neo4jDateTimeInput + released_not: _Neo4jDateTimeInput + released_in: [_Neo4jDateTimeInput!] + released_not_in: [_Neo4jDateTimeInput!] + released_lt: _Neo4jDateTimeInput + released_lte: _Neo4jDateTimeInput + released_gt: _Neo4jDateTimeInput + released_gte: _Neo4jDateTimeInput plot: String plot_not: String plot_in: [String!] @@ -315,6 +382,14 @@ input _MovieFilter { filmedIn_not: _StateFilter filmedIn_in: [_StateFilter!] filmedIn_not_in: [_StateFilter!] + ratings: _MovieRatedFilter + ratings_not: _MovieRatedFilter + ratings_in: [_MovieRatedFilter!] + ratings_not_in: [_MovieRatedFilter!] + ratings_some: _MovieRatedFilter + ratings_none: _MovieRatedFilter + ratings_single: _MovieRatedFilter + ratings_every: _MovieRatedFilter } input _MovieInput { @@ -326,6 +401,60 @@ enum _MovieOrdering { title_asc } +input _MovieRatedFilter { + AND: [_MovieRatedFilter!] + OR: [_MovieRatedFilter!] + rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + User: _UserFilter +} + type _MovieRatings { currentUserId(strArg: String): String rating: Int @@ -516,8 +645,8 @@ type _RemoveUserRatedPayload { } input _StateFilter { - AND: [_StateFilter] - OR: [_StateFilter] + AND: [_StateFilter!] + OR: [_StateFilter!] name: String name_not: String name_in: [String!] @@ -542,8 +671,16 @@ enum _StateOrdering { } input _TemporalNodeFilter { - AND: [_TemporalNodeFilter] - OR: [_TemporalNodeFilter] + AND: [_TemporalNodeFilter!] + OR: [_TemporalNodeFilter!] + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput name: String name_not: String name_in: [String!] @@ -554,6 +691,38 @@ input _TemporalNodeFilter { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput temporalNodes: _TemporalNodeFilter temporalNodes_not: _TemporalNodeFilter temporalNodes_in: [_TemporalNodeFilter!] @@ -588,8 +757,8 @@ enum _TemporalNodeOrdering { } input _UserFilter { - AND: [_UserFilter] - OR: [_UserFilter] + AND: [_UserFilter!] + OR: [_UserFilter!] userId: ID userId_not: ID userId_in: [ID!] @@ -610,6 +779,22 @@ input _UserFilter { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + rated: _UserRatedFilter + rated_not: _UserRatedFilter + rated_in: [_UserRatedFilter!] + rated_not_in: [_UserRatedFilter!] + rated_some: _UserRatedFilter + rated_none: _UserRatedFilter + rated_single: _UserRatedFilter + rated_every: _UserRatedFilter + friends: _FriendOfDirectionsFilter + friends_not: _FriendOfDirectionsFilter + friends_in: [_FriendOfDirectionsFilter!] + friends_not_in: [_FriendOfDirectionsFilter!] + friends_some: _FriendOfDirectionsFilter + friends_none: _FriendOfDirectionsFilter + friends_single: _FriendOfDirectionsFilter + friends_every: _FriendOfDirectionsFilter favorites: _MovieFilter favorites_not: _MovieFilter favorites_in: [_MovieFilter!] @@ -633,8 +818,8 @@ type _UserFriends { } type _UserFriendsDirections { - from(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput): [_UserFriends] - to(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput): [_UserFriends] + from(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] + to(since: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _FriendOfFilter): [_UserFriends] } input _UserInput { @@ -665,6 +850,60 @@ type _UserRated { Movie: Movie } +input _UserRatedFilter { + AND: [_UserRatedFilter!] + OR: [_UserRatedFilter!] + rating: Int + rating_not: Int + rating_in: [Int!] + rating_not_in: [Int!] + rating_lt: Int + rating_lte: Int + rating_gt: Int + rating_gte: Int + time: _Neo4jTimeInput + time_not: _Neo4jTimeInput + time_in: [_Neo4jTimeInput!] + time_not_in: [_Neo4jTimeInput!] + time_lt: _Neo4jTimeInput + time_lte: _Neo4jTimeInput + time_gt: _Neo4jTimeInput + time_gte: _Neo4jTimeInput + date: _Neo4jDateInput + date_not: _Neo4jDateInput + date_in: [_Neo4jDateInput!] + date_not_in: [_Neo4jDateInput!] + date_lt: _Neo4jDateInput + date_lte: _Neo4jDateInput + date_gt: _Neo4jDateInput + date_gte: _Neo4jDateInput + datetime: _Neo4jDateTimeInput + datetime_not: _Neo4jDateTimeInput + datetime_in: [_Neo4jDateTimeInput!] + datetime_not_in: [_Neo4jDateTimeInput!] + datetime_lt: _Neo4jDateTimeInput + datetime_lte: _Neo4jDateTimeInput + datetime_gt: _Neo4jDateTimeInput + datetime_gte: _Neo4jDateTimeInput + localtime: _Neo4jLocalTimeInput + localtime_not: _Neo4jLocalTimeInput + localtime_in: [_Neo4jLocalTimeInput!] + localtime_not_in: [_Neo4jLocalTimeInput!] + localtime_lt: _Neo4jLocalTimeInput + localtime_lte: _Neo4jLocalTimeInput + localtime_gt: _Neo4jLocalTimeInput + localtime_gte: _Neo4jLocalTimeInput + localdatetime: _Neo4jLocalDateTimeInput + localdatetime_not: _Neo4jLocalDateTimeInput + localdatetime_in: [_Neo4jLocalDateTimeInput!] + localdatetime_not_in: [_Neo4jLocalDateTimeInput!] + localdatetime_lt: _Neo4jLocalDateTimeInput + localdatetime_lte: _Neo4jLocalDateTimeInput + localdatetime_gt: _Neo4jLocalDateTimeInput + localdatetime_gte: _Neo4jLocalDateTimeInput + Movie: _MovieFilter +} + type Actor implements Person { userId: ID! name: String @@ -739,7 +978,7 @@ type Movie { scaleRating(scale: Int = 3): Float scaleRatingFloat(scale: Float = 1.5): Float actorMovies(first: Int, offset: Int, orderBy: [_MovieOrdering]): [Movie] - ratings(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput): [_MovieRatings] + ratings(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _MovieRatedFilter): [_MovieRatings] years: [Int] titles: [String] imdbRatings: [Float] @@ -875,7 +1114,7 @@ type User implements Person { userId: ID! name: String currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String - rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput): [_UserRated] + rated(rating: Int, time: _Neo4jTimeInput, date: _Neo4jDateInput, datetime: _Neo4jDateTimeInput, localtime: _Neo4jLocalTimeInput, localdatetime: _Neo4jLocalDateTimeInput, filter: _UserRatedFilter): [_UserRated] friends: _UserFriendsDirections favorites(first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] _id: String diff --git a/test/helpers/filterTestHelpers.js b/test/helpers/filterTestHelpers.js index 8d021c61..684c385a 100644 --- a/test/helpers/filterTestHelpers.js +++ b/test/helpers/filterTestHelpers.js @@ -15,7 +15,9 @@ export const filterTestRunner = ( person(object, params, ctx, resolveInfo) { const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); t.is(query, expectedCypherQuery); - t.deepEqual(queryParams, expectedCypherParams); + // need to turn neo4j Integers (used for temporal params) back to just JSON + const deserializedParams = JSON.parse(JSON.stringify(queryParams)); + t.deepEqual(deserializedParams, expectedCypherParams); }, Company(object, params, ctx, resolveInfo) { const [query, queryParams] = cypherQuery(params, ctx, resolveInfo); diff --git a/test/tck/filterTck.md b/test/tck/filterTck.md index 0f104d21..dfbc2c63 100644 --- a/test/tck/filterTck.md +++ b/test/tck/filterTck.md @@ -1,6 +1,10 @@ ## Filter Test TCK ```schema +type Query { + person(filter: _PersonFilter): [Person] + Company(filter: _CompanyFilter): [Company] +} enum Gender { female male @@ -12,15 +16,24 @@ type Person { height: Float fun: Boolean gender: Gender + birthday: _Neo4jDateTime company(filter: _CompanyFilter): Company @relation(name: "WORKS_AT", direction: OUT) + employmentHistory(filter: _PersonEmploymentHistoryFilter): [_PersonEmploymentHistory] + knows: _PersonKnowsDirections } -type Company { - name: String - employees(filter: _PersonFilter): [Person] @relation(name: "WORKS_AT", direction: IN) +type _PersonEmploymentHistory @relation(name: "WORKED_AT", from: "Person", to: "Company") { + role: String! + start: _Neo4jDateTime! + end: _Neo4jDateTime! + Company: Company } -type Query { - person(filter: _PersonFilter): [Person] - Company(filter: _CompanyFilter): [Company] +type _PersonKnowsDirections @relation(name: "KNOWS", from: "Person", to: "Person") { + from(filter: _PersonKnowsFilter): [_PersonKnows] + to(filter: _PersonKnowsFilter): [_PersonKnows] +} +type _PersonKnows @relation(name: "KNOWS", from: "Person", to: "Person") { + since: _Neo4jDateTime! + Person: Person } input _PersonFilter { AND: [_PersonFilter!] @@ -67,10 +80,94 @@ input _PersonFilter { gender_not: Gender gender_in: [Gender!] gender_not_in: [Gender!] + birthday: _Neo4jDateTimeInput + birthday_not: _Neo4jDateTimeInput + birthday_in: [_Neo4jDateTimeInput!] + birthday_not_in: [_Neo4jDateTimeInput!] + birthday_lt: _Neo4jDateTimeInput + birthday_lte: _Neo4jDateTimeInput + birthday_gt: _Neo4jDateTimeInput + birthday_gte: _Neo4jDateTimeInput company: _CompanyFilter company_not: _CompanyFilter company_in: [_CompanyFilter!] company_not_in: [_CompanyFilter!] + employmentHistory: _PersonEmploymentHistoryFilter + employmentHistory_not: _PersonEmploymentHistoryFilter + employmentHistory_in: [_PersonEmploymentHistoryFilter!] + employmentHistory_not_in: [_PersonEmploymentHistoryFilter!] + employmentHistory_some: _PersonEmploymentHistoryFilter + employmentHistory_none: _PersonEmploymentHistoryFilter + employmentHistory_single: _PersonEmploymentHistoryFilter + employmentHistory_every: _PersonEmploymentHistoryFilter + knows: _PersonKnowsDirectionsFilter + knows_not: _PersonKnowsDirectionsFilter + knows_in: [_PersonKnowsDirectionsFilter!] + knows_not_in: [_PersonKnowsDirectionsFilter!] + knows_some: _PersonKnowsDirectionsFilter + knows_none: _PersonKnowsDirectionsFilter + knows_single: _PersonKnowsDirectionsFilter + knows_every: _PersonKnowsDirectionsFilter +} +input _PersonEmploymentHistoryFilter { + AND: [_PersonEmploymentHistoryFilter!] + OR: [_PersonEmploymentHistoryFilter!] + role: String + role_not: String + role_in: [String!] + role_not_in: [String!] + role_contains: String + role_not_contains: String + role_starts_with: String + role_not_starts_with: String + role_ends_with: String + role_not_ends_with: String + start: _Neo4jDateTimeInput + start_not: _Neo4jDateTimeInput + start_in: [_Neo4jDateTimeInput!] + start_not_in: [_Neo4jDateTimeInput!] + start_lt: _Neo4jDateTimeInput + start_lte: _Neo4jDateTimeInput + start_gt: _Neo4jDateTimeInput + start_gte: _Neo4jDateTimeInput + end: _Neo4jDateTimeInput + end_not: _Neo4jDateTimeInput + end_in: [_Neo4jDateTimeInput!] + end_not_in: [_Neo4jDateTimeInput!] + end_lt: _Neo4jDateTimeInput + end_lte: _Neo4jDateTimeInput + end_gt: _Neo4jDateTimeInput + end_gte: _Neo4jDateTimeInput + Company: _CompanyFilter +} +input _PersonKnowsDirectionsFilter { + from: _PersonKnowsFilter + to: _PersonKnowsFilter +} +input _PersonKnowsFilter { + AND: [_PersonKnowsFilter!] + OR: [_PersonKnowsFilter!] + since: _Neo4jDateTimeInput + since_not: _Neo4jDateTimeInput + since_in: [_Neo4jDateTimeInput!] + since_not_in: [_Neo4jDateTimeInput!] + since_lt: _Neo4jDateTimeInput + since_lte: _Neo4jDateTimeInput + since_gt: _Neo4jDateTimeInput + since_gte: _Neo4jDateTimeInput + Person: _PersonFilter +} +type Company { + name: String! + founded: _Neo4jDateTime + employees(filter: _PersonFilter): [Person] @relation(name: "WORKS_AT", direction: IN) + employeeHistory(filter: _CompanyEmploymentHistoryFilter): [_CompanyEmployeeHistory] +} +type _CompanyEmployeeHistory @relation(name: "WORKED_AT", from: "Person", to: "Company") { + role: String! + start: _Neo4jDateTime! + end: _Neo4jDateTime! + Person: Person } input _CompanyFilter { AND: [_CompanyFilter!] @@ -85,6 +182,14 @@ input _CompanyFilter { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + founded: _Neo4jDateTimeInput + founded_not: _Neo4jDateTimeInput + founded_in: [_Neo4jDateTimeInput!] + founded_not_in: [_Neo4jDateTimeInput!] + founded_lt: _Neo4jDateTimeInput + founded_lte: _Neo4jDateTimeInput + founded_gt: _Neo4jDateTimeInput + founded_gte: _Neo4jDateTimeInput employees: _PersonFilter employees_not: _PersonFilter employees_in: [_PersonFilter!] @@ -93,187 +198,253 @@ input _CompanyFilter { employees_none: _PersonFilter employees_single: _PersonFilter employees_every: _PersonFilter + employeeHistory: _CompanyEmploymentHistoryFilter + employeeHistory_not: _CompanyEmploymentHistoryFilter + employeeHistory_in: [_CompanyEmploymentHistoryFilter!] + employeeHistory_not_in: [_CompanyEmploymentHistoryFilter!] + employeeHistory_some: _CompanyEmploymentHistoryFilter + employeeHistory_none: _CompanyEmploymentHistoryFilter + employeeHistory_single: _CompanyEmploymentHistoryFilter + employeeHistory_every: _CompanyEmploymentHistoryFilter +} +input _CompanyEmploymentHistoryFilter { + AND: [_CompanyEmploymentHistoryFilter!] + OR: [_CompanyEmploymentHistoryFilter!] + role: String + role_not: String + role_in: [String!] + role_not_in: [String!] + role_contains: String + role_not_contains: String + role_starts_with: String + role_not_starts_with: String + role_ends_with: String + role_not_ends_with: String + start: _Neo4jDateTimeInput + start_not: _Neo4jDateTimeInput + start_in: [_Neo4jDateTimeInput!] + start_not_in: [_Neo4jDateTimeInput!] + start_lt: _Neo4jDateTimeInput + start_lte: _Neo4jDateTimeInput + start_gt: _Neo4jDateTimeInput + start_gte: _Neo4jDateTimeInput + end: _Neo4jDateTimeInput + end_not: _Neo4jDateTimeInput + end_in: [_Neo4jDateTimeInput!] + end_not_in: [_Neo4jDateTimeInput!] + end_lt: _Neo4jDateTimeInput + end_lte: _Neo4jDateTimeInput + end_gt: _Neo4jDateTimeInput + end_gte: _Neo4jDateTimeInput + Person: _PersonFilter +} +type _Neo4jTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String +} +input _Neo4jTimeInput { + hour: Int + minute: Int + second: Int + nanosecond: Int + millisecond: Int + microsecond: Int + timezone: String + formatted: String +} +type _Neo4jDate { + year: Int + month: Int + day: Int + formatted: String +} +input _Neo4jDateInput { + year: Int + month: Int + day: Int + formatted: String +} +type _Neo4jDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String +} +input _Neo4jDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + timezone: String + formatted: String +} +type _Neo4jLocalTime { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String +} +input _Neo4jLocalTimeInput { + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String +} +type _Neo4jLocalDateTime { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String +} +input _Neo4jLocalDateTimeInput { + year: Int + month: Int + day: Int + hour: Int + minute: Int + second: Int + millisecond: Int + microsecond: Int + nanosecond: Int + formatted: String +} +directive @cypher(statement: String) on FIELD_DEFINITION +directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT +enum _RelationDirections { + IN + OUT } ``` ### ID field equal to given value - ```graphql -{ - person(filter: { id: "jane" }) { - name - } -} +{ person(filter: { id: "jane" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.id = $filter.id) RETURN `person` { .name } AS `person` ``` ### ID field that starts with given substring - ```graphql -{ - person(filter: { id_starts_with: "ja" }) { - name - } -} +{ person(filter: { id_starts_with: "ja" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.id STARTS WITH $filter.id_starts_with) RETURN `person` { .name } AS `person` ``` - -### ID field that does not start with given substring - +### ID field that does NOT start with given substring ```graphql -{ - person(filter: { id_not_starts_with: "ja" }) { - name - } -} +{ person(filter: { id_not_starts_with: "ja" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.id STARTS WITH $filter.id_not_starts_with) RETURN `person` { .name } AS `person` ``` ### ID field that ends with given substring - ```graphql -{ - person(filter: { id_ends_with: "ne" }) { - name - } -} +{ person(filter: { id_ends_with: "ne" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.id ENDS WITH $filter.id_ends_with) RETURN `person` { .name } AS `person` ``` -### ID field that does not end with given substring - +### ID field that does NOT end with given substring ```graphql -{ - person(filter: { id_not_ends_with: "ne" }) { - name - } -} +{ person(filter: { id_not_ends_with: "ne" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.id ENDS WITH $filter.id_not_ends_with) RETURN `person` { .name } AS `person` ``` ### ID field that contains given substring - ```graphql -{ - person(filter: { id_contains: "an" }) { - name - } -} +{ person(filter: { id_contains: "an" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.id CONTAINS $filter.id_contains) RETURN `person` { .name } AS `person` ``` -### ID field that does not contain given substring - +### ID field that does NOT contain given substring ```graphql -{ - person(filter: { id_not_contains: "an" }) { - name - } -} +{ person(filter: { id_not_contains: "an" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.id CONTAINS $filter.id_not_contains) RETURN `person` { .name } AS `person` ``` ### ID field in given list - ```graphql -{ - person(filter: { id_in: ["jane"] }) { - name - } -} +{ person(filter: { id_in: ["jane"] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.id IN $filter.id_in) RETURN `person` { .name } AS `person` ``` -### ID field not in given list - +### ID field NOT in given list ```graphql -{ - person(filter: { id_not_in: ["joe"] }) { - name - } -} +{ person(filter: { id_not_in: ["joe"] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.id IN $filter.id_not_in) RETURN `person` { .name } AS `person` ``` ### ID field different from given value - ```graphql -{ - person(filter: { id_not: "joe" }) { - name - } -} +{ person(filter: { id_not: "joe" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.id = $filter.id_not) RETURN `person` { .name } AS `person` ``` -### String field does not exist - +### String field does NOT exist ```graphql -{ - person(filter: { id: null }) { - name - } -} +{ person(filter: { id: null }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE ($filter._id_null = TRUE AND NOT EXISTS(`person`.id)) RETURN `person` { .name } AS `person` ``` ### String field exists - ```graphql -{ - person(filter: { id_not: null }) { - name - } -} +{ person(filter: { id_not: null }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE ($filter._id_not_null = TRUE AND EXISTS(`person`.id)) RETURN `person` { .name } AS `person` ``` ### String field equal to given value (parameterized filter) - ```graphql -query filterQuery($filter: _PersonFilter) { - person(filter: $filter) { - name - } -} +query filterQuery($filter: _PersonFilter) { person(filter: $filter) { name }} ``` - ```params { "filter": { @@ -281,772 +452,790 @@ query filterQuery($filter: _PersonFilter) { } } ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { .name } AS `person` ``` ### String field equal to given value (parameterized) - ```graphql -query filterQuery($name: String) { - person(filter: { name: $name }) { - name - } -} +query filterQuery($name: String) { person(filter: {name: $name}) { name }} ``` - ```params { "name": "Jane" } ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { .name } AS `person` ``` ### String field equal to given value - ```graphql -{ - person(filter: { name: "Jane" }) { - name - } -} +{ person(filter: { name: "Jane" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { .name } AS `person` ``` ### String field that starts with given substring - ```graphql -{ - person(filter: { name_starts_with: "Ja" }) { - name - } -} +{ person(filter: { name_starts_with: "Ja" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name STARTS WITH $filter.name_starts_with) RETURN `person` { .name } AS `person` ``` -### String field that does not start with given substring - +### String field that does NOT start with given substring ```graphql -{ - person(filter: { name_not_starts_with: "Ja" }) { - name - } -} +{ person(filter: { name_not_starts_with: "Ja" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.name STARTS WITH $filter.name_not_starts_with) RETURN `person` { .name } AS `person` ``` ### String field that ends with given substring - ```graphql -{ - person(filter: { name_ends_with: "ne" }) { - name - } -} +{ person(filter: { name_ends_with: "ne" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name ENDS WITH $filter.name_ends_with) RETURN `person` { .name } AS `person` ``` -### String field that does not end with given substring - +### String field that does NOT end with given substring ```graphql -{ - person(filter: { name_not_ends_with: "ne" }) { - name - } -} +{ person(filter: { name_not_ends_with: "ne" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.name ENDS WITH $filter.name_not_ends_with) RETURN `person` { .name } AS `person` ``` ### String field that contains given substring - ```graphql -{ - person(filter: { name_contains: "an" }) { - name - } -} +{ person(filter: { name_contains: "an" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name CONTAINS $filter.name_contains) RETURN `person` { .name } AS `person` ``` -### String field that does not contain given substring - +### String field that does NOT contain given substring ```graphql -{ - person(filter: { name_not_contains: "an" }) { - name - } -} +{ person(filter: { name_not_contains: "an" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.name CONTAINS $filter.name_not_contains) RETURN `person` { .name } AS `person` ``` ### String field in given list - ```graphql -{ - person(filter: { name_in: ["Jane"] }) { - name - } -} +{ person(filter: { name_in: ["Jane"] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.name IN $filter.name_in) RETURN `person` { .name } AS `person` ``` -### String field not in given list - +### String field NOT in given list ```graphql -{ - person(filter: { name_not_in: ["Joe"] }) { - name - } -} +{ person(filter: { name_not_in: ["Joe"] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.name IN $filter.name_not_in) RETURN `person` { .name } AS `person` ``` ### String field different from given value - ```graphql -{ - person(filter: { name_not: "Joe" }) { - name - } -} +{ person(filter: { name_not: "Joe" }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.name = $filter.name_not) RETURN `person` { .name } AS `person` ``` ### Boolean field equal to given value - ```graphql -{ - person(filter: { fun: true }) { - name - } -} +{ person(filter: { fun: true }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.fun = $filter.fun) RETURN `person` { .name } AS `person` ``` ### Boolean field different from given value - ```graphql -{ - person(filter: { fun_not: true }) { - name - } -} +{ person(filter: { fun_not: true }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.fun = $filter.fun_not) RETURN `person` { .name } AS `person` ``` ### Enum field equal to given value (parameterized) - ```graphql -query filterQuery($filterPersonGender: Gender) { - person(filter: { gender: $filterPersonGender }) { - name - } -} +query filterQuery($filterPersonGender: Gender) { person(filter: { gender: $filterPersonGender }) { name }} ``` - ```params {"filterPersonGender":"male"} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.gender = $filter.gender) RETURN `person` { .name } AS `person` ``` ### Enum field different from given value (parameterized) - ```graphql -query filterQuery($filterPersonGender: Gender) { - person(filter: { gender_not: $filterPersonGender }) { - name - } -} +query filterQuery($filterPersonGender: Gender) { person(filter: { gender_not: $filterPersonGender }) { name }} ``` - ```params {"filterPersonGender":"male"} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.gender = $filter.gender_not) RETURN `person` { .name } AS `person` ``` -### Enum field not in given list (parameterized) - +### Enum field NOT in given list (parameterized) ```graphql -query filterQuery($filterPersonGender: [Gender!]) { - person(filter: { gender_not_in: $filterPersonGender }) { - name - } -} +query filterQuery($filterPersonGender: [Gender!]) { person(filter: { gender_not_in: $filterPersonGender }) { name }} ``` - ```params {"filterPersonGender":["male"]} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.gender IN $filter.gender_not_in) RETURN `person` { .name } AS `person` ``` ### Enum field in given list - ```graphql -{ - person(filter: { gender_in: male }) { - name - } -} +{ person(filter: { gender_in: male }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.gender IN $filter.gender_in) RETURN `person` { .name } AS `person` ``` ### Int field equal to given value - ```graphql -{ - person(filter: { age: 38 }) { - name - } -} +{ person(filter: { age: 38 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age = $filter.age) RETURN `person` { .name } AS `person` ``` ### Int field in given list - ```graphql -{ - person(filter: { age_in: [38] }) { - name - } -} +{ person(filter: { age_in: [38] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age IN $filter.age_in) RETURN `person` { .name } AS `person` ``` -### Int field not in given list - +### Int field NOT in given list ```graphql -{ - person(filter: { age_not_in: [38] }) { - name - } -} +{ person(filter: { age_not_in: [38] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.age IN $filter.age_not_in) RETURN `person` { .name } AS `person` ``` ### Int field less than or equal to given value - ```graphql -{ - person(filter: { age_lte: 40 }) { - name - } -} +{ person(filter: { age_lte: 40 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age <= $filter.age_lte) RETURN `person` { .name } AS `person` ``` ### Int field less than given value - ```graphql -{ - person(filter: { age_lt: 40 }) { - name - } -} +{ person(filter: { age_lt: 40 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age < $filter.age_lt) RETURN `person` { .name } AS `person` ``` ### Int field greater than given value - ```graphql -{ - person(filter: { age_gt: 40 }) { - name - } -} +{ person(filter: { age_gt: 40 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age > $filter.age_gt) RETURN `person` { .name } AS `person` ``` ### Int field greater than or equal to given value - ```graphql -{ - person(filter: { age_gte: 40 }) { - name - } -} +{ person(filter: { age_gte: 40 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.age >= $filter.age_gte) RETURN `person` { .name } AS `person` ``` ### Float field equal to given value - ```graphql -{ - person(filter: { height: 1.75 }) { - name - } -} +{ person(filter: { height: 1.75 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height = $filter.height) RETURN `person` { .name } AS `person` ``` ### Float field different from given value - ```graphql -{ - person(filter: { height_not: 1.75 }) { - name - } -} +{ person(filter: { height_not: 1.75 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.height = $filter.height_not) RETURN `person` { .name } AS `person` ``` ### Float field in given list - ```graphql -{ - person(filter: { height_in: [1.75] }) { - name - } -} +{ person(filter: { height_in: [1.75] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height IN $filter.height_in) RETURN `person` { .name } AS `person` ``` -### Float field not in given list - +### Float field NOT in given list ```graphql -{ - person(filter: { height_not_in: [1.75] }) { - name - } -} +{ person(filter: { height_not_in: [1.75] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (NOT `person`.height IN $filter.height_not_in) RETURN `person` { .name } AS `person` ``` ### Float field less than or equal to given value - ```graphql -{ - person(filter: { height_lte: 1.80 }) { - name - } -} +{ person(filter: { height_lte: 1.80 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height <= $filter.height_lte) RETURN `person` { .name } AS `person` ``` ### Float field less than to given value - ```graphql -{ - person(filter: { height_lt: 1.80 }) { - name - } -} +{ person(filter: { height_lt: 1.80 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height < $filter.height_lt) RETURN `person` { .name } AS `person` ``` ### Float field greater than or equal to given value - ```graphql -{ - person(filter: { height_gte: 1.80 }) { - name - } -} +{ person(filter: { height_gte: 1.80 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height >= $filter.height_gte) RETURN `person` { .name } AS `person` ``` ### Float field greater than given value - ```graphql -{ - person(filter: { height_gt: 1.80 }) { - name - } -} +{ person(filter: { height_gt: 1.80 }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (`person`.height > $filter.height_gt) RETURN `person` { .name } AS `person` ``` ### Boolean AND Float field OR String field equal to given value - ```graphql -{ - person( - filter: { - OR: [{ AND: [{ fun: true }, { height: 1.75 }] }, { name_in: ["Jane"] }] - } - ) { - name - } -} +{ person(filter: { OR: [{ AND: [{fun: true},{height:1.75}]},{name_in: ["Jane"]}] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (ANY(_OR IN $filter.OR WHERE (_OR.AND IS NULL OR ALL(_AND IN _OR.AND WHERE (_AND.fun IS NULL OR `person`.fun = _AND.fun) AND (_AND.height IS NULL OR `person`.height = _AND.height))) AND (_OR.name_in IS NULL OR `person`.name IN _OR.name_in))) RETURN `person` { .name } AS `person` ``` ### Boolean AND String field equal to given value - ```graphql -{ - person(filter: { AND: [{ fun: true, name: "Jane" }] }) { - name - } -} +{ person(filter: { AND: [{ fun: true, name: "Jane"}] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (ALL(_AND IN $filter.AND WHERE (_AND.name IS NULL OR `person`.name = _AND.name) AND (_AND.fun IS NULL OR `person`.fun = _AND.fun))) RETURN `person` { .name } AS `person` ``` ### Boolean AND String field equal to value given in separate filters - ```graphql -{ - person(filter: { AND: [{ fun: true }, { name: "Jane" }] }) { - name - } -} +{ person(filter: { AND: [{ fun: true},{name: "Jane"}] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (ALL(_AND IN $filter.AND WHERE (_AND.fun IS NULL OR `person`.fun = _AND.fun) AND (_AND.name IS NULL OR `person`.name = _AND.name))) RETURN `person` { .name } AS `person` ``` ### Boolean field equal to OR String field NOT equal to given value - ```graphql -{ - person(filter: { OR: [{ fun: false, name_not: "Jane" }] }) { - name - } -} +{ person(filter: { OR: [{ fun: false, name_not: "Jane"}] }) { name }} ``` - ```cypher MATCH (`person`:`Person`) WHERE (ANY(_OR IN $filter.OR WHERE (_OR.name_not IS NULL OR NOT `person`.name = _OR.name_not) AND (_OR.fun IS NULL OR `person`.fun = _OR.fun))) RETURN `person` { .name } AS `person` ``` ### Boolean field equal to given value OR String value in given list +```graphql +{ person(filter: { OR: [{ fun: true},{name_in: ["Jane"]}] }) { name }} +``` +```cypher +MATCH (`person`:`Person`) WHERE (ANY(_OR IN $filter.OR WHERE (_OR.fun IS NULL OR `person`.fun = _OR.fun) AND (_OR.name_in IS NULL OR `person`.name IN _OR.name_in))) RETURN `person` { .name } AS `person` +``` +### Related node does NOT exist ```graphql -{ - person(filter: { OR: [{ fun: true }, { name_in: ["Jane"] }] }) { - name - } -} +{ person(filter: { company: null }) { name }} +``` +```cypher +MATCH (`person`:`Person`) WHERE ($filter._company_null = TRUE AND NOT EXISTS((`person`)-[:WORKS_AT]->(:Company))) RETURN `person` { .name } AS `person` ``` +### Related node exists +```graphql +{ person(filter: { company_not: null }) { name }} +``` ```cypher -MATCH (`person`:`Person`) WHERE (ANY(_OR IN $filter.OR WHERE (_OR.fun IS NULL OR `person`.fun = _OR.fun) AND (_OR.name_in IS NULL OR `person`.name IN _OR.name_in))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE ($filter._company_not_null = TRUE AND EXISTS((`person`)-[:WORKS_AT]->(:Company))) RETURN `person` { .name } AS `person` ``` -### Related node does not exist +### ALL related nodes matching filter +```graphql +{ person(filter: { company: { name: "ACME" } }) { name }} +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.company.name))) RETURN `person` { .name } AS `person` +``` +### ALL related nodes NOT matching filter ```graphql -{ - person(filter: { company: null }) { - name - } -} +{ person(filter: { company_not: { name: "ACME" } }) { name }} +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND NONE(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.company_not.name))) RETURN `person` { .name } AS `person` ``` +### ALL related nodes matching filter in given list +```graphql +{ person( filter: { company_in: [ { name: "Neo4j" }, { name: "ACME" } ] }) { name } } +``` ```cypher -MATCH (`person`:`Person`) WHERE ($filter._company_null = TRUE AND NOT EXISTS((`person`)-[:WORKS_AT]->(:Company))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE ANY(_company_in IN $filter.company_in WHERE (_company_in.name IS NULL OR `company`.name = _company_in.name)))) RETURN `person` { .name } AS `person` ``` -### Related node exists +### ALL related nodes NOT matching filter in given list +```graphql +{ person( filter: { company_not_in: [ { name: "Neo4j" }, { name: "ACME" } ] }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE NONE(_company_not_in IN $filter.company_not_in WHERE (_company_not_in.name IS NULL OR `company`.name = _company_not_in.name)))) RETURN `person` { .name } AS `person` +``` + +### ALL related nodes matching filter nested in given logical OR filters +```graphql +{ person( filter: { OR:[ { company: { name: "Neo4j" } }, { company: null } ] } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (ANY(_OR IN $filter.OR WHERE (_OR.company IS NULL OR EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (_OR.company.name IS NULL OR `company`.name = _OR.company.name))) AND (_OR._company_null IS NULL OR _OR._company_null = TRUE AND NOT EXISTS((`person`)-[:WORKS_AT]->(:Company))))) RETURN `person` { .name } AS `person` +``` +### String field equal to given value AND String field on ALL related nodes ends with given substring (parameterized filter) ```graphql +query filterQuery($filter: _PersonFilter) { person(filter: $filter) { name }} +``` +```params { - person(filter: { company_not: null }) { - name + "filter": { + "name": "Jane", + "company": { + "name_ends_with": "ME" + } } } ``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name ENDS WITH $filter.company.name_ends_with))) RETURN `person` { .name } AS `person` +``` +### ALL related nodes matching String field equal to given value +```graphql +{ p: Company { employees(filter: { name: "Jane" }) { name }}} +``` ```cypher -MATCH (`person`:`Person`) WHERE ($filter._company_not_null = TRUE AND EXISTS((`person`)-[:WORKS_AT]->(:Company))) RETURN `person` { .name } AS `person` +MATCH (`company`:`Company`) RETURN `company` {employees: [(`company`)<-[:`WORKS_AT`]-(`company_employees`:`Person`) WHERE (`company_employees`.name = $1_filter.name) | company_employees { .name }] } AS `company` ``` -### ALL related nodes matching filter +### ALL related nodes matching filter given in separate OR filters +```graphql +{ p: Company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}) { name }}} +``` +```cypher +MATCH (`company`:`Company`) RETURN `company` {employees: [(`company`)<-[:`WORKS_AT`]-(`company_employees`:`Person`) WHERE (ANY(_OR IN $1_filter.OR WHERE (_OR.name IS NULL OR `company_employees`.name = _OR.name))) | company_employees { .name }] } AS `company` +``` +### ALL related nodes matching String field in given list ```graphql -{ - person(filter: { company: { name: "ACME" } }) { - name - } -} +{ p: Company(filter: { employees: { name_in: ["Jane","Joe"] } }) { name }} +``` +```cypher +MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ALL(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name IN $filter.employees.name_in))) RETURN `company` { .name } AS `company` ``` +### SOME related nodes matching given filter +```graphql +{ p: Company(filter: { employees_some: { name: "Jane" } }) { name }} +``` ```cypher -MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.company.name))) RETURN `person` { .name } AS `person` +MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ANY(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_some.name))) RETURN `company` { .name } AS `company` ``` -### ALL related nodes NOT matching filter +### EVERY related node matching given filter +```graphql +{ p: Company(filter: { employees_every: { name: "Jill" } }) { name }} +``` +```cypher +MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ALL(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_every.name))) RETURN `company` { .name } AS `company` +``` +### NONE of any related nodes match given filter ```graphql -{ - person(filter: { company_not: { name: "ACME" } }) { - name - } -} +{ p: Company(filter: { employees_none: { name: "Jane" } }) { name }} +``` +```cypher +MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND NONE(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_none.name))) RETURN `company` { .name } AS `company` ``` +### SINGLE related node matching given filter +```graphql +{ p: Company(filter: { employees_single: { name: "Jill" } }) { name }} +``` ```cypher -MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND NONE(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.company_not.name))) RETURN `person` { .name } AS `person` +MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND SINGLE(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_single.name))) RETURN `company` { .name } AS `company` ``` -### ALL related nodes matching filter in given list +### Nested relationship filter +```graphql +{ person(filter: { company: { employees_some: { name: "Jane" } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ANY(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.company.employees_some.name))))) RETURN `person` { .name } AS `person` +``` + +### Temporal field equal to given value +```graphql +{ person( filter: { birthday: { year: 2020, day: 1, month: 1 hour: 0 minute: 0 second: 0 millisecond: 0 nanosecond: 0 timezone: "Z" } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ((`person`.birthday = datetime($filter.birthday))) RETURN `person` { .name } AS `person` +``` +### Temporal field different from given value ```graphql -{ - person(filter: { company_in: [{ name: "Neo4j" }, { name: "ACME" }] }) { - name - } -} +{ person( filter: { birthday_not: { year: 2020 } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ((NOT `person`.birthday = datetime($filter.birthday_not))) RETURN `person` { .name } AS `person` ``` +### Temporal field before or equal to given value +```graphql +{ person(filter: { birthday_lte: { year: 2020 } }) { name } } +``` ```cypher -MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE ANY(_company_in IN $filter.company_in WHERE ((_company_in.name IS NULL OR `company`.name = _company_in.name))))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE ((`person`.birthday <= datetime($filter.birthday_lte))) RETURN `person` { .name } AS `person` ``` -### ALL related nodes not matching filter in given list +### Temporal field before given value +```graphql +{ person(filter: { birthday_lt: { year: 2021 } }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ((`person`.birthday < datetime($filter.birthday_lt))) RETURN `person` { .name } AS `person` +``` +### Temporal field after or equal to given value ```graphql -{ - person(filter: { company_not_in: [{ name: "Neo4j" }, { name: "ACME" }] }) { - name - } -} +{ person(filter: { birthday_gte: { year: 2020 } }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ((`person`.birthday >= datetime($filter.birthday_gte))) RETURN `person` { .name } AS `person` ``` +### Temporal field after given value +```graphql +{ person(filter: { birthday_gt: { year: 2020 } }) { name } } +``` ```cypher -MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE NONE(_company_not_in IN $filter.company_not_in WHERE ((_company_not_in.name IS NULL OR `company`.name = _company_not_in.name))))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE ((`person`.birthday > datetime($filter.birthday_gt))) RETURN `person` { .name } AS `person` ``` -### String field equal to given value AND String field on ALL related nodes ends with given substring (parameterized filter) +### Temporal field in given list +```graphql +{ person(filter: { birthday_in: [ { year: 2020 }, { formatted: "2021-01-01T00:00:00Z" } ] }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (ANY(_birthday_in IN $filter.birthday_in WHERE (`person`.birthday = datetime(_birthday_in)))) RETURN `person` { .name } AS `person` +``` +### Temporal field NOT in given list ```graphql -query filterQuery($filter: _PersonFilter) { - person(filter: $filter) { - name - } -} +{ person(filter: { birthday_not_in: [ { year: 2021 }, { formatted: "2021-01-01T00:00:00Z" } ] }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (NONE(_birthday_not_in IN $filter.birthday_not_in WHERE (`person`.birthday = datetime(_birthday_not_in)))) RETURN `person` { .name } AS `person` ``` -```params -{ - "filter": { - "name": "Jane", - "company": { - "name_ends_with": "ME" - } - } -} +### Temporal field does NOT exist +```graphql +{ person( filter: { birthday: null } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ($filter._birthday_null = TRUE AND NOT EXISTS(`person`.birthday)) RETURN `person` { .name } AS `person` ``` +### Temporal field exists +```graphql +{ person( filter: { birthday_not: null } ) { name } } +``` ```cypher -MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name ENDS WITH $filter.company.name_ends_with))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE ($filter._birthday_not_null = TRUE AND EXISTS(`person`.birthday)) RETURN `person` { .name } AS `person` ``` -### ALL related nodes matching String field equal to given value +### Temporal field does NOT exist on related node +```graphql +{ person( filter: { company: { founded: null } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE ($filter.company._founded_null = TRUE AND NOT EXISTS(`company`.founded)))) RETURN `person` { .name } AS `person` +``` +### Temporal field on related node equal to given value ```graphql -{ - p: Company { - employees(filter: { name: "Jane" }) { - name - } - } -} +{ person( filter: { company: { founded: { year: 2007 } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE ((`company`.founded = datetime($filter.company.founded))))) RETURN `person` { .name } AS `person` ``` +### Temporal field on related node equal to given year OR formatted value OR does NOT exist +```graphql +{ person( filter: { company: { OR: [ { founded: { year: 2007 } } { founded: { formatted: "2007-01-01T00:00:00Z" } } { founded: null } ] } } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) RETURN `company` {employees: [(`company`)<-[:`WORKS_AT`]-(`company_employees`:`Person`) WHERE (`company_employees`.name = $1_filter.name) | company_employees { .name }] } AS `company` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (ANY(_OR IN $filter.company.OR WHERE ((_OR.founded IS NULL OR `company`.founded = datetime(_OR.founded))) AND (_OR._founded_null IS NULL OR _OR._founded_null = TRUE AND NOT EXISTS(`company`.founded)))))) RETURN `person` { .name } AS `person` ``` -### ALL related nodes matching filter given in separate OR filters +### Temporal and scalar field on relationship match given logical AND filters +```graphql +{ person( filter: { employmentHistory: { AND: [ { role: "Developer" }, { start: { year: 2019 } }, { end: { year: 2020 } } ] } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (ALL(_AND IN $filter.employmentHistory.AND WHERE (_AND.role IS NULL OR `person_filter_company`.role = _AND.role) AND ((_AND.start IS NULL OR `person_filter_company`.start = datetime(_AND.start))) AND ((_AND.end IS NULL OR `person_filter_company`.end = datetime(_AND.end))))))) RETURN `person` { .name } AS `person` +``` +### Related node does NOT exist (relationship type) ```graphql -{ - p: Company { - employees(filter: { OR: [{ name: "Jane" }, { name: "Joe" }] }) { - name - } - } -} +{ person( filter: { employmentHistory: null }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE ($filter._employmentHistory_null = TRUE AND NOT EXISTS((`person`)-[:WORKED_AT]->(:Company))) RETURN `person` { .name } AS `person` ``` +### Related node exists (relationship type) +```graphql +{ person( filter: { employmentHistory_not: null }) { name } } +``` ```cypher -MATCH (`company`:`Company`) RETURN `company` {employees: [(`company`)<-[:`WORKS_AT`]-(`company_employees`:`Person`) WHERE (ANY(_OR IN $1_filter.OR WHERE (_OR.name IS NULL OR `company_employees`.name = _OR.name))) | company_employees { .name }] } AS `company` +MATCH (`person`:`Person`) WHERE ($filter._employmentHistory_not_null = TRUE AND EXISTS((`person`)-[:WORKED_AT]->(:Company))) RETURN `person` { .name } AS `person` ``` -### ALL related nodes matching String field in given list +### Temporal fields on relationship do NOT exist +```graphql +{ person( filter: { employmentHistory: { start: null, end: null } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE ($filter.employmentHistory._start_null = TRUE AND NOT EXISTS(`person_filter_company`.start)) AND ($filter.employmentHistory._end_null = TRUE AND NOT EXISTS(`person_filter_company`.end)))) RETURN `person` { .name } AS `person` +``` +### Temporal fields on relationship exist ```graphql -{ - p: Company(filter: { employees: { name_in: ["Jane", "Joe"] } }) { - name - } -} +{ person( filter: { employmentHistory: { start_not: null, end_not: null } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE ($filter.employmentHistory._start_not_null = TRUE AND EXISTS(`person_filter_company`.start)) AND ($filter.employmentHistory._end_not_null = TRUE AND EXISTS(`person_filter_company`.end)))) RETURN `person` { .name } AS `person` ``` +### Temporal fields on relationship equal to given values +```graphql +{ person( filter: { employmentHistory: { start:{ year: 2019 }, end: { formatted: "2020-01-01T00:00:00Z" } } } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ALL(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name IN $filter.employees.name_in))) RETURN `company` { .name } AS `company` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE ((`person_filter_company`.start = datetime($filter.employmentHistory.start))) AND ((`person_filter_company`.end = datetime($filter.employmentHistory.end))))) RETURN `person` { .name } AS `person` ``` -### SOME related nodes matching given filter +### ALL relationships matching filter +```graphql +{ person( filter: { employmentHistory: { role: "Developer" } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory.role))) RETURN `person` { .name } AS `person` +``` +### ALL relationships NOT matching filter ```graphql -{ - p: Company(filter: { employees_some: { name: "Jane" } }) { - name - } -} +{ person( filter: { employmentHistory_not: { role: "Developer" } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND NONE(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory_not.role))) RETURN `person` { .name } AS `person` ``` +### SOME relationships matching given filter +```graphql +{ person( filter: { employmentHistory_some: { role: "Developer" } } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ANY(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_some.name))) RETURN `company` { .name } AS `company` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ANY(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory_some.role))) RETURN `person` { .name } AS `person` ``` -### EVERY related node matching given filter +### EVERY relationship matching given filter +```graphql +{ person( filter: { employmentHistory_every: { role: "Developer" } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory_every.role))) RETURN `person` { .name } AS `person` +``` +### NONE of any relationships match given filter ```graphql -{ - p: Company(filter: { employees_every: { name: "Jill" } }) { - name - } -} +{ person( filter: { employmentHistory_none: { role: "Developer" } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND NONE(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory_none.role))) RETURN `person` { .name } AS `person` ``` +### SINGLE relationship matching given filter +```graphql +{ person( filter: { employmentHistory_single: { role: "Developer" } } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ALL(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_every.name))) RETURN `company` { .name } AS `company` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND SINGLE(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory_single.role))) RETURN `person` { .name } AS `person` ``` -### NONE of related nodes matching given filter +### Scalar fields on relationship AND related node equal to given values +```graphql +{ person( filter: { employmentHistory: { role: "Developer", Company: { name: "ACME" } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE (`person_filter_company`.role = $filter.employmentHistory.role) AND (ALL(`company` IN [(`person`)-[`person_filter_company`]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.employmentHistory.Company.name))))) RETURN `person` { .name } AS `person` +``` +### ALL relationships matching filter in given list ```graphql -{ - p: Company(filter: { employees_none: { name: "Jane" } }) { - name - } -} +{ person( filter: { employmentHistory_in: [ { role: "Manager", start: { year: 2013 } }, { role: "Developer", start: { formatted: "2019-01-01T00:00:00Z" } } ] } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE ANY(_employmentHistory_in IN $filter.employmentHistory_in WHERE (_employmentHistory_in.role IS NULL OR `person_filter_company`.role = _employmentHistory_in.role) AND ((_employmentHistory_in.start IS NULL OR `person_filter_company`.start = datetime(_employmentHistory_in.start)))))) RETURN `person` { .name } AS `person` ``` +### ALL relationships NOT matching filter in given list +```graphql +{ person( filter: { employmentHistory_not_in: [ { role: "Advisor", start: { year: 2015 } } ] } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND NONE(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_none.name))) RETURN `company` { .name } AS `company` +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND ALL(`person_filter_company` IN [(`person`)-[`_person_filter_company`:WORKED_AT]->(:Company) | `_person_filter_company`] WHERE NONE(_employmentHistory_not_in IN $filter.employmentHistory_not_in WHERE (_employmentHistory_not_in.role IS NULL OR `person_filter_company`.role = _employmentHistory_not_in.role) AND ((_employmentHistory_not_in.start IS NULL OR `person_filter_company`.start = datetime(_employmentHistory_not_in.start)))))) RETURN `person` { .name } AS `person` ``` -### SINGLE related node matching given filter +### ALL outgoing reflexive type relationships matching filter +```graphql +{ person( filter: { name: "jane", knows: { to: { since: { year: 2016 } } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)-[:KNOWS]->(:Person)) AND ALL(`person_filter_person` IN [(`person`)-[`_person_filter_person`:KNOWS]->(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows.to.since)))))) RETURN `person` { .name } AS `person` +``` +### ALL incoming reflexive type relationships NOT matching filter ```graphql -{ - p: Company(filter: { employees_single: { name: "Jill" } }) { - name - } -} +{ person(filter: { name: "jane", knows_not: { from: { since: { year: 2018 } } } }) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND NONE(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_not.from.since)))))) RETURN `person` { .name } AS `person` ``` +### ALL outgoing reflexive type relationships matching given filter +```graphql +{ person( filter: { name: "jane", knows: { from: { since: { year: 2018 } } } } ) { name } } +``` ```cypher -MATCH (`company`:`Company`) WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND SINGLE(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employees_single.name))) RETURN `company` { .name } AS `company` +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND ALL(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows.from.since)))))) RETURN `person` { .name } AS `person` ``` -### Nested relationship filter +### SOME incoming reflexive type relationships matching given filter +```graphql +{ person( filter: { name: "jane", knows_some: { from: { since: { year: 2018 } } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND ANY(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_some.from.since)))))) RETURN `person` { .name } AS `person` +``` +### EVERY incoming and outgoing reflexive type relationship matching given filters ```graphql -{ - person(filter: { company: { employees_some: { name: "Jane" } } }) { - name - } -} +{ person( filter: { name: "jane", knows_every: { to: { since: { year: 2009 } }, from: { since: { year: 2018 } } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND ALL(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_every.from.since))))) AND (EXISTS((`person`)-[:KNOWS]->(:Person)) AND ALL(`person_filter_person` IN [(`person`)-[`_person_filter_person`:KNOWS]->(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_every.to.since)))))) RETURN `person` { .name } AS `person` +``` + +### NONE of any incoming and outgoing reflexive type relationships match given filters +```graphql +{ person( filter: { name: "jane", knows_none: { to: { since: { year: 2229 } }, from: { since: { year: 2218 } } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND NONE(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_none.from.since))))) AND (EXISTS((`person`)-[:KNOWS]->(:Person)) AND NONE(`person_filter_person` IN [(`person`)-[`_person_filter_person`:KNOWS]->(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_none.to.since)))))) RETURN `person` { .name } AS `person` +``` + +### SINGLE incoming reflexive type relationships matching given filter +```graphql +{ person( filter: { name: "jane", knows_single: { from: { since: { year: 2018 } } } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND ((EXISTS((`person`)<-[:KNOWS]-(:Person)) AND SINGLE(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((`person_filter_person`.since = datetime($filter.knows_single.from.since)))))) RETURN `person` { .name } AS `person` +``` + +### ALL outgoing reflexive relationships matching filter in given list +```graphql +{ person( filter: { name: "jane", knows_in: [ { to: { since: { year: 3000 } } }, { to: { since: { year: 2009 } } } ] } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (ANY(_knows_in IN $filter.knows_in WHERE (_knows_in.to IS NULL OR EXISTS((`person`)-[:KNOWS]->(:Person)) AND ANY(`person_filter_person` IN [(`person`)-[`_person_filter_person`:KNOWS]->(:Person) | `_person_filter_person`] WHERE ((_knows_in.to.since IS NULL OR `person_filter_person`.since = datetime(_knows_in.to.since))))))) RETURN `person` { .name } AS `person` +``` + +### ALL incoming reflexive relationships NOT matching filter in given list +```graphql +{ person( filter: { name: "jane", knows_in: [ { from: { since: { year: 3000 } } }, { from: { since: { year: 2018 } } } ] } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (ANY(_knows_in IN $filter.knows_in WHERE (_knows_in.from IS NULL OR EXISTS((`person`)<-[:KNOWS]-(:Person)) AND ANY(`person_filter_person` IN [(`person`)<-[`_person_filter_person`:KNOWS]-(:Person) | `_person_filter_person`] WHERE ((_knows_in.from.since IS NULL OR `person_filter_person`.since = datetime(_knows_in.from.since))))))) RETURN `person` { .name } AS `person` +``` + +### Incoming and outgoing reflexive relationships do NOT exist +```graphql +{ person( filter: { knows: { from: null, to: null } } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (($filter.knows._from_null = TRUE AND NOT EXISTS((`person`)<-[:KNOWS]-(:Person))) AND ($filter.knows._to_null = TRUE AND NOT EXISTS((`person`)-[:KNOWS]->(:Person)))) RETURN `person` { .name } AS `person` ``` +### Deeply nested list filters containing differences +```graphql +{ person( filter: { company_in: [ { OR: [ { name: "Neo4j", employees: { name: "jane" } } ] }, { OR: [ { name: "Neo4j" } ] } ] } ) { name } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE ANY(_company_in IN $filter.company_in WHERE (_company_in.OR IS NULL OR ANY(_OR IN _company_in.OR WHERE (_OR.name IS NULL OR `company`.name = _OR.name) AND (_OR.employees IS NULL OR EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ALL(`person` IN [(`company`)<-[:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (_OR.employees.name IS NULL OR `person`.name = _OR.employees.name)))))))) RETURN `person` { .name } AS `person` +``` + +### Nested filter on relationship field +```graphql +{ person( filter: { name: "jane", company: { name: "Neo4j" } } ) { name company(filter: { name: "Neo4j", founded: { year: 2007 } }) { name } } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (`company`.name = $filter.company.name))) RETURN `person` { .name ,company: head([(`person`)-[:`WORKS_AT`]->(`person_company`:`Company`) WHERE (`person_company`.name = $1_filter.name) AND ((`person_company`.founded = datetime($1_filter.founded))) | person_company { .name }]) } AS `person` +``` + +### Nested filter on relationship type field +```graphql +{ person( filter: { name: "jane" }) { name employmentHistory( filter: { role: "Developer", Company: { name: "Neo4j" } }) { start { year } Company { name } } } } +``` +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { .name ,employmentHistory: [(`person`)-[`person_employmentHistory_relation`:`WORKED_AT`]->(:`Company`) WHERE (`person_employmentHistory_relation`.role = $1_filter.role) AND (ALL(`person_filter_company` IN [(`person`)-[`person_employmentHistory_relation`]->(`_company`:Company) | `_company`] WHERE (`person_filter_company`.name = $1_filter.Company.name))) | person_employmentHistory_relation {start: { year: `person_employmentHistory_relation`.start.year },Company: head([(:`Person`)-[`person_employmentHistory_relation`]->(`person_employmentHistory_Company`:`Company`) | person_employmentHistory_Company { .name }]) }] } AS `person` +``` + +### Nested filters on reflexive relationship type field +```graphql +{ person( filter: { name: "jane" }) { name knows { from( filter: { since: { year: 2018 }, Person: { name: "Joe" } } ) { since { year } Person { name } } to( filter: { since: { year: 2019 } Person: { name: "Jill" } } ) { since { year } Person { name } } } } } +``` ```cypher -MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND ALL(`company` IN [(`person`)-[`person_filter_company`:WORKS_AT]->(`_company`:Company) | `_company`] WHERE (EXISTS((`company`)<-[:WORKS_AT]-(:Person)) AND ANY(`person` IN [(`company`)<-[`company_filter_person`:WORKS_AT]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.company.employees_some.name))))) RETURN `person` { .name } AS `person` +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { .name ,knows: {from: [(`person`)<-[`person_from_relation`:`KNOWS`]-(`person_from`:`Person`) WHERE ((`person_from_relation`.since = datetime($1_filter.since))) AND (ALL(`person_filter_person` IN [(`person`)-[`person_from_relation`]->(`_person`:Person) | `_person`] WHERE (`person_filter_person`.name = $1_filter.Person.name))) | person_from_relation {since: { year: `person_from_relation`.since.year },Person: person_from { .name } }] ,to: [(`person`)-[`person_to_relation`:`KNOWS`]->(`person_to`:`Person`) WHERE ((`person_to_relation`.since = datetime($3_filter.since))) AND (ALL(`person_filter_person` IN [(`person`)-[`person_to_relation`]->(`_person`:Person) | `_person`] WHERE (`person_filter_person`.name = $3_filter.Person.name))) | person_to_relation {since: { year: `person_to_relation`.since.year },Person: person_to { .name } }] } } AS `person` ```