diff --git a/example/apollo-server/movies-schema.js b/example/apollo-server/movies-schema.js index 893e0dc3..0dc0e5ba 100644 --- a/example/apollo-server/movies-schema.js +++ b/example/apollo-server/movies-schema.js @@ -20,6 +20,8 @@ type Movie { actors(first: Int = 3, offset: Int = 0): [Actor] @relation(name: "ACTED_IN", direction:"IN") avgStars: Float filmedIn: State @relation(name: "FILMED_IN", direction: "OUT") + location: Point + locations: [Point] scaleRating(scale: Int = 3): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") scaleRatingFloat(scale: Float = 1.5): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") } @@ -68,6 +70,13 @@ type OnlyDate { date: Date } +type SpatialNode { + pointKey: Point + point: Point + spatialNodes(pointKey: Point): [SpatialNode] + @relation(name: "SPATIAL", direction: OUT) +} + type Book { genre: BookGenre } diff --git a/src/augment/fields.js b/src/augment/fields.js index ede6baab..b9d1bbce 100644 --- a/src/augment/fields.js +++ b/src/augment/fields.js @@ -19,6 +19,7 @@ export const isPropertyTypeField = ({ kind, type }) => isBooleanField({ type }) || isCustomScalarField({ kind }) || isTemporalField({ type }) || + isSpatialField({ type }) || isNeo4jPropertyType({ type }); export const isIntegerField = ({ type }) => @@ -34,9 +35,15 @@ export const isStringField = ({ kind, type }) => export const isBooleanField = ({ type }) => Neo4jDataType.PROPERTY[type] === 'Boolean'; +export const isNeo4jTypeField = ({ type }) => + isTemporalField({ type }) || isSpatialField({ type }); + export const isTemporalField = ({ type }) => Neo4jDataType.PROPERTY[type] === 'Temporal'; +export const isSpatialField = ({ type }) => + Neo4jDataType.PROPERTY[type] === 'Spatial'; + export const isNeo4jIDField = ({ name }) => name === Neo4jSystemIDField; export const isCustomScalarField = ({ kind }) => diff --git a/src/augment/types/spatial.js b/src/augment/types/spatial.js new file mode 100644 index 00000000..e43e2d1a --- /dev/null +++ b/src/augment/types/spatial.js @@ -0,0 +1,71 @@ +import { GraphQLInt, GraphQLString } from 'graphql'; +import { buildNeo4jTypes } from '../types/types'; + +/** + * An enum describing the name of the Neo4j Point type + */ +export const SpatialType = { + POINT: 'Point' +}; + +/** + * An enum describing the property names of the Neo4j Point type + * See: https://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial-instants + */ +const Neo4jPointField = { + X: 'x', + Y: 'y', + Z: 'z', + LONGITUDE: 'longitude', + LATITUDE: 'latitude', + HEIGHT: 'height', + CRS: 'crs', + SRID: 'srid' +}; + +/** + * A map of the Neo4j Temporal Time type fields to their respective + * GraphQL types + */ +export const Neo4jPoint = { + [Neo4jPointField.X]: GraphQLInt.name, + [Neo4jPointField.Y]: GraphQLInt.name, + [Neo4jPointField.Z]: GraphQLInt.name, + [Neo4jPointField.LONGITUDE]: GraphQLInt.name, + [Neo4jPointField.LATITUDE]: GraphQLInt.name, + [Neo4jPointField.HEIGHT]: GraphQLInt.name, + [Neo4jPointField.CRS]: GraphQLString.name, + [Neo4jPointField.SRID]: GraphQLInt.name +}; + +/** + * The main export for building the GraphQL input and output type definitions + * for Neo4j Temporal property types + */ +export const augmentSpatialTypes = ({ typeMap, config = {} }) => { + config.spatial = decideSpatialConfig({ config }); + return buildNeo4jTypes({ + typeMap, + neo4jTypes: SpatialType, + config: config.spatial + }); +}; + +/** + * A helper function for ensuring a fine-grained spatial + * configmration + */ +const decideSpatialConfig = ({ config }) => { + let defaultConfig = { + point: true + }; + const providedConfig = config ? config.spatial : defaultConfig; + if (typeof providedConfig === 'boolean') { + if (providedConfig === false) { + defaultConfig.point = false; + } + } else if (typeof providedConfig === 'object') { + defaultConfig = providedConfig; + } + return defaultConfig; +}; diff --git a/src/augment/types/temporal.js b/src/augment/types/temporal.js index 1b53c6f7..e2eff7c6 100644 --- a/src/augment/types/temporal.js +++ b/src/augment/types/temporal.js @@ -1,9 +1,9 @@ import { GraphQLInt, GraphQLString } from 'graphql'; -import { Neo4jTypeName, buildNeo4jType } from '../types/types'; -import { buildName, buildField, buildNamedType, buildInputValue } from '../ast'; +import { buildNeo4jTypes } from '../types/types'; /** * An enum describing the names of Neo4j Temporal types + * See: https://neo4j.com/docs/cypher-manual/current/syntax/temporal/#cypher-temporal-instants */ export const TemporalType = { TIME: 'Time', @@ -16,7 +16,7 @@ export const TemporalType = { /** * An enum describing the property names of the Neo4j Time type */ -const Neo4jTimeField = { +export const Neo4jTimeField = { HOUR: 'hour', MINUTE: 'minute', SECOND: 'second', @@ -29,26 +29,17 @@ const Neo4jTimeField = { /** * An enum describing the property names of the Neo4j Date type */ -const Neo4jDateField = { +export const Neo4jDateField = { YEAR: 'year', MONTH: 'month', DAY: 'day' }; -/** - * An enum describing the names of fields computed and added to the input - * and output type definitions representing non-scalar Neo4j property types - * TODO support for the Neo4j Point data type should also use this - */ -export const Neo4jTypeFormatted = { - FORMATTED: 'formatted' -}; - /** * A map of the Neo4j Temporal Time type fields to their respective * GraphQL types */ -const Neo4jTime = { +export const Neo4jTime = { [Neo4jTimeField.HOUR]: GraphQLInt.name, [Neo4jTimeField.MINUTE]: GraphQLInt.name, [Neo4jTimeField.SECOND]: GraphQLInt.name, @@ -62,7 +53,7 @@ const Neo4jTime = { * A map of the Neo4j Temporal Date type fields to their respective * GraphQL types */ -const Neo4jDate = { +export const Neo4jDate = { [Neo4jDateField.YEAR]: GraphQLInt.name, [Neo4jDateField.MONTH]: GraphQLInt.name, [Neo4jDateField.DAY]: GraphQLInt.name @@ -70,70 +61,16 @@ const Neo4jDate = { /** * The main export for building the GraphQL input and output type definitions - * for the Neo4j Temporal property types. Each TemporalType can be constructed - * using either or both of the Time and Date type fields. + * for Neo4j Temporal property types. Each TemporalType can be constructed + * using either or both of the Time and Date type fields */ -export const buildTemporalTypes = ({ typeMap, config = {} }) => { - config.temporal = decideTemporalConfig(config); - const temporalConfig = config.temporal; - Object.values(TemporalType).forEach(typeName => { - const typeNameLower = typeName.toLowerCase(); - if (temporalConfig[typeNameLower] === true) { - const objectTypeName = `${Neo4jTypeName}${typeName}`; - const inputTypeName = `${objectTypeName}Input`; - let fields = []; - if (typeName === TemporalType.DATE) { - fields = Object.entries(Neo4jDate); - } else if (typeName === TemporalType.TIME) { - fields = Object.entries(Neo4jTime); - } else if (typeName === TemporalType.LOCALTIME) { - fields = Object.entries({ - ...Neo4jTime - }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); - } else if (typeName === TemporalType.DATETIME) { - fields = Object.entries({ - ...Neo4jDate, - ...Neo4jTime - }); - } else if (typeName === TemporalType.LOCALDATETIME) { - fields = Object.entries({ - ...Neo4jDate, - ...Neo4jTime - }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); - } - let inputFields = []; - let outputFields = []; - fields.forEach(([fieldName, fieldType]) => { - const fieldNameLower = fieldName.toLowerCase(); - const fieldConfig = { - name: buildName({ name: fieldNameLower }), - type: buildNamedType({ - name: fieldType - }) - }; - inputFields.push(buildInputValue(fieldConfig)); - outputFields.push(buildField(fieldConfig)); - }); - const formattedFieldConfig = { - name: buildName({ - name: Neo4jTypeFormatted.FORMATTED - }), - type: buildNamedType({ - name: GraphQLString.name - }) - }; - inputFields.push(buildInputValue(formattedFieldConfig)); - outputFields.push(buildField(formattedFieldConfig)); - typeMap = buildNeo4jType({ - inputTypeName, - inputFields, - objectTypeName, - outputFields, - typeMap - }); - } +export const augmentTemporalTypes = ({ typeMap, config = {} }) => { + config.temporal = decideTemporalConfig({ config }); + return buildNeo4jTypes({ + typeMap, + neo4jTypes: TemporalType, + config: config.temporal }); - return typeMap; }; /** @@ -141,7 +78,7 @@ export const buildTemporalTypes = ({ typeMap, config = {} }) => { * configmration, used to simplify checking it * throughout the augmnetation process */ -const decideTemporalConfig = config => { +const decideTemporalConfig = ({ config }) => { let defaultConfig = { time: true, date: true, diff --git a/src/augment/types/types.js b/src/augment/types/types.js index 93054ade..351165b6 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -16,20 +16,28 @@ import { buildName, buildNamedType, buildObjectType, - buildInputObjectType + buildInputObjectType, + buildInputValue, + buildField } from '../ast'; -import { TemporalType, buildTemporalTypes } from './temporal'; import { - isTemporalField, + TemporalType, + augmentTemporalTypes, + Neo4jTime, + Neo4jTimeField, + Neo4jDate +} from './temporal'; +import { SpatialType, Neo4jPoint, augmentSpatialTypes } from './spatial'; +import { + isNeo4jTypeField, unwrapNamedType, - getFieldDefinition + getFieldDefinition, + isTemporalField } from '../fields'; + import { augmentNodeType } from './node/node'; import { RelationshipDirectionField } from '../types/relationship/relationship'; -// The prefix added to the name of any type representing a managed Neo4j data type -export const Neo4jTypeName = `_Neo4j`; - /** * An enum describing Neo4j entity types, used in type predicate functions */ @@ -47,6 +55,17 @@ export const OperationType = { SUBSCRIPTION: 'Subscription' }; +// The prefix added to the name of any type representing a managed Neo4j data type +export const Neo4jTypeName = `_Neo4j`; + +/** + * An enum describing the names of fields computed and added to the input + * and output type definitions representing non-scalar Neo4j property types + */ +const Neo4jTypeFormatted = { + FORMATTED: 'formatted' +}; + /** * A map of the semantics of the GraphQL type system to Neo4j data types */ @@ -63,7 +82,8 @@ export const Neo4jDataType = { [TemporalType.DATE]: 'Temporal', [TemporalType.DATETIME]: 'Temporal', [TemporalType.LOCALTIME]: 'Temporal', - [TemporalType.LOCALDATETIME]: 'Temporal' + [TemporalType.LOCALDATETIME]: 'Temporal', + [SpatialType.POINT]: 'Spatial' }, STRUCTURAL: { [Kind.OBJECT_TYPE_DEFINITION]: Neo4jStructuralType @@ -152,7 +172,8 @@ export const isSubscriptionTypeDefinition = ({ * A predicate function for identifying a GraphQL type definition representing * complex Neo4j property types (Temporal, Spatial) managed by the translation process */ -export const isNeo4jPropertyType = ({ type }) => isNeo4jTemporalType({ type }); +export const isNeo4jPropertyType = ({ type }) => + isNeo4jTemporalType({ type }) || isNeo4jPointType({ type }); /** * A predicate function for identifying a GraphQL type definition representing @@ -162,6 +183,13 @@ export const isNeo4jPropertyType = ({ type }) => isNeo4jTemporalType({ type }); export const isNeo4jTemporalType = ({ type }) => Object.values(TemporalType).some(name => type === `${Neo4jTypeName}${name}`); +/** + * A predicate function for identifying a GraphQL type definition representing + * a Neo4j Spatial type (Point) + */ +export const isNeo4jPointType = ({ type }) => + Object.values(SpatialType).some(name => type === `${Neo4jTypeName}${name}`); + /** * A predicate function for identifying which Neo4j entity type, if any, a given * GraphQL type definition represents @@ -231,7 +259,7 @@ export const augmentTypes = ({ } return definition; }); - generatedTypeMap = buildNeo4jTypes({ + generatedTypeMap = augmentNeo4jTypes({ generatedTypeMap, config }); @@ -240,10 +268,14 @@ export const augmentTypes = ({ /** * Builds the GraphQL AST type definitions that represent complex Neo4j - * property types (Temporal, Spatial) managed by the translation process + * property types (Temporal, Spatial) picked by the translation process */ -const buildNeo4jTypes = ({ generatedTypeMap, config }) => { - generatedTypeMap = buildTemporalTypes({ +const augmentNeo4jTypes = ({ generatedTypeMap, config }) => { + generatedTypeMap = augmentTemporalTypes({ + typeMap: generatedTypeMap, + config + }); + generatedTypeMap = augmentSpatialTypes({ typeMap: generatedTypeMap, config }); @@ -251,28 +283,91 @@ const buildNeo4jTypes = ({ generatedTypeMap, config }) => { }; /** - * Builds a GraphQL Object type and Input Object type representing - * the input and output schema for a complex Neo4j property type, - * ex: _Neo4jTime, _Neo4jTimeInput, etc. + * Builds the AST definitions for the object and input object + * types used for non-scalar Neo4j property types (Temporal, Spatial) */ -export const buildNeo4jType = ({ - inputTypeName, - inputFields, - objectTypeName, - outputFields, - typeMap +export const buildNeo4jTypes = ({ + typeMap = {}, + neo4jTypes = {}, + config = {} }) => { - typeMap[objectTypeName] = buildObjectType({ - name: buildName({ name: objectTypeName }), - fields: outputFields - }); - typeMap[inputTypeName] = buildInputObjectType({ - name: buildName({ name: inputTypeName }), - fields: inputFields + Object.values(neo4jTypes).forEach(typeName => { + const typeNameLower = typeName.toLowerCase(); + if (config[typeNameLower] === true) { + const fields = buildNeo4jTypeFields({ typeName }); + let inputFields = []; + let outputFields = []; + fields.forEach(([fieldName, fieldType]) => { + const fieldNameLower = fieldName.toLowerCase(); + const fieldConfig = { + name: buildName({ name: fieldNameLower }), + type: buildNamedType({ + name: fieldType + }) + }; + inputFields.push(buildInputValue(fieldConfig)); + outputFields.push(buildField(fieldConfig)); + }); + const formattedFieldConfig = { + name: buildName({ + name: Neo4jTypeFormatted.FORMATTED + }), + type: buildNamedType({ + name: GraphQLString.name + }) + }; + if (isTemporalField({ type: typeName })) { + inputFields.push(buildInputValue(formattedFieldConfig)); + outputFields.push(buildField(formattedFieldConfig)); + } + const objectTypeName = `${Neo4jTypeName}${typeName}`; + const inputTypeName = `${objectTypeName}Input`; + typeMap[objectTypeName] = buildObjectType({ + name: buildName({ name: objectTypeName }), + fields: outputFields + }); + typeMap[inputTypeName] = buildInputObjectType({ + name: buildName({ name: inputTypeName }), + fields: inputFields + }); + } }); return typeMap; }; +/** + * Builds the configuration objects for the field and input value + * definitions used by a given Neo4j type, built into AST by + * buildNeo4jTypes, then used in buildNeo4jType + */ +const buildNeo4jTypeFields = ({ typeName = '' }) => { + let fields = []; + if (typeName === TemporalType.DATE) { + fields = Object.entries(Neo4jDate); + } else if (typeName === TemporalType.TIME) { + fields = Object.entries(Neo4jTime); + } else if (typeName === TemporalType.LOCALTIME) { + fields = Object.entries({ + ...Neo4jTime + }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); + } else if (typeName === TemporalType.DATETIME) { + fields = Object.entries({ + ...Neo4jDate, + ...Neo4jTime + }); + } else if (typeName === TemporalType.LOCALDATETIME) { + fields = Object.entries({ + ...Neo4jDate, + ...Neo4jTime + }).filter(([name]) => name !== Neo4jTimeField.TIMEZONE); + } else if (typeName === SpatialType.POINT) { + fields = Object.entries({ + ...Neo4jPoint + }); + } + return fields; +}; + /** * Applies the Neo4jTypeName prefix to any Field or Input Value definition * with a type representing a complex Neo4j property type, to align with the @@ -287,9 +382,9 @@ export const transformNeo4jTypes = ({ definitions = [], config }) => { const type = field.type; const unwrappedType = unwrapNamedType({ type }); const typeName = unwrappedType.name; - if (isTemporalField({ type: typeName })) { + if (isNeo4jTypeField({ type: typeName })) { const typeNameLower = typeName.toLowerCase(); - if (config.temporal[typeNameLower]) { + if (config.temporal[typeNameLower] || config.spatial[typeNameLower]) { unwrappedType.name = `${Neo4jTypeName}${typeName}${inputTypeSuffix}`; } } else if (isNeo4jPropertyType({ type: typeName })) { @@ -305,9 +400,9 @@ export const transformNeo4jTypes = ({ definitions = [], config }) => { const type = field.type; const unwrappedType = unwrapNamedType({ type }); const typeName = unwrappedType.name; - if (isTemporalField({ type: typeName })) { + if (isNeo4jTypeField({ type: typeName })) { const typeNameLower = typeName.toLowerCase(); - if (config.temporal[typeNameLower]) { + if (config.temporal[typeNameLower] || config.spatial[typeNameLower]) { unwrappedType.name = `${Neo4jTypeName}${typeName}`; } } diff --git a/src/index.js b/src/index.js index fc4ceb43..0bcc8dc6 100644 --- a/src/index.js +++ b/src/index.js @@ -172,7 +172,8 @@ export const augmentSchema = ( config = { query: true, mutation: true, - temporal: true + temporal: true, + spatial: true } ) => { return augmentedSchema(schema, config); @@ -192,7 +193,8 @@ export const makeAugmentedSchema = ({ config = { query: true, mutation: true, - temporal: true + temporal: true, + spatial: true } }) => { if (schema) { diff --git a/src/selections.js b/src/selections.js index e16c8252..3dbd6726 100644 --- a/src/selections.js +++ b/src/selections.js @@ -13,10 +13,10 @@ import { getRelationTypeDirective, decideNestedVariableName, safeVar, - isTemporalType, - isTemporalField, - getTemporalArguments, - temporalPredicateClauses, + isNeo4jType, + isNeo4jTypeField, + getNeo4jTypeArguments, + neo4jTypePredicateClauses, removeIgnoredFields } from './utils'; import { @@ -24,8 +24,8 @@ import { relationFieldOnNodeType, relationTypeFieldOnNodeType, nodeTypeFieldOnRelationType, - temporalType, - temporalField + neo4jType, + neo4jTypeField } from './translate'; export function buildCypherSelection({ @@ -157,9 +157,9 @@ export function buildCypherSelection({ )}}, false)${commaIfTail}`, ...tailParams }); - } else if (isTemporalField(schemaType, fieldName)) { + } else if (isNeo4jTypeField(schemaType, fieldName)) { return recurse( - temporalField({ + neo4jTypeField({ initial, fieldName, variableName, @@ -229,9 +229,10 @@ export function buildCypherSelection({ !isScalarSchemaType && schemaTypeField && schemaTypeField.args ? schemaTypeField.args.map(e => e.astNode) : []; - const temporalArgs = getTemporalArguments(fieldArgs); + + const neo4jTypeArgs = getNeo4jTypeArguments(fieldArgs); const queryParams = paramsToString( - innerFilterParams(filterParams, temporalArgs) + innerFilterParams(filterParams, neo4jTypeArgs) ); const fieldInfo = { initial, @@ -241,7 +242,7 @@ export function buildCypherSelection({ nestedVariable, queryParams, filterParams, - temporalArgs, + neo4jTypeArgs, subSelection, skipLimit, commaIfTail, @@ -261,9 +262,9 @@ export function buildCypherSelection({ resolveInfo }) ); - } else if (isTemporalType(innerSchemaType.name)) { + } else if (isNeo4jType(innerSchemaType.name)) { selection = recurse( - temporalType({ + neo4jType({ schemaType, schemaTypeRelation, parentSelectionInfo, @@ -272,10 +273,10 @@ export function buildCypherSelection({ ); } else if (relType && relDirection) { // Object type field with relation directive - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, nestedVariable, - temporalArgs + neo4jTypeArgs ); // translate field, arguments and argument params const translation = relationFieldOnNodeType({ @@ -287,7 +288,7 @@ export function buildCypherSelection({ relType, isInlineFragment, innerSchemaType, - temporalClauses, + neo4jTypeClauses, resolveInfo, paramIndex, fieldArgs, @@ -307,7 +308,7 @@ export function buildCypherSelection({ paramIndex, schemaType, filterParams, - temporalArgs, + neo4jTypeArgs, parentSelectionInfo, resolveInfo, selectionFilters, @@ -325,7 +326,7 @@ export function buildCypherSelection({ schemaType, innerSchemaType, filterParams, - temporalArgs, + neo4jTypeArgs, resolveInfo, selectionFilters, paramIndex, diff --git a/src/translate.js b/src/translate.js index 61090e6b..d5e5d761 100644 --- a/src/translate.js +++ b/src/translate.js @@ -28,16 +28,17 @@ import { isRelationTypePayload, isRootSelection, splitSelectionParameters, - getTemporalArguments, - temporalPredicateClauses, + getNeo4jTypeArguments, + neo4jTypePredicateClauses, + isNeo4jType, isTemporalType, - isTemporalInputType, + isNeo4jTypeInput, isGraphqlScalarType, isGraphqlInterfaceType, innerType, relationDirective, typeIdentifiers, - decideTemporalConstructor, + decideNeo4jTypeConstructor, getAdditionalLabels } from './utils'; import { @@ -116,19 +117,19 @@ export const relationFieldOnNodeType = ({ fieldArgs, filterParams, selectionFilters, - temporalArgs, + neo4jTypeArgs, selections, schemaType, subSelection, skipLimit, commaIfTail, tailParams, - temporalClauses, + neo4jTypeClauses, resolveInfo, cypherParams }) => { const safeVariableName = safeVar(nestedVariable); - const allParams = innerFilterParams(filterParams, temporalArgs); + const allParams = innerFilterParams(filterParams, neo4jTypeArgs); const queryParams = paramsToString( _.filter(allParams, param => !Array.isArray(param.value)) ); @@ -162,7 +163,7 @@ export const relationFieldOnNodeType = ({ }_${key}`; }); const whereClauses = [ - ...temporalClauses, + ...neo4jTypeClauses, ...arrayPredicates, ...filterPredicates ]; @@ -207,7 +208,7 @@ export const relationFieldOnNodeType = ({ orderByParam ? `, [${buildSortMultiArgs(orderByParam)}])${ temporalOrdering - ? ` | sortedElement { .*, ${temporalTypeSelections( + ? ` | sortedElement { .*, ${neo4jTypeOrderingClauses( selections, innerSchemaType )}}]` @@ -236,7 +237,7 @@ export const relationTypeFieldOnNodeType = ({ nestedVariable, queryParams, filterParams, - temporalArgs, + neo4jTypeArgs, resolveInfo, selectionFilters, paramIndex, @@ -255,10 +256,10 @@ export const relationTypeFieldOnNodeType = ({ }; } const relationshipVariableName = `${nestedVariable}_relation`; - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, relationshipVariableName, - temporalArgs + neo4jTypeArgs ); const [filterPredicates, serializedFilterParam] = processFilterArgument({ fieldArgs, @@ -279,7 +280,7 @@ export const relationTypeFieldOnNodeType = ({ subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; } - const whereClauses = [...temporalClauses, ...filterPredicates]; + const whereClauses = [...neo4jTypeClauses, ...filterPredicates]; return { selection: { initial: `${initial}${fieldName}: ${ @@ -325,7 +326,7 @@ export const nodeTypeFieldOnRelationType = ({ paramIndex, schemaType, filterParams, - temporalArgs, + neo4jTypeArgs, parentSelectionInfo, resolveInfo, selectionFilters, @@ -356,7 +357,7 @@ export const nodeTypeFieldOnRelationType = ({ paramIndex, schemaType, filterParams, - temporalArgs, + neo4jTypeArgs, resolveInfo, selectionFilters, fieldArgs, @@ -400,7 +401,7 @@ const directedNodeTypeFieldOnRelationType = ({ innerSchemaType, isInlineFragment, filterParams, - temporalArgs, + neo4jTypeArgs, paramIndex, resolveInfo, selectionFilters, @@ -420,10 +421,10 @@ const directedNodeTypeFieldOnRelationType = ({ }_relation`; if (isRelationTypeDirectedField(fieldName)) { const temporalFieldRelationshipVariableName = `${nestedVariable}_relation`; - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, temporalFieldRelationshipVariableName, - temporalArgs + neo4jTypeArgs ); const [filterPredicates, serializedFilterParam] = processFilterArgument({ fieldArgs, @@ -443,7 +444,7 @@ const directedNodeTypeFieldOnRelationType = ({ ) { subSelection[1][filterParamKey] = serializedFilterParam[filterParamKey]; } - const whereClauses = [...temporalClauses, ...filterPredicates]; + const whereClauses = [...neo4jTypeClauses, ...filterPredicates]; return { selection: { initial: `${initial}${fieldName}: ${ @@ -538,7 +539,7 @@ const directedNodeTypeFieldOnRelationType = ({ } }; -export const temporalField = ({ +export const neo4jTypeField = ({ initial, fieldName, commaIfTail, @@ -582,8 +583,8 @@ export const temporalField = ({ fieldIsArray ? `${ fieldName === 'formatted' - ? `toString(TEMPORAL_INSTANCE)` - : `TEMPORAL_INSTANCE.${fieldName}` + ? `toString(INSTANCE)` + : `INSTANCE.${fieldName}` } ${commaIfTail}` : `${ fieldName === 'formatted' @@ -599,7 +600,7 @@ export const temporalField = ({ }; }; -export const temporalType = ({ +export const neo4jType = ({ initial, fieldName, subSelection, @@ -616,13 +617,14 @@ export const temporalType = ({ const parentFilterParams = parentSelectionInfo.filterParams; const parentSchemaType = parentSelectionInfo.schemaType; const safeVariableName = safeVar(variableName); + const relationshipVariableSuffix = `relation`; let fieldIsArray = isArrayType(fieldType); if (!isNodeType(schemaType.astNode)) { if ( isRelationTypePayload(schemaType) && schemaTypeRelation.from === schemaTypeRelation.to ) { - variableName = `${nestedVariable}_relation`; + variableName = `${nestedVariable}_${relationshipVariableSuffix}`; } else { if (fieldIsArray) { if ( @@ -631,23 +633,19 @@ export const temporalType = ({ rootType: 'relationship' }) ) { - if (schemaTypeRelation.from === schemaTypeRelation.to) { - variableName = `${parentVariableName}_relation`; - } else { - variableName = `${parentVariableName}_relation`; - } + variableName = `${parentVariableName}_${relationshipVariableSuffix}`; } else { - variableName = `${variableName}_relation`; + variableName = `${variableName}_${relationshipVariableSuffix}`; } } else { - variableName = `${nestedVariable}_relation`; + variableName = `${nestedVariable}_${relationshipVariableSuffix}`; } } } return { initial: `${initial}${fieldName}: ${ fieldIsArray - ? `reduce(a = [], TEMPORAL_INSTANCE IN ${variableName}.${fieldName} | a + {${ + ? `reduce(a = [], INSTANCE IN ${variableName}.${fieldName} | a + {${ subSelection[0] }})${commaIfTail}` : temporalOrderingFieldExists(parentSchemaType, parentFilterParams) @@ -679,23 +677,23 @@ export const translateQuery = ({ }); const filterParams = getFilterParams(nonNullParams); const queryArgs = getQueryArguments(resolveInfo); - const temporalArgs = getTemporalArguments(queryArgs); + const neo4jTypeArgs = getNeo4jTypeArguments(queryArgs); const queryTypeCypherDirective = getQueryCypherDirective(resolveInfo); const cypherParams = getCypherParams(context); const queryParams = paramsToString( innerFilterParams( filterParams, - temporalArgs, + neo4jTypeArgs, null, queryTypeCypherDirective ? true : false ), cypherParams ); const safeVariableName = safeVar(variableName); - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( filterParams, safeVariableName, - temporalArgs + neo4jTypeArgs ); const outerSkipLimit = getOuterSkipLimit(first, offset); const orderByValue = computeOrderBy(resolveInfo, schemaType); @@ -716,7 +714,6 @@ export const translateQuery = ({ }); } else { const additionalLabels = getAdditionalLabels(schemaType, cypherParams); - return nodeQuery({ resolveInfo, cypherParams, @@ -726,13 +723,13 @@ export const translateQuery = ({ variableName, typeName, additionalLabels, - temporalClauses, + neo4jTypeClauses, orderByValue, outerSkipLimit, nullParams, nonNullParams, filterParams, - temporalArgs, + neo4jTypeArgs, _id }); } @@ -779,7 +776,7 @@ const customQuery = ({ }); const isScalarType = isGraphqlScalarType(schemaType); const isInterfaceType = isGraphqlInterfaceType(schemaType); - const temporalType = isTemporalType(schemaType.name); + const isNeo4jTypeOutput = isNeo4jType(schemaType.name); const { cypherPart: orderByClause } = orderByValue; const query = `WITH apoc.cypher.runFirstColumn("${ cypherQueryArg.value.value @@ -787,7 +784,7 @@ const customQuery = ({ 'null'}, True) AS x UNWIND x AS ${safeVariableName} RETURN ${safeVariableName} ${ // Don't add subQuery for scalar type payloads // FIXME: fix subselection translation for temporal type payload - !temporalType && !isScalarType + !isNeo4jTypeOutput && !isScalarType ? `{${ isInterfaceType ? `FRAGMENT_TYPE: labels(${safeVariableName})[0],` @@ -807,13 +804,13 @@ const nodeQuery = ({ variableName, typeName, additionalLabels = [], - temporalClauses, + neo4jTypeClauses, orderByValue, outerSkipLimit, nullParams, nonNullParams, filterParams, - temporalArgs, + neo4jTypeArgs, _id }) => { const safeVariableName = safeVar(variableName); @@ -844,7 +841,7 @@ const nodeQuery = ({ } const arrayParams = _.pickBy(filterParams, Array.isArray); - const args = innerFilterParams(filterParams, temporalArgs); + const args = innerFilterParams(filterParams, neo4jTypeArgs); const argString = paramsToString( _.filter(args, arg => !Array.isArray(arg.value)) @@ -866,7 +863,7 @@ const nodeQuery = ({ idWherePredicate, ...filterPredicates, ...nullFieldPredicates, - ...temporalClauses, + ...neo4jTypeClauses, ...arrayPredicates ] .filter(predicate => !!predicate) @@ -1004,7 +1001,7 @@ const customMutation = ({ }); const isScalarType = isGraphqlScalarType(schemaType); const isInterfaceType = isGraphqlInterfaceType(schemaType); - const temporalType = isTemporalType(schemaType.name); + const isNeo4jTypeOutput = isNeo4jType(schemaType.name); params = { ...params, ...subParams }; if (cypherParams) { params['cypherParams'] = cypherParams; @@ -1015,7 +1012,7 @@ const customMutation = ({ }", ${argString}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS ${safeVariableName} RETURN ${safeVariableName} ${ - !temporalType && !isScalarType + !isNeo4jTypeOutput && !isScalarType ? `{${ isInterfaceType ? `FRAGMENT_TYPE: labels(${safeVariableName})[0],` @@ -1081,19 +1078,19 @@ const nodeUpdate = ({ const args = getMutationArguments(resolveInfo); const primaryKeyArg = args[0]; const primaryKeyArgName = primaryKeyArg.name.value; - const temporalArgs = getTemporalArguments(args); + const neo4jTypeArgs = getNeo4jTypeArguments(args); const [primaryKeyParam, updateParams] = splitSelectionParameters( params, primaryKeyArgName, 'params' ); - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( primaryKeyParam, safeVariableName, - temporalArgs, + neo4jTypeArgs, 'params' ); - const predicateClauses = [...temporalClauses] + const predicateClauses = [...neo4jTypeClauses] .filter(predicate => !!predicate) .join(' AND '); const predicate = predicateClauses ? `WHERE ${predicateClauses} ` : ''; @@ -1137,17 +1134,17 @@ const nodeDelete = ({ const args = getMutationArguments(resolveInfo); const primaryKeyArg = args[0]; const primaryKeyArgName = primaryKeyArg.name.value; - const temporalArgs = getTemporalArguments(args); + const neo4jTypeArgs = getNeo4jTypeArguments(args); const [primaryKeyParam] = splitSelectionParameters(params, primaryKeyArgName); - const temporalClauses = temporalPredicateClauses( + const neo4jTypeClauses = neo4jTypePredicateClauses( primaryKeyParam, safeVariableName, - temporalArgs + neo4jTypeArgs ); let [preparedParams] = buildCypherParameters({ args, params }); let query = `MATCH (${safeVariableName}:${safeLabelName}${ - temporalClauses.length > 0 - ? `) WHERE ${temporalClauses.join(' AND ')}` + neo4jTypeClauses.length > 0 + ? `) WHERE ${neo4jTypeClauses.join(' AND ')}` : ` {${primaryKeyArgName}: $${primaryKeyArgName}})` }`; const [subQuery, subParams] = buildCypherSelection({ @@ -1216,7 +1213,7 @@ const relationshipCreate = ({ typeMap[getNamedType(fromInputArg).type.name.value].astNode; const fromFields = fromInputAst.fields; const fromParam = fromFields[0].name.value; - const fromTemporalArgs = getTemporalArguments(fromFields); + const fromNodeNeo4jTypeArgs = getNeo4jTypeArguments(fromFields); const toType = toTypeArg.value.value; const toVar = `${lowFirstLetter(toType)}_to`; @@ -1224,7 +1221,7 @@ const relationshipCreate = ({ const toInputAst = typeMap[getNamedType(toInputArg).type.name.value].astNode; const toFields = toInputAst.fields; const toParam = toFields[0].name.value; - const toTemporalArgs = getTemporalArguments(toFields); + const toNodeNeo4jTypeArgs = getNeo4jTypeArguments(toFields); const relationshipName = relationshipNameArg.value.value; const lowercased = relationshipName.toLowerCase(); @@ -1254,16 +1251,16 @@ const relationshipCreate = ({ const toLabel = safeLabel([toType, ...toAdditionalLabels]); const relationshipVariable = safeVar(lowercased + '_relation'); const relationshipLabel = safeLabel(relationshipName); - const fromTemporalClauses = temporalPredicateClauses( + const fromNodeNeo4jTypeClauses = neo4jTypePredicateClauses( preparedParams.from, fromVariable, - fromTemporalArgs, + fromNodeNeo4jTypeArgs, 'from' ); - const toTemporalClauses = temporalPredicateClauses( + const toNodeNeo4jTypeClauses = neo4jTypePredicateClauses( preparedParams.to, toVariable, - toTemporalArgs, + toNodeNeo4jTypeArgs, 'to' ); const [subQuery, subParams] = buildCypherSelection({ @@ -1282,16 +1279,16 @@ const relationshipCreate = ({ params = { ...preparedParams, ...subParams }; let query = ` MATCH (${fromVariable}:${fromLabel}${ - fromTemporalClauses && fromTemporalClauses.length > 0 + fromNodeNeo4jTypeClauses && fromNodeNeo4jTypeClauses.length > 0 ? // uses either a WHERE clause for managed type primary keys (temporal, etc.) - `) WHERE ${fromTemporalClauses.join(' AND ')} ` + `) WHERE ${fromNodeNeo4jTypeClauses.join(' AND ')} ` : // or a an internal matching clause for normal, scalar property primary keys // NOTE this will need to change if we at some point allow for multi field node selection ` {${fromParam}: $from.${fromParam}})` } MATCH (${toVariable}:${toLabel}${ - toTemporalClauses && toTemporalClauses.length > 0 - ? `) WHERE ${toTemporalClauses.join(' AND ')} ` + toNodeNeo4jTypeClauses && toNodeNeo4jTypeClauses.length > 0 + ? `) WHERE ${toNodeNeo4jTypeClauses.join(' AND ')} ` : ` {${toParam}: $to.${toParam}})` } CREATE (${fromVariable})-[${relationshipVariable}:${relationshipLabel}${ @@ -1352,7 +1349,7 @@ const relationshipDelete = ({ typeMap[getNamedType(fromInputArg).type.name.value].astNode; const fromFields = fromInputAst.fields; const fromParam = fromFields[0].name.value; - const fromTemporalArgs = getTemporalArguments(fromFields); + const fromNodeNeo4jTypeArgs = getNeo4jTypeArguments(fromFields); const toType = toTypeArg.value.value; const toVar = `${lowFirstLetter(toType)}_to`; @@ -1360,7 +1357,7 @@ const relationshipDelete = ({ const toInputAst = typeMap[getNamedType(toInputArg).type.name.value].astNode; const toFields = toInputAst.fields; const toParam = toFields[0].name.value; - const toTemporalArgs = getTemporalArguments(toFields); + const toNodeNeo4jTypeArgs = getNeo4jTypeArguments(toFields); const relationshipName = relationshipNameArg.value.value; @@ -1381,16 +1378,16 @@ const relationshipDelete = ({ const relationshipLabel = safeLabel(relationshipName); const fromRootVariable = safeVar('_' + fromVar); const toRootVariable = safeVar('_' + toVar); - const fromTemporalClauses = temporalPredicateClauses( + const fromNodeNeo4jTypeClauses = neo4jTypePredicateClauses( params.from, fromVariable, - fromTemporalArgs, + fromNodeNeo4jTypeArgs, 'from' ); - const toTemporalClauses = temporalPredicateClauses( + const toNodeNeo4jTypeClauses = neo4jTypePredicateClauses( params.to, toVariable, - toTemporalArgs, + toNodeNeo4jTypeArgs, 'to' ); // TODO cleaner semantics: remove use of _ prefixes in root variableNames and variableName @@ -1409,15 +1406,15 @@ const relationshipDelete = ({ params = { ...params, ...subParams }; let query = ` MATCH (${fromVariable}:${fromLabel}${ - fromTemporalClauses && fromTemporalClauses.length > 0 + fromNodeNeo4jTypeClauses && fromNodeNeo4jTypeClauses.length > 0 ? // uses either a WHERE clause for managed type primary keys (temporal, etc.) - `) WHERE ${fromTemporalClauses.join(' AND ')} ` + `) WHERE ${fromNodeNeo4jTypeClauses.join(' AND ')} ` : // or a an internal matching clause for normal, scalar property primary keys ` {${fromParam}: $from.${fromParam}})` } MATCH (${toVariable}:${toLabel}${ - toTemporalClauses && toTemporalClauses.length > 0 - ? `) WHERE ${toTemporalClauses.join(' AND ')} ` + toNodeNeo4jTypeClauses && toNodeNeo4jTypeClauses.length > 0 + ? `) WHERE ${toNodeNeo4jTypeClauses.join(' AND ')} ` : ` {${toParam}: $to.${toParam}})` } OPTIONAL MATCH (${fromVariable})-[${relationshipVariable}:${relationshipLabel}]->(${toVariable}) @@ -1428,7 +1425,7 @@ const relationshipDelete = ({ return [query, params]; }; -const temporalTypeSelections = (selections, innerSchemaType) => { +const neo4jTypeOrderingClauses = (selections, innerSchemaType) => { // TODO use extractSelections instead? const selectedTypes = selections && selections[0] && selections[0].selectionSet @@ -1650,8 +1647,8 @@ const analyzeFilterArgument = ({ if (isExistentialFilter(filterOperationType, filterValue)) { serializedFilterParam = true; filterMapValue = null; - } else if (isTemporalInputType(typeName)) { - serializedFilterParam = serializeTemporalParam(filterValue); + } else if (isNeo4jTypeInput(typeName)) { + serializedFilterParam = serializeNeo4jTypeParam(filterValue); } else if (isRelation || isRelationType || isRelationTypeNode) { // recursion [serializedFilterParam, filterMapValue] = analyzeNestedFilterArgument( @@ -1737,7 +1734,7 @@ const serializeFilterFieldName = (name, value) => { return name; }; -const serializeTemporalParam = filterValue => { +const serializeNeo4jTypeParam = filterValue => { const isList = Array.isArray(filterValue); if (!isList) filterValue = [filterValue]; let serializedValues = filterValue.reduce((serializedValues, filter) => { @@ -2089,9 +2086,9 @@ const translateInputFilter = ({ innerSchemaType, filterOperationField }); - if (isTemporalInputType(typeName)) { - const temporalFunction = decideTemporalConstructor(typeName); - return translateTemporalFilter({ + if (isNeo4jTypeInput(typeName)) { + return translateNeo4jTypeFilter({ + typeName, isRelationTypeNode, filterValue, variableName, @@ -2103,8 +2100,7 @@ const translateInputFilter = ({ parameterPath, parentParamPath, isListFilterArgument, - nullFieldPredicate, - temporalFunction + nullFieldPredicate }); } else if (isRelation || isRelationType || isRelationTypeNode) { return translateRelationFilter({ @@ -2521,23 +2517,16 @@ const buildRelatedTypeListComprehension = ({ isRelationType }) => { let relationVariable = buildRelationVariable(thisType, relatedType); - let nodeVariable = safeVar(variableName); - - // prevents related node variable from - // conflicting with parent variables and relation variable - // and conflicting with left node variable if (rootIsRelationType) { relationVariable = variableName; - nodeVariable = safeVar(lowFirstLetter(thisType)); - } - if (relationVariable === variableName) { - nodeVariable = safeVar(lowFirstLetter(thisType)); } - + const thisTypeVariable = safeVar(lowFirstLetter(thisType)); + // prevents related node variable from + // conflicting with parent variables const relatedTypeVariable = safeVar(`_${relatedType.toLowerCase()}`); // builds a path pattern within a list comprehension // that extracts related nodes - return `[(${nodeVariable})${relationDirection === 'IN' ? '<' : ''}-[${ + return `[(${thisTypeVariable})${relationDirection === 'IN' ? '<' : ''}-[${ isRelationType ? safeVar(`_${relationVariable}`) : isRelationTypeNode @@ -2605,7 +2594,8 @@ const buildFilterPredicates = ({ .join(' AND '); }; -const translateTemporalFilter = ({ +const translateNeo4jTypeFilter = ({ + typeName, isRelationTypeNode, filterValue, variableName, @@ -2617,9 +2607,9 @@ const translateTemporalFilter = ({ parameterPath, parentParamPath, isListFilterArgument, - nullFieldPredicate, - temporalFunction + nullFieldPredicate }) => { + const cypherTypeConstructor = decideNeo4jTypeConstructor(typeName); const safeVariableName = safeVar(variableName); const propertyPath = `${safeVariableName}.${filterOperationField}`; if (isExistentialFilter(filterOperationType, filterValue)) { @@ -2647,7 +2637,7 @@ const translateTemporalFilter = ({ variableName, nullFieldPredicate, rootPredicateFunction, - temporalFunction + cypherTypeConstructor }); }; @@ -2660,7 +2650,7 @@ const buildTemporalPredicate = ({ variableName, nullFieldPredicate, rootPredicateFunction, - temporalFunction + cypherTypeConstructor }) => { // ex: project -> person_filter_project const isListFilterArgument = isListType(fieldType); @@ -2674,7 +2664,7 @@ const buildTemporalPredicate = ({ propertyPath, isListFilterArgument ); - let translation = `(${nullFieldPredicate}${operatorExpression} ${temporalFunction}(${listVariable}))`; + let translation = `(${nullFieldPredicate}${operatorExpression} ${cypherTypeConstructor}(${listVariable}))`; if (isListFilterArgument) { translation = buildPredicateFunction({ predicateListVariable: parameterPath, diff --git a/src/utils.js b/src/utils.js index 25f04cc3..123adea3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -257,12 +257,12 @@ export function getFilterParams(filters, index) { export function innerFilterParams( filters, - temporalArgs, + neo4jTypeArgs, paramKey, cypherDirective ) { - const temporalArgNames = temporalArgs - ? temporalArgs.reduce((acc, t) => { + const temporalArgNames = neo4jTypeArgs + ? neo4jTypeArgs.reduce((acc, t) => { acc.push(t.name.value); return acc; }, []) @@ -406,16 +406,19 @@ export const buildCypherParameters = ({ const fieldAst = args.find(arg => arg.name.value === paramName); if (fieldAst) { const fieldType = _getNamedType(fieldAst.type); - if (isTemporalInputType(fieldType.name.value)) { + const fieldTypeName = fieldType.name.value; + if (isNeo4jTypeInput(fieldTypeName)) { const formatted = param.formatted; - const temporalFunction = getTemporalCypherConstructor(fieldAst); - if (temporalFunction) { + const neo4jTypeConstructor = decideNeo4jTypeConstructor( + fieldTypeName + ); + if (neo4jTypeConstructor) { // Prefer only using formatted, if provided if (formatted) { if (paramKey) params[paramKey][paramName] = formatted; else params[paramName] = formatted; acc.push( - `${paramName}: ${temporalFunction}($${ + `${paramName}: ${neo4jTypeConstructor}($${ paramKey ? `${paramKey}.` : '' }${paramName})` ); @@ -448,7 +451,7 @@ export const buildCypherParameters = ({ acc.push( `${paramName}: [value IN $${ paramKey ? `${paramKey}.` : '' - }${paramName} | ${temporalFunction}(value)]` + }${paramName} | ${neo4jTypeConstructor}(value)]` ); } else { temporalParam = paramKey @@ -471,7 +474,7 @@ export const buildCypherParameters = ({ }); } acc.push( - `${paramName}: ${temporalFunction}($${ + `${paramName}: ${neo4jTypeConstructor}($${ paramKey ? `${paramKey}.` : '' }${paramName})` ); @@ -805,6 +808,45 @@ export const splitSelectionParameters = ( return [primaryKeyParam, updateParams]; }; +export const isNeo4jTypeField = (schemaType, fieldName) => + isTemporalField(schemaType, fieldName) || + isSpatialField(schemaType, fieldName); + +export const isNeo4jType = name => isTemporalType(name) || isSpatialType(name); + +export const isNeo4jTypeInput = name => + isTemporalInputType(name) || isSpatialInputType(name); + +export const isSpatialType = name => name === '_Neo4jPoint'; + +export const isSpatialField = (schemaType, name) => { + const type = schemaType ? schemaType.name : ''; + return ( + isSpatialType(type) && + (name === 'x' || + name === 'y' || + name === 'z' || + name === 'longitude' || + name === 'latitude' || + name === 'height' || + name === 'crs' || + name === 'srid' || + name === 'formatted') + ); +}; + +export const isSpatialInputType = name => name === '_Neo4jPointInput'; + +export const isTemporalType = name => { + return ( + name === '_Neo4jTime' || + name === '_Neo4jDate' || + name === '_Neo4jDateTime' || + name === '_Neo4jLocalTime' || + name === '_Neo4jLocalDateTime' + ); +}; + export const isTemporalField = (schemaType, name) => { const type = schemaType ? schemaType.name : ''; return ( @@ -823,22 +865,17 @@ export const isTemporalField = (schemaType, name) => { ); }; -export const isTemporalType = name => { +export const isTemporalInputType = name => { return ( - name === '_Neo4jTime' || - name === '_Neo4jDate' || - name === '_Neo4jDateTime' || - name === '_Neo4jLocalTime' || - name === '_Neo4jLocalDateTime' + name === '_Neo4jTimeInput' || + name === '_Neo4jDateInput' || + name === '_Neo4jDateTimeInput' || + name === '_Neo4jLocalTimeInput' || + name === '_Neo4jLocalDateTimeInput' ); }; -export const getTemporalCypherConstructor = fieldAst => { - const type = fieldAst ? _getNamedType(fieldAst.type).name.value : ''; - return decideTemporalConstructor(type); -}; - -export const decideTemporalConstructor = typeName => { +export const decideNeo4jTypeConstructor = typeName => { switch (typeName) { case '_Neo4jTimeInput': return 'time'; @@ -850,55 +887,35 @@ export const decideTemporalConstructor = typeName => { return 'localtime'; case '_Neo4jLocalDateTimeInput': return 'localdatetime'; + case '_Neo4jPointInput': + return 'point'; default: return ''; } }; -export const getTemporalArguments = args => { - return args - ? args.reduce((acc, t) => { - if (!t) { - return acc; - } - const fieldType = _getNamedType(t.type).name.value; - if (isTemporalInputType(fieldType)) acc.push(t); - return acc; - }, []) - : []; -}; - -export const isTemporalInputType = name => { - return ( - name === '_Neo4jTimeInput' || - name === '_Neo4jDateInput' || - name === '_Neo4jDateTimeInput' || - name === '_Neo4jLocalTimeInput' || - name === '_Neo4jLocalDateTimeInput' - ); -}; - -export const temporalPredicateClauses = ( +export const neo4jTypePredicateClauses = ( filters, variableName, - temporalArgs, + fieldArguments, parentParam ) => { - return temporalArgs.reduce((acc, t) => { + return fieldArguments.reduce((acc, t) => { // For every temporal argument const argName = t.name.value; - let temporalParam = filters[argName]; - if (temporalParam) { + let neo4jTypeParam = filters[argName]; + if (neo4jTypeParam) { // If a parameter value has been provided for it check whether // the provided param value is in an indexed object for a nested argument - const paramIndex = temporalParam.index; - const paramValue = temporalParam.value; + const paramIndex = neo4jTypeParam.index; + const paramValue = neo4jTypeParam.value; // If it is, set and use its .value - if (paramValue) temporalParam = paramValue; - if (temporalParam['formatted']) { + if (paramValue) neo4jTypeParam = paramValue; + if (neo4jTypeParam['formatted']) { // Only the dedicated 'formatted' arg is used if it is provided + const type = t ? _getNamedType(t.type).name.value : ''; acc.push( - `${variableName}.${argName} = ${getTemporalCypherConstructor(t)}($${ + `${variableName}.${argName} = ${decideNeo4jTypeConstructor(type)}($${ // use index if provided, for nested arguments typeof paramIndex === 'undefined' ? `${parentParam ? `${parentParam}.` : ''}${argName}.formatted` @@ -908,7 +925,7 @@ export const temporalPredicateClauses = ( })` ); } else { - Object.keys(temporalParam).forEach(e => { + Object.keys(neo4jTypeParam).forEach(e => { acc.push( `${variableName}.${argName}.${e} = $${ typeof paramIndex === 'undefined' @@ -925,6 +942,19 @@ export const temporalPredicateClauses = ( }, []); }; +export const getNeo4jTypeArguments = args => { + return args + ? args.reduce((acc, t) => { + if (!t) { + return acc; + } + const fieldType = _getNamedType(t.type).name.value; + if (isNeo4jTypeInput(fieldType)) acc.push(t); + return acc; + }, []) + : []; +}; + export const removeIgnoredFields = (schemaType, selections) => { if (!isGraphqlScalarType(schemaType) && selections && selections.length) { let schemaTypeField = ''; diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 752b6647..2d6e0409 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -29,6 +29,7 @@ type MutationB { computedObjectWithCypherParams: currentUserId @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") computedStringList: [String] @cypher(statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList") computedTemporal: DateTime @cypher(statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }") + computedSpatial: Point @cypher(statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }") customWithArguments(strArg: String, strInputArg: strInput): String @cypher(statement: "RETURN $strInputArg.strArg") } `; @@ -62,6 +63,7 @@ type MutationB { computedFloat: checkCypherQuery, currentUserId: checkCypherQuery, computedTemporal: checkCypherQuery, + computedSpatial: checkCypherQuery, computedObjectWithCypherParams: checkCypherQuery, computedStringList: checkCypherQuery, computedIntList: checkCypherQuery, @@ -77,6 +79,7 @@ type MutationB { computedObjectWithCypherParams: checkCypherMutation, computedStringList: checkCypherMutation, computedTemporal: checkCypherMutation, + computedSpatial: checkCypherMutation, customWithArguments: checkCypherMutation } }; @@ -165,6 +168,7 @@ export function augmentedSchemaCypherTestRunner( t.is(query, expectedCypherQuery); t.deepEqual(queryParams, expectedCypherParams); }, + SpatialNode: checkCypherQuery, State: checkCypherQuery, CasedType: checkCypherQuery, computedBoolean: checkCypherQuery, @@ -172,6 +176,7 @@ export function augmentedSchemaCypherTestRunner( computedFloat: checkCypherQuery, currentUserId: checkCypherQuery, computedTemporal: checkCypherQuery, + computedSpatial: checkCypherQuery, computedObjectWithCypherParams: checkCypherQuery, computedStringList: checkCypherQuery, computedIntList: checkCypherQuery, @@ -185,6 +190,11 @@ export function augmentedSchemaCypherTestRunner( DeleteTemporalNode: checkCypherMutation, AddTemporalNodeTemporalNodes: checkCypherMutation, RemoveTemporalNodeTemporalNodes: checkCypherMutation, + CreateSpatialNode: checkCypherMutation, + UpdateSpatialNode: checkCypherMutation, + DeleteSpatialNode: checkCypherMutation, + AddSpatialNodeSpatialNodes: checkCypherMutation, + RemoveSpatialNodeSpatialNodes: checkCypherMutation, AddMovieGenres: checkCypherMutation, RemoveMovieGenres: checkCypherMutation, AddUserRated: checkCypherMutation, @@ -195,6 +205,7 @@ export function augmentedSchemaCypherTestRunner( computedObjectWithCypherParams: checkCypherMutation, computedStringList: checkCypherMutation, computedTemporal: checkCypherMutation, + computedSpatial: checkCypherMutation, customWithArguments: checkCypherMutation } }; diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index a26d5185..65747cac 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -28,6 +28,8 @@ export const testSchema = /* GraphQL */ ` ): [Actor] @relation(name: "ACTED_IN", direction: "IN") avgStars: Float filmedIn: State @relation(name: "FILMED_IN", direction: "OUT") + location: Point + locations: [Point] scaleRating(scale: Int = 3): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") scaleRatingFloat(scale: Float = 1.5): Float @@ -43,6 +45,7 @@ export const testSchema = /* GraphQL */ ` datetime: DateTime localtime: LocalTime localdatetime: LocalDateTime + location: Point ): [Rated] years: [Int] titles: [String] @@ -96,6 +99,7 @@ export const testSchema = /* GraphQL */ ` datetime: DateTime localtime: LocalTime localdatetime: LocalDateTime + location: Point ): [Rated] friends( since: Int @@ -104,6 +108,7 @@ export const testSchema = /* GraphQL */ ` datetime: DateTime localtime: LocalTime localdatetime: LocalDateTime + location: Point ): [FriendOf] favorites: [Movie] @relation(name: "FAVORITED", direction: "OUT") } @@ -121,6 +126,7 @@ export const testSchema = /* GraphQL */ ` datetimes: [DateTime] localtime: LocalTime localdatetime: LocalDateTime + location: Point to: User } @@ -138,6 +144,7 @@ export const testSchema = /* GraphQL */ ` localtime: LocalTime localdatetime: LocalDateTime datetimes: [DateTime] + location: Point to: Movie } @@ -171,6 +178,7 @@ export const testSchema = /* GraphQL */ ` plot: String poster: String imdbRating: Float + location: Point first: Int offset: Int orderBy: _MovieOrdering @@ -201,6 +209,10 @@ export const testSchema = /* GraphQL */ ` @cypher( statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" ) + computedSpatial: Point + @cypher( + statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }" + ) computedObjectWithCypherParams: currentUserId @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") customWithArguments(strArg: String, strInputArg: strInput): String @@ -217,6 +229,10 @@ export const testSchema = /* GraphQL */ ` @cypher( statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" ) + computedSpatial: Point + @cypher( + statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }" + ) computedStringList: [String] @cypher( statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" @@ -248,6 +264,13 @@ export const testSchema = /* GraphQL */ ` ): [TemporalNode] @relation(name: "TEMPORAL", direction: OUT) } + type SpatialNode { + pointKey: Point + point: Point + spatialNodes(pointKey: Point): [SpatialNode] + @relation(name: "SPATIAL", direction: OUT) + } + type ignoredType { ignoredField: String @neo4j_ignore } diff --git a/test/integration/integration.test.js b/test/integration/integration.test.js index 6a6158f2..5e512593 100644 --- a/test/integration/integration.test.js +++ b/test/integration/integration.test.js @@ -1100,6 +1100,251 @@ test.serial( } ); +/* + * Spatial type tests + */ + +// Spatial node property +test.serial( + 'Spatial - Create node with spatial property (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + CreateMovie: { + __typename: 'Movie', + title: 'Bob Loblaw 4', + location: { + __typename: '_Neo4jPoint', + latitude: 20, + longitude: 10, + height: 30 + } + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation { + CreateMovie( + title: "Bob Loblaw 4" + location: { longitude: 10, latitude: 20, height: 30 } + ) { + title + location { + longitude + latitude + height + } + } + } + ` + }) + .then(data => { + t.deepEqual(data, expected); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Spatial - Create node with spatial property - with GraphQL variables (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + CreateMovie: { + __typename: 'Movie', + title: 'Bob Loblaw 5', + location: { + __typename: '_Neo4jPoint', + latitude: 40, + longitude: 50, + height: 60 + } + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation createWithSpatialFields( + $title: String + $locationInput: _Neo4jPointInput + ) { + CreateMovie(title: $title, location: $locationInput) { + title + location { + longitude + latitude + height + } + } + } + `, + variables: { + title: 'Bob Loblaw 5', + locationInput: { + longitude: 50, + latitude: 40, + height: 60 + } + } + }) + .then(data => { + t.deepEqual(data, expected); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Spatial - Query node with spatial field (not-isolated)', + async t => { + let expected = { + data: { + Movie: [ + { + __typename: 'Movie', + location: { + __typename: '_Neo4jPoint', + crs: 'wgs-84-3d', + height: 60, + latitude: 40, + longitude: 50 + }, + title: 'Bob Loblaw 5' + } + ] + } + }; + + await client + .query({ + query: gql` + { + Movie(title: "Bob Loblaw 5") { + title + location { + longitude + latitude + height + crs + } + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Spatial - create node with only a spatial property (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + CreateSpatialNode: { + __typename: 'SpatialNode', + pointKey: { + __typename: '_Neo4jPoint', + crs: 'wgs-84-3d', + latitude: 20, + longitude: 10, + height: 30 + } + } + } + }; + + await client + .mutate({ + mutation: gql` + mutation { + CreateSpatialNode( + pointKey: { longitude: 10, latitude: 20, height: 30 } + ) { + pointKey { + longitude + latitude + height + crs + } + } + } + ` + }) + .then(data => { + t.deepEqual(data, expected); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + +test.serial( + 'Spatial - spatial query argument, components (not-isolated)', + async t => { + t.plan(1); + + let expected = { + data: { + SpatialNode: [ + { + __typename: 'SpatialNode', + pointKey: { + __typename: '_Neo4jPoint', + crs: 'wgs-84-3d', + latitude: 20, + longitude: 10, + height: 30 + } + } + ] + } + }; + + await client + .query({ + query: gql` + { + SpatialNode(pointKey: { longitude: 10, latitude: 20, height: 30 }) { + pointKey { + longitude + latitude + height + crs + } + } + } + ` + }) + .then(data => { + t.deepEqual(data.data, expected.data); + }) + .catch(error => { + t.fail(error.message); + }); + } +); + test('Basic filter', async t => { t.plan(1); diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index f64d9d5d..21abfd4e 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -49,6 +49,7 @@ test.cb('Test augmented schema', t => { plot: String poster: String imdbRating: Float + location: _Neo4jPointInput first: Int offset: Int orderBy: [_MovieOrdering] @@ -117,6 +118,10 @@ test.cb('Test augmented schema', t => { @cypher( statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" ) + computedSpatial: _Neo4jPoint + @cypher( + statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }" + ) computedObjectWithCypherParams: currentUserId @cypher(statement: "RETURN { userId: $cypherParams.currentUserId }") customWithArguments(strArg: String, strInputArg: strInput): String @@ -167,6 +172,15 @@ test.cb('Test augmented schema', t => { orderBy: [_TemporalNodeOrdering] filter: _TemporalNodeFilter ): [TemporalNode] @hasScope(scopes: ["TemporalNode: Read"]) + SpatialNode( + pointKey: _Neo4jPointInput + point: _Neo4jPointInput + _id: String + first: Int + offset: Int + orderBy: [_SpatialNodeOrdering] + filter: _SpatialNodeFilter + ): [SpatialNode] @hasScope(scopes: ["SpatialNode: Read"]) } input _Neo4jDateTimeInput { @@ -672,6 +686,8 @@ test.cb('Test augmented schema', t => { avgStars: Float filmedIn(filter: _StateFilter): State @relation(name: "FILMED_IN", direction: "OUT") + location: _Neo4jPoint + locations: [_Neo4jPoint] scaleRating(scale: Int = 3): Float @cypher(statement: "WITH $this AS this RETURN $scale * this.imdbRating") scaleRatingFloat(scale: Float = 1.5): Float @@ -687,6 +703,7 @@ test.cb('Test augmented schema', t => { datetime: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput + location: _Neo4jPointInput filter: _MovieRatedFilter ): [_MovieRatings] years: [Int] @@ -779,6 +796,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] + location: _Neo4jPoint User: User } @@ -837,6 +855,7 @@ test.cb('Test augmented schema', t => { datetime: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput + location: _Neo4jPointInput filter: _UserRatedFilter ): [_UserRated] friends: _UserFriendsDirections @@ -866,6 +885,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] + location: _Neo4jPoint Movie: Movie } @@ -878,6 +898,7 @@ test.cb('Test augmented schema', t => { datetime: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput + location: _Neo4jPointInput filter: _FriendOfFilter ): [_UserFriends] to( @@ -887,6 +908,7 @@ test.cb('Test augmented schema', t => { datetime: _Neo4jDateTimeInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput + location: _Neo4jPointInput filter: _FriendOfFilter ): [_UserFriends] } @@ -903,6 +925,7 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTime] localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime + location: _Neo4jPoint User: User } @@ -1104,6 +1127,10 @@ test.cb('Test augmented schema', t => { @cypher( statement: "WITH datetime() AS now RETURN { year: now.year, month: now.month , day: now.day , hour: now.hour , minute: now.minute , second: now.second , millisecond: now.millisecond , microsecond: now.microsecond , nanosecond: now.nanosecond , timezone: now.timezone , formatted: toString(now) }" ) + computedSpatial: _Neo4jPoint + @cypher( + statement: "WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }" + ) computedStringList: [String] @cypher( statement: "UNWIND ['hello', 'world'] AS stringList RETURN stringList" @@ -1167,6 +1194,8 @@ test.cb('Test augmented schema', t => { poster: String imdbRating: Float avgStars: Float + location: _Neo4jPointInput + locations: [_Neo4jPointInput] years: [Int] titles: [String] imdbRatings: [Float] @@ -1182,6 +1211,8 @@ test.cb('Test augmented schema', t => { poster: String imdbRating: Float avgStars: Float + location: _Neo4jPointInput + locations: [_Neo4jPointInput] years: [Int] titles: [String] imdbRatings: [Float] @@ -1306,6 +1337,35 @@ test.cb('Test augmented schema', t => { ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode @hasScope(scopes: ["TemporalNode: Delete"]) + AddSpatialNodeSpatialNodes( + from: _SpatialNodeInput! + to: _SpatialNodeInput! + ): _AddSpatialNodeSpatialNodesPayload + @MutationMeta( + relationship: "SPATIAL" + from: "SpatialNode" + to: "SpatialNode" + ) + RemoveSpatialNodeSpatialNodes( + from: _SpatialNodeInput! + to: _SpatialNodeInput! + ): _RemoveSpatialNodeSpatialNodesPayload + @MutationMeta( + relationship: "SPATIAL" + from: "SpatialNode" + to: "SpatialNode" + ) + @hasScope(scopes: ["SpatialNode: Delete", "SpatialNode: Delete"]) + CreateSpatialNode( + pointKey: _Neo4jPointInput + point: _Neo4jPointInput + ): SpatialNode @hasScope(scopes: ["SpatialNode: Create"]) + UpdateSpatialNode( + pointKey: _Neo4jPointInput! + point: _Neo4jPointInput + ): SpatialNode @hasScope(scopes: ["SpatialNode: Update"]) + DeleteSpatialNode(pointKey: _Neo4jPointInput!): SpatialNode + @hasScope(scopes: ["SpatialNode: Delete"]) AddCasedTypeState( from: _CasedTypeInput! to: _StateInput! @@ -1388,6 +1448,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput datetimes: [_Neo4jDateTimeInput] + location: _Neo4jPointInput } type _AddMovieRatingsPayload @@ -1406,6 +1467,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] + location: _Neo4jPoint } type _RemoveMovieRatingsPayload @@ -1454,6 +1516,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] + location: _Neo4jPoint } type _RemoveUserRatedPayload @@ -1470,6 +1533,7 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTimeInput] localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput + location: _Neo4jPointInput } type _AddUserFriendsPayload @@ -1487,6 +1551,7 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTime] localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime + location: _Neo4jPoint } type _RemoveUserFriendsPayload @@ -1556,6 +1621,7 @@ test.cb('Test augmented schema', t => { datetimes: [_Neo4jDateTime] localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime + location: _Neo4jPoint to: User } @@ -1573,6 +1639,7 @@ test.cb('Test augmented schema', t => { localtime: _Neo4jLocalTime localdatetime: _Neo4jLocalDateTime datetimes: [_Neo4jDateTime] + location: _Neo4jPoint to: Movie } @@ -1626,6 +1693,28 @@ test.cb('Test augmented schema', t => { admin } + type _Neo4jPoint { + x: Int + y: Int + z: Int + longitude: Int + latitude: Int + height: Int + crs: String + srid: Int + } + + input _Neo4jPointInput { + x: Int + y: Int + z: Int + longitude: Int + latitude: Int + height: Int + crs: String + srid: Int + } + enum _RelationDirections { IN OUT diff --git a/test/unit/configTest.test.js b/test/unit/configTest.test.js index c2925ae0..f4897f71 100644 --- a/test/unit/configTest.test.js +++ b/test/unit/configTest.test.js @@ -185,6 +185,57 @@ test.cb('Config - temporal - disable temporal schema augmentation', t => { } }); + t.is(printSchema(schema).includes('_Neo4jDateTime'), false); t.is(printSchema(schema).includes('_Neo4jDateTimeInput'), false); t.end(); }); + +test.cb( + 'Config - temporal - disable temporal schema augmentation (type specific)', + t => { + const schema = makeAugmentedSchema({ + typeDefs, + config: { + temporal: { + time: false, + date: false, + datetime: false, + localtime: false + } + } + }); + + t.is(printSchema(schema).includes('_Neo4jDateTime'), false); + t.is(printSchema(schema).includes('_Neo4jDateTimeInput'), false); + t.end(); + } +); + +test.cb('Config - spatial - disable spatial schema augmentation', t => { + const schema = makeAugmentedSchema({ + typeDefs, + config: { + spatial: false + } + }); + t.is(printSchema(schema).includes('_Neo4jPoint'), false); + t.is(printSchema(schema).includes('_Neo4jPointInput'), false); + t.end(); +}); + +test.cb( + 'Config - spatial - disable spatial schema augmentation (type specific)', + t => { + const schema = makeAugmentedSchema({ + typeDefs, + config: { + spatial: { + point: false + } + } + }); + t.is(printSchema(schema).includes('_Neo4jPoint'), false); + t.is(printSchema(schema).includes('_Neo4jPointInput'), false); + t.end(); + } +); diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index baf91dd9..20fe6446 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -1748,6 +1748,50 @@ test('Create node with temporal properties', t => { ); }); +test('Create node with spatial properties', t => { + const graphQLQuery = `mutation { + CreateSpatialNode( + pointKey: { + x: 10, + y: 20, + z: 30 + }, + point: { + longitude: 40, + latitude: 50, + height: 60 + } + ) { + pointKey { + x + y + z + crs + } + point { + longitude + latitude + height + crs + } + } + }`, + expectedCypherQuery = ` + CREATE (\`spatialNode\`:\`SpatialNode\` {pointKey: point($params.pointKey),point: point($params.point)}) + RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z , crs: \`spatialNode\`.pointKey.crs },point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\` + `; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Query node with temporal properties using temporal arguments', t => { const graphQLQuery = `query { TemporalNode( @@ -1865,6 +1909,43 @@ test('Query node with temporal properties using temporal arguments', t => { ); }); +test('Query node with spatial properties using spatial arguments', t => { + const graphQLQuery = `query { + SpatialNode( + pointKey: { + x: 10 + }, + point: { + longitude: 40 + } + ) { + pointKey { + x + y + z + crs + } + point { + longitude + latitude + height + crs + } + } + }`, + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x AND \`spatialNode\`.point.longitude = $point.longitude RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z , crs: \`spatialNode\`.pointKey.crs },point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height , crs: \`spatialNode\`.point.crs }} AS \`spatialNode\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Nested Query with temporal property arguments', t => { const graphQLQuery = `query { TemporalNode( @@ -2025,6 +2106,44 @@ test('Nested Query with temporal property arguments', t => { ); }); +test('Nested Query with spatial property arguments', t => { + const graphQLQuery = `query { + SpatialNode( + pointKey: { + x: 50 + } + ) { + pointKey { + x + y + z + } + spatialNodes( + pointKey: { + y: 20 + } + ) { + pointKey { + x + y + z + } + } + } + }`, + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x RETURN \`spatialNode\` {pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z },spatialNodes: [(\`spatialNode\`)-[:\`SPATIAL\`]->(\`spatialNode_spatialNodes\`:\`SpatialNode\`) WHERE spatialNode_spatialNodes.pointKey.y = $1_pointKey.y | spatialNode_spatialNodes {pointKey: { x: \`spatialNode_spatialNodes\`.pointKey.x , y: \`spatialNode_spatialNodes\`.pointKey.y , z: \`spatialNode_spatialNodes\`.pointKey.z }}] } AS \`spatialNode\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Update temporal and non-temporal properties on node using temporal property node selection', t => { const graphQLQuery = `mutation { UpdateTemporalNode( @@ -2113,6 +2232,39 @@ test('Update temporal and non-temporal properties on node using temporal propert ); }); +test('Update node spatial property using spatial property node selection', t => { + const graphQLQuery = `mutation { + UpdateSpatialNode( + pointKey: { + y: 60 + } + point: { + x: 100, + y: 200, + z: 300 + } + ) { + point { + x + y + z + } + } + }`, + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.y = $params.pointKey.y + SET \`spatialNode\` += {point: point($params.point)} RETURN \`spatialNode\` {point: { x: \`spatialNode\`.point.x , y: \`spatialNode\`.point.y , z: \`spatialNode\`.point.z }} AS \`spatialNode\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Update temporal list property on node using temporal property node selection', t => { const graphQLQuery = `mutation { UpdateTemporalNode( @@ -2154,7 +2306,7 @@ test('Update temporal list property on node using temporal property node selecti } }`, expectedCypherQuery = `MATCH (\`temporalNode\`:\`TemporalNode\`) WHERE \`temporalNode\`.datetime.year = $params.datetime.year AND \`temporalNode\`.datetime.month = $params.datetime.month AND \`temporalNode\`.datetime.day = $params.datetime.day AND \`temporalNode\`.datetime.hour = $params.datetime.hour AND \`temporalNode\`.datetime.minute = $params.datetime.minute AND \`temporalNode\`.datetime.second = $params.datetime.second AND \`temporalNode\`.datetime.millisecond = $params.datetime.millisecond AND \`temporalNode\`.datetime.microsecond = $params.datetime.microsecond AND \`temporalNode\`.datetime.nanosecond = $params.datetime.nanosecond AND \`temporalNode\`.datetime.timezone = $params.datetime.timezone - SET \`temporalNode\` += {localdatetimes: [value IN $params.localdatetimes | localdatetime(value)]} RETURN \`temporalNode\` {_id: ID(\`temporalNode\`), .name ,localdatetimes: reduce(a = [], TEMPORAL_INSTANCE IN temporalNode.localdatetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , formatted: toString(TEMPORAL_INSTANCE) })} AS \`temporalNode\``; + SET \`temporalNode\` += {localdatetimes: [value IN $params.localdatetimes | localdatetime(value)]} RETURN \`temporalNode\` {_id: ID(\`temporalNode\`), .name ,localdatetimes: reduce(a = [], INSTANCE IN temporalNode.localdatetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , formatted: toString(INSTANCE) })} AS \`temporalNode\``; t.plan(1); @@ -2253,6 +2405,37 @@ RETURN \`temporalNode\``; ); }); +test('Delete node using spatial property node selection', t => { + const graphQLQuery = `mutation { + DeleteSpatialNode( + pointKey: { + x: 50 + } + ) { + _id + pointKey { + x + y + z + } + } + }`, + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`) WHERE \`spatialNode\`.pointKey.x = $pointKey.x +WITH \`spatialNode\` AS \`spatialNode_toDelete\`, \`spatialNode\` {_id: ID(\`spatialNode\`),pointKey: { x: \`spatialNode\`.pointKey.x , y: \`spatialNode\`.pointKey.y , z: \`spatialNode\`.pointKey.z }} AS \`spatialNode\` +DETACH DELETE \`spatialNode_toDelete\` +RETURN \`spatialNode\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Add relationship mutation using temporal property node selection', t => { const graphQLQuery = `mutation { AddTemporalNodeTemporalNodes( @@ -2411,6 +2594,42 @@ test('Add relationship mutation using temporal property node selection', t => { ); }); +test('Add relationship mutation using spatial property node selection', t => { + const graphQLQuery = `mutation { + AddSpatialNodeSpatialNodes( + from: { pointKey: { x: 50 } } + to: { pointKey: { y: 20 } } + ) { + from { + pointKey { + x + } + } + to { + pointKey { + y + } + } + } + }`, + expectedCypherQuery = ` + MATCH (\`spatialNode_from\`:\`SpatialNode\`) WHERE \`spatialNode_from\`.pointKey.x = $from.pointKey.x + MATCH (\`spatialNode_to\`:\`SpatialNode\`) WHERE \`spatialNode_to\`.pointKey.y = $to.pointKey.y + CREATE (\`spatialNode_from\`)-[\`spatial_relation\`:\`SPATIAL\`]->(\`spatialNode_to\`) + RETURN \`spatial_relation\` { from: \`spatialNode_from\` {pointKey: { x: \`spatialNode_from\`.pointKey.x }} ,to: \`spatialNode_to\` {pointKey: { y: \`spatialNode_to\`.pointKey.y }} } AS \`_AddSpatialNodeSpatialNodesPayload\`; + `; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Remove relationship mutation using temporal property node selection', t => { const graphQLQuery = `mutation { RemoveTemporalNodeTemporalNodes( @@ -2571,6 +2790,44 @@ test('Remove relationship mutation using temporal property node selection', t => ); }); +test('Remove relationship mutation using spatial property node selection', t => { + const graphQLQuery = `mutation { + RemoveSpatialNodeSpatialNodes( + from: { pointKey: { x: 50 } } + to: { pointKey: { y: 20 } } + ) { + from { + pointKey { + x + } + } + to { + pointKey { + y + } + } + } + }`, + expectedCypherQuery = ` + MATCH (\`spatialNode_from\`:\`SpatialNode\`) WHERE \`spatialNode_from\`.pointKey.x = $from.pointKey.x + MATCH (\`spatialNode_to\`:\`SpatialNode\`) WHERE \`spatialNode_to\`.pointKey.y = $to.pointKey.y + OPTIONAL MATCH (\`spatialNode_from\`)-[\`spatialNode_fromspatialNode_to\`:\`SPATIAL\`]->(\`spatialNode_to\`) + DELETE \`spatialNode_fromspatialNode_to\` + WITH COUNT(*) AS scope, \`spatialNode_from\` AS \`_spatialNode_from\`, \`spatialNode_to\` AS \`_spatialNode_to\` + RETURN {from: \`_spatialNode_from\` {pointKey: { x: \`_spatialNode_from\`.pointKey.x }} ,to: \`_spatialNode_to\` {pointKey: { y: \`_spatialNode_to\`.pointKey.y }} } AS \`_RemoveSpatialNodeSpatialNodesPayload\`; + `; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Query relationship with temporal properties', t => { const graphQLQuery = `query { Movie { @@ -2601,6 +2858,32 @@ test('Query relationship with temporal properties', t => { ); }); +test('Query relationship with spatial properties', t => { + const graphQLQuery = `query { + User { + rated { + location { + x + y + z + srid + } + } + } + }`, + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` {rated: [(\`user\`)-[\`user_rated_relation\`:\`RATED\`]->(:\`Movie\`:\`u_user-id\`:\`newMovieLabel\`) | user_rated_relation {location: { x: \`user_rated_relation\`.location.x , y: \`user_rated_relation\`.location.y , z: \`user_rated_relation\`.location.z , srid: \`user_rated_relation\`.location.srid }}] } AS \`user\``; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Add relationship mutation with temporal properties', t => { const graphQLQuery = `mutation { AddUserRated( @@ -2751,6 +3034,55 @@ test('Add relationship mutation with temporal properties', t => { ); }); +test('Add relationship mutation with spatial properties', t => { + const graphQLQuery = `mutation { + AddUserRated( + from: { + userId: "6973aff4-3113-45b0-9ce4-9879f0077b46" + }, + to: { + movieId: "6f565c2a-cf1b-4969-951e-d0adade1e48c" + }, + data: { + rating: 10, + location: { + x: 10, + y: 20, + z: 30 + } + } + ) { + location { + x + y + z + } + from { + _id + } + to { + _id + } + } + }`, + expectedCypherQuery = ` + MATCH (\`user_from\`:\`User\` {userId: $from.userId}) + MATCH (\`movie_to\`:\`Movie\`:\`u_user-id\`:\`newMovieLabel\` {movieId: $to.movieId}) + CREATE (\`user_from\`)-[\`rated_relation\`:\`RATED\` {rating:$data.rating,location: point($data.location)}]->(\`movie_to\`) + RETURN \`rated_relation\` { location: { x: \`rated_relation\`.location.x , y: \`rated_relation\`.location.y , z: \`rated_relation\`.location.z },from: \`user_from\` {_id: ID(\`user_from\`)} ,to: \`movie_to\` {_id: ID(\`movie_to\`)} } AS \`_AddUserRatedPayload\`; + `; + + t.plan(1); + + return augmentedSchemaCypherTestRunner( + t, + graphQLQuery, + {}, + expectedCypherQuery, + {} + ); +}); + test('Add relationship mutation with list properties', t => { const graphQLQuery = `mutation { AddUserRated( @@ -2817,7 +3149,7 @@ test('Add relationship mutation with list properties', t => { MATCH (\`user_from\`:\`User\` {userId: $from.userId}) MATCH (\`movie_to\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: $to.movieId}) CREATE (\`user_from\`)-[\`rated_relation\`:\`RATED\` {ratings:$data.ratings,datetimes: [value IN $data.datetimes | datetime(value)]}]->(\`movie_to\`) - RETURN \`rated_relation\` { .ratings ,datetimes: reduce(a = [], TEMPORAL_INSTANCE IN rated_relation.datetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , timezone: TEMPORAL_INSTANCE.timezone , formatted: toString(TEMPORAL_INSTANCE) }),from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation {datetime: { year: \`user_from_rated_relation\`.datetime.year }}] } ,to: \`movie_to\` {_id: ID(\`movie_to\`), .movieId , .title ,ratings: [(\`movie_to\`)<-[\`movie_to_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_to_ratings_relation {datetime: { year: \`movie_to_ratings_relation\`.datetime.year }}] } } AS \`_AddUserRatedPayload\`; + RETURN \`rated_relation\` { .ratings ,datetimes: reduce(a = [], INSTANCE IN rated_relation.datetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , timezone: INSTANCE.timezone , formatted: toString(INSTANCE) }),from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation {datetime: { year: \`user_from_rated_relation\`.datetime.year }}] } ,to: \`movie_to\` {_id: ID(\`movie_to\`), .movieId , .title ,ratings: [(\`movie_to\`)<-[\`movie_to_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_to_ratings_relation {datetime: { year: \`movie_to_ratings_relation\`.datetime.year }}] } } AS \`_AddUserRatedPayload\`; `; t.plan(1); @@ -3029,7 +3361,7 @@ test('Add reflexive relationship mutation with temporal properties', t => { MATCH (\`user_from\`:\`User\` {userId: $from.userId}) MATCH (\`user_to\`:\`User\` {userId: $to.userId}) CREATE (\`user_from\`)-[\`friend_of_relation\`:\`FRIEND_OF\` {since:$data.since,time: time($data.time),date: date($data.date),datetime: datetime($data.datetime),datetimes: [value IN $data.datetimes | datetime(value)],localtime: localtime($data.localtime),localdatetime: localdatetime($data.localdatetime)}]->(\`user_to\`) - RETURN \`friend_of_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,friends: {from: [(\`user_from\`)<-[\`user_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from\`:\`User\`) | user_from_from_relation { .since ,User: user_from_from {_id: ID(\`user_from_from\`), .name ,friends: {from: [(\`user_from_from\`)<-[\`user_from_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from_from\`:\`User\`) | user_from_from_from_relation { .since ,User: user_from_from_from {_id: ID(\`user_from_from_from\`), .name } }] ,to: [(\`user_from_from\`)-[\`user_from_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_from_to\`:\`User\`) | user_from_from_to_relation { .since ,User: user_from_from_to {_id: ID(\`user_from_from_to\`), .name } }] } } }] ,to: [(\`user_from\`)-[\`user_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_to\`:\`User\`) | user_from_to_relation { .since ,datetime: { year: \`user_from_to_relation\`.datetime.year },User: user_from_to {_id: ID(\`user_from_to\`), .name } }] } } ,to: \`user_to\` {_id: ID(\`user_to\`), .name ,friends: {from: [(\`user_to\`)<-[\`user_to_from_relation\`:\`FRIEND_OF\`]-(\`user_to_from\`:\`User\`) | user_to_from_relation { .since ,User: user_to_from {_id: ID(\`user_to_from\`), .name } }] ,to: [(\`user_to\`)-[\`user_to_to_relation\`:\`FRIEND_OF\`]->(\`user_to_to\`:\`User\`) | user_to_to_relation { .since ,User: user_to_to {_id: ID(\`user_to_to\`), .name } }] } } , .since ,time: { hour: \`friend_of_relation\`.time.hour , minute: \`friend_of_relation\`.time.minute , second: \`friend_of_relation\`.time.second , millisecond: \`friend_of_relation\`.time.millisecond , microsecond: \`friend_of_relation\`.time.microsecond , nanosecond: \`friend_of_relation\`.time.nanosecond , timezone: \`friend_of_relation\`.time.timezone , formatted: toString(\`friend_of_relation\`.time) },date: { year: \`friend_of_relation\`.date.year , month: \`friend_of_relation\`.date.month , day: \`friend_of_relation\`.date.day , formatted: toString(\`friend_of_relation\`.date) },datetime: { year: \`friend_of_relation\`.datetime.year , month: \`friend_of_relation\`.datetime.month , day: \`friend_of_relation\`.datetime.day , hour: \`friend_of_relation\`.datetime.hour , minute: \`friend_of_relation\`.datetime.minute , second: \`friend_of_relation\`.datetime.second , millisecond: \`friend_of_relation\`.datetime.millisecond , microsecond: \`friend_of_relation\`.datetime.microsecond , nanosecond: \`friend_of_relation\`.datetime.nanosecond , timezone: \`friend_of_relation\`.datetime.timezone , formatted: toString(\`friend_of_relation\`.datetime) },datetimes: reduce(a = [], TEMPORAL_INSTANCE IN friend_of_relation.datetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , timezone: TEMPORAL_INSTANCE.timezone , formatted: toString(TEMPORAL_INSTANCE) }),localtime: { hour: \`friend_of_relation\`.localtime.hour , minute: \`friend_of_relation\`.localtime.minute , second: \`friend_of_relation\`.localtime.second , millisecond: \`friend_of_relation\`.localtime.millisecond , microsecond: \`friend_of_relation\`.localtime.microsecond , nanosecond: \`friend_of_relation\`.localtime.nanosecond , formatted: toString(\`friend_of_relation\`.localtime) },localdatetime: { year: \`friend_of_relation\`.localdatetime.year , month: \`friend_of_relation\`.localdatetime.month , day: \`friend_of_relation\`.localdatetime.day , hour: \`friend_of_relation\`.localdatetime.hour , minute: \`friend_of_relation\`.localdatetime.minute , second: \`friend_of_relation\`.localdatetime.second , millisecond: \`friend_of_relation\`.localdatetime.millisecond , microsecond: \`friend_of_relation\`.localdatetime.microsecond , nanosecond: \`friend_of_relation\`.localdatetime.nanosecond , formatted: toString(\`friend_of_relation\`.localdatetime) } } AS \`_AddUserFriendsPayload\`; + RETURN \`friend_of_relation\` { from: \`user_from\` {_id: ID(\`user_from\`), .userId , .name ,friends: {from: [(\`user_from\`)<-[\`user_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from\`:\`User\`) | user_from_from_relation { .since ,User: user_from_from {_id: ID(\`user_from_from\`), .name ,friends: {from: [(\`user_from_from\`)<-[\`user_from_from_from_relation\`:\`FRIEND_OF\`]-(\`user_from_from_from\`:\`User\`) | user_from_from_from_relation { .since ,User: user_from_from_from {_id: ID(\`user_from_from_from\`), .name } }] ,to: [(\`user_from_from\`)-[\`user_from_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_from_to\`:\`User\`) | user_from_from_to_relation { .since ,User: user_from_from_to {_id: ID(\`user_from_from_to\`), .name } }] } } }] ,to: [(\`user_from\`)-[\`user_from_to_relation\`:\`FRIEND_OF\`]->(\`user_from_to\`:\`User\`) | user_from_to_relation { .since ,datetime: { year: \`user_from_to_relation\`.datetime.year },User: user_from_to {_id: ID(\`user_from_to\`), .name } }] } } ,to: \`user_to\` {_id: ID(\`user_to\`), .name ,friends: {from: [(\`user_to\`)<-[\`user_to_from_relation\`:\`FRIEND_OF\`]-(\`user_to_from\`:\`User\`) | user_to_from_relation { .since ,User: user_to_from {_id: ID(\`user_to_from\`), .name } }] ,to: [(\`user_to\`)-[\`user_to_to_relation\`:\`FRIEND_OF\`]->(\`user_to_to\`:\`User\`) | user_to_to_relation { .since ,User: user_to_to {_id: ID(\`user_to_to\`), .name } }] } } , .since ,time: { hour: \`friend_of_relation\`.time.hour , minute: \`friend_of_relation\`.time.minute , second: \`friend_of_relation\`.time.second , millisecond: \`friend_of_relation\`.time.millisecond , microsecond: \`friend_of_relation\`.time.microsecond , nanosecond: \`friend_of_relation\`.time.nanosecond , timezone: \`friend_of_relation\`.time.timezone , formatted: toString(\`friend_of_relation\`.time) },date: { year: \`friend_of_relation\`.date.year , month: \`friend_of_relation\`.date.month , day: \`friend_of_relation\`.date.day , formatted: toString(\`friend_of_relation\`.date) },datetime: { year: \`friend_of_relation\`.datetime.year , month: \`friend_of_relation\`.datetime.month , day: \`friend_of_relation\`.datetime.day , hour: \`friend_of_relation\`.datetime.hour , minute: \`friend_of_relation\`.datetime.minute , second: \`friend_of_relation\`.datetime.second , millisecond: \`friend_of_relation\`.datetime.millisecond , microsecond: \`friend_of_relation\`.datetime.microsecond , nanosecond: \`friend_of_relation\`.datetime.nanosecond , timezone: \`friend_of_relation\`.datetime.timezone , formatted: toString(\`friend_of_relation\`.datetime) },datetimes: reduce(a = [], INSTANCE IN friend_of_relation.datetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , timezone: INSTANCE.timezone , formatted: toString(INSTANCE) }),localtime: { hour: \`friend_of_relation\`.localtime.hour , minute: \`friend_of_relation\`.localtime.minute , second: \`friend_of_relation\`.localtime.second , millisecond: \`friend_of_relation\`.localtime.millisecond , microsecond: \`friend_of_relation\`.localtime.microsecond , nanosecond: \`friend_of_relation\`.localtime.nanosecond , formatted: toString(\`friend_of_relation\`.localtime) },localdatetime: { year: \`friend_of_relation\`.localdatetime.year , month: \`friend_of_relation\`.localdatetime.month , day: \`friend_of_relation\`.localdatetime.day , hour: \`friend_of_relation\`.localdatetime.hour , minute: \`friend_of_relation\`.localdatetime.minute , second: \`friend_of_relation\`.localdatetime.second , millisecond: \`friend_of_relation\`.localdatetime.millisecond , microsecond: \`friend_of_relation\`.localdatetime.microsecond , nanosecond: \`friend_of_relation\`.localdatetime.nanosecond , formatted: toString(\`friend_of_relation\`.localdatetime) } } AS \`_AddUserFriendsPayload\`; `; t.plan(1); @@ -3324,7 +3656,7 @@ test('Query nested temporal properties on reflexive relationship using temporal } } }`, - expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) WHERE user_from_relation.time = time($1_time.formatted) AND user_from_relation.date.year = $1_date.year AND user_from_relation.date.month = $1_date.month AND user_from_relation.date.day = $1_date.day AND user_from_relation.datetime.year = $1_datetime.year AND user_from_relation.datetime.month = $1_datetime.month AND user_from_relation.datetime.day = $1_datetime.day AND user_from_relation.datetime.hour = $1_datetime.hour AND user_from_relation.datetime.minute = $1_datetime.minute AND user_from_relation.datetime.second = $1_datetime.second AND user_from_relation.datetime.millisecond = $1_datetime.millisecond AND user_from_relation.datetime.microsecond = $1_datetime.microsecond AND user_from_relation.datetime.nanosecond = $1_datetime.nanosecond AND user_from_relation.datetime.timezone = $1_datetime.timezone AND user_from_relation.localtime.hour = $1_localtime.hour AND user_from_relation.localtime.minute = $1_localtime.minute AND user_from_relation.localtime.second = $1_localtime.second AND user_from_relation.localtime.millisecond = $1_localtime.millisecond AND user_from_relation.localtime.microsecond = $1_localtime.microsecond AND user_from_relation.localtime.nanosecond = $1_localtime.nanosecond AND user_from_relation.localdatetime.year = $1_localdatetime.year AND user_from_relation.localdatetime.month = $1_localdatetime.month AND user_from_relation.localdatetime.day = $1_localdatetime.day AND user_from_relation.localdatetime.hour = $1_localdatetime.hour AND user_from_relation.localdatetime.minute = $1_localdatetime.minute AND user_from_relation.localdatetime.second = $1_localdatetime.second AND user_from_relation.localdatetime.millisecond = $1_localdatetime.millisecond AND user_from_relation.localdatetime.microsecond = $1_localdatetime.microsecond AND user_from_relation.localdatetime.nanosecond = $1_localdatetime.nanosecond | user_from_relation { .since ,time: { hour: \`user_from_relation\`.time.hour , minute: \`user_from_relation\`.time.minute , second: \`user_from_relation\`.time.second , millisecond: \`user_from_relation\`.time.millisecond , microsecond: \`user_from_relation\`.time.microsecond , nanosecond: \`user_from_relation\`.time.nanosecond , timezone: \`user_from_relation\`.time.timezone , formatted: toString(\`user_from_relation\`.time) },date: { year: \`user_from_relation\`.date.year , month: \`user_from_relation\`.date.month , day: \`user_from_relation\`.date.day , formatted: toString(\`user_from_relation\`.date) },datetime: { year: \`user_from_relation\`.datetime.year , month: \`user_from_relation\`.datetime.month , day: \`user_from_relation\`.datetime.day , hour: \`user_from_relation\`.datetime.hour , minute: \`user_from_relation\`.datetime.minute , second: \`user_from_relation\`.datetime.second , millisecond: \`user_from_relation\`.datetime.millisecond , microsecond: \`user_from_relation\`.datetime.microsecond , nanosecond: \`user_from_relation\`.datetime.nanosecond , timezone: \`user_from_relation\`.datetime.timezone , formatted: toString(\`user_from_relation\`.datetime) },datetimes: reduce(a = [], TEMPORAL_INSTANCE IN user_from_relation.datetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , timezone: TEMPORAL_INSTANCE.timezone , formatted: toString(TEMPORAL_INSTANCE) }),localtime: { hour: \`user_from_relation\`.localtime.hour , minute: \`user_from_relation\`.localtime.minute , second: \`user_from_relation\`.localtime.second , millisecond: \`user_from_relation\`.localtime.millisecond , microsecond: \`user_from_relation\`.localtime.microsecond , nanosecond: \`user_from_relation\`.localtime.nanosecond , formatted: toString(\`user_from_relation\`.localtime) },localdatetime: { year: \`user_from_relation\`.localdatetime.year , month: \`user_from_relation\`.localdatetime.month , day: \`user_from_relation\`.localdatetime.day , hour: \`user_from_relation\`.localdatetime.hour , minute: \`user_from_relation\`.localdatetime.minute , second: \`user_from_relation\`.localdatetime.second , millisecond: \`user_from_relation\`.localdatetime.millisecond , microsecond: \`user_from_relation\`.localdatetime.microsecond , nanosecond: \`user_from_relation\`.localdatetime.nanosecond , formatted: toString(\`user_from_relation\`.localdatetime) },User: user_from {_id: ID(\`user_from\`), .userId ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation {datetime: { year: \`user_from_rated_relation\`.datetime.year }}] } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) WHERE user_to_relation.time = time($3_time.formatted) AND user_to_relation.date.year = $3_date.year AND user_to_relation.date.month = $3_date.month AND user_to_relation.date.day = $3_date.day AND user_to_relation.datetime.year = $3_datetime.year AND user_to_relation.datetime.month = $3_datetime.month AND user_to_relation.datetime.day = $3_datetime.day AND user_to_relation.datetime.hour = $3_datetime.hour AND user_to_relation.datetime.minute = $3_datetime.minute AND user_to_relation.datetime.second = $3_datetime.second AND user_to_relation.datetime.millisecond = $3_datetime.millisecond AND user_to_relation.datetime.microsecond = $3_datetime.microsecond AND user_to_relation.datetime.nanosecond = $3_datetime.nanosecond AND user_to_relation.datetime.timezone = $3_datetime.timezone AND user_to_relation.localtime.hour = $3_localtime.hour AND user_to_relation.localtime.minute = $3_localtime.minute AND user_to_relation.localtime.second = $3_localtime.second AND user_to_relation.localtime.millisecond = $3_localtime.millisecond AND user_to_relation.localtime.microsecond = $3_localtime.microsecond AND user_to_relation.localtime.nanosecond = $3_localtime.nanosecond AND user_to_relation.localdatetime.year = $3_localdatetime.year AND user_to_relation.localdatetime.month = $3_localdatetime.month AND user_to_relation.localdatetime.day = $3_localdatetime.day AND user_to_relation.localdatetime.hour = $3_localdatetime.hour AND user_to_relation.localdatetime.minute = $3_localdatetime.minute AND user_to_relation.localdatetime.second = $3_localdatetime.second AND user_to_relation.localdatetime.millisecond = $3_localdatetime.millisecond AND user_to_relation.localdatetime.microsecond = $3_localdatetime.microsecond AND user_to_relation.localdatetime.nanosecond = $3_localdatetime.nanosecond | user_to_relation { .since ,time: { hour: \`user_to_relation\`.time.hour , minute: \`user_to_relation\`.time.minute , second: \`user_to_relation\`.time.second , millisecond: \`user_to_relation\`.time.millisecond , microsecond: \`user_to_relation\`.time.microsecond , nanosecond: \`user_to_relation\`.time.nanosecond , timezone: \`user_to_relation\`.time.timezone , formatted: toString(\`user_to_relation\`.time) },date: { year: \`user_to_relation\`.date.year , month: \`user_to_relation\`.date.month , day: \`user_to_relation\`.date.day , formatted: toString(\`user_to_relation\`.date) },datetime: { year: \`user_to_relation\`.datetime.year , month: \`user_to_relation\`.datetime.month , day: \`user_to_relation\`.datetime.day , hour: \`user_to_relation\`.datetime.hour , minute: \`user_to_relation\`.datetime.minute , second: \`user_to_relation\`.datetime.second , millisecond: \`user_to_relation\`.datetime.millisecond , microsecond: \`user_to_relation\`.datetime.microsecond , nanosecond: \`user_to_relation\`.datetime.nanosecond , timezone: \`user_to_relation\`.datetime.timezone , formatted: toString(\`user_to_relation\`.datetime) },localtime: { hour: \`user_to_relation\`.localtime.hour , minute: \`user_to_relation\`.localtime.minute , second: \`user_to_relation\`.localtime.second , millisecond: \`user_to_relation\`.localtime.millisecond , microsecond: \`user_to_relation\`.localtime.microsecond , nanosecond: \`user_to_relation\`.localtime.nanosecond , formatted: toString(\`user_to_relation\`.localtime) },localdatetime: { year: \`user_to_relation\`.localdatetime.year , month: \`user_to_relation\`.localdatetime.month , day: \`user_to_relation\`.localdatetime.day , hour: \`user_to_relation\`.localdatetime.hour , minute: \`user_to_relation\`.localdatetime.minute , second: \`user_to_relation\`.localdatetime.second , millisecond: \`user_to_relation\`.localdatetime.millisecond , microsecond: \`user_to_relation\`.localdatetime.microsecond , nanosecond: \`user_to_relation\`.localdatetime.nanosecond , formatted: toString(\`user_to_relation\`.localdatetime) },User: user_to {_id: ID(\`user_to\`), .userId ,rated: [(\`user_to\`)-[\`user_to_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_relation {datetime: { year: \`user_to_rated_relation\`.datetime.year }}] } }] } } AS \`user\``; + expectedCypherQuery = `MATCH (\`user\`:\`User\`) RETURN \`user\` { .userId , .name ,friends: {from: [(\`user\`)<-[\`user_from_relation\`:\`FRIEND_OF\`]-(\`user_from\`:\`User\`) WHERE user_from_relation.time = time($1_time.formatted) AND user_from_relation.date.year = $1_date.year AND user_from_relation.date.month = $1_date.month AND user_from_relation.date.day = $1_date.day AND user_from_relation.datetime.year = $1_datetime.year AND user_from_relation.datetime.month = $1_datetime.month AND user_from_relation.datetime.day = $1_datetime.day AND user_from_relation.datetime.hour = $1_datetime.hour AND user_from_relation.datetime.minute = $1_datetime.minute AND user_from_relation.datetime.second = $1_datetime.second AND user_from_relation.datetime.millisecond = $1_datetime.millisecond AND user_from_relation.datetime.microsecond = $1_datetime.microsecond AND user_from_relation.datetime.nanosecond = $1_datetime.nanosecond AND user_from_relation.datetime.timezone = $1_datetime.timezone AND user_from_relation.localtime.hour = $1_localtime.hour AND user_from_relation.localtime.minute = $1_localtime.minute AND user_from_relation.localtime.second = $1_localtime.second AND user_from_relation.localtime.millisecond = $1_localtime.millisecond AND user_from_relation.localtime.microsecond = $1_localtime.microsecond AND user_from_relation.localtime.nanosecond = $1_localtime.nanosecond AND user_from_relation.localdatetime.year = $1_localdatetime.year AND user_from_relation.localdatetime.month = $1_localdatetime.month AND user_from_relation.localdatetime.day = $1_localdatetime.day AND user_from_relation.localdatetime.hour = $1_localdatetime.hour AND user_from_relation.localdatetime.minute = $1_localdatetime.minute AND user_from_relation.localdatetime.second = $1_localdatetime.second AND user_from_relation.localdatetime.millisecond = $1_localdatetime.millisecond AND user_from_relation.localdatetime.microsecond = $1_localdatetime.microsecond AND user_from_relation.localdatetime.nanosecond = $1_localdatetime.nanosecond | user_from_relation { .since ,time: { hour: \`user_from_relation\`.time.hour , minute: \`user_from_relation\`.time.minute , second: \`user_from_relation\`.time.second , millisecond: \`user_from_relation\`.time.millisecond , microsecond: \`user_from_relation\`.time.microsecond , nanosecond: \`user_from_relation\`.time.nanosecond , timezone: \`user_from_relation\`.time.timezone , formatted: toString(\`user_from_relation\`.time) },date: { year: \`user_from_relation\`.date.year , month: \`user_from_relation\`.date.month , day: \`user_from_relation\`.date.day , formatted: toString(\`user_from_relation\`.date) },datetime: { year: \`user_from_relation\`.datetime.year , month: \`user_from_relation\`.datetime.month , day: \`user_from_relation\`.datetime.day , hour: \`user_from_relation\`.datetime.hour , minute: \`user_from_relation\`.datetime.minute , second: \`user_from_relation\`.datetime.second , millisecond: \`user_from_relation\`.datetime.millisecond , microsecond: \`user_from_relation\`.datetime.microsecond , nanosecond: \`user_from_relation\`.datetime.nanosecond , timezone: \`user_from_relation\`.datetime.timezone , formatted: toString(\`user_from_relation\`.datetime) },datetimes: reduce(a = [], INSTANCE IN user_from_relation.datetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , timezone: INSTANCE.timezone , formatted: toString(INSTANCE) }),localtime: { hour: \`user_from_relation\`.localtime.hour , minute: \`user_from_relation\`.localtime.minute , second: \`user_from_relation\`.localtime.second , millisecond: \`user_from_relation\`.localtime.millisecond , microsecond: \`user_from_relation\`.localtime.microsecond , nanosecond: \`user_from_relation\`.localtime.nanosecond , formatted: toString(\`user_from_relation\`.localtime) },localdatetime: { year: \`user_from_relation\`.localdatetime.year , month: \`user_from_relation\`.localdatetime.month , day: \`user_from_relation\`.localdatetime.day , hour: \`user_from_relation\`.localdatetime.hour , minute: \`user_from_relation\`.localdatetime.minute , second: \`user_from_relation\`.localdatetime.second , millisecond: \`user_from_relation\`.localdatetime.millisecond , microsecond: \`user_from_relation\`.localdatetime.microsecond , nanosecond: \`user_from_relation\`.localdatetime.nanosecond , formatted: toString(\`user_from_relation\`.localdatetime) },User: user_from {_id: ID(\`user_from\`), .userId ,rated: [(\`user_from\`)-[\`user_from_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_from_rated_relation {datetime: { year: \`user_from_rated_relation\`.datetime.year }}] } }] ,to: [(\`user\`)-[\`user_to_relation\`:\`FRIEND_OF\`]->(\`user_to\`:\`User\`) WHERE user_to_relation.time = time($3_time.formatted) AND user_to_relation.date.year = $3_date.year AND user_to_relation.date.month = $3_date.month AND user_to_relation.date.day = $3_date.day AND user_to_relation.datetime.year = $3_datetime.year AND user_to_relation.datetime.month = $3_datetime.month AND user_to_relation.datetime.day = $3_datetime.day AND user_to_relation.datetime.hour = $3_datetime.hour AND user_to_relation.datetime.minute = $3_datetime.minute AND user_to_relation.datetime.second = $3_datetime.second AND user_to_relation.datetime.millisecond = $3_datetime.millisecond AND user_to_relation.datetime.microsecond = $3_datetime.microsecond AND user_to_relation.datetime.nanosecond = $3_datetime.nanosecond AND user_to_relation.datetime.timezone = $3_datetime.timezone AND user_to_relation.localtime.hour = $3_localtime.hour AND user_to_relation.localtime.minute = $3_localtime.minute AND user_to_relation.localtime.second = $3_localtime.second AND user_to_relation.localtime.millisecond = $3_localtime.millisecond AND user_to_relation.localtime.microsecond = $3_localtime.microsecond AND user_to_relation.localtime.nanosecond = $3_localtime.nanosecond AND user_to_relation.localdatetime.year = $3_localdatetime.year AND user_to_relation.localdatetime.month = $3_localdatetime.month AND user_to_relation.localdatetime.day = $3_localdatetime.day AND user_to_relation.localdatetime.hour = $3_localdatetime.hour AND user_to_relation.localdatetime.minute = $3_localdatetime.minute AND user_to_relation.localdatetime.second = $3_localdatetime.second AND user_to_relation.localdatetime.millisecond = $3_localdatetime.millisecond AND user_to_relation.localdatetime.microsecond = $3_localdatetime.microsecond AND user_to_relation.localdatetime.nanosecond = $3_localdatetime.nanosecond | user_to_relation { .since ,time: { hour: \`user_to_relation\`.time.hour , minute: \`user_to_relation\`.time.minute , second: \`user_to_relation\`.time.second , millisecond: \`user_to_relation\`.time.millisecond , microsecond: \`user_to_relation\`.time.microsecond , nanosecond: \`user_to_relation\`.time.nanosecond , timezone: \`user_to_relation\`.time.timezone , formatted: toString(\`user_to_relation\`.time) },date: { year: \`user_to_relation\`.date.year , month: \`user_to_relation\`.date.month , day: \`user_to_relation\`.date.day , formatted: toString(\`user_to_relation\`.date) },datetime: { year: \`user_to_relation\`.datetime.year , month: \`user_to_relation\`.datetime.month , day: \`user_to_relation\`.datetime.day , hour: \`user_to_relation\`.datetime.hour , minute: \`user_to_relation\`.datetime.minute , second: \`user_to_relation\`.datetime.second , millisecond: \`user_to_relation\`.datetime.millisecond , microsecond: \`user_to_relation\`.datetime.microsecond , nanosecond: \`user_to_relation\`.datetime.nanosecond , timezone: \`user_to_relation\`.datetime.timezone , formatted: toString(\`user_to_relation\`.datetime) },localtime: { hour: \`user_to_relation\`.localtime.hour , minute: \`user_to_relation\`.localtime.minute , second: \`user_to_relation\`.localtime.second , millisecond: \`user_to_relation\`.localtime.millisecond , microsecond: \`user_to_relation\`.localtime.microsecond , nanosecond: \`user_to_relation\`.localtime.nanosecond , formatted: toString(\`user_to_relation\`.localtime) },localdatetime: { year: \`user_to_relation\`.localdatetime.year , month: \`user_to_relation\`.localdatetime.month , day: \`user_to_relation\`.localdatetime.day , hour: \`user_to_relation\`.localdatetime.hour , minute: \`user_to_relation\`.localdatetime.minute , second: \`user_to_relation\`.localdatetime.second , millisecond: \`user_to_relation\`.localdatetime.millisecond , microsecond: \`user_to_relation\`.localdatetime.microsecond , nanosecond: \`user_to_relation\`.localdatetime.nanosecond , formatted: toString(\`user_to_relation\`.localdatetime) },User: user_to {_id: ID(\`user_to\`), .userId ,rated: [(\`user_to\`)-[\`user_to_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | user_to_rated_relation {datetime: { year: \`user_to_rated_relation\`.datetime.year }}] } }] } } AS \`user\``; t.plan(1); return augmentedSchemaCypherTestRunner( @@ -3659,7 +3991,7 @@ test('Query nested list properties on relationship', t => { } } }`, - expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) RETURN \`movie\` {_id: ID(\`movie\`), .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating , .ratings ,time: { hour: \`movie_ratings_relation\`.time.hour , minute: \`movie_ratings_relation\`.time.minute , second: \`movie_ratings_relation\`.time.second , millisecond: \`movie_ratings_relation\`.time.millisecond , microsecond: \`movie_ratings_relation\`.time.microsecond , nanosecond: \`movie_ratings_relation\`.time.nanosecond , timezone: \`movie_ratings_relation\`.time.timezone , formatted: toString(\`movie_ratings_relation\`.time) },date: { year: \`movie_ratings_relation\`.date.year , month: \`movie_ratings_relation\`.date.month , day: \`movie_ratings_relation\`.date.day , formatted: toString(\`movie_ratings_relation\`.date) },datetime: { year: \`movie_ratings_relation\`.datetime.year , month: \`movie_ratings_relation\`.datetime.month , day: \`movie_ratings_relation\`.datetime.day , hour: \`movie_ratings_relation\`.datetime.hour , minute: \`movie_ratings_relation\`.datetime.minute , second: \`movie_ratings_relation\`.datetime.second , millisecond: \`movie_ratings_relation\`.datetime.millisecond , microsecond: \`movie_ratings_relation\`.datetime.microsecond , nanosecond: \`movie_ratings_relation\`.datetime.nanosecond , timezone: \`movie_ratings_relation\`.datetime.timezone , formatted: toString(\`movie_ratings_relation\`.datetime) },datetimes: reduce(a = [], TEMPORAL_INSTANCE IN movie_ratings_relation.datetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , timezone: TEMPORAL_INSTANCE.timezone , formatted: toString(TEMPORAL_INSTANCE) }),localtime: { hour: \`movie_ratings_relation\`.localtime.hour , minute: \`movie_ratings_relation\`.localtime.minute , second: \`movie_ratings_relation\`.localtime.second , millisecond: \`movie_ratings_relation\`.localtime.millisecond , microsecond: \`movie_ratings_relation\`.localtime.microsecond , nanosecond: \`movie_ratings_relation\`.localtime.nanosecond , formatted: toString(\`movie_ratings_relation\`.localtime) },localdatetime: { year: \`movie_ratings_relation\`.localdatetime.year , month: \`movie_ratings_relation\`.localdatetime.month , day: \`movie_ratings_relation\`.localdatetime.day , hour: \`movie_ratings_relation\`.localdatetime.hour , minute: \`movie_ratings_relation\`.localdatetime.minute , second: \`movie_ratings_relation\`.localdatetime.second , millisecond: \`movie_ratings_relation\`.localdatetime.millisecond , microsecond: \`movie_ratings_relation\`.localdatetime.microsecond , nanosecond: \`movie_ratings_relation\`.localdatetime.nanosecond , formatted: toString(\`movie_ratings_relation\`.localdatetime) },User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User {_id: ID(\`movie_ratings_User\`), .name ,rated: [(\`movie_ratings_User\`)-[\`movie_ratings_User_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | movie_ratings_User_rated_relation { .rating ,time: { hour: \`movie_ratings_User_rated_relation\`.time.hour , minute: \`movie_ratings_User_rated_relation\`.time.minute , second: \`movie_ratings_User_rated_relation\`.time.second , millisecond: \`movie_ratings_User_rated_relation\`.time.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.time.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.time.nanosecond , timezone: \`movie_ratings_User_rated_relation\`.time.timezone , formatted: toString(\`movie_ratings_User_rated_relation\`.time) },date: { year: \`movie_ratings_User_rated_relation\`.date.year , month: \`movie_ratings_User_rated_relation\`.date.month , day: \`movie_ratings_User_rated_relation\`.date.day , formatted: toString(\`movie_ratings_User_rated_relation\`.date) },datetime: { year: \`movie_ratings_User_rated_relation\`.datetime.year , month: \`movie_ratings_User_rated_relation\`.datetime.month , day: \`movie_ratings_User_rated_relation\`.datetime.day , hour: \`movie_ratings_User_rated_relation\`.datetime.hour , minute: \`movie_ratings_User_rated_relation\`.datetime.minute , second: \`movie_ratings_User_rated_relation\`.datetime.second , millisecond: \`movie_ratings_User_rated_relation\`.datetime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.datetime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.datetime.nanosecond , timezone: \`movie_ratings_User_rated_relation\`.datetime.timezone , formatted: toString(\`movie_ratings_User_rated_relation\`.datetime) },datetimes: reduce(a = [], TEMPORAL_INSTANCE IN movie_ratings_User_rated_relation.datetimes | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , minute: TEMPORAL_INSTANCE.minute , second: TEMPORAL_INSTANCE.second , millisecond: TEMPORAL_INSTANCE.millisecond , microsecond: TEMPORAL_INSTANCE.microsecond , nanosecond: TEMPORAL_INSTANCE.nanosecond , timezone: TEMPORAL_INSTANCE.timezone , formatted: toString(TEMPORAL_INSTANCE) }),localtime: { hour: \`movie_ratings_User_rated_relation\`.localtime.hour , minute: \`movie_ratings_User_rated_relation\`.localtime.minute , second: \`movie_ratings_User_rated_relation\`.localtime.second , millisecond: \`movie_ratings_User_rated_relation\`.localtime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.localtime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.localtime.nanosecond , formatted: toString(\`movie_ratings_User_rated_relation\`.localtime) },localdatetime: { year: \`movie_ratings_User_rated_relation\`.localdatetime.year , month: \`movie_ratings_User_rated_relation\`.localdatetime.month , day: \`movie_ratings_User_rated_relation\`.localdatetime.day , hour: \`movie_ratings_User_rated_relation\`.localdatetime.hour , minute: \`movie_ratings_User_rated_relation\`.localdatetime.minute , second: \`movie_ratings_User_rated_relation\`.localdatetime.second , millisecond: \`movie_ratings_User_rated_relation\`.localdatetime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.localdatetime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.localdatetime.nanosecond , formatted: toString(\`movie_ratings_User_rated_relation\`.localdatetime) }}] }]) }] } AS \`movie\``; + expectedCypherQuery = `MATCH (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) RETURN \`movie\` {_id: ID(\`movie\`), .title ,ratings: [(\`movie\`)<-[\`movie_ratings_relation\`:\`RATED\`]-(:\`User\`) | movie_ratings_relation { .rating , .ratings ,time: { hour: \`movie_ratings_relation\`.time.hour , minute: \`movie_ratings_relation\`.time.minute , second: \`movie_ratings_relation\`.time.second , millisecond: \`movie_ratings_relation\`.time.millisecond , microsecond: \`movie_ratings_relation\`.time.microsecond , nanosecond: \`movie_ratings_relation\`.time.nanosecond , timezone: \`movie_ratings_relation\`.time.timezone , formatted: toString(\`movie_ratings_relation\`.time) },date: { year: \`movie_ratings_relation\`.date.year , month: \`movie_ratings_relation\`.date.month , day: \`movie_ratings_relation\`.date.day , formatted: toString(\`movie_ratings_relation\`.date) },datetime: { year: \`movie_ratings_relation\`.datetime.year , month: \`movie_ratings_relation\`.datetime.month , day: \`movie_ratings_relation\`.datetime.day , hour: \`movie_ratings_relation\`.datetime.hour , minute: \`movie_ratings_relation\`.datetime.minute , second: \`movie_ratings_relation\`.datetime.second , millisecond: \`movie_ratings_relation\`.datetime.millisecond , microsecond: \`movie_ratings_relation\`.datetime.microsecond , nanosecond: \`movie_ratings_relation\`.datetime.nanosecond , timezone: \`movie_ratings_relation\`.datetime.timezone , formatted: toString(\`movie_ratings_relation\`.datetime) },datetimes: reduce(a = [], INSTANCE IN movie_ratings_relation.datetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , timezone: INSTANCE.timezone , formatted: toString(INSTANCE) }),localtime: { hour: \`movie_ratings_relation\`.localtime.hour , minute: \`movie_ratings_relation\`.localtime.minute , second: \`movie_ratings_relation\`.localtime.second , millisecond: \`movie_ratings_relation\`.localtime.millisecond , microsecond: \`movie_ratings_relation\`.localtime.microsecond , nanosecond: \`movie_ratings_relation\`.localtime.nanosecond , formatted: toString(\`movie_ratings_relation\`.localtime) },localdatetime: { year: \`movie_ratings_relation\`.localdatetime.year , month: \`movie_ratings_relation\`.localdatetime.month , day: \`movie_ratings_relation\`.localdatetime.day , hour: \`movie_ratings_relation\`.localdatetime.hour , minute: \`movie_ratings_relation\`.localdatetime.minute , second: \`movie_ratings_relation\`.localdatetime.second , millisecond: \`movie_ratings_relation\`.localdatetime.millisecond , microsecond: \`movie_ratings_relation\`.localdatetime.microsecond , nanosecond: \`movie_ratings_relation\`.localdatetime.nanosecond , formatted: toString(\`movie_ratings_relation\`.localdatetime) },User: head([(:\`Movie\`${ADDITIONAL_MOVIE_LABELS})<-[\`movie_ratings_relation\`]-(\`movie_ratings_User\`:\`User\`) | movie_ratings_User {_id: ID(\`movie_ratings_User\`), .name ,rated: [(\`movie_ratings_User\`)-[\`movie_ratings_User_rated_relation\`:\`RATED\`]->(:\`Movie\`${ADDITIONAL_MOVIE_LABELS}) | movie_ratings_User_rated_relation { .rating ,time: { hour: \`movie_ratings_User_rated_relation\`.time.hour , minute: \`movie_ratings_User_rated_relation\`.time.minute , second: \`movie_ratings_User_rated_relation\`.time.second , millisecond: \`movie_ratings_User_rated_relation\`.time.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.time.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.time.nanosecond , timezone: \`movie_ratings_User_rated_relation\`.time.timezone , formatted: toString(\`movie_ratings_User_rated_relation\`.time) },date: { year: \`movie_ratings_User_rated_relation\`.date.year , month: \`movie_ratings_User_rated_relation\`.date.month , day: \`movie_ratings_User_rated_relation\`.date.day , formatted: toString(\`movie_ratings_User_rated_relation\`.date) },datetime: { year: \`movie_ratings_User_rated_relation\`.datetime.year , month: \`movie_ratings_User_rated_relation\`.datetime.month , day: \`movie_ratings_User_rated_relation\`.datetime.day , hour: \`movie_ratings_User_rated_relation\`.datetime.hour , minute: \`movie_ratings_User_rated_relation\`.datetime.minute , second: \`movie_ratings_User_rated_relation\`.datetime.second , millisecond: \`movie_ratings_User_rated_relation\`.datetime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.datetime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.datetime.nanosecond , timezone: \`movie_ratings_User_rated_relation\`.datetime.timezone , formatted: toString(\`movie_ratings_User_rated_relation\`.datetime) },datetimes: reduce(a = [], INSTANCE IN movie_ratings_User_rated_relation.datetimes | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , minute: INSTANCE.minute , second: INSTANCE.second , millisecond: INSTANCE.millisecond , microsecond: INSTANCE.microsecond , nanosecond: INSTANCE.nanosecond , timezone: INSTANCE.timezone , formatted: toString(INSTANCE) }),localtime: { hour: \`movie_ratings_User_rated_relation\`.localtime.hour , minute: \`movie_ratings_User_rated_relation\`.localtime.minute , second: \`movie_ratings_User_rated_relation\`.localtime.second , millisecond: \`movie_ratings_User_rated_relation\`.localtime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.localtime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.localtime.nanosecond , formatted: toString(\`movie_ratings_User_rated_relation\`.localtime) },localdatetime: { year: \`movie_ratings_User_rated_relation\`.localdatetime.year , month: \`movie_ratings_User_rated_relation\`.localdatetime.month , day: \`movie_ratings_User_rated_relation\`.localdatetime.day , hour: \`movie_ratings_User_rated_relation\`.localdatetime.hour , minute: \`movie_ratings_User_rated_relation\`.localdatetime.minute , second: \`movie_ratings_User_rated_relation\`.localdatetime.second , millisecond: \`movie_ratings_User_rated_relation\`.localdatetime.millisecond , microsecond: \`movie_ratings_User_rated_relation\`.localdatetime.microsecond , nanosecond: \`movie_ratings_User_rated_relation\`.localdatetime.nanosecond , formatted: toString(\`movie_ratings_User_rated_relation\`.localdatetime) }}] }]) }] } AS \`movie\``; t.plan(1); return augmentedSchemaCypherTestRunner( @@ -3715,6 +4047,18 @@ test('Create node with list arguments', t => { formatted: "2020-11-23T10:30:01.002003004-08:00[America/Los_Angeles]" } ] + locations: [ + { + x: 10, + y: 20, + z: 30 + }, + { + x: 30, + y: 20, + z: 10 + } + ] ) { movieId title @@ -3729,11 +4073,16 @@ test('Create node with list arguments', t => { second formatted } + locations { + x + y + z + } } }`, expectedCypherQuery = ` - CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released),years:$params.years,titles:$params.titles,imdbRatings:$params.imdbRatings,releases: [value IN $params.releases | datetime(value)]}) - RETURN \`movie\` { .movieId , .title , .titles , .imdbRatings , .years ,releases: reduce(a = [], TEMPORAL_INSTANCE IN movie.releases | a + { year: TEMPORAL_INSTANCE.year , month: TEMPORAL_INSTANCE.month , day: TEMPORAL_INSTANCE.day , hour: TEMPORAL_INSTANCE.hour , second: TEMPORAL_INSTANCE.second , formatted: toString(TEMPORAL_INSTANCE) })} AS \`movie\` + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS} {movieId: apoc.create.uuid(),title:$params.title,released: datetime($params.released),locations: [value IN $params.locations | point(value)],years:$params.years,titles:$params.titles,imdbRatings:$params.imdbRatings,releases: [value IN $params.releases | datetime(value)]}) + RETURN \`movie\` { .movieId , .title , .titles , .imdbRatings , .years ,releases: reduce(a = [], INSTANCE IN movie.releases | a + { year: INSTANCE.year , month: INSTANCE.month , day: INSTANCE.day , hour: INSTANCE.hour , second: INSTANCE.second , formatted: toString(INSTANCE) }),locations: reduce(a = [], INSTANCE IN movie.locations | a + { x: INSTANCE.x , y: INSTANCE.y , z: INSTANCE.z })} AS \`movie\` `; t.plan(1); @@ -4180,7 +4529,7 @@ test('Handle @cypher query with Int list payload', t => { ]); }); -test('Handle @cypher query with Temporal payload', t => { +test('Handle @cypher query with temporal payload', t => { const graphQLQuery = `query { computedTemporal { year @@ -4209,6 +4558,28 @@ test('Handle @cypher query with Temporal payload', t => { ]); }); +test('Handle @cypher query with spatial payload', t => { + const graphQLQuery = `query { + computedSpatial { + x + y + z + crs + } + }`, + expectedCypherQuery = `WITH apoc.cypher.runFirstColumn("WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }", {offset:$offset, first:$first, cypherParams: $cypherParams}, True) AS x UNWIND x AS \`_Neo4jPoint\` RETURN \`_Neo4jPoint\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + test('Handle @cypher mutation using cypherParams with String payload', t => { const graphQLQuery = `mutation { currentUserId @@ -4268,7 +4639,7 @@ test('Handle @cypher mutation with String list payload', t => { ]); }); -test('Handle @cypher mutation with Temporal payload', t => { +test('Handle @cypher mutation with temporal payload', t => { const graphQLQuery = `mutation { computedTemporal { year @@ -4299,6 +4670,30 @@ test('Handle @cypher mutation with Temporal payload', t => { ]); }); +test('Handle @cypher mutation with spatial payload', t => { + const graphQLQuery = `mutation { + computedSpatial { + x + y + z + crs + } + }`, + expectedCypherQuery = `CALL apoc.cypher.doIt("WITH point({ x: 10, y: 20, z: 15 }) AS instance RETURN { x: instance.x, y: instance.y, z: instance.z, crs: instance.crs }", {first:$first, offset:$offset, cypherParams: $cypherParams}) YIELD value + WITH apoc.map.values(value, [keys(value)[0]])[0] AS \`_Neo4jPoint\` + RETURN \`_Neo4jPoint\` `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + first: -1, + cypherParams: CYPHER_PARAMS, + offset: 0 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery) + ]); +}); + test('Handle nested @cypher fields using parameterized arguments and cypherParams', t => { const graphQLQuery = `query someQuery( $strArg1: String