From 8f90abbb507598b3caa62bb08dd01be9438a3f8d Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:52:57 -0700 Subject: [PATCH 01/15] adds default selection set to buildFieldSelection used when adding selections for unselected ordered fields --- src/augment/ast.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/augment/ast.js b/src/augment/ast.js index 0ec87752..a61e23fa 100644 --- a/src/augment/ast.js +++ b/src/augment/ast.js @@ -209,7 +209,10 @@ export const buildFieldSelection = ({ args = [], directives = [], name = {}, - selectionSet = {} + selectionSet = { + kind: Kind.SELECTION_SET, + selections: [] + } }) => { return { kind: Kind.FIELD, From 3c19dcb67d659893e0e3cb44c104741e79177215 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:53:11 -0700 Subject: [PATCH 02/15] removes unused variable --- src/augment/fields.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/augment/fields.js b/src/augment/fields.js index 6e35551a..ff32005e 100644 --- a/src/augment/fields.js +++ b/src/augment/fields.js @@ -251,7 +251,6 @@ export const propertyFieldExists = ({ }) => { const fields = definition.fields || []; return fields.find(field => { - const fieldName = field.name.value; const fieldType = field.type; const unwrappedType = unwrapNamedType({ type: fieldType }); const outputType = unwrappedType.name; From 8f9cd92a101b0c07d90c7c11e58215b9943b63f0 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:54:54 -0700 Subject: [PATCH 03/15] adds selectUnselectedOrderedFields for selection independet ordering updates buildQueryFieldArguments to support appropriate type names for filtering vs ordering argument types --- src/augment/input-values.js | 64 ++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/augment/input-values.js b/src/augment/input-values.js index 21e7bb14..c3b8a282 100644 --- a/src/augment/input-values.js +++ b/src/augment/input-values.js @@ -5,7 +5,8 @@ import { buildInputValue, buildInputObjectType, buildEnumType, - buildEnumValue + buildEnumValue, + buildFieldSelection } from './ast'; import { isNeo4jTemporalType, @@ -97,6 +98,7 @@ export const buildQueryFieldArguments = ({ argumentMap = {}, fieldArguments, fieldDirectives, + typeName, outputType, outputTypeWrappers, isUnionType, @@ -167,6 +169,9 @@ export const buildQueryFieldArguments = ({ const argumentIndex = fieldArguments.findIndex( arg => arg.name.value === FilteringArgument.FILTER ); + if (typeName) { + outputType = `${typeName}${outputType}`; + } // Does overwrite if (argumentIndex === -1) { fieldArguments.push( @@ -439,9 +444,7 @@ export const buildFilters = ({ fieldName, fieldConfig, filterTypes = [] }) => { [TypeWrappers.LIST_TYPE]: true }; } else if (isPointDistanceFilter) { - fieldConfig.type.name = `${Neo4jTypeName}${ - SpatialType.POINT - }DistanceFilter`; + fieldConfig.type.name = `${Neo4jTypeName}${SpatialType.POINT}DistanceFilter`; } inputValues.push( buildInputValue({ @@ -462,3 +465,56 @@ export const buildFilters = ({ fieldName, fieldConfig, filterTypes = [] }) => { ] ); }; + +export const selectUnselectedOrderedFields = ({ + selectionFilters, + fieldSelectionSet +}) => { + let orderingArguments = selectionFilters['orderBy']; + const orderedFieldSelectionSet = []; + // cooerce to array if not provided as list + if (orderingArguments) { + // if a single ordering enum argument value is provided, + // cooerce back into an array + if (typeof orderingArguments === 'string') { + orderingArguments = [orderingArguments]; + } + orderedFieldSelectionSet.push(...fieldSelectionSet); + // add field selection AST for ordered fields if those fields are + // not selected, since apoc.coll.sortMulti requires data to sort + const orderedFieldNameMap = orderingArguments.reduce( + (uniqueFieldMap, orderingArg) => { + const fieldName = orderingArg.substring( + 0, + orderingArg.lastIndexOf('_') + ); + // prevent redundant selections + // ex: [datetime_asc, datetime_desc], if provided, would result + // in adding two selections for the datetime field + if (!uniqueFieldMap[fieldName]) uniqueFieldMap[fieldName] = true; + return uniqueFieldMap; + }, + {} + ); + const orderingArgumentFieldNames = Object.keys(orderedFieldNameMap); + orderingArgumentFieldNames.forEach(orderedFieldName => { + if ( + !fieldSelectionSet.some( + field => field.name && field.name.value === orderedFieldName + ) + ) { + // add the field so that its data can be used for ordering + // since as it is not actually selected, it will be removed + // by default GraphQL post-processing field resolvers + orderedFieldSelectionSet.push( + buildFieldSelection({ + name: buildName({ + name: orderedFieldName + }) + }) + ); + } + }); + } + return orderedFieldSelectionSet; +}; From 492c3765caf4365a3f934caceff26719962eea9b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:55:15 -0700 Subject: [PATCH 04/15] Update node.js --- src/augment/types/node/node.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js index 364ee53c..2e36fb39 100644 --- a/src/augment/types/node/node.js +++ b/src/augment/types/node/node.js @@ -197,12 +197,13 @@ export const augmentNodeTypeFields = ({ config }) => { let isIgnoredType = true; + let filterTypeName = `_${typeName}Filter`; + const fields = definition.fields; if (!isUnionType && !isUnionExtension) { - const fields = definition.fields; if (!isQueryType) { if (!nodeInputTypeMap[FilteringArgument.FILTER]) { nodeInputTypeMap[FilteringArgument.FILTER] = { - name: `_${typeName}Filter`, + name: filterTypeName, fields: [] }; } From 7d115e6e2351021b0d4144d4d7fc24007bc993db Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:57:11 -0700 Subject: [PATCH 05/15] updates for pagination and ordering on relationship types adds getTypeDefiningField to support appropriate translation of relationship type fields when querying incoming nodes of the .from field in a sub-selection of the .to field, or visa versa --- src/augment/types/relationship/query.js | 208 ++++++++++++++---------- 1 file changed, 124 insertions(+), 84 deletions(-) diff --git a/src/augment/types/relationship/query.js b/src/augment/types/relationship/query.js index 9eab5332..517593da 100644 --- a/src/augment/types/relationship/query.js +++ b/src/augment/types/relationship/query.js @@ -3,10 +3,13 @@ import { shouldAugmentRelationshipField } from '../../augment'; import { OperationType } from '../../types/types'; import { TypeWrappers, isListTypeField, unwrapNamedType } from '../../fields'; import { + PagingArgument, + OrderingArgument, FilteringArgument, buildFilters, buildQueryFieldArguments, - buildQueryFilteringInputType + buildQueryFilteringInputType, + buildQueryOrderingEnumType } from '../../input-values'; import { buildRelationDirective } from '../../directives'; import { @@ -24,8 +27,8 @@ import { isExternalTypeExtension } from '../../../federation'; * relationship type fields in the Query API */ const RelationshipQueryArgument = { - // ...PagingArgument, - // ...OrderingArgument, + ...PagingArgument, + ...OrderingArgument, ...FilteringArgument }; @@ -36,6 +39,7 @@ const RelationshipQueryArgument = { */ export const augmentRelationshipQueryAPI = ({ typeName, + definition, fieldArguments, fieldName, outputType, @@ -56,56 +60,57 @@ export const augmentRelationshipQueryAPI = ({ if ( shouldAugmentRelationshipField(config, queryTypeNameLower, fromType, toType) ) { + const [definingType, isImplementedField] = getTypeDefiningField({ + typeName, + definition, + fieldName, + typeDefinitionMap, + typeExtensionDefinitionMap + }); + if (isImplementedField) typeName = definingType; const relatedType = decideRelatedType({ typeName, + definition, fromType, toType }); - if ( - validateRelationTypeDirectedFields( - typeName, - fieldName, - fromType, - toType, - outputType - ) - ) { - [fieldType, generatedTypeMap] = transformRelationshipTypeFieldOutput({ - typeName, - relatedType, - fieldArguments, - fieldName, - outputType, - fromType, - toType, - generatedTypeMap, - outputTypeWrappers, - config, - relationshipName, - fieldType, - propertyOutputFields - }); - [ - fieldArguments, - generatedTypeMap, - nodeInputTypeMap - ] = augmentRelationshipTypeFieldInput({ - typeName, - relatedType, - fieldArguments, - fieldName, - outputType, - fromType, - toType, - typeDefinitionMap, - typeExtensionDefinitionMap, - generatedTypeMap, - nodeInputTypeMap, - relationshipInputTypeMap, - outputTypeWrappers, - config - }); - } + [fieldType, generatedTypeMap] = transformRelationshipTypeFieldOutput({ + typeName, + relatedType, + fieldArguments, + fieldName, + outputType, + fromType, + toType, + typeDefinitionMap, + generatedTypeMap, + outputTypeWrappers, + config, + relationshipName, + fieldType, + propertyOutputFields + }); + [ + fieldArguments, + generatedTypeMap, + nodeInputTypeMap + ] = augmentRelationshipTypeFieldInput({ + typeName, + relatedType, + fieldArguments, + fieldName, + isImplementedField, + outputType, + fromType, + toType, + typeDefinitionMap, + typeExtensionDefinitionMap, + generatedTypeMap, + nodeInputTypeMap, + relationshipInputTypeMap, + outputTypeWrappers, + config + }); } return [ fieldType, @@ -116,6 +121,44 @@ export const augmentRelationshipQueryAPI = ({ ]; }; +const getTypeDefiningField = ({ + typeName, + definition, + fieldName, + typeDefinitionMap, + typeExtensionDefinitionMap +}) => { + const definitionInterfaces = definition.interfaces || []; + const interfaces = [...definitionInterfaces]; + const typeExtensions = typeExtensionDefinitionMap[typeName] || []; + typeExtensions.forEach(extension => { + const extendedImplementations = extension.interfaces; + if (extendedImplementations && extendedImplementations.length) { + interfaces.push(...extendedImplementations); + } + }); + let definingType = typeName; + // field is defined by interface implemented by this type + let isImplementedField = false; + if (interfaces && interfaces.length) { + interfaces.forEach(namedType => { + const unwrappedType = unwrapNamedType({ type: namedType }); + const interfaceName = unwrappedType.name; + const interfaceTypes = []; + const typeDefinition = typeDefinitionMap[interfaceName]; + if (typeDefinition) interfaceTypes.push(typeDefinition); + const interfaceDefinesField = interfaceTypes.some(type => + type.fields.some(field => field.name.value === fieldName) + ); + if (interfaceDefinesField) { + isImplementedField = true; + definingType = interfaceName; + } + }); + } + return [definingType, isImplementedField]; +}; + /** * Given a relationship type field, builds the input value * definitions for its Query arguments, along with those needed @@ -127,6 +170,7 @@ const augmentRelationshipTypeFieldInput = ({ relatedType, fieldArguments, fieldName, + isImplementedField, outputType, fromType, toType, @@ -150,15 +194,13 @@ const augmentRelationshipTypeFieldInput = ({ typeExtensionDefinitionMap }) ) { - const nodeFilteringFields = - nodeInputTypeMap[FilteringArgument.FILTER].fields; let relationshipFilterTypeName = `_${typeName}${outputType[0].toUpperCase() + outputType.substr(1)}`; // Assume outgoing relationship if (fromType === toType) { relationshipFilterTypeName = `_${outputType}Directions`; } - nodeFilteringFields.push( + nodeInputTypeMap[FilteringArgument.FILTER].fields.push( ...buildRelationshipFilters({ typeName, fieldName, @@ -173,6 +215,7 @@ const augmentRelationshipTypeFieldInput = ({ typeName, fromType, toType, + isImplementedField, outputType, relatedType, relationshipFilterTypeName, @@ -194,6 +237,7 @@ const augmentRelationshipTypeFieldArguments = ({ typeName, fromType, toType, + isImplementedField, outputType, relatedType, relationshipFilterTypeName, @@ -206,21 +250,28 @@ const augmentRelationshipTypeFieldArguments = ({ fieldArguments = buildQueryFieldArguments({ argumentMap: RelationshipQueryArgument, fieldArguments, - outputType: `${typeName}${outputType}`, - outputTypeWrappers + typeName, + outputType, + outputTypeWrappers, + typeDefinitionMap }); } else { fieldArguments = []; } - generatedTypeMap = buildRelationshipSelectionArgumentInputTypes({ - fromType, - toType, - relatedType, - relationshipFilterTypeName, - generatedTypeMap, - relationshipInputTypeMap, - typeDefinitionMap - }); + if (!isImplementedField) { + // If this relationship type field is on an object type implementing an + // interface that defines it, then the argument input types and output type + // must be those used on that interface's field definition + generatedTypeMap = buildRelationshipSelectionArgumentInputTypes({ + fromType, + toType, + relatedType, + relationshipFilterTypeName, + generatedTypeMap, + relationshipInputTypeMap, + typeDefinitionMap + }); + } return [fieldArguments, generatedTypeMap]; }; @@ -238,13 +289,14 @@ const transformRelationshipTypeFieldOutput = ({ outputType, fromType, toType, + typeDefinitionMap, generatedTypeMap, outputTypeWrappers, relationshipName, fieldType, propertyOutputFields }) => { - const relationshipOutputName = `_${typeName}${fieldName[0].toUpperCase() + + let relationshipOutputName = `_${typeName}${fieldName[0].toUpperCase() + fieldName.substr(1)}`; const unwrappedType = unwrapNamedType({ type: fieldType }); if (fromType === toType) { @@ -267,6 +319,7 @@ const transformRelationshipTypeFieldOutput = ({ relationshipName, relatedType, propertyOutputFields, + typeDefinitionMap, generatedTypeMap }); return [fieldType, generatedTypeMap]; @@ -370,6 +423,7 @@ const buildRelationshipFieldOutputTypes = ({ relationshipName, relatedType, propertyOutputFields, + typeDefinitionMap, generatedTypeMap }) => { const relationTypeDirective = buildRelationDirective({ @@ -382,7 +436,8 @@ const buildRelationshipFieldOutputTypes = ({ argumentMap: RelationshipQueryArgument, fieldArguments, outputType, - outputTypeWrappers + outputTypeWrappers, + typeDefinitionMap }); const reflexiveOutputName = `${relationshipOutputName}Directions`; generatedTypeMap[reflexiveOutputName] = buildObjectType({ @@ -457,6 +512,11 @@ const buildRelationshipSelectionArgumentInputTypes = ({ generatedTypeMap, inputTypeMap: relationshipInputTypeMap }); + generatedTypeMap = buildQueryOrderingEnumType({ + nodeInputTypeMap: relationshipInputTypeMap, + typeDefinitionMap, + generatedTypeMap + }); return generatedTypeMap; }; @@ -502,23 +562,3 @@ const decideRelatedType = ({ typeName, fromType, toType }) => { } return relatedType; }; - -/** - * Validates that a given relationship type field on a node type - * has that node type as its 'from' or 'to' node type field - */ -const validateRelationTypeDirectedFields = ( - typeName, - fieldName, - fromName, - toName, - outputType -) => { - // directive to and from are not the same and neither are equal to this - if (fromName !== toName && toName !== typeName && fromName !== typeName) { - throw new Error( - `The ${fieldName} field on the ${typeName} node type uses the ${outputType} relationship type but ${outputType} comes from ${fromName} and goes to ${toName}` - ); - } - return true; -}; From 7cab92ea055314cd0f58b08ac5959fb57bf50170 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:57:29 -0700 Subject: [PATCH 06/15] updates for pagination and ordering on relationship types --- src/augment/types/relationship/relationship.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/augment/types/relationship/relationship.js b/src/augment/types/relationship/relationship.js index 582ed000..de072a43 100644 --- a/src/augment/types/relationship/relationship.js +++ b/src/augment/types/relationship/relationship.js @@ -7,6 +7,7 @@ import { toSnakeCase } from '../../fields'; import { + OrderingArgument, FilteringArgument, augmentInputTypePropertyFields } from '../../input-values'; @@ -82,6 +83,7 @@ export const augmentRelationshipTypeField = ({ nodeInputTypeMap ] = augmentRelationshipQueryAPI({ typeName, + definition, fieldArguments, fieldName, outputType, @@ -151,13 +153,19 @@ const augmentRelationshipTypeFields = ({ name: RelationshipDirectionField.TO }); let relatedTypeFilterName = `_${typeName}${outputType}Filter`; + let relatedTypeOrderingName = `_${outputType}Ordering`; if (fromTypeName === toTypeName) { relatedTypeFilterName = `_${outputType}Filter`; + relatedTypeOrderingName = `_${outputType}Ordering`; } let relationshipInputTypeMap = { [FilteringArgument.FILTER]: { name: relatedTypeFilterName, fields: [] + }, + [OrderingArgument.ORDER_BY]: { + name: relatedTypeOrderingName, + values: [] } }; const propertyInputValues = []; From 7824cb7f59291a2bf740d2b419f0b8ad5f7f0dd5 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:57:40 -0700 Subject: [PATCH 07/15] removes comments --- src/augment/types/types.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/augment/types/types.js b/src/augment/types/types.js index 76c38bc1..29e0b37d 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -87,7 +87,6 @@ export const Neo4jDataType = { [TemporalType.LOCALDATETIME]: 'Temporal', [SpatialType.POINT]: 'Spatial' }, - // TODO probably revise and remove... STRUCTURAL: { [Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType, [Kind.INTERFACE_TYPE_DEFINITION]: Neo4jStructuralType, @@ -689,9 +688,6 @@ const augmentOperationType = ({ propertyInputValues, config }); - // FIXME fieldArguments are modified through reference so - // this branch doesn't end up mattereing. A case of isIgnoredType - // being true may also be highly improbable, though it is posisble if (!isIgnoredType) { extension.fields = propertyOutputFields; } From 8dffd75a6ed63e5eb89d870d5c3a30c415ccc5d4 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 21:59:32 -0700 Subject: [PATCH 08/15] uses selectUnselectedOrderedFields to support selection independent ordering also updates function arguments for the @relation field and type translation functions for use in support for ordering --- src/selections.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/selections.js b/src/selections.js index dd2ef65b..36ce83d3 100644 --- a/src/selections.js +++ b/src/selections.js @@ -39,6 +39,7 @@ import { TypeWrappers, Neo4jSystemIDField } from './augment/fields'; +import { selectUnselectedOrderedFields } from './augment/input-values'; export function buildCypherSelection({ initial = '', @@ -290,13 +291,23 @@ export function buildCypherSelection({ fieldName, parentSelectionInfo }); + const fieldSelectionSet = headSelection && headSelection.selectionSet ? headSelection.selectionSet.selections : []; + const orderedFieldSelectionSet = selectUnselectedOrderedFields({ + selectionFilters, + fieldSelectionSet + }); + + const fieldsForTranslation = orderedFieldSelectionSet.length + ? orderedFieldSelectionSet + : fieldSelectionSet; + subSelection = recurse({ - selections: fieldSelectionSet, + selections: fieldsForTranslation, variableName: nestedVariable, paramIndex, schemaType: innerSchemaType, @@ -406,6 +417,7 @@ export function buildCypherSelection({ initial, fieldName, fieldType, + fieldSelectionSet, variableName, relDirection, relType, @@ -422,7 +434,7 @@ export function buildCypherSelection({ filterParams, selectionFilters, neo4jTypeArgs, - selections, + fieldsForTranslation, schemaType, subSelection, skipLimit, @@ -436,6 +448,7 @@ export function buildCypherSelection({ // (from, to, renamed, relation mutation payloads...) [translationConfig, subSelection] = nodeTypeFieldOnRelationType({ initial, + schemaType, fieldName, fieldType, variableName, @@ -449,6 +462,8 @@ export function buildCypherSelection({ neo4jTypeArgs, schemaTypeRelation, innerSchemaType, + fieldSelectionSet, + fieldsForTranslation, schemaTypeFields, derivedTypeMap, isObjectTypeField, @@ -469,12 +484,14 @@ export function buildCypherSelection({ innerSchemaTypeRelation, initial, fieldName, + fieldSelectionSet, subSelection, skipLimit, commaIfTail, tailParams, fieldType, variableName, + fieldsForTranslation, schemaType, innerSchemaType, nestedVariable, From 1daeb49a5fef2dcde20c5070d9d5c59a26a276c7 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:03:30 -0700 Subject: [PATCH 09/15] translates ordering arguments and interfaces for relationship types updates decideRelationFilterMetadata to better determine the direction of the relationship path pattern used when filtering relationship type fields --- src/translate.js | 496 +++++++++++++++++++++++++++++++---------------- 1 file changed, 326 insertions(+), 170 deletions(-) diff --git a/src/translate.js b/src/translate.js index 362c33db..4aec173d 100644 --- a/src/translate.js +++ b/src/translate.js @@ -51,8 +51,7 @@ import { isScalarType, isEnumType, isObjectType, - isInterfaceType, - Kind + isInterfaceType } from 'graphql'; import { buildCypherSelection, @@ -63,10 +62,7 @@ import { } from './selections'; import _ from 'lodash'; import neo4j from 'neo4j-driver'; -import { - isUnionTypeDefinition, - isUnionTypeExtensionDefinition -} from './augment/types/types'; +import { isUnionTypeDefinition } from './augment/types/types'; import { getFederatedOperationData, setCompoundKeyFilter, @@ -178,6 +174,7 @@ export const relationFieldOnNodeType = ({ initial, fieldName, fieldType, + fieldSelectionSet, variableName, relDirection, relType, @@ -194,7 +191,7 @@ export const relationFieldOnNodeType = ({ filterParams, selectionFilters, neo4jTypeArgs, - selections, + fieldsForTranslation, schemaType, subSelection, skipLimit, @@ -265,17 +262,29 @@ export const relationFieldOnNodeType = ({ ...arrayPredicates, ...filterPredicates ].filter(predicate => !!predicate); + const orderByParam = filterParams['orderBy']; - const temporalOrdering = temporalOrderingFieldExists( + + const usesTemporalOrdering = temporalOrderingFieldExists( schemaType, filterParams ); + const selectedFieldNames = fieldSelectionSet.reduce((fieldNames, field) => { + if (field.name) fieldNames.push(field.name.value); + return fieldNames; + }, []); + const neo4jTypeFieldSelections = buildOrderedNeo4jTypeSelections({ + schemaType: innerSchemaType, + selections: fieldsForTranslation, + usesTemporalOrdering, + selectedFieldNames + }); tailParams.initial = `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' }${ orderByParam - ? temporalOrdering + ? usesTemporalOrdering ? `[sortedElement IN apoc.coll.sortMulti(` : `apoc.coll.sortMulti(` : '' @@ -298,11 +307,10 @@ export const relationFieldOnNodeType = ({ } | ${mapProjection}]${ orderByParam ? `, [${buildSortMultiArgs(orderByParam)}])${ - temporalOrdering - ? ` | sortedElement { .*, ${neo4jTypeOrderingClauses( - selections, - innerSchemaType - )}}]` + usesTemporalOrdering + ? ` | sortedElement { .* ${ + neo4jTypeFieldSelections ? `, ${neo4jTypeFieldSelections}` : '' + }}]` : `` }` : '' @@ -314,12 +322,14 @@ export const relationTypeFieldOnNodeType = ({ innerSchemaTypeRelation, initial, fieldName, + fieldSelectionSet, subSelection, skipLimit, commaIfTail, tailParams, fieldType, variableName, + fieldsForTranslation, schemaType, innerSchemaType, nestedVariable, @@ -332,66 +342,112 @@ export const relationTypeFieldOnNodeType = ({ fieldArgs, cypherParams }) => { + let translation = ''; if (innerSchemaTypeRelation.from === innerSchemaTypeRelation.to) { - tailParams.initial = `${initial}${fieldName}: {${subSelection[0]}}${skipLimit} ${commaIfTail}`; - return [tailParams, subSelection]; - } - const relationshipVariableName = `${nestedVariable}_relation`; - const neo4jTypeClauses = neo4jTypePredicateClauses( - filterParams, - relationshipVariableName, - neo4jTypeArgs - ); - 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 && - typeof serializedFilterParam[filterParamKey] !== 'undefined' - ) { - subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; - } + translation = `${initial}${fieldName}: {${subSelection[0]}}${skipLimit} ${commaIfTail}`; + } else { + const relationshipVariableName = `${nestedVariable}_relation`; + + const neo4jTypeClauses = neo4jTypePredicateClauses( + filterParams, + relationshipVariableName, + neo4jTypeArgs + ); + const [filterPredicates, serializedFilterParam] = processFilterArgument({ + fieldArgs, + parentSchemaType: schemaType, + 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 && + typeof serializedFilterParam[filterParamKey] !== 'undefined' + ) { + subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; + } + const whereClauses = [...neo4jTypeClauses, ...filterPredicates]; - const whereClauses = [...neo4jTypeClauses, ...filterPredicates]; + const orderByParam = filterParams['orderBy']; + const usesTemporalOrdering = temporalOrderingFieldExists( + innerSchemaType, + filterParams + ); + const selectedFieldNames = fieldSelectionSet.reduce((fieldNames, field) => { + if (field.name) fieldNames.push(field.name.value); + return fieldNames; + }, []); + const neo4jTypeFieldSelections = buildOrderedNeo4jTypeSelections({ + schemaType: innerSchemaType, + selections: fieldsForTranslation, + usesTemporalOrdering, + selectedFieldNames + }); - tailParams.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, - ...getAdditionalLabels( - resolveInfo.schema.getType(innerSchemaTypeRelation.to), - cypherParams - ) - ] - : [ - innerSchemaTypeRelation.from, - ...getAdditionalLabels( - resolveInfo.schema.getType(innerSchemaTypeRelation.from), - cypherParams - ) - ] - )}) ${ - whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')} ` : '' - }| ${relationshipVariableName} {${subSelection[0]}}]${ - !isArrayType(fieldType) ? ')' : '' - }${skipLimit} ${commaIfTail}`; + const lhsOrdering = `${ + orderByParam + ? usesTemporalOrdering + ? `[sortedElement IN apoc.coll.sortMulti(` + : `apoc.coll.sortMulti(` + : '' + }`; + + const rhsOrdering = `${ + orderByParam + ? `, [${buildSortMultiArgs(orderByParam)}])${ + usesTemporalOrdering + ? ` | sortedElement { .* ${ + neo4jTypeFieldSelections + ? `, ${neo4jTypeFieldSelections}` + : '' + }}]` + : `` + }` + : '' + }`; + + const incomingNodeTypeName = innerSchemaTypeRelation.from; + const outgoingNodeTypeName = innerSchemaTypeRelation.to; + const innerSchemaTypeFields = innerSchemaType.getFields(); + const selectsIncomingField = innerSchemaTypeFields[incomingNodeTypeName]; + const selectsOutgoingField = innerSchemaTypeFields[outgoingNodeTypeName]; + + translation = `${initial}${fieldName}: ${ + !isArrayType(fieldType) ? 'head(' : '' + }${lhsOrdering}[(${safeVar(variableName)})${ + // if its fromField -- is this logically equivalent? + selectsIncomingField ? '<' : '' + }-[${safeVar(relationshipVariableName)}:${safeLabel( + innerSchemaTypeRelation.name + )}${queryParams}]-${selectsOutgoingField ? '>' : ''}(:${safeLabel( + selectsOutgoingField + ? [ + outgoingNodeTypeName, + ...getAdditionalLabels( + resolveInfo.schema.getType(outgoingNodeTypeName), + cypherParams + ) + ] + : [ + incomingNodeTypeName, + ...getAdditionalLabels( + resolveInfo.schema.getType(incomingNodeTypeName), + cypherParams + ) + ] + )}) ${ + whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')} ` : '' + }| ${relationshipVariableName} {${subSelection[0]}}]${rhsOrdering}${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`; + } + tailParams.initial = translation; return [tailParams, subSelection]; }; @@ -410,6 +466,8 @@ export const nodeTypeFieldOnRelationType = ({ neo4jTypeArgs, schemaTypeRelation, innerSchemaType, + fieldSelectionSet, + fieldsForTranslation, schemaTypeFields, derivedTypeMap, isObjectTypeField, @@ -468,12 +526,19 @@ export const nodeTypeFieldOnRelationType = ({ tailParams, schemaTypeRelation, innerSchemaType, + fieldSelectionSet, + fieldsForTranslation, + usesFragments, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, filterParams, neo4jTypeArgs, paramIndex, resolveInfo, selectionFilters, + schemaTypeFields, + derivedTypeMap, fieldArgs, cypherParams }); @@ -496,7 +561,6 @@ const relationTypeMutationPayloadField = ({ }; }; -// TODO refactor const directedNodeTypeFieldOnRelationType = ({ initial, fieldName, @@ -510,12 +574,19 @@ const directedNodeTypeFieldOnRelationType = ({ tailParams, schemaTypeRelation, innerSchemaType, + fieldSelectionSet, + fieldsForTranslation, + usesFragments, + isObjectTypeField, isInterfaceTypeField, + isUnionTypeField, filterParams, neo4jTypeArgs, paramIndex, resolveInfo, selectionFilters, + schemaTypeFields, + derivedTypeMap, fieldArgs, cypherParams }) => { @@ -524,6 +595,19 @@ const directedNodeTypeFieldOnRelationType = ({ const toTypeName = schemaTypeRelation.to; const isFromField = fieldName === fromTypeName || fieldName === 'from'; const isToField = fieldName === toTypeName || fieldName === 'to'; + const safeVariableName = nestedVariable; + const [mapProjection, labelPredicate] = buildMapProjection({ + schemaType: innerSchemaType, + isObjectType: isObjectTypeField, + isInterfaceType: isInterfaceTypeField, + isUnionType: isUnionTypeField, + usesFragments, + safeVariableName, + subQuery: subSelection[0], + schemaTypeFields, + derivedTypeMap, + resolveInfo + }); // Since the translations are significantly different, // we first check whether the relationship is reflexive if (fromTypeName === toTypeName) { @@ -531,6 +615,47 @@ const directedNodeTypeFieldOnRelationType = ({ isFromField ? 'from' : 'to' }_relation`; if (isRelationTypeDirectedField(fieldName)) { + const orderByParam = filterParams['orderBy']; + const usesTemporalOrdering = temporalOrderingFieldExists( + innerSchemaType, + filterParams + ); + const selectedFieldNames = fieldSelectionSet.reduce( + (fieldNames, field) => { + if (field.name) fieldNames.push(field.name.value); + return fieldNames; + }, + [] + ); + const neo4jTypeFieldSelections = buildOrderedNeo4jTypeSelections({ + schemaType: innerSchemaType, + selections: fieldsForTranslation, + usesTemporalOrdering, + selectedFieldNames + }); + + const lhsOrdering = `${ + orderByParam + ? usesTemporalOrdering + ? `[sortedElement IN apoc.coll.sortMulti(` + : `apoc.coll.sortMulti(` + : '' + }`; + + const rhsOrdering = `${ + orderByParam + ? `, [${buildSortMultiArgs(orderByParam)}])${ + usesTemporalOrdering + ? ` | sortedElement { .* ${ + neo4jTypeFieldSelections + ? `, ${neo4jTypeFieldSelections}` + : '' + }}]` + : `` + }` + : '' + }`; + const temporalFieldRelationshipVariableName = `${nestedVariable}_relation`; const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, @@ -549,6 +674,7 @@ const directedNodeTypeFieldOnRelationType = ({ const filterParamKey = `${tailParams.paramIndex}_filter`; const fieldArgumentParams = subSelection[1]; const filterParam = fieldArgumentParams[filterParamKey]; + if ( filterParam && typeof serializedFilterParam[filterParamKey] !== 'undefined' @@ -556,41 +682,36 @@ const directedNodeTypeFieldOnRelationType = ({ subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; } const whereClauses = [...neo4jTypeClauses, ...filterPredicates]; + tailParams.initial = `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' - }[(${safeVar(variableName)})${isFromField ? '<' : ''}-[${safeVar( - relationshipVariableName - )}:${safeLabel(relType)}${queryParams}]-${isToField ? '>' : ''}(${safeVar( + }${lhsOrdering}[(${safeVar(variableName)})${ + isFromField ? '<' : '' + }-[${safeVar(relationshipVariableName)}:${safeLabel( + relType + )}${queryParams}]-${isToField ? '>' : ''}(${safeVar( nestedVariable - )}${ - !isInterfaceTypeField - ? `:${safeLabel([ - fromTypeName, - ...getAdditionalLabels( - resolveInfo.schema.getType(fromTypeName), - cypherParams - ) - ])}` - : '' - }) ${ + )}:${safeLabel([ + fromTypeName, + ...getAdditionalLabels( + resolveInfo.schema.getType(fromTypeName), + cypherParams + ) + ])}) ${ whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')} ` : '' - }| ${relationshipVariableName} {${ - // TODO switch to using buildMapProjection to support fragments - isInterfaceTypeField - ? `${fragmentType(nestedVariable, innerSchemaType.name)}${ - subSelection[0] ? `, ${subSelection[0]}` : '' - }` - : subSelection[0] - }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`; + }| ${relationshipVariableName} {${subSelection[0]}}]${rhsOrdering}${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`; return [tailParams, subSelection]; } else { - tailParams.initial = `${initial}${fieldName}: ${variableName} {${subSelection[0]}}${skipLimit} ${commaIfTail}`; // Case of a renamed directed field // e.g., 'from: Movie' -> 'Movie: Movie' + tailParams.initial = `${initial}${fieldName}: ${mapProjection}${skipLimit} ${commaIfTail}`; return [tailParams, subSelection]; } } else { - variableName = variableName + '_relation'; + let whereClauses = [labelPredicate].filter(predicate => !!predicate); + const safeRelationshipVar = safeVar(`${variableName}_relation`); tailParams.initial = `${initial}${fieldName}: ${ !isArrayType(fieldType) ? 'head(' : '' }[(:${safeLabel( @@ -609,26 +730,23 @@ const directedNodeTypeFieldOnRelationType = ({ cypherParams ) ] - )})${isFromField ? '<' : ''}-[${safeVar(variableName)}]-${ - isToField ? '>' : '' - }(${safeVar(nestedVariable)}:${ - !isInterfaceTypeField - ? safeLabel([ - innerSchemaType.name, - ...getAdditionalLabels( - resolveInfo.schema.getType(innerSchemaType.name), - cypherParams - ) - ]) - : '' - }${queryParams}) | ${nestedVariable} {${ - // TODO switch to using buildMapProjection to support fragments - isInterfaceTypeField - ? `${fragmentType(nestedVariable, innerSchemaType.name)}${ - subSelection[0] ? `, ${subSelection[0]}` : '' + )})${ + isUnionTypeField + ? `--` + : `${isFromField ? '<' : ''}-[${safeRelationshipVar}]-${ + isToField ? '>' : '' }` - : subSelection[0] - }}]${!isArrayType(fieldType) ? ')' : ''}${skipLimit} ${commaIfTail}`; + }(${safeVar(nestedVariable)}:${safeLabel([ + innerSchemaType.name, + ...getAdditionalLabels( + resolveInfo.schema.getType(innerSchemaType.name), + cypherParams + ) + ])}${queryParams})${ + whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '' + } | ${mapProjection}]${ + !isArrayType(fieldType) ? ')' : '' + }${skipLimit} ${commaIfTail}`; return [tailParams, subSelection]; } }; @@ -710,9 +828,16 @@ export const neo4jType = ({ const parentVariableName = parentSelectionInfo.variableName; const parentFilterParams = parentSelectionInfo.filterParams; const parentSchemaType = parentSelectionInfo.schemaType; - const safeVariableName = safeVar(variableName); const relationshipVariableSuffix = `relation`; let fieldIsArray = isArrayType(fieldType); + const isOrderedForNodeType = temporalOrderingFieldExists( + parentSchemaType, + parentFilterParams + ); + const isOrderedForRelationshipType = temporalOrderingFieldExists( + schemaType, + parentFilterParams + ); if (!isNodeType(schemaType.astNode)) { if ( isRelationTypePayload(schemaType) && @@ -732,15 +857,22 @@ export const neo4jType = ({ variableName = `${variableName}_${relationshipVariableSuffix}`; } } else { - variableName = `${nestedVariable}_${relationshipVariableSuffix}`; + if (isOrderedForRelationshipType) { + variableName = `${variableName}_${relationshipVariableSuffix}`; + } else { + variableName = `${nestedVariable}_${relationshipVariableSuffix}`; + } } } } + const safeVariableName = safeVar(variableName); + const usesTemporalOrdering = + isOrderedForNodeType || isOrderedForRelationshipType; return { initial: `${initial}${fieldName}: ${ fieldIsArray ? `reduce(a = [], INSTANCE IN ${variableName}.${fieldName} | a + {${subSelection[0]}})${commaIfTail}` - : temporalOrderingFieldExists(parentSchemaType, parentFilterParams) + : usesTemporalOrdering ? `${safeVariableName}.${fieldName}${commaIfTail}` : `{${subSelection[0]}}${commaIfTail}` }`, @@ -839,7 +971,6 @@ export const translateQuery = ({ }); const hasOnlySchemaTypeFragments = schemaTypeFields.length > 0 && Object.keys(derivedTypeMap).length === 0; - // TODO refactor if (hasOnlySchemaTypeFragments) usesFragments = false; if (queryTypeCypherDirective) { return customQuery({ @@ -947,7 +1078,6 @@ const buildTypeCompositionPredicate = ({ // Otherwise, use only those types provided in fragments derivedTypes = Object.keys(derivedTypeMap); } - // TODO refactor above branch now that more specific branching was needed const typeSelectionPredicates = derivedTypes.map(selectedType => { return `"${selectedType}" IN labels(${safeVariableName})`; }); @@ -1015,7 +1145,6 @@ const customQuery = ({ const isNeo4jTypeOutput = isNeo4jType(schemaType.name); const { cypherPart: orderByClause } = orderByValue; // Don't add subQuery for scalar type payloads - // FIXME: fix subselection translation for temporal type payload const isScalarPayload = isNeo4jTypeOutput || isScalarType; const fragmentTypeParams = derivedTypesParams({ isInterfaceType, @@ -1452,7 +1581,6 @@ const customMutation = ({ resolveInfo }); let query = ''; - // TODO refactor if (labelPredicate) { query = `CALL apoc.cypher.doIt("${ cypherQueryArg.value.value @@ -2040,46 +2168,55 @@ const nodeMergeOrUpdate = ({ return [query, params]; }; -const neo4jTypeOrderingClauses = (selections, innerSchemaType) => { - const selectedTypes = - selections && selections[0] && selections[0].selectionSet - ? selections[0].selectionSet.selections - : []; - return selectedTypes - .reduce((temporalTypeFields, innerSelection) => { - // name of temporal type field - const fieldName = innerSelection.name.value; - const fieldTypeName = getFieldTypeName(innerSchemaType, fieldName); - if (isTemporalType(fieldTypeName)) { - const innerSelectedTypes = innerSelection.selectionSet - ? innerSelection.selectionSet.selections - : []; - temporalTypeFields.push( - `${fieldName}: {${innerSelectedTypes - .reduce((temporalSubFields, t) => { - // temporal type subfields, year, minute, etc. - const subFieldName = t.name.value; - if (subFieldName === 'formatted') { - temporalSubFields.push( - `${subFieldName}: toString(sortedElement.${fieldName})` - ); - } else { - temporalSubFields.push( - `${subFieldName}: sortedElement.${fieldName}.${subFieldName}` - ); - } - return temporalSubFields; - }, []) - .join(',')}}` +const buildOrderedNeo4jTypeSelections = ({ + schemaType, + selections, + usesTemporalOrdering, + selectedFieldNames +}) => { + let neo4jTypeSelections = ''; + if (usesTemporalOrdering) { + neo4jTypeSelections = selections + .reduce((temporalTypeFields, innerSelection) => { + // name of temporal type field + const fieldName = innerSelection.name.value; + const fieldTypeName = getFieldTypeName(schemaType, fieldName); + const fieldIsSelected = selectedFieldNames.some( + name => name === fieldName ); - } - return temporalTypeFields; - }, []) - .join(','); + const isTemporalTypeField = isTemporalType(fieldTypeName); + if (isTemporalTypeField && fieldIsSelected) { + const innerSelectedTypes = innerSelection.selectionSet + ? innerSelection.selectionSet.selections + : []; + + temporalTypeFields.push( + `${fieldName}: {${innerSelectedTypes + .reduce((temporalSubFields, t) => { + // temporal type subfields, year, minute, etc. + const subFieldName = t.name.value; + if (subFieldName === 'formatted') { + temporalSubFields.push( + `${subFieldName}: toString(sortedElement.${fieldName})` + ); + } else { + temporalSubFields.push( + `${subFieldName}: sortedElement.${fieldName}.${subFieldName}` + ); + } + return temporalSubFields; + }, []) + .join(',')}}` + ); + } + return temporalTypeFields; + }, []) + .join(','); + } + return neo4jTypeSelections; }; const getFieldTypeName = (schemaType, fieldName) => { - // TODO handle for fragments? const field = schemaType && fieldName ? schemaType.getFields()[fieldName] : undefined; return field ? field.type.name : ''; @@ -2119,6 +2256,7 @@ const processFilterArgument = ({ resolveInfo, params, paramIndex, + parentSchemaType, rootIsRelationType = false }) => { const filterArg = fieldArgs.find(e => e.name.value === 'filter'); @@ -2146,6 +2284,7 @@ const processFilterArgument = ({ rootIsRelationType, variableName, schemaType, + parentSchemaType, schema }); params = { @@ -2436,6 +2575,7 @@ const translateFilterArguments = ({ variableName, rootIsRelationType, schemaType, + parentSchemaType, schema }) => { return Object.entries(filterFieldMap).reduce( @@ -2449,6 +2589,7 @@ const translateFilterArguments = ({ rootIsRelationType, variableName, schemaType, + parentSchemaType, schema }); if (translation) { @@ -2469,8 +2610,8 @@ const translateFilterArgument = ({ rootIsRelationType, variableName, filterParam, - parentSchemaType, schemaType, + parentSchemaType, schema }) => { // parse field name into prefix (ex: name, company) and @@ -2725,12 +2866,12 @@ const translateInputFilter = ({ fieldName, filterParam, schema, - parentSchemaType, schemaType, parameterPath, + nullFieldPredicate, + parentSchemaType, parentParamPath, - parentFieldName, - nullFieldPredicate + parentFieldName }) => { if (fieldName === 'AND' || fieldName === 'OR') { return translateLogicalFilter({ @@ -2764,6 +2905,7 @@ const translateInputFilter = ({ isReflexiveTypeDirectedField ] = decideRelationFilterMetadata({ fieldName, + parentSchemaType, schemaType, variableName, innerSchemaType, @@ -2970,6 +3112,7 @@ const translateRelationFilter = ({ const decideRelationFilterMetadata = ({ fieldName, + parentSchemaType, schemaType, variableName, innerSchemaType, @@ -3014,6 +3157,16 @@ const decideRelationFilterMetadata = ({ } else if (fieldName === 'to') { isReflexiveTypeDirectedField = true; } + } else if (thisType !== relatedType) { + const filteredType = schemaType && schemaType.name ? schemaType.name : ''; + if (filteredType === relatedType) { + // then a filter argument for the incoming direction is being used + // when querying the node type it goes out from + const temp = thisType; + thisType = relatedType; + relatedType = temp; + relDirection = 'IN'; + } } } else if (relationTypeDirective) { isRelationTypeNode = true; @@ -3023,15 +3176,20 @@ const decideRelationFilterMetadata = ({ relDirection = 'OUT'; // if not a reflexive relationship type if (thisType !== relatedType) { + // When buildFilterPredicates is used in buildRelationPredicate, + // parentSchemaType is provided and used here to decide whether + // to filter the incoming or outgoing node type const filteredType = - innerSchemaType && innerSchemaType.name ? innerSchemaType.name : ''; - // then the connecting node type field on a relationship type filter + parentSchemaType && parentSchemaType.name ? parentSchemaType.name : ''; + // the connecting node type field on a relationship type filter // may be incoming or outgoing; thisType could be .from or .to - if (filteredType === thisType) { - // then a filter argument for the incoming direction is being used - // when querying the node type it goes out from + if (filteredType === relatedType) { + // then this filter argument is being used on a field of the node type + // the relationship goes .to, so we need to filter for the node types + // it comes .from + const temp = thisType; thisType = relatedType; - relatedType = filteredType; + relatedType = temp; relDirection = 'IN'; } } @@ -3051,7 +3209,6 @@ const decideRelationFilterMetadata = ({ const buildRelationPredicate = ({ rootIsRelationType, - parentFieldName, isRelationType, isReflexiveRelationType, isReflexiveTypeDirectedField, @@ -3205,7 +3362,6 @@ const buildRelatedTypeListComprehension = ({ if (rootIsRelationType) { relationVariable = variableName; } - const thisTypeVariable = !rootIsRelationType && !isRelationTypeNode ? safeVar(lowFirstLetter(variableName)) From 6090c4a5408db64d4dc6aa23e274479e81176e3a Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:03:37 -0700 Subject: [PATCH 10/15] Update cypherTestHelpers.js --- test/helpers/cypherTestHelpers.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 2e3e44b7..1f867b11 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -165,8 +165,10 @@ export function augmentedSchemaCypherTestRunner( const resolvers = { QueryA: { + Person: checkCypherQuery, Actor: checkCypherQuery, User: checkCypherQuery, + Genre: checkCypherQuery, Movie: checkCypherQuery, MoviesByYear: checkCypherQuery, MoviesByYears: checkCypherQuery, @@ -188,6 +190,7 @@ export function augmentedSchemaCypherTestRunner( State: checkCypherQuery, CasedType: checkCypherQuery, Camera: checkCypherQuery, + NewCamera: checkCypherQuery, CustomCameras: checkCypherQuery, CustomCamera: checkCypherQuery, computedBoolean: checkCypherQuery, @@ -240,7 +243,12 @@ export function augmentedSchemaCypherTestRunner( CustomCamera: checkCypherMutation, CustomCameras: checkCypherMutation, CreateNewCamera: checkCypherMutation, - computedMovieSearch: checkCypherMutation + computedMovieSearch: checkCypherMutation, + AddActorInterfacedRelationshipType: checkCypherMutation, + RemoveActorInterfacedRelationshipType: checkCypherMutation, + MergeActorInterfacedRelationshipType: checkCypherMutation, + UpdateActorInterfacedRelationshipType: checkCypherMutation, + MergeGenreInterfacedRelationshipType: checkCypherMutation } }; From d46653106aed88c19b4c6c03a8fcaa2ee2319b09 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:03:43 -0700 Subject: [PATCH 11/15] Update filterTck.md --- test/helpers/tck/filterTck.md | 89 +++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/test/helpers/tck/filterTck.md b/test/helpers/tck/filterTck.md index 6000ba5c..64f39ad5 100644 --- a/test/helpers/tck/filterTck.md +++ b/test/helpers/tck/filterTck.md @@ -1617,7 +1617,7 @@ MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND 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.year IS NULL OR `person_filter_company`.start.year = _AND.start.year))) AND (((_AND.end.year IS NULL OR `person_filter_company`.end.year = _AND.end.year))))))) RETURN `person` { .name } AS `person` ``` -### Related node does NOT exist (relationship type) +### Relationship type outgoing node does NOT exist ```graphql { @@ -1631,7 +1631,7 @@ MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKED_AT]->(:Company)) AND 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) +### Relationship type outgoing node exists ```graphql { @@ -2350,7 +2350,7 @@ MATCH (`person`:`Person`) WHERE (EXISTS((`person`)-[:WORKS_AT]->(:Company)) AND 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.year = $1_filter.founded.year))) | `person_company` { .name }]) } AS `person` ``` -### Nested filter on relationship type field (filter outgoing) +### Relationship type field filtering outgoing nodes ```graphql { @@ -2374,7 +2374,7 @@ MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (EXISTS((`per 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 filter on relationship type field (filter incoming) +### Relationship type field filtering incoming nodes ```graphql { @@ -2396,6 +2396,87 @@ MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) RETURN `person` { MATCH (`company`:`Company`) WHERE (`company`.name = $filter.name) RETURN `company` { .name ,employeeHistory: [(`company`)<-[`company_employeeHistory_relation`:`WORKED_AT`]-(:`Person`) WHERE (`company_employeeHistory_relation`.role = $1_filter.role) AND (ALL(`company_filter_person` IN [(`company`)<-[`company_employeeHistory_relation`]-(`_person`:Person) | `_person`] WHERE (`company_filter_person`.name = $1_filter.Person.name))) | company_employeeHistory_relation {start: { year: `company_employeeHistory_relation`.start.year },Person: head([(:`Company`)<-[`company_employeeHistory_relation`]-(`company_employeeHistory_Person`:`Person`) | company_employeeHistory_Person { .name }]) }] } AS `company` ``` +### Nested filter for relationship type outgoing nodes + +```graphql +{ + person( + filter: { + name: "jane" + employmentHistory: { role: "Developer", Company: { name: "Neo4j" } } + } + ) { + name + employmentHistory { + start { + year + } + Company { + name + } + } + } +} +``` + +```cypher +MATCH (`person`:`Person`) WHERE (`person`.name = $filter.name) AND (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 ,employmentHistory: [(`person`)-[`person_employmentHistory_relation`:`WORKED_AT`]->(:`Company`) | 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 filter for relationship type incoming nodes + +```graphql +{ + Company( + filter: { + name: "Neo4j" + employeeHistory: { role: "Developer", Person: { name: "jane" } } + } + ) { + name + employeeHistory { + start { + year + } + Person { + name + } + } + } +} +``` + +```cypher +MATCH (`company`:`Company`) WHERE (`company`.name = $filter.name) AND (EXISTS((`company`)<-[:WORKED_AT]-(:Person)) AND ALL(`company_filter_person` IN [(`company`)<-[`_company_filter_person`:WORKED_AT]-(:Person) | `_company_filter_person`] WHERE (`company_filter_person`.role = $filter.employeeHistory.role) AND (ALL(`person` IN [(`company`)<-[`company_filter_person`]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employeeHistory.Person.name))))) RETURN `company` { .name ,employeeHistory: [(`company`)<-[`company_employeeHistory_relation`:`WORKED_AT`]-(:`Person`) | company_employeeHistory_relation {start: { year: `company_employeeHistory_relation`.start.year },Person: head([(:`Company`)<-[`company_employeeHistory_relation`]-(`company_employeeHistory_Person`:`Person`) | company_employeeHistory_Person { .name }]) }] } AS `company` +``` + +### Root and nested relationship type filter + +```graphql +{ + Company( + filter: { + name: "Neo4j" + employeeHistory: { role: "Developer", Person: { name: "jane" } } + } + ) { + name + employeeHistory(filter: { role: "Developer" }) { + start { + year + } + Person { + name + } + } + } +} +``` + +```cypher +MATCH (`company`:`Company`) WHERE (`company`.name = $filter.name) AND (EXISTS((`company`)<-[:WORKED_AT]-(:Person)) AND ALL(`company_filter_person` IN [(`company`)<-[`_company_filter_person`:WORKED_AT]-(:Person) | `_company_filter_person`] WHERE (`company_filter_person`.role = $filter.employeeHistory.role) AND (ALL(`person` IN [(`company`)<-[`company_filter_person`]-(`_person`:Person) | `_person`] WHERE (`person`.name = $filter.employeeHistory.Person.name))))) RETURN `company` { .name ,employeeHistory: [(`company`)<-[`company_employeeHistory_relation`:`WORKED_AT`]-(:`Person`) WHERE (`company_employeeHistory_relation`.role = $1_filter.role) | company_employeeHistory_relation {start: { year: `company_employeeHistory_relation`.start.year },Person: head([(:`Company`)<-[`company_employeeHistory_relation`]-(`company_employeeHistory_Person`:`Person`) | company_employeeHistory_Person { .name }]) }] } AS `company` +``` + ### Nested filters on reflexive relationship type field ```graphql From bee388a76d1095d1c20dacefb797d7c8283b1297 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:03:52 -0700 Subject: [PATCH 12/15] Update parser.js --- test/helpers/tck/parser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/helpers/tck/parser.js b/test/helpers/tck/parser.js index d2a9853c..3b0e9cd9 100644 --- a/test/helpers/tck/parser.js +++ b/test/helpers/tck/parser.js @@ -82,7 +82,6 @@ const extractTestBlocks = data => { const buildTestDeclarations = (tck, extractionLimit) => { const schema = makeTestDataSchema(tck); const testData = buildTestData(schema, tck); - //! refactor to involve forEach and a counter that is compared against extractionLimit return testData .reduce((acc, test) => { // escape " so that we can wrap the cypher in "s From eaaa05639175339c1d316106b359cab46a998f0d Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:03:58 -0700 Subject: [PATCH 13/15] Update testSchema.js --- test/helpers/testSchema.js | 123 +++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index cacf925d..2ea8cbcd 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -81,6 +81,7 @@ export const testSchema = ` @cypher( statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" ) + interfacedRelationshipType: [InterfacedRelationshipType] } type State { @@ -91,6 +92,21 @@ export const testSchema = ` interface Person { userId: ID! name: String + interfacedRelationshipType: [InterfacedRelationshipType] + reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] + } + + type ReflexiveInterfacedRelationshipType @relation(name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE") { + from: Person! + boolean: Boolean + to: Person! + } + + type InterfacedRelationshipType @relation(name: "INTERFACED_RELATIONSHIP_TYPE") { + from: Person! + string: String! + boolean: Boolean + to: Genre! } extend interface Person { @@ -127,6 +143,104 @@ export const testSchema = ` name_not_starts_with: String name_ends_with: String name_not_ends_with: String + interfacedRelationshipType: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _PersonInterfacedRelationshipTypeFilter + reflexiveInterfacedRelationshipType: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_not: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_not_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_some: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_none: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_single: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_every: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String + } + + input _PersonInterfacedRelationshipTypeFilter { + AND: [_PersonInterfacedRelationshipTypeFilter!] + OR: [_PersonInterfacedRelationshipTypeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + boolean: Boolean + boolean_not: Boolean + Genre: _GenreFilter + } + + input _GenreFilter { + AND: [_GenreFilter!] + OR: [_GenreFilter!] + name: String + name_not: String + name_in: [String!] + name_not_in: [String!] + name_contains: String + name_not_contains: String + name_starts_with: String + name_not_starts_with: String + name_ends_with: String + name_not_ends_with: String + interfacedRelationshipType: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_GenreInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_GenreInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _GenreInterfacedRelationshipTypeFilter + } + + input _GenreInterfacedRelationshipTypeFilter { + AND: [_GenreInterfacedRelationshipTypeFilter!] + OR: [_GenreInterfacedRelationshipTypeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + boolean: Boolean + boolean_not: Boolean + Person: _PersonFilter + } + + input _ReflexiveInterfacedRelationshipTypeDirectionsFilter { + from: _ReflexiveInterfacedRelationshipTypeFilter + to: _ReflexiveInterfacedRelationshipTypeFilter + } + + input _ReflexiveInterfacedRelationshipTypeFilter { + AND: [_ReflexiveInterfacedRelationshipTypeFilter!] + OR: [_ReflexiveInterfacedRelationshipTypeFilter!] + boolean: Boolean + boolean_not: Boolean + Person: _PersonFilter } type Actor { @@ -135,6 +249,8 @@ export const testSchema = ` movies: [Movie] @relation(name: "ACTED_IN", direction: "OUT") knows: [Person] @relation(name: "KNOWS", direction: "OUT") extensionScalar: String + interfacedRelationshipType: [InterfacedRelationshipType] + reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] } extend type Actor implements Person @@ -142,6 +258,8 @@ export const testSchema = ` type User implements Person { userId: ID! name: String + interfacedRelationshipType: [InterfacedRelationshipType] + reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String @cypher( statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" @@ -459,6 +577,7 @@ export const testSchema = ` ): [Person] @relation(name: "cameras", direction: IN) computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship: [Camera] @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) } enum _CameraOrdering { @@ -486,6 +605,7 @@ export const testSchema = ` ): [Person] @relation(name: "cameras", direction: IN) computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship: [Camera] @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) } type NewCamera implements Camera { @@ -502,6 +622,7 @@ export const testSchema = ` ): [Person] @relation(name: "cameras", direction: IN) computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship: [Camera] @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) } union MovieSearch = Movie | Genre | Book @@ -519,6 +640,8 @@ export const testSchema = ` cameras: [Camera!]! @relation(name: "cameras", direction: "OUT") cameraBuddy: Person @relation(name: "cameraBuddy", direction: "OUT") extensionScalar: String + interfacedRelationshipType: [InterfacedRelationshipType] + reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] } type SubscriptionC { From 7f8a9ed38020233a617d04936a3f35e8774412e0 Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:04:04 -0700 Subject: [PATCH 14/15] Update augmentSchemaTest.test.js --- test/unit/augmentSchemaTest.test.js | 1425 ++++++++++++++++++++++++++- 1 file changed, 1382 insertions(+), 43 deletions(-) diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 58e3ae71..0afcd047 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -464,14 +464,83 @@ test.cb('Test augmented schema', t => { name_not_starts_with: String name_ends_with: String name_not_ends_with: String - movies: _MovieFilter - movies_not: _MovieFilter - movies_in: [_MovieFilter!] - movies_not_in: [_MovieFilter!] - movies_some: _MovieFilter - movies_none: _MovieFilter - movies_single: _MovieFilter - movies_every: _MovieFilter + interfacedRelationshipType: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_GenreInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_GenreInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _GenreInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _GenreInterfacedRelationshipTypeFilter + } + + input _GenreInterfacedRelationshipTypeFilter { + AND: [_GenreInterfacedRelationshipTypeFilter!] + OR: [_GenreInterfacedRelationshipTypeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + boolean: Boolean + boolean_not: Boolean + Person: _PersonFilter + } + + input _InterfacedRelationshipTypeInput { + string: String! + boolean: Boolean + } + + type _AddGenreInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _RemoveGenreInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + } + + type _UpdateGenreInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _MergeGenreInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean } input _ActorFilter { @@ -523,6 +592,22 @@ test.cb('Test augmented schema', t => { extensionScalar_not_starts_with: String extensionScalar_ends_with: String extensionScalar_not_ends_with: String + interfacedRelationshipType: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _PersonInterfacedRelationshipTypeFilter + reflexiveInterfacedRelationshipType: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_not: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_not_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_some: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_none: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_single: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_every: _ReflexiveInterfacedRelationshipTypeDirectionsFilter } input _StateFilter { @@ -601,6 +686,23 @@ test.cb('Test augmented schema', t => { User: _UserFilter } + enum _MovieRatedOrdering { + currentUserId_asc + currentUserId_desc + rating_asc + rating_desc + time_asc + time_desc + date_asc + date_desc + datetime_asc + datetime_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + } + input _Neo4jTimeInput { hour: Int minute: Int @@ -665,6 +767,22 @@ test.cb('Test augmented schema', t => { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + interfacedRelationshipType: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _PersonInterfacedRelationshipTypeFilter + reflexiveInterfacedRelationshipType: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_not: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_not_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_some: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_none: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_single: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_every: _ReflexiveInterfacedRelationshipTypeDirectionsFilter rated: _UserRatedFilter rated_not: _UserRatedFilter rated_in: [_UserRatedFilter!] @@ -900,6 +1018,9 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput location: _Neo4jPointInput + first: Int + offset: Int + orderBy: [_RatedOrdering] filter: _MovieRatedFilter ): [_MovieRatings] years: [Int] @@ -965,6 +1086,48 @@ test.cb('Test augmented schema', t => { @cypher( statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1" ) + interfacedRelationshipType( + first: Int + offset: Int + orderBy: [_InterfacedRelationshipTypeOrdering] + filter: _GenreInterfacedRelationshipTypeFilter + ): [_GenreInterfacedRelationshipType] + } + + type _GenreInterfacedRelationshipType + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + string: String! + boolean: Boolean + Person: Person + } + + enum _InterfacedRelationshipTypeOrdering { + string_asc + string_desc + boolean_asc + boolean_desc + } + + input _GenreInterfacedRelationshipTypeFilter { + AND: [_GenreInterfacedRelationshipTypeFilter!] + OR: [_GenreInterfacedRelationshipTypeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + boolean: Boolean + boolean_not: Boolean + Person: _PersonFilter } enum _ActorOrdering { @@ -994,6 +1157,13 @@ test.cb('Test augmented schema', t => { filter: _PersonFilter ): [Person] @relation(name: "KNOWS", direction: "OUT") extensionScalar: String + interfacedRelationshipType( + first: Int + offset: Int + orderBy: [_InterfacedRelationshipTypeOrdering] + filter: _PersonInterfacedRelationshipTypeFilter + ): [_PersonInterfacedRelationshipType] + reflexiveInterfacedRelationshipType: _PersonReflexiveInterfacedRelationshipTypeDirections _id: String } @@ -1002,6 +1172,28 @@ test.cb('Test augmented schema', t => { interface Person { userId: ID! name: String + interfacedRelationshipType( + first: Int + offset: Int + orderBy: [_InterfacedRelationshipTypeOrdering] + filter: _PersonInterfacedRelationshipTypeFilter + ): [_PersonInterfacedRelationshipType] + reflexiveInterfacedRelationshipType: _PersonReflexiveInterfacedRelationshipTypeDirections + } + + type ReflexiveInterfacedRelationshipType + @relation(name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE") { + from: Person! + boolean: Boolean + to: Person! + } + + type InterfacedRelationshipType + @relation(name: "INTERFACED_RELATIONSHIP_TYPE") { + from: Person! + string: String! + boolean: Boolean + to: Genre! } extend interface Person { @@ -1014,6 +1206,35 @@ test.cb('Test augmented schema', t => { _id: String } + type _PersonInterfacedRelationshipType + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + string: String! + boolean: Boolean + Genre: Genre + } + + input _PersonInterfacedRelationshipTypeFilter { + AND: [_PersonInterfacedRelationshipTypeFilter!] + OR: [_PersonInterfacedRelationshipTypeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + boolean: Boolean + boolean_not: Boolean + Genre: _GenreFilter + } + type _MovieRatings @relation(name: "RATED", from: "User", to: "Movie") { currentUserId(strArg: String): String @cypher( @@ -1075,6 +1296,13 @@ test.cb('Test augmented schema', t => { type User implements Person { userId: ID! name: String + interfacedRelationshipType( + first: Int + offset: Int + orderBy: [_InterfacedRelationshipTypeOrdering] + filter: _PersonInterfacedRelationshipTypeFilter + ): [_PersonInterfacedRelationshipType] + reflexiveInterfacedRelationshipType: _PersonReflexiveInterfacedRelationshipTypeDirections currentUserId(strArg: String = "Neo4j", strInputArg: strInput): String @cypher( statement: "RETURN $cypherParams.currentUserId AS cypherParamsUserId" @@ -1087,6 +1315,9 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput location: _Neo4jPointInput + first: Int + offset: Int + orderBy: [_RatedOrdering] filter: _UserRatedFilter ): [_UserRated] friends: _UserFriendsDirections @@ -1111,6 +1342,95 @@ test.cb('Test augmented schema', t => { extensionArg: String } + type _AddUserInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _RemoveUserInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + } + + type _UpdateUserInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _MergeUserInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _AddUserReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _RemoveUserReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + } + + type _UpdateUserReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _MergeUserReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + type _UserRated @relation(name: "RATED", from: "User", to: "Movie") { currentUserId(strArg: String): String @cypher( @@ -1138,6 +1458,9 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput location: _Neo4jPointInput + first: Int + offset: Int + orderBy: [_FriendOfOrdering] filter: _FriendOfFilter ): [_UserFriends] to( @@ -1148,6 +1471,9 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput location: _Neo4jPointInput + first: Int + offset: Int + orderBy: [_FriendOfOrdering] filter: _FriendOfFilter ): [_UserFriends] } @@ -1299,6 +1625,32 @@ test.cb('Test augmented schema', t => { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + interfacedRelationshipType: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _PersonInterfacedRelationshipTypeFilter + reflexiveInterfacedRelationshipType: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_not: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_not_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_some: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_none: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_single: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_every: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + extensionScalar: String + extensionScalar_not: String + extensionScalar_in: [String!] + extensionScalar_not_in: [String!] + extensionScalar_contains: String + extensionScalar_not_contains: String + extensionScalar_starts_with: String + extensionScalar_not_starts_with: String + extensionScalar_ends_with: String + extensionScalar_not_ends_with: String } enum _TemporalNodeOrdering { @@ -1507,6 +1859,13 @@ test.cb('Test augmented schema', t => { orderBy: [_PersonOrdering] ): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship( + first: Int + offset: Int + orderBy: [_CameraOrdering] + filter: _CameraFilter + ): [Camera] + @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) } enum _OldCameraOrdering { @@ -1583,6 +1942,14 @@ test.cb('Test augmented schema', t => { operators_none: _PersonFilter operators_single: _PersonFilter operators_every: _PersonFilter + reflexiveInterfaceRelationship: _CameraFilter + reflexiveInterfaceRelationship_not: _CameraFilter + reflexiveInterfaceRelationship_in: [_CameraFilter!] + reflexiveInterfaceRelationship_not_in: [_CameraFilter!] + reflexiveInterfaceRelationship_some: _CameraFilter + reflexiveInterfaceRelationship_none: _CameraFilter + reflexiveInterfaceRelationship_single: _CameraFilter + reflexiveInterfaceRelationship_every: _CameraFilter } type OldCamera implements Camera { @@ -1604,6 +1971,13 @@ test.cb('Test augmented schema', t => { orderBy: [_PersonOrdering] ): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship( + first: Int + offset: Int + orderBy: [_CameraOrdering] + filter: _CameraFilter + ): [Camera] + @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) _id: String } @@ -1669,6 +2043,14 @@ test.cb('Test augmented schema', t => { operators_none: _PersonFilter operators_single: _PersonFilter operators_every: _PersonFilter + reflexiveInterfaceRelationship: _CameraFilter + reflexiveInterfaceRelationship_not: _CameraFilter + reflexiveInterfaceRelationship_in: [_CameraFilter!] + reflexiveInterfaceRelationship_not_in: [_CameraFilter!] + reflexiveInterfaceRelationship_some: _CameraFilter + reflexiveInterfaceRelationship_none: _CameraFilter + reflexiveInterfaceRelationship_single: _CameraFilter + reflexiveInterfaceRelationship_every: _CameraFilter } type NewCamera implements Camera { @@ -1690,6 +2072,13 @@ test.cb('Test augmented schema', t => { orderBy: [_PersonOrdering] ): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p") + reflexiveInterfaceRelationship( + first: Int + offset: Int + orderBy: [_CameraOrdering] + filter: _CameraFilter + ): [Camera] + @relation(name: "REFLEXIVE_INTERFACE_RELATIONSHIP", direction: OUT) _id: String } @@ -1753,6 +2142,22 @@ test.cb('Test augmented schema', t => { extensionScalar_not_starts_with: String extensionScalar_ends_with: String extensionScalar_not_ends_with: String + interfacedRelationshipType: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_not: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_not_in: [_PersonInterfacedRelationshipTypeFilter!] + interfacedRelationshipType_some: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_none: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_single: _PersonInterfacedRelationshipTypeFilter + interfacedRelationshipType_every: _PersonInterfacedRelationshipTypeFilter + reflexiveInterfacedRelationshipType: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_not: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_not_in: [_ReflexiveInterfacedRelationshipTypeDirectionsFilter!] + reflexiveInterfacedRelationshipType_some: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_none: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_single: _ReflexiveInterfacedRelationshipTypeDirectionsFilter + reflexiveInterfacedRelationshipType_every: _ReflexiveInterfacedRelationshipTypeDirectionsFilter } union MovieSearch = Movie | Genre | Book @@ -1781,6 +2186,13 @@ test.cb('Test augmented schema', t => { cameraBuddy(filter: _PersonFilter): Person @relation(name: "cameraBuddy", direction: "OUT") extensionScalar: String + interfacedRelationshipType( + first: Int + offset: Int + orderBy: [_InterfacedRelationshipTypeOrdering] + filter: _PersonInterfacedRelationshipTypeFilter + ): [_PersonInterfacedRelationshipType] + reflexiveInterfacedRelationshipType: _PersonReflexiveInterfacedRelationshipTypeDirections _id: String } @@ -2008,10 +2420,139 @@ test.cb('Test augmented schema', t => { ): _MergeGenreMoviesPayload @MutationMeta(relationship: "IN_GENRE", from: "Movie", to: "Genre") @hasScope(scopes: ["Movie: Merge", "Genre: Merge"]) + AddGenreInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _AddGenreInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Create", "Genre: Create"]) + RemoveGenreInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + ): _RemoveGenreInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Delete", "Genre: Delete"]) + UpdateGenreInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _UpdateGenreInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Update", "Genre: Update"]) + MergeGenreInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _MergeGenreInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Merge", "Genre: Merge"]) CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) DeleteState(name: String!): State @hasScope(scopes: ["State: Delete"]) + AddPersonInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _AddPersonInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Create", "Genre: Create"]) + RemovePersonInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + ): _RemovePersonInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Delete", "Genre: Delete"]) + UpdatePersonInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _UpdatePersonInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Update", "Genre: Update"]) + MergePersonInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _MergePersonInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Merge", "Genre: Merge"]) + AddPersonReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _AddPersonReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Create", "Person: Create"]) + RemovePersonReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + ): _RemovePersonReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Delete", "Person: Delete"]) + UpdatePersonReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _UpdatePersonReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Update", "Person: Update"]) + MergePersonReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _MergePersonReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Merge", "Person: Merge"]) AddActorMovies( from: _ActorInput! to: _MovieInput! @@ -2048,6 +2589,92 @@ test.cb('Test augmented schema', t => { ): _MergeActorKnowsPayload @MutationMeta(relationship: "KNOWS", from: "Actor", to: "Person") @hasScope(scopes: ["Actor: Merge", "Person: Merge"]) + AddActorInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _AddActorInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Create", "Genre: Create"]) + RemoveActorInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + ): _RemoveActorInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Delete", "Genre: Delete"]) + UpdateActorInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _UpdateActorInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Update", "Genre: Update"]) + MergeActorInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _MergeActorInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Merge", "Genre: Merge"]) + AddActorReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _AddActorReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Create", "Person: Create"]) + RemoveActorReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + ): _RemoveActorReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Delete", "Person: Delete"]) + UpdateActorReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _UpdateActorReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Update", "Person: Update"]) + MergeActorReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _MergeActorReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Merge", "Person: Merge"]) CreateActor(userId: ID, name: String, extensionScalar: String): Actor @hasScope(scopes: ["Actor: Create"]) UpdateActor(userId: ID!, name: String, extensionScalar: String): Actor @@ -2055,41 +2682,127 @@ test.cb('Test augmented schema', t => { DeleteActor(userId: ID!): Actor @hasScope(scopes: ["Actor: Delete"]) MergeActor(userId: ID!, name: String, extensionScalar: String): Actor @hasScope(scopes: ["Actor: Merge"]) - AddUserRated( - from: _UserInput! - to: _MovieInput! - data: _RatedInput! - ): _AddUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Create", "Movie: Create"]) - RemoveUserRated( - from: _UserInput! - to: _MovieInput! - ): _RemoveUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Delete", "Movie: Delete"]) - UpdateUserRated( - from: _UserInput! - to: _MovieInput! - data: _RatedInput! - ): _UpdateUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Update", "Movie: Update"]) - MergeUserRated( - from: _UserInput! - to: _MovieInput! - data: _RatedInput! - ): _MergeUserRatedPayload - @MutationMeta(relationship: "RATED", from: "User", to: "Movie") - @hasScope(scopes: ["User: Merge", "Movie: Merge"]) - AddUserFriends( - from: _UserInput! - to: _UserInput! - data: _FriendOfInput! - ): _AddUserFriendsPayload - @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") - @hasScope(scopes: ["User: Create", "User: Create"]) - RemoveUserFriends( + AddUserInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _AddUserInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Create", "Genre: Create"]) + RemoveUserInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + ): _RemoveUserInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Delete", "Genre: Delete"]) + UpdateUserInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _UpdateUserInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Update", "Genre: Update"]) + MergeUserInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _MergeUserInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Merge", "Genre: Merge"]) + AddUserReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _AddUserReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Create", "Person: Create"]) + RemoveUserReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + ): _RemoveUserReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Delete", "Person: Delete"]) + UpdateUserReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _UpdateUserReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Update", "Person: Update"]) + MergeUserReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _MergeUserReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Merge", "Person: Merge"]) + AddUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _AddUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Create", "Movie: Create"]) + RemoveUserRated( + from: _UserInput! + to: _MovieInput! + ): _RemoveUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Delete", "Movie: Delete"]) + UpdateUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _UpdateUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Update", "Movie: Update"]) + MergeUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! + ): _MergeUserRatedPayload + @MutationMeta(relationship: "RATED", from: "User", to: "Movie") + @hasScope(scopes: ["User: Merge", "Movie: Merge"]) + AddUserFriends( + from: _UserInput! + to: _UserInput! + data: _FriendOfInput! + ): _AddUserFriendsPayload + @MutationMeta(relationship: "FRIEND_OF", from: "User", to: "User") + @hasScope(scopes: ["User: Create", "User: Create"]) + RemoveUserFriends( from: _UserInput! to: _UserInput! ): _RemoveUserFriendsPayload @@ -2277,6 +2990,36 @@ test.cb('Test augmented schema', t => { ): _MergeCameraOperatorsPayload @MutationMeta(relationship: "cameras", from: "Person", to: "Camera") @hasScope(scopes: ["Person: Merge", "Camera: Merge"]) + AddCameraReflexiveInterfaceRelationship( + from: _CameraInput! + to: _CameraInput! + ): _AddCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) + @hasScope(scopes: ["Camera: Create", "Camera: Create"]) + RemoveCameraReflexiveInterfaceRelationship( + from: _CameraInput! + to: _CameraInput! + ): _RemoveCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) + @hasScope(scopes: ["Camera: Delete", "Camera: Delete"]) + MergeCameraReflexiveInterfaceRelationship( + from: _CameraInput! + to: _CameraInput! + ): _MergeCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) + @hasScope(scopes: ["Camera: Merge", "Camera: Merge"]) AddOldCameraOperators( from: _PersonInput! to: _OldCameraInput! @@ -2295,6 +3038,36 @@ test.cb('Test augmented schema', t => { ): _MergeOldCameraOperatorsPayload @MutationMeta(relationship: "cameras", from: "Person", to: "OldCamera") @hasScope(scopes: ["Person: Merge", "OldCamera: Merge"]) + AddOldCameraReflexiveInterfaceRelationship( + from: _OldCameraInput! + to: _CameraInput! + ): _AddOldCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) + @hasScope(scopes: ["OldCamera: Create", "Camera: Create"]) + RemoveOldCameraReflexiveInterfaceRelationship( + from: _OldCameraInput! + to: _CameraInput! + ): _RemoveOldCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) + @hasScope(scopes: ["OldCamera: Delete", "Camera: Delete"]) + MergeOldCameraReflexiveInterfaceRelationship( + from: _OldCameraInput! + to: _CameraInput! + ): _MergeOldCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) + @hasScope(scopes: ["OldCamera: Merge", "Camera: Merge"]) CreateOldCamera( id: ID type: String @@ -2336,6 +3109,36 @@ test.cb('Test augmented schema', t => { ): _MergeNewCameraOperatorsPayload @MutationMeta(relationship: "cameras", from: "Person", to: "NewCamera") @hasScope(scopes: ["Person: Merge", "NewCamera: Merge"]) + AddNewCameraReflexiveInterfaceRelationship( + from: _NewCameraInput! + to: _CameraInput! + ): _AddNewCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) + @hasScope(scopes: ["NewCamera: Create", "Camera: Create"]) + RemoveNewCameraReflexiveInterfaceRelationship( + from: _NewCameraInput! + to: _CameraInput! + ): _RemoveNewCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) + @hasScope(scopes: ["NewCamera: Delete", "Camera: Delete"]) + MergeNewCameraReflexiveInterfaceRelationship( + from: _NewCameraInput! + to: _CameraInput! + ): _MergeNewCameraReflexiveInterfaceRelationshipPayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) + @hasScope(scopes: ["NewCamera: Merge", "Camera: Merge"]) CreateNewCamera( id: ID type: String @@ -2437,6 +3240,92 @@ test.cb('Test augmented schema', t => { to: "Person" ) @hasScope(scopes: ["CameraMan: Merge", "Person: Merge"]) + AddCameraManInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _AddCameraManInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Create", "Genre: Create"]) + RemoveCameraManInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + ): _RemoveCameraManInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Delete", "Genre: Delete"]) + UpdateCameraManInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _UpdateCameraManInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Update", "Genre: Update"]) + MergeCameraManInterfacedRelationshipType( + from: _PersonInput! + to: _GenreInput! + data: _InterfacedRelationshipTypeInput! + ): _MergeCameraManInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) + @hasScope(scopes: ["Person: Merge", "Genre: Merge"]) + AddCameraManReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _AddCameraManReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Create", "Person: Create"]) + RemoveCameraManReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + ): _RemoveCameraManReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Delete", "Person: Delete"]) + UpdateCameraManReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _UpdateCameraManReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Update", "Person: Update"]) + MergeCameraManReflexiveInterfacedRelationshipType( + from: _PersonInput! + to: _PersonInput! + data: _ReflexiveInterfacedRelationshipTypeInput! + ): _MergeCameraManReflexiveInterfacedRelationshipTypePayload + @MutationMeta( + relationship: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) + @hasScope(scopes: ["Person: Merge", "Person: Merge"]) CreateCameraMan( userId: ID name: String @@ -2681,12 +3570,118 @@ test.cb('Test augmented schema', t => { to: Person } + type _AddActorInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _RemoveActorInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + } + + type _UpdateActorInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _MergeActorInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _AddActorReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _RemoveActorReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + } + + type _UpdateActorReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _MergeActorReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + type _RemoveActorKnowsPayload @relation(name: "KNOWS", from: "Actor", to: "Person") { from: Actor to: Person } + enum _RatedOrdering { + currentUserId_asc + currentUserId_desc + rating_asc + rating_desc + time_asc + time_desc + date_asc + date_desc + datetime_asc + datetime_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + } + type _AddUserRatedPayload @relation(name: "RATED", from: "User", to: "Movie") { from: User @@ -2750,6 +3745,23 @@ test.cb('Test augmented schema', t => { location: _Neo4jPoint } + enum _FriendOfOrdering { + currentUserId_asc + currentUserId_desc + since_asc + since_desc + time_asc + time_desc + date_asc + date_desc + datetime_asc + datetime_desc + localtime_asc + localtime_desc + localdatetime_asc + localdatetime_desc + } + input _FriendOfInput { since: Int time: _Neo4jTimeInput @@ -2905,6 +3917,95 @@ test.cb('Test augmented schema', t => { to: State } + type _AddCameraManInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _RemoveCameraManInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + } + + type _UpdateCameraManInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _MergeCameraManInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _AddCameraManReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _RemoveCameraManReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + } + + type _UpdateCameraManReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _MergeCameraManReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + input _CameraManInput { userId: ID! } @@ -2931,6 +4032,36 @@ test.cb('Test augmented schema', t => { to: Camera } + type _AddCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) { + from: Camera + to: Camera + } + + type _RemoveCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) { + from: Camera + to: Camera + } + + type _MergeCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "Camera" + to: "Camera" + ) { + from: Camera + to: Camera + } + type _AddCameraManFavoriteCameraPayload @relation(name: "favoriteCamera", from: "CameraMan", to: "Camera") { from: CameraMan @@ -2964,6 +4095,154 @@ test.cb('Test augmented schema', t => { from: CameraMan to: Camera } + + enum InterfacedRelationshipTypeOrdering { + string_asc + string_desc + boolean_asc + boolean_desc + } + + type _AddPersonInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _RemovePersonInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + } + + type _UpdatePersonInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + + type _MergePersonInterfacedRelationshipTypePayload + @relation( + name: "INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Genre" + ) { + from: Person + to: Genre + string: String! + boolean: Boolean + } + type _PersonReflexiveInterfacedRelationshipTypeDirections + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from( + first: Int + offset: Int + orderBy: [_ReflexiveInterfacedRelationshipTypeOrdering] + filter: _ReflexiveInterfacedRelationshipTypeFilter + ): [_PersonReflexiveInterfacedRelationshipType] + to( + first: Int + offset: Int + orderBy: [_ReflexiveInterfacedRelationshipTypeOrdering] + filter: _ReflexiveInterfacedRelationshipTypeFilter + ): [_PersonReflexiveInterfacedRelationshipType] + } + + type _PersonReflexiveInterfacedRelationshipType + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + boolean: Boolean + Person: Person + } + + input _ReflexiveInterfacedRelationshipTypeDirectionsFilter { + from: _ReflexiveInterfacedRelationshipTypeFilter + to: _ReflexiveInterfacedRelationshipTypeFilter + } + + enum _ReflexiveInterfacedRelationshipTypeOrdering { + boolean_asc + boolean_desc + } + + input _ReflexiveInterfacedRelationshipTypeFilter { + AND: [_ReflexiveInterfacedRelationshipTypeFilter!] + OR: [_ReflexiveInterfacedRelationshipTypeFilter!] + boolean: Boolean + boolean_not: Boolean + Person: _PersonFilter + } + + input _ReflexiveInterfacedRelationshipTypeInput { + boolean: Boolean + } + + type _AddPersonReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _RemovePersonReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + } + + type _UpdatePersonReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + + type _MergePersonReflexiveInterfacedRelationshipTypePayload + @relation( + name: "REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE" + from: "Person" + to: "Person" + ) { + from: Person + to: Person + boolean: Boolean + } + input _PersonInput { userId: ID! } @@ -3099,6 +4378,36 @@ test.cb('Test augmented schema', t => { to: OldCamera } + type _AddOldCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) { + from: OldCamera + to: Camera + } + + type _RemoveOldCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) { + from: OldCamera + to: Camera + } + + type _MergeOldCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "OldCamera" + to: "Camera" + ) { + from: OldCamera + to: Camera + } + input _NewCameraInput { id: ID! } @@ -3121,6 +4430,36 @@ test.cb('Test augmented schema', t => { to: NewCamera } + type _AddNewCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) { + from: NewCamera + to: Camera + } + + type _RemoveNewCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) { + from: NewCamera + to: Camera + } + + type _MergeNewCameraReflexiveInterfaceRelationshipPayload + @relation( + name: "REFLEXIVE_INTERFACE_RELATIONSHIP" + from: "NewCamera" + to: "Camera" + ) { + from: NewCamera + to: Camera + } + type _Neo4jPoint { x: Float y: Float From b5f857fa05b53c7b7e5263df6716e3dc9f71b62b Mon Sep 17 00:00:00 2001 From: Michael Graham <38390185+michaeldgraham@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:04:13 -0700 Subject: [PATCH 15/15] Update cypherTest.test.js --- test/unit/cypherTest.test.js | 1143 ++++++++++++++++++++++++++++++++-- 1 file changed, 1080 insertions(+), 63 deletions(-) diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index bd602cb6..4c09adc7 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -1381,6 +1381,56 @@ test('Add interfaced relationship mutation', t => { ); }); +test('Add interfaced relationship type mutation', t => { + const graphQLQuery = `mutation { + AddActorInterfacedRelationshipType( + from: { userId: "744c23c8-2042-402d-b482-28d089f976f9" } + to: { name: "Wildlife Documentary" } + data: { string: "data" } + ) { + from { + userId + interfacedRelationshipType { + string + Genre { + name + } + } + } + string + } + }`, + expectedCypherQuery = ` + MATCH (\`person_from\`:\`Person\` {userId: $from.userId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + CREATE (\`person_from\`)-[\`interfaced_relationship_type_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\` {string:$data.string}]->(\`genre_to\`) + RETURN \`interfaced_relationship_type_relation\` { from: \`person_from\` {FRAGMENT_TYPE: head( [ label IN labels(\`person_from\`) WHERE label IN $Person_derivedTypes ] ), .userId ,interfacedRelationshipType: [(\`person_from\`)-[\`person_from_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_from_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_from_interfacedRelationshipType_relation\`]->(\`person_from_interfacedRelationshipType_Genre\`:\`Genre\`) | person_from_interfacedRelationshipType_Genre { .name }]) }] } , .string } AS \`_AddActorInterfacedRelationshipTypePayload\`; + `; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + from: { + userId: '744c23c8-2042-402d-b482-28d089f976f9' + }, + to: { + name: 'Wildlife Documentary' + }, + data: { + string: 'data' + }, + first: -1, + offset: 0, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } + ); +}); + test('Merge interfaced relationship mutation', t => { const graphQLQuery = `mutation someMutation { MergeActorKnows( @@ -1419,6 +1469,167 @@ test('Merge interfaced relationship mutation', t => { ); }); +test('Merge interfaced relationship type mutation', t => { + const graphQLQuery = `mutation { + MergeActorInterfacedRelationshipType( + from: { userId: "744c23c8-2042-402d-b482-28d089f976f9" } + to: { name: "Wildlife Documentary" } + data: { string: "data" } + ) { + from { + userId + interfacedRelationshipType { + string + Genre { + name + } + } + } + string + } + }`, + expectedCypherQuery = ` + MATCH (\`person_from\`:\`Person\` {userId: $from.userId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + MERGE (\`person_from\`)-[\`interfaced_relationship_type_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(\`genre_to\`) + SET \`interfaced_relationship_type_relation\` += {string:$data.string} + RETURN \`interfaced_relationship_type_relation\` { from: \`person_from\` {FRAGMENT_TYPE: head( [ label IN labels(\`person_from\`) WHERE label IN $Person_derivedTypes ] ), .userId ,interfacedRelationshipType: [(\`person_from\`)-[\`person_from_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_from_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_from_interfacedRelationshipType_relation\`]->(\`person_from_interfacedRelationshipType_Genre\`:\`Genre\`) | person_from_interfacedRelationshipType_Genre { .name }]) }] } , .string } AS \`_MergeActorInterfacedRelationshipTypePayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { + userId: '744c23c8-2042-402d-b482-28d089f976f9' + }, + to: { + name: 'Wildlife Documentary' + }, + data: { + string: 'data' + }, + first: -1, + offset: 0, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + }, + expectedCypherQuery, + {} + ); +}); + +test('Update interfaced relationship type mutation', t => { + const graphQLQuery = `mutation { + UpdateActorInterfacedRelationshipType( + from: { userId: "744c23c8-2042-402d-b482-28d089f976f9" } + to: { name: "Wildlife Documentary" } + data: { string: "value" } + ) { + from { + userId + interfacedRelationshipType { + string + Genre { + name + } + } + } + string + } + }`, + expectedCypherQuery = ` + MATCH (\`person_from\`:\`Person\` {userId: $from.userId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + MATCH (\`person_from\`)-[\`interfaced_relationship_type_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(\`genre_to\`) + SET \`interfaced_relationship_type_relation\` += {string:$data.string} + RETURN \`interfaced_relationship_type_relation\` { from: \`person_from\` {FRAGMENT_TYPE: head( [ label IN labels(\`person_from\`) WHERE label IN $Person_derivedTypes ] ), .userId ,interfacedRelationshipType: [(\`person_from\`)-[\`person_from_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_from_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_from_interfacedRelationshipType_relation\`]->(\`person_from_interfacedRelationshipType_Genre\`:\`Genre\`) | person_from_interfacedRelationshipType_Genre { .name }]) }] } , .string } AS \`_UpdateActorInterfacedRelationshipTypePayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { + userId: '744c23c8-2042-402d-b482-28d089f976f9' + }, + to: { + name: 'Wildlife Documentary' + }, + data: { + string: 'value' + }, + first: -1, + offset: 0, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + }, + expectedCypherQuery, + {} + ); +}); + +test('Merge interfaced relationship type mutation (additional operation)', t => { + const graphQLQuery = `mutation { + MergeGenreInterfacedRelationshipType( + from: { userId: "744c23c8-2042-402d-b482-28d089f976f9" } + to: { name: "Wildlife Documentary" } + data: { string: "data" } + ) { + from { + userId + name + interfacedRelationshipType { + string + Genre { + name + } + } + } + to { + name + interfacedRelationshipType { + string + Person { + userId + name + } + } + } + string + } + }`, + expectedCypherQuery = ` + MATCH (\`person_from\`:\`Person\` {userId: $from.userId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + MERGE (\`person_from\`)-[\`interfaced_relationship_type_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(\`genre_to\`) + SET \`interfaced_relationship_type_relation\` += {string:$data.string} + RETURN \`interfaced_relationship_type_relation\` { from: \`person_from\` {FRAGMENT_TYPE: head( [ label IN labels(\`person_from\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,interfacedRelationshipType: [(\`person_from\`)-[\`person_from_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_from_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_from_interfacedRelationshipType_relation\`]->(\`person_from_interfacedRelationshipType_Genre\`:\`Genre\`) | person_from_interfacedRelationshipType_Genre { .name }]) }] } ,to: \`genre_to\` { .name ,interfacedRelationshipType: [(\`genre_to\`)<-[\`genre_to_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) | genre_to_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_to_interfacedRelationshipType_relation\`]-(\`genre_to_interfacedRelationshipType_Person\`:\`Person\`) | genre_to_interfacedRelationshipType_Person {FRAGMENT_TYPE: head( [ label IN labels(genre_to_interfacedRelationshipType_Person) WHERE label IN $Person_derivedTypes ] ), .userId , .name }]) }] } , .string } AS \`_MergeGenreInterfacedRelationshipTypePayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { + userId: '744c23c8-2042-402d-b482-28d089f976f9' + }, + to: { + name: 'Wildlife Documentary' + }, + data: { + string: 'data' + }, + first: -1, + offset: 0, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + }, + expectedCypherQuery, + {} + ); +}); + test('Remove interfaced relationship mutation', t => { const graphQLQuery = `mutation someMutation { RemoveActorKnows( @@ -1459,6 +1670,52 @@ test('Remove interfaced relationship mutation', t => { ); }); +test('Remove interfaced relationship type mutation', t => { + const graphQLQuery = `mutation { + RemoveActorInterfacedRelationshipType( + from: { userId: "744c23c8-2042-402d-b482-28d089f976f9" } + to: { name: "Wildlife Documentary" } + ) { + from { + userId + interfacedRelationshipType { + string + Genre { + name + } + } + } + } + }`, + expectedCypherQuery = ` + MATCH (\`person_from\`:\`Person\` {userId: $from.userId}) + MATCH (\`genre_to\`:\`Genre\` {name: $to.name}) + OPTIONAL MATCH (\`person_from\`)-[\`person_fromgenre_to\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(\`genre_to\`) + DELETE \`person_fromgenre_to\` + WITH COUNT(*) AS scope, \`person_from\` AS \`_person_from\`, \`genre_to\` AS \`_genre_to\` + RETURN {from: \`_person_from\` {FRAGMENT_TYPE: head( [ label IN labels(\`_person_from\`) WHERE label IN $Person_derivedTypes ] ), .userId ,interfacedRelationshipType: [(\`_person_from\`)-[\`_person_from_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | _person_from_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`_person_from_interfacedRelationshipType_relation\`]->(\`_person_from_interfacedRelationshipType_Genre\`:\`Genre\`) | _person_from_interfacedRelationshipType_Genre { .name }]) }] } } AS \`_RemoveActorInterfacedRelationshipTypePayload\`; + `; + + t.plan(1); + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + { + from: { + userId: '744c23c8-2042-402d-b482-28d089f976f9' + }, + to: { + name: 'Wildlife Documentary' + }, + first: -1, + offset: 0, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + }, + expectedCypherQuery, + {} + ); +}); + test('Remove relationship mutation', t => { const graphQLQuery = `mutation someMutation { RemoveMovieGenres( @@ -1964,6 +2221,39 @@ test('query for relationship properties', t => { ); }); +test('query for relationship properties using orderBy argument', t => { + const graphQLQuery = `query { + Movie(title: "River Runs Through It, A") { + title + ratings(orderBy: [datetime_asc]) { + rating + User { + userId + name + } + } + } + } + `, + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\` {title:$title}) RETURN \`movie\` { .title ,ratings: [sortedElement IN apoc.coll.sortMulti([(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`)<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .userId , .name }]) ,datetime: \`movie_ratings_relation\`.datetime}], ['^datetime']) | sortedElement { .* }] } AS \`movie\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + title: 'River Runs Through It, A', + '1_orderBy': ['datetime_asc'], + cypherParams: CYPHER_PARAMS + } + ); +}); + test('query reflexive relation nested in non-reflexive relation', t => { const graphQLQuery = `query { Movie { @@ -2077,18 +2367,510 @@ test('query non-reflexive relation nested in reflexive relation', t => { User { _id name - rated { - rating - Movie { - _id - } - } + rated { + rating + Movie { + _id + } + } + } + } + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {_id: ID(\`user\`), .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from {_id: ID(\`user_from\`), .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_Movie {_id: ID(\`user_from_rated_Movie\`),ratings: [(\`user_from_rated_Movie\`)<-[\`user_from_rated_Movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | user_from_rated_Movie_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`user_from_rated_Movie_ratings_relation\`]-(\`user_from_rated_Movie_ratings_User\`:\`User\`) | user_from_rated_Movie_ratings_User {_id: ID(\`user_from_rated_Movie_ratings_User\`),friends: {from: [(\`user_from_rated_Movie_ratings_User\`)<-[\`user_from_rated_Movie_ratings_User_from_relation\`:\`FRIEND_OF\`]-(\`user_from_rated_Movie_ratings_User_from\`:\`User\`) | user_from_rated_Movie_ratings_User_from_relation { .since ,User: user_from_rated_Movie_ratings_User_from {_id: ID(\`user_from_rated_Movie_ratings_User_from\`)} }] ,to: [(\`user_from_rated_Movie_ratings_User\`)-[\`user_from_rated_Movie_ratings_User_to_relation\`:\`FRIEND_OF\`]->(\`user_from_rated_Movie_ratings_User_to\`:\`User\`) | user_from_rated_Movie_ratings_User_to_relation { .since ,User: user_from_rated_Movie_ratings_User_to {_id: ID(\`user_from_rated_Movie_ratings_User_to\`)} }] } }]) }] }]) }] } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to {_id: ID(\`user_to\`), .name ,rated: [(\`user_to\`)-[\`user_to_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_to_rated_relation\`]->(\`user_to_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_Movie {_id: ID(\`user_to_rated_Movie\`)}]) }] } }] } } AS \`user\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query relation type with argument', t => { + const graphQLQuery = `query { + User { + _id + name + rated(rating: 5) { + rating + Movie { + title + } + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {_id: ID(\`user\`), .name ,rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`{rating:$1_rating}]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_rated_relation\`]->(\`user_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_rated_Movie { .title }]) }] } AS \`user\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query reflexive relation type with arguments', t => { + const graphQLQuery = `query { + User { + userId + name + friends { + from(since: 3) { + since + User { + name + } + } + to(since: 5) { + since + User { + name + } + } + } + } + } + `, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`{since:$1_since}]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from { .name } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`{since:$3_since}]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to { .name } }] } } AS \`user\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query using inline fragment on object type - including cypherParams', t => { + const graphQLQuery = ` + { + Movie(title: "River Runs Through It, A") { + title + ratings { + rating + User { + ... on User { + name + userId + currentUserId + } + } + } + } + } + `, + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {title:$title}) RETURN \`movie\` { .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .name , .userId ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: movie_ratings_User, cypherParams: $cypherParams, strArg: "Neo4j"}, false)}]) }] } AS \`movie\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('query interfaced relation using inline fragment and pagination', t => { + const graphQLQuery = `query { + Actor { + name + knows(first: 0, offset: 1) { + ...userFavorites + } + } + } + + fragment userFavorites on User { + name + favorites { + movieId + title + year + } + }`, + expectedCypherQuery = `MATCH (\`actor\`:\`Actor\`) RETURN \`actor\` { .name ,knows: [(\`actor\`)-[:\`KNOWS\`]->(\`actor_knows\`:\`Person\`) WHERE ("User" IN labels(\`actor_knows\`)) | head([\`actor_knows\` IN [\`actor_knows\`] WHERE "User" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "User", .name ,favorites: [(\`actor_knows\`)-[:\`FAVORITED\`]->(\`actor_knows_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`actor_knows_favorites\` { .movieId , .title , .year }] }])][1..1] } AS \`actor\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + +test('order interfaced relation using inline fragment', t => { + const graphQLQuery = `query { + Actor { + name + knows(orderBy: userId_asc) { + name + ... on User { + userId + favorites { + movieId + title + year + } + } + } + } + } +`, + expectedCypherQuery = `MATCH (\`actor\`:\`Actor\`) RETURN \`actor\` { .name ,knows: apoc.coll.sortMulti([(\`actor\`)-[:\`KNOWS\`]->(\`actor_knows\`:\`Person\`) WHERE ("Actor" IN labels(\`actor_knows\`) OR "CameraMan" IN labels(\`actor_knows\`) OR "User" IN labels(\`actor_knows\`)) | head([\`actor_knows\` IN [\`actor_knows\`] WHERE "Actor" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "Actor", .name , .userId }] + [\`actor_knows\` IN [\`actor_knows\`] WHERE "CameraMan" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "CameraMan", .name , .userId }] + [\`actor_knows\` IN [\`actor_knows\`] WHERE "User" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "User", .userId ,favorites: [(\`actor_knows\`)-[:\`FAVORITED\`]->(\`actor_knows_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`actor_knows_favorites\` { .movieId , .title , .year }] , .name }])], ['^userId']) } AS \`actor\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + '1_orderBy': 'userId_asc', + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('query interfaced relationship type using inline fragment and pagination', t => { + const graphQLQuery = `query { + Person { + name + ... on Actor { + userId + interfacedRelationshipType(first: 0, offset: 1) { + string + Genre { + name + } + __typename + } + } + __typename + } + } + `, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) WHERE ("Actor" IN labels(\`person\`) OR "CameraMan" IN labels(\`person\`) OR "User" IN labels(\`person\`)) RETURN head([\`person\` IN [\`person\`] WHERE "Actor" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "Actor", .userId ,interfacedRelationshipType: [(\`person\`)-[\`person_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_interfacedRelationshipType_relation\`]->(\`person_interfacedRelationshipType_Genre\`:\`Genre\`) | person_interfacedRelationshipType_Genre { .name }]) }][1..1] , .name }] + [\`person\` IN [\`person\`] WHERE "CameraMan" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "CameraMan", .name }] + [\`person\` IN [\`person\`] WHERE "User" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "User", .name }]) AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + } + ); +}); +test('order interfaced relationship type using inline fragment', t => { + const graphQLQuery = `query { + Person { + name + ... on Actor { + userId + interfacedRelationshipType(orderBy: [string_desc]) { + string + Genre { + name + } + __typename + } + } + __typename + } + } + `, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) WHERE ("Actor" IN labels(\`person\`) OR "CameraMan" IN labels(\`person\`) OR "User" IN labels(\`person\`)) RETURN head([\`person\` IN [\`person\`] WHERE "Actor" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "Actor", .userId ,interfacedRelationshipType: apoc.coll.sortMulti([(\`person\`)-[\`person_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_interfacedRelationshipType_relation\`]->(\`person_interfacedRelationshipType_Genre\`:\`Genre\`) | person_interfacedRelationshipType_Genre { .name }]) }], ['string']) , .name }] + [\`person\` IN [\`person\`] WHERE "CameraMan" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "CameraMan", .name }] + [\`person\` IN [\`person\`] WHERE "User" IN labels(\`person\`) | \`person\` { FRAGMENT_TYPE: "User", .name }]) AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + '1_orderBy': ['string_desc'], + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('query interfaced relationship type field for outgoing object type nodes', t => { + const graphQLQuery = `query { + Person { + userId + name + interfacedRelationshipType { + string + Genre { + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,interfacedRelationshipType: [(\`person\`)-[\`person_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_interfacedRelationshipType_relation\`]->(\`person_interfacedRelationshipType_Genre\`:\`Genre\`) | person_interfacedRelationshipType_Genre { .name }]) }] } AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } + ); +}); + +test('order interfaced relationship type field for outgoing object type nodes', t => { + const graphQLQuery = `query { + Person { + userId + name + interfacedRelationshipType(orderBy: [string_asc]) { + string + Genre { + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,interfacedRelationshipType: apoc.coll.sortMulti([(\`person\`)-[\`person_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_interfacedRelationshipType_relation\`]->(\`person_interfacedRelationshipType_Genre\`:\`Genre\`) | person_interfacedRelationshipType_Genre { .name }]) }], ['^string']) } AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + '1_orderBy': ['string_asc'], + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } + ); +}); + +test('fliter interfaced relationship type field for outgoing object type nodes', t => { + const graphQLQuery = `query { + Person(filter: { + interfacedRelationshipType: { + Genre: { + name: "Action" + } + } + }) { + userId + name + interfacedRelationshipType(filter: { + string: "data" + }) { + string + Genre { + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) WHERE (EXISTS((\`person\`)-[:INTERFACED_RELATIONSHIP_TYPE]->(:Genre)) AND ALL(\`person_filter_genre\` IN [(\`person\`)-[\`_person_filter_genre\`:INTERFACED_RELATIONSHIP_TYPE]->(:Genre) | \`_person_filter_genre\`] WHERE (ALL(\`genre\` IN [(\`person\`)-[\`person_filter_genre\`]->(\`_genre\`:Genre) | \`_genre\`] WHERE (\`genre\`.name = $filter.interfacedRelationshipType.Genre.name))))) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,interfacedRelationshipType: [(\`person\`)-[\`person_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) WHERE (\`person_interfacedRelationshipType_relation\`.string = $1_filter.string) | person_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_interfacedRelationshipType_relation\`]->(\`person_interfacedRelationshipType_Genre\`:\`Genre\`) | person_interfacedRelationshipType_Genre { .name }]) }] } AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + filter: { + interfacedRelationshipType: { + Genre: { + name: 'Action' + } + } + }, + '1_filter': { + string: 'data' + }, + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } + ); +}); + +test('query interfaced relationship type field for incoming interface type nodes', t => { + const graphQLQuery = `query { + Genre { + name + interfacedRelationshipType { + string + Person { + userId + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) RETURN \`genre\` { .name ,interfacedRelationshipType: [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) | genre_interfacedRelationshipType_Person {FRAGMENT_TYPE: head( [ label IN labels(genre_interfacedRelationshipType_Person) WHERE label IN $Person_derivedTypes ] ), .userId , .name }]) }] } AS \`genre\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('order interfaced relationship type field for incoming interface type nodes', t => { + const graphQLQuery = `query { + Genre { + name + interfacedRelationshipType(orderBy: string_desc) { + string + Person { + userId + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) RETURN \`genre\` { .name ,interfacedRelationshipType: apoc.coll.sortMulti([(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) | genre_interfacedRelationshipType_Person {FRAGMENT_TYPE: head( [ label IN labels(genre_interfacedRelationshipType_Person) WHERE label IN $Person_derivedTypes ] ), .userId , .name }]) }], ['string']) } AS \`genre\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + '1_orderBy': 'string_desc', + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('fliter interfaced relationship type field for incoming interface type nodes', t => { + const graphQLQuery = `query { + Genre(filter: { + interfacedRelationshipType: { + Person: { + userId: "45add9f4-e6c7-4ded-b951-59e3947240fe" + } + } + }) { + name + interfacedRelationshipType(filter: { + string: "data", + Person: { + name_in: ["Michael"] + } + }) { + string + Person { + userId + name + } + } + } + }`, + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) WHERE (EXISTS((\`genre\`)<-[:INTERFACED_RELATIONSHIP_TYPE]-(:Person)) AND ALL(\`genre_filter_person\` IN [(\`genre\`)<-[\`_genre_filter_person\`:INTERFACED_RELATIONSHIP_TYPE]-(:Person) | \`_genre_filter_person\`] WHERE (ALL(\`person\` IN [(\`genre\`)<-[\`genre_filter_person\`]-(\`_person\`:Person) | \`_person\`] WHERE (\`person\`.userId = $filter.interfacedRelationshipType.Person.userId))))) RETURN \`genre\` { .name ,interfacedRelationshipType: [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) WHERE (\`genre_interfacedRelationshipType_relation\`.string = $1_filter.string) AND (ALL(\`genre_filter_person\` IN [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`_person\`:Person) | \`_person\`] WHERE (\`genre_filter_person\`.name IN $1_filter.Person.name_in))) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) | genre_interfacedRelationshipType_Person {FRAGMENT_TYPE: head( [ label IN labels(genre_interfacedRelationshipType_Person) WHERE label IN $Person_derivedTypes ] ), .userId , .name }]) }] } AS \`genre\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + filter: { + interfacedRelationshipType: { + Person: { + userId: '45add9f4-e6c7-4ded-b951-59e3947240fe' + } + } + }, + '1_filter': { + string: 'data', + Person: { + name_in: ['Michael'] + } + }, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('query incoming interface type nodes using fragments in relationship type field', t => { + const graphQLQuery = `query { + Genre { + name + interfacedRelationshipType { + string + Person { + ... on User { + name } + ...UserFragment + __typename } } } + } + + fragment UserFragment on User { + userId }`, - expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {_id: ID(\`user\`), .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from {_id: ID(\`user_from\`), .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_from_rated_relation\`]->(\`user_from_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_Movie {_id: ID(\`user_from_rated_Movie\`),ratings: [(\`user_from_rated_Movie\`)<-[\`user_from_rated_Movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | user_from_rated_Movie_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`user_from_rated_Movie_ratings_relation\`]-(\`user_from_rated_Movie_ratings_User\`:\`User\`) | user_from_rated_Movie_ratings_User {_id: ID(\`user_from_rated_Movie_ratings_User\`),friends: {from: [(\`user_from_rated_Movie_ratings_User\`)<-[\`user_from_rated_Movie_ratings_User_from_relation\`:\`FRIEND_OF\`]-(\`user_from_rated_Movie_ratings_User_from\`:\`User\`) | user_from_rated_Movie_ratings_User_from_relation { .since ,User: user_from_rated_Movie_ratings_User_from {_id: ID(\`user_from_rated_Movie_ratings_User_from\`)} }] ,to: [(\`user_from_rated_Movie_ratings_User\`)-[\`user_from_rated_Movie_ratings_User_to_relation\`:\`FRIEND_OF\`]->(\`user_from_rated_Movie_ratings_User_to\`:\`User\`) | user_from_rated_Movie_ratings_User_to_relation { .since ,User: user_from_rated_Movie_ratings_User_to {_id: ID(\`user_from_rated_Movie_ratings_User_to\`)} }] } }]) }] }]) }] } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to {_id: ID(\`user_to\`), .name ,rated: [(\`user_to\`)-[\`user_to_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_to_rated_relation\`]->(\`user_to_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_Movie {_id: ID(\`user_to_rated_Movie\`)}]) }] } }] } } AS \`user\``; + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) RETURN \`genre\` { .name ,interfacedRelationshipType: [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) WHERE ("User" IN labels(genre_interfacedRelationshipType_Person)) | head([\`genre_interfacedRelationshipType_Person\` IN [\`genre_interfacedRelationshipType_Person\`] WHERE "User" IN labels(\`genre_interfacedRelationshipType_Person\`) | \`genre_interfacedRelationshipType_Person\` { FRAGMENT_TYPE: "User", .name , .userId }])]) }] } AS \`genre\``; t.plan(1); @@ -2097,24 +2879,35 @@ test('query non-reflexive relation nested in reflexive relation', t => { graphQLQuery, {}, expectedCypherQuery, - {} + { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + } ); }); -test('query relation type with argument', t => { +test('order incoming interface type nodes using fragments in relationship type field', t => { const graphQLQuery = `query { - User { - _id + Genre { name - rated(rating: 5) { - rating - Movie { - title + interfacedRelationshipType(orderBy: string_asc) { + string + Person { + ... on User { + name + } + ...UserFragment + __typename } } } + } + + fragment UserFragment on User { + userId }`, - expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {_id: ID(\`user\`), .name ,rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`{rating:$1_rating}]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_rated_relation { .rating ,Movie: head([(:\`User\`)-[\`user_rated_relation\`]->(\`user_rated_Movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_rated_Movie { .title }]) }] } AS \`user\``; + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) RETURN \`genre\` { .name ,interfacedRelationshipType: apoc.coll.sortMulti([(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) WHERE ("User" IN labels(genre_interfacedRelationshipType_Person)) | head([\`genre_interfacedRelationshipType_Person\` IN [\`genre_interfacedRelationshipType_Person\`] WHERE "User" IN labels(\`genre_interfacedRelationshipType_Person\`) | \`genre_interfacedRelationshipType_Person\` { FRAGMENT_TYPE: "User", .name , .userId }])]) }], ['^string']) } AS \`genre\``; t.plan(1); @@ -2123,33 +2916,32 @@ test('query relation type with argument', t => { graphQLQuery, {}, expectedCypherQuery, - {} + { + offset: 0, + first: -1, + '1_orderBy': 'string_asc', + cypherParams: CYPHER_PARAMS + } ); }); -test('query reflexive relation type with arguments', t => { +test('filter incoming interface type nodes using only fragments in relationship type field', t => { const graphQLQuery = `query { - User { - userId + Genre { name - friends { - from(since: 3) { - since - User { - name - } - } - to(since: 5) { - since - User { - name + interfacedRelationshipType(filter: { Person: { name_not_in: ["John"] } }) { + string + Person { + ... on User { + userId } } } + __typename } } `, - expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`{since:$1_since}]-(\`user_from\`:\`User\`) | user_from_relation { .since ,User: user_from { .name } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`{since:$3_since}]->(\`user_to\`:\`User\`) | user_to_relation { .since ,User: user_to { .name } }] } } AS \`user\``; + expectedCypherQuery = `MATCH (\`genre\`:\`Genre\`) RETURN \`genre\` { .name ,interfacedRelationshipType: [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]-(:\`Person\`) WHERE (ALL(\`genre_filter_person\` IN [(\`genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`_person\`:Person) | \`_person\`] WHERE (NOT \`genre_filter_person\`.name IN $1_filter.Person.name_not_in))) | genre_interfacedRelationshipType_relation { .string ,Person: head([(:\`Genre\`)<-[\`genre_interfacedRelationshipType_relation\`]-(\`genre_interfacedRelationshipType_Person\`:\`Person\`) WHERE ("User" IN labels(genre_interfacedRelationshipType_Person)) | head([\`genre_interfacedRelationshipType_Person\` IN [\`genre_interfacedRelationshipType_Person\`] WHERE "User" IN labels(\`genre_interfacedRelationshipType_Person\`) | \`genre_interfacedRelationshipType_Person\` { FRAGMENT_TYPE: "User", .userId }])]) }] } AS \`genre\``; t.plan(1); @@ -2158,29 +2950,85 @@ test('query reflexive relation type with arguments', t => { graphQLQuery, {}, expectedCypherQuery, - {} + { + offset: 0, + first: -1, + '1_filter': { + Person: { + name_not_in: ['John'] + } + }, + cypherParams: CYPHER_PARAMS + } ); }); -test('query using inline fragment on object type - including cypherParams', t => { - const graphQLQuery = ` - { - Movie(title: "River Runs Through It, A") { - title - ratings { - rating - User { - ... on User { - name +test('query reflexive interfaced relationship type field using fragments', t => { + const graphQLQuery = `query { + Person { + userId + name + reflexiveInterfacedRelationshipType { + from { + boolean + Person { + ...UserFragment + __typename + } + } + to { + boolean + Person { userId - currentUserId + ... on Actor { + name + } + __typename } + } + } + } + } + + fragment UserFragment on User { + name + userId + }`, + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,reflexiveInterfacedRelationshipType: {from: [(\`person\`)<-[\`person_from_relation\`:\`REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE\`]-(\`person_from\`:\`Person\`) | person_from_relation { .boolean ,Person: head([\`person_from\` IN [\`person_from\`] WHERE "User" IN labels(\`person_from\`) | \`person_from\` { FRAGMENT_TYPE: "User", .name , .userId }]) }] ,to: [(\`person\`)-[\`person_to_relation\`:\`REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE\`]->(\`person_to\`:\`Person\`) | person_to_relation { .boolean ,Person: head([\`person_to\` IN [\`person_to\`] WHERE "Actor" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "Actor", .name , .userId }] + [\`person_to\` IN [\`person_to\`] WHERE "CameraMan" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "CameraMan", .userId }] + [\`person_to\` IN [\`person_to\`] WHERE "User" IN labels(\`person_to\`) | \`person_to\` { FRAGMENT_TYPE: "User", .userId }]) }] } } AS \`person\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } + ); +}); + +test('order incoming and outgoing nodes of reflexive interfaced relationship type field', t => { + const graphQLQuery = `query { + Person { + userId + name + reflexiveInterfacedRelationshipType { + from(orderBy: boolean_desc) { + boolean + } + to(orderBy: boolean_asc) { + boolean } } } } `, - expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {title:$title}) RETURN \`movie\` { .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating ,User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User { .name , .userId ,currentUserId: apoc.cypher.runFirstColumn("RETURN $cypherParams.currentUserId AS cypherParamsUserId", {this: movie_ratings_User, cypherParams: $cypherParams, strArg: "Neo4j"}, false)}]) }] } AS \`movie\``; + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,reflexiveInterfacedRelationshipType: {from: apoc.coll.sortMulti([(\`person\`)<-[\`person_from_relation\`:\`REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE\`]-(\`person_from\`:\`Person\`) | person_from_relation { .boolean }], ['boolean']) ,to: apoc.coll.sortMulti([(\`person\`)-[\`person_to_relation\`:\`REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE\`]->(\`person_to\`:\`Person\`) | person_to_relation { .boolean }], ['^boolean']) } } AS \`person\``; t.plan(1); @@ -2189,29 +3037,53 @@ test('query using inline fragment on object type - including cypherParams', t => graphQLQuery, {}, expectedCypherQuery, - {} + { + offset: 0, + first: -1, + '1_orderBy': 'boolean_desc', + '3_orderBy': 'boolean_asc', + cypherParams: CYPHER_PARAMS, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'] + } ); }); -test('query interfaced relation using inline fragment', t => { +test('filter reflexive interfaced relationship type field', t => { const graphQLQuery = `query { - Actor { + Person(filter: { + name_in: ["Michael"] + reflexiveInterfacedRelationshipType: { + to: { + Person: { + name_in: ["John"] + } + } + } + }) { + userId name - knows { - ...userFavorites + reflexiveInterfacedRelationshipType { + to(first: 1, offset: 0, filter: { + boolean: true + }) { + boolean + Person { + userId + name + interfacedRelationshipType(first: 1) { + string + Genre { + name + } + __typename + } + __typename + } + } } } - } - - fragment userFavorites on User { - name - favorites { - movieId - title - year - } }`, - expectedCypherQuery = `MATCH (\`actor\`:\`Actor\`) RETURN \`actor\` { .name ,knows: [(\`actor\`)-[:\`KNOWS\`]->(\`actor_knows\`:\`Person\`) WHERE ("User" IN labels(\`actor_knows\`)) | head([\`actor_knows\` IN [\`actor_knows\`] WHERE "User" IN labels(\`actor_knows\`) | \`actor_knows\` { FRAGMENT_TYPE: "User", .name ,favorites: [(\`actor_knows\`)-[:\`FAVORITED\`]->(\`actor_knows_favorites\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | \`actor_knows_favorites\` { .movieId , .title , .year }] }])] } AS \`actor\``; + expectedCypherQuery = `MATCH (\`person\`:\`Person\`) WHERE (\`person\`.name IN $filter.name_in) AND ((EXISTS((\`person\`)-[:REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE]->(:Person)) AND ALL(\`person_filter_person\` IN [(\`person\`)-[\`_person_filter_person\`:REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE]->(:Person) | \`_person_filter_person\`] WHERE (ALL(\`person\` IN [(\`person\`)-[\`person_filter_person\`]->(\`_person\`:Person) | \`_person\`] WHERE (\`person\`.name IN $filter.reflexiveInterfacedRelationshipType.to.Person.name_in)))))) RETURN \`person\` {FRAGMENT_TYPE: head( [ label IN labels(\`person\`) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,reflexiveInterfacedRelationshipType: {to: [(\`person\`)-[\`person_to_relation\`:\`REFLEXIVE_INTERFACED_RELATIONSHIP_TYPE\`]->(\`person_to\`:\`Person\`) WHERE (\`person_to_relation\`.boolean = $1_filter.boolean) | person_to_relation { .boolean ,Person: person_to {FRAGMENT_TYPE: head( [ label IN labels(person_to) WHERE label IN $Person_derivedTypes ] ), .userId , .name ,interfacedRelationshipType: [(\`person_to\`)-[\`person_to_interfacedRelationshipType_relation\`:\`INTERFACED_RELATIONSHIP_TYPE\`]->(:\`Genre\`) | person_to_interfacedRelationshipType_relation { .string ,Genre: head([(:\`Person\`)-[\`person_to_interfacedRelationshipType_relation\`]->(\`person_to_interfacedRelationshipType_Genre\`:\`Genre\`) | person_to_interfacedRelationshipType_Genre { .name }]) }][..1] } }][0..1] } } AS \`person\``; t.plan(1); @@ -2220,7 +3092,25 @@ test('query interfaced relation using inline fragment', t => { graphQLQuery, {}, expectedCypherQuery, - {} + { + offset: 0, + first: -1, + filter: { + name_in: ['Michael'], + reflexiveInterfacedRelationshipType: { + to: { + Person: { + name_in: ['John'] + } + } + } + }, + '1_filter': { + boolean: true + }, + Person_derivedTypes: ['Actor', 'CameraMan', 'User'], + cypherParams: CYPHER_PARAMS + } ); }); @@ -4796,7 +5686,63 @@ test('Deeply nested query using temporal orderBy', t => { } }`, expectedCypherQuery = - "MATCH (`temporalNode`:`TemporalNode`) WITH `temporalNode` ORDER BY temporalNode.datetime DESC RETURN `temporalNode` {_id: ID(`temporalNode`),datetime: { year: `temporalNode`.datetime.year , month: `temporalNode`.datetime.month , day: `temporalNode`.datetime.day , hour: `temporalNode`.datetime.hour , minute: `temporalNode`.datetime.minute , second: `temporalNode`.datetime.second , millisecond: `temporalNode`.datetime.millisecond , microsecond: `temporalNode`.datetime.microsecond , nanosecond: `temporalNode`.datetime.nanosecond , timezone: `temporalNode`.datetime.timezone , formatted: toString(`temporalNode`.datetime) },temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes` {_id: ID(`temporalNode_temporalNodes`),datetime: `temporalNode_temporalNodes`.datetime,time: `temporalNode_temporalNodes`.time,temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode_temporalNodes`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes_temporalNodes` {_id: ID(`temporalNode_temporalNodes_temporalNodes`),datetime: `temporalNode_temporalNodes_temporalNodes`.datetime,time: `temporalNode_temporalNodes_temporalNodes`.time}], ['datetime','time']) | sortedElement { .*, datetime: {year: sortedElement.datetime.year,formatted: toString(sortedElement.datetime)},time: {hour: sortedElement.time.hour}}][1..3] }], ['^datetime']) | sortedElement { .*, datetime: {year: sortedElement.datetime.year,month: sortedElement.datetime.month,day: sortedElement.datetime.day,hour: sortedElement.datetime.hour,minute: sortedElement.datetime.minute,second: sortedElement.datetime.second,millisecond: sortedElement.datetime.millisecond,microsecond: sortedElement.datetime.microsecond,nanosecond: sortedElement.datetime.nanosecond,timezone: sortedElement.datetime.timezone,formatted: toString(sortedElement.datetime)},time: {hour: sortedElement.time.hour}}] } AS `temporalNode`"; + "MATCH (`temporalNode`:`TemporalNode`) WITH `temporalNode` ORDER BY temporalNode.datetime DESC RETURN `temporalNode` {_id: ID(`temporalNode`),datetime: { year: `temporalNode`.datetime.year , month: `temporalNode`.datetime.month , day: `temporalNode`.datetime.day , hour: `temporalNode`.datetime.hour , minute: `temporalNode`.datetime.minute , second: `temporalNode`.datetime.second , millisecond: `temporalNode`.datetime.millisecond , microsecond: `temporalNode`.datetime.microsecond , nanosecond: `temporalNode`.datetime.nanosecond , timezone: `temporalNode`.datetime.timezone , formatted: toString(`temporalNode`.datetime) },temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes` {_id: ID(`temporalNode_temporalNodes`),datetime: `temporalNode_temporalNodes`.datetime,time: `temporalNode_temporalNodes`.time,temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode_temporalNodes`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes_temporalNodes` {_id: ID(`temporalNode_temporalNodes_temporalNodes`),datetime: `temporalNode_temporalNodes_temporalNodes`.datetime,time: `temporalNode_temporalNodes_temporalNodes`.time}], ['datetime','time']) | sortedElement { .* , datetime: {year: sortedElement.datetime.year,formatted: toString(sortedElement.datetime)},time: {hour: sortedElement.time.hour}}][1..3] }], ['^datetime']) | sortedElement { .* , datetime: {year: sortedElement.datetime.year,month: sortedElement.datetime.month,day: sortedElement.datetime.day,hour: sortedElement.datetime.hour,minute: sortedElement.datetime.minute,second: sortedElement.datetime.second,millisecond: sortedElement.datetime.millisecond,microsecond: sortedElement.datetime.microsecond,nanosecond: sortedElement.datetime.nanosecond,timezone: sortedElement.datetime.timezone,formatted: toString(sortedElement.datetime)},time: {hour: sortedElement.time.hour}}] } AS `temporalNode`"; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + +test('query nested relationship with differences between selected and ordered fields', t => { + const graphQLQuery = `query { + TemporalNode(orderBy: [datetime_desc]) { + datetime { + year + formatted + } + temporalNodes(orderBy: [datetime_asc, datetime_desc]) { + _id + name + time { + hour + } + temporalNodes(first: 2, offset: 1, orderBy: [datetime_desc, name_asc]) { + _id + name + } + } + } + } + `, + expectedCypherQuery = + "MATCH (`temporalNode`:`TemporalNode`) WITH `temporalNode` ORDER BY temporalNode.datetime DESC RETURN `temporalNode` {datetime: { year: `temporalNode`.datetime.year , formatted: toString(`temporalNode`.datetime) },temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes` {_id: ID(`temporalNode_temporalNodes`), .name ,time: `temporalNode_temporalNodes`.time,temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode_temporalNodes`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes_temporalNodes` {_id: ID(`temporalNode_temporalNodes_temporalNodes`), .name ,datetime: `temporalNode_temporalNodes_temporalNodes`.datetime}], ['datetime','^name']) | sortedElement { .* }][1..3] ,datetime: `temporalNode_temporalNodes`.datetime}], ['^datetime','datetime']) | sortedElement { .* , time: {hour: sortedElement.time.hour}}] } AS `temporalNode`"; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + offset: 0, + first: -1, + '1_orderBy': ['datetime_asc', 'datetime_desc'], + '2_first': 2, + '2_offset': 1, + '2_orderBy': ['datetime_desc', 'name_asc'], + cypherParams: CYPHER_PARAMS + }) + ]); +}); + +test('Deeply nested query using temporal orderBy without temporal field selection', t => { + const graphQLQuery = `query { + TemporalNode(orderBy: [datetime_desc]) { + _id + temporalNodes(first: 2, offset: 1, orderBy: [datetime_desc, time_desc]) { + _id + } + } + }`, + expectedCypherQuery = + "MATCH (`temporalNode`:`TemporalNode`) WITH `temporalNode` ORDER BY temporalNode.datetime DESC RETURN `temporalNode` {_id: ID(`temporalNode`),temporalNodes: [sortedElement IN apoc.coll.sortMulti([(`temporalNode`)-[:`TEMPORAL`]->(`temporalNode_temporalNodes`:`TemporalNode`) | `temporalNode_temporalNodes` {_id: ID(`temporalNode_temporalNodes`),datetime: `temporalNode_temporalNodes`.datetime,time: `temporalNode_temporalNodes`.time}], ['datetime','time']) | sortedElement { .* }][1..3] } AS `temporalNode`"; t.plan(1); return Promise.all([ @@ -5346,7 +6292,7 @@ test('Handle order by field with underscores - nested field ', t => { } `, expectedCypherQuery = - 'WITH apoc.cypher.runFirstColumn("MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g", {offset:$offset, first:$first, substring:$substring, cypherParams: $cypherParams}, True) AS x UNWIND x AS `genre` RETURN `genre` {movies: apoc.coll.sortMulti([(`genre`)<-[:`IN_GENRE`]-(`genre_movies`:`Movie`:`u_user-id`:`newMovieLabel`) | `genre_movies` { .title }], [\'someprefix_title_with_underscores\']) } AS `genre`'; + 'WITH apoc.cypher.runFirstColumn("MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g", {offset:$offset, first:$first, substring:$substring, cypherParams: $cypherParams}, True) AS x UNWIND x AS `genre` RETURN `genre` {movies: apoc.coll.sortMulti([(`genre`)<-[:`IN_GENRE`]-(`genre_movies`:`Movie`:`u_user-id`:`newMovieLabel`) | `genre_movies` { .title , .someprefix_title_with_underscores }], [\'someprefix_title_with_underscores\']) } AS `genre`'; t.plan(1); return Promise.all([ @@ -5494,6 +6440,77 @@ test('query interface type relationship field', t => { ]); }); +test('query reflexive interface type relationship field', t => { + const graphQLQuery = `query { + NewCamera { + id + make + reflexiveInterfaceRelationship { + id + type + weight + } + } + } + `, + expectedCypherQuery = `MATCH (\`newCamera\`:\`NewCamera\`) RETURN \`newCamera\` { .id , .make ,reflexiveInterfaceRelationship: [(\`newCamera\`)-[:\`REFLEXIVE_INTERFACE_RELATIONSHIP\`]->(\`newCamera_reflexiveInterfaceRelationship\`:\`Camera\`) | \`newCamera_reflexiveInterfaceRelationship\` {FRAGMENT_TYPE: head( [ label IN labels(\`newCamera_reflexiveInterfaceRelationship\`) WHERE label IN $Camera_derivedTypes ] ), .id , .type , .weight }] } AS \`newCamera\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + Camera_derivedTypes: ['NewCamera', 'OldCamera'], + cypherParams: CYPHER_PARAMS + } + ); +}); + +test('query reflexive interface type relationship field using fragments', t => { + const graphQLQuery = `query { + Camera { + id + ... on NewCamera { + make + reflexiveInterfaceRelationship { + ... on Camera { + id + type + weight + make + } + } + } + reflexiveInterfaceRelationship { + ... on OldCamera { + weight + } + } + } + } + `, + expectedCypherQuery = `MATCH (\`camera\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera\`) OR "OldCamera" IN labels(\`camera\`)) RETURN head([\`camera\` IN [\`camera\`] WHERE "NewCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "NewCamera", .make ,reflexiveInterfaceRelationship: [(\`camera\`)-[:\`REFLEXIVE_INTERFACE_RELATIONSHIP\`]->(\`camera_reflexiveInterfaceRelationship\`:\`Camera\`) WHERE ("NewCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`) OR "OldCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`)) | head([\`camera_reflexiveInterfaceRelationship\` IN [\`camera_reflexiveInterfaceRelationship\`] WHERE "NewCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`) | \`camera_reflexiveInterfaceRelationship\` { FRAGMENT_TYPE: "NewCamera", .id , .type , .weight , .make }] + [\`camera_reflexiveInterfaceRelationship\` IN [\`camera_reflexiveInterfaceRelationship\`] WHERE "OldCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`) | \`camera_reflexiveInterfaceRelationship\` { FRAGMENT_TYPE: "OldCamera", .weight , .id , .type , .make }])] , .id }] + [\`camera\` IN [\`camera\`] WHERE "OldCamera" IN labels(\`camera\`) | \`camera\` { FRAGMENT_TYPE: "OldCamera", .id ,reflexiveInterfaceRelationship: [(\`camera\`)-[:\`REFLEXIVE_INTERFACE_RELATIONSHIP\`]->(\`camera_reflexiveInterfaceRelationship\`:\`Camera\`) WHERE ("OldCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`)) | head([\`camera_reflexiveInterfaceRelationship\` IN [\`camera_reflexiveInterfaceRelationship\`] WHERE "OldCamera" IN labels(\`camera_reflexiveInterfaceRelationship\`) | \`camera_reflexiveInterfaceRelationship\` { FRAGMENT_TYPE: "OldCamera", .weight }])] }]) AS \`camera\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + { + offset: 0, + first: -1, + cypherParams: CYPHER_PARAMS + } + ); +}); + test('query only __typename field on interface type relationship field', t => { const graphQLQuery = `query { Camera {