diff --git a/src/augment/directives.js b/src/augment/directives.js index a30cd503..4b2e6e39 100644 --- a/src/augment/directives.js +++ b/src/augment/directives.js @@ -10,6 +10,7 @@ import { buildDirective, buildName } from './ast'; +import { ApolloError } from 'apollo-server-errors'; /** * An enum describing the names of directive definitions and instances @@ -23,7 +24,10 @@ export const DirectiveDefinition = { IS_AUTHENTICATED: 'isAuthenticated', HAS_ROLE: 'hasRole', HAS_SCOPE: 'hasScope', - ADDITIONAL_LABELS: 'additionalLabels' + ADDITIONAL_LABELS: 'additionalLabels', + ID: 'id', + UNIQUE: 'unique', + INDEX: 'index' }; // The name of Role type used in authorization logic @@ -37,24 +41,84 @@ const RelationshipDirectionField = { TO: 'to' }; -/** - * A predicate function for cypher directive fields - */ export const isCypherField = ({ directives = [] }) => getDirective({ directives, name: DirectiveDefinition.CYPHER }); -/** - * A predicate function for neo4j_ignore directive fields - */ export const isIgnoredField = ({ directives = [] }) => getDirective({ directives, name: DirectiveDefinition.NEO4J_IGNORE }); +export const isRelationField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.RELATION + }); + +export const isPrimaryKeyField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.ID + }); + +export const isUniqueField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.UNIQUE + }); + +export const isIndexedField = ({ directives = [] }) => + getDirective({ + directives, + name: DirectiveDefinition.INDEX + }); + +export const validateFieldDirectives = ({ fields = [], directives = [] }) => { + const primaryKeyFields = fields.filter(field => + isPrimaryKeyField({ + directives: field.directives + }) + ); + if (primaryKeyFields.length > 1) + throw new ApolloError( + `The @id directive can only be used once per node type.` + ); + const isPrimaryKey = isPrimaryKeyField({ directives }); + const isUnique = isUniqueField({ directives }); + const isIndex = isIndexedField({ directives }); + const isComputed = isCypherField({ directives }); + if (isComputed) { + if (isPrimaryKey) + throw new ApolloError( + `The @id directive cannot be used with the @cypher directive because computed fields are not stored as properties.` + ); + if (isUnique) + throw new ApolloError( + `The @unique directive cannot be used with the @cypher directive because computed fields are not stored as properties.` + ); + if (isIndex) + throw new ApolloError( + `The @index directive cannot used with the @cypher directive because computed fields are not stored as properties.` + ); + } + if (isPrimaryKey && isUnique) + throw new ApolloError( + `The @id and @unique directive combined are redunant. The @id directive already sets a unique property constraint and an index.` + ); + if (isPrimaryKey && isIndex) + throw new ApolloError( + `The @id and @index directive combined are redundant. The @id directive already sets a unique property constraint and an index.` + ); + if (isUnique && isIndex) + throw new ApolloError( + `The @unique and @index directive combined are redunant. The @unique directive sets both a unique property constraint and an index.` + ); +}; + /** * The main export for augmenting directive definitions */ @@ -363,6 +427,24 @@ const directiveDefinitionBuilderMap = { name: DirectiveDefinition.NEO4J_IGNORE, locations: [DirectiveLocation.FIELD_DEFINITION] }; + }, + [DirectiveDefinition.ID]: ({ config }) => { + return { + name: DirectiveDefinition.ID, + locations: [DirectiveLocation.FIELD_DEFINITION] + }; + }, + [DirectiveDefinition.UNIQUE]: ({ config }) => { + return { + name: DirectiveDefinition.UNIQUE, + locations: [DirectiveLocation.FIELD_DEFINITION] + }; + }, + [DirectiveDefinition.INDEX]: ({ config }) => { + return { + name: DirectiveDefinition.INDEX, + locations: [DirectiveLocation.FIELD_DEFINITION] + }; } }; diff --git a/src/augment/fields.js b/src/augment/fields.js index 659df474..292350ce 100644 --- a/src/augment/fields.js +++ b/src/augment/fields.js @@ -267,3 +267,20 @@ export const propertyFieldExists = ({ ); }); }; + +export const getTypeFields = ({ + typeName = '', + definition = {}, + typeExtensionDefinitionMap = {} +}) => { + const allFields = []; + const fields = definition.fields; + if (fields && fields.length) { + // if there are .fields, return them + allFields.push(...fields); + const extensions = typeExtensionDefinitionMap[typeName] || []; + // also return any .fields of extensions of this type + extensions.forEach(extension => allFields.push(...extension.fields)); + } + return allFields; +}; diff --git a/src/augment/types/node/mutation.js b/src/augment/types/node/mutation.js index e49cf5e0..3afee062 100644 --- a/src/augment/types/node/mutation.js +++ b/src/augment/types/node/mutation.js @@ -11,14 +11,15 @@ import { useAuthDirective, isCypherField } from '../../directives'; -import { getPrimaryKey } from '../../../utils'; +import { getPrimaryKey } from './selection'; import { shouldAugmentType } from '../../augment'; import { OperationType } from '../../types/types'; import { TypeWrappers, getFieldDefinition, isNeo4jIDField, - getTypeExtensionFieldDefinition + getTypeExtensionFieldDefinition, + getTypeFields } from '../../fields'; /** @@ -46,7 +47,12 @@ export const augmentNodeMutationAPI = ({ typeExtensionDefinitionMap, config }) => { - const primaryKey = getPrimaryKey(definition); + const fields = getTypeFields({ + typeName, + definition, + typeExtensionDefinitionMap + }); + const primaryKey = getPrimaryKey({ fields }); const mutationTypeName = OperationType.MUTATION; const mutationType = operationTypeMap[mutationTypeName]; const mutationTypeNameLower = mutationTypeName.toLowerCase(); diff --git a/src/augment/types/node/node.js b/src/augment/types/node/node.js index 0db2cbfb..85482102 100644 --- a/src/augment/types/node/node.js +++ b/src/augment/types/node/node.js @@ -11,7 +11,8 @@ import { TypeWrappers, unwrapNamedType, isPropertyTypeField, - buildNeo4jSystemIDField + buildNeo4jSystemIDField, + getTypeFields } from '../../fields'; import { FilteringArgument, @@ -23,7 +24,11 @@ import { getRelationName, getDirective, isIgnoredField, - DirectiveDefinition + isPrimaryKeyField, + isUniqueField, + isIndexedField, + DirectiveDefinition, + validateFieldDirectives } from '../../directives'; import { buildName, @@ -40,7 +45,8 @@ import { isObjectTypeExtensionDefinition, isInterfaceTypeExtensionDefinition } from '../../types/types'; -import { getPrimaryKey } from '../../../utils'; +import { getPrimaryKey } from './selection'; +import { ApolloError } from 'apollo-server-errors'; /** * The main export for the augmentation process of a GraphQL @@ -216,7 +222,8 @@ export const augmentNodeTypeFields = ({ let fieldType = field.type; let fieldArguments = field.arguments; const fieldDirectives = field.directives; - if (!isIgnoredField({ directives: fieldDirectives })) { + const isIgnored = isIgnoredField({ directives: fieldDirectives }); + if (!isIgnored) { isIgnoredType = false; const fieldName = field.name.value; const unwrappedType = unwrapNamedType({ type: fieldType }); @@ -236,6 +243,10 @@ export const augmentNodeTypeFields = ({ type: outputType }) ) { + validateFieldDirectives({ + fields, + directives: fieldDirectives + }); nodeInputTypeMap = augmentInputTypePropertyFields({ inputTypeMap: nodeInputTypeMap, fieldName, @@ -361,6 +372,21 @@ const augmentNodeTypeField = ({ relationshipDirective, outputTypeWrappers }) => { + const isPrimaryKey = isPrimaryKeyField({ directives: fieldDirectives }); + const isUnique = isUniqueField({ directives: fieldDirectives }); + const isIndex = isIndexedField({ directives: fieldDirectives }); + if (isPrimaryKey) + throw new ApolloError( + `The @id directive cannot be used on @relation fields.` + ); + if (isUnique) + throw new ApolloError( + `The @unique directive cannot be used on @relation fields.` + ); + if (isIndex) + throw new ApolloError( + `The @index directive cannot be used on @relation fields.` + ); const isUnionType = isUnionTypeDefinition({ definition: outputDefinition }); fieldArguments = augmentNodeTypeFieldArguments({ fieldArguments, @@ -458,6 +484,7 @@ const augmentNodeTypeAPI = ({ typeName, propertyInputValues, generatedTypeMap, + typeExtensionDefinitionMap, config }); } @@ -490,12 +517,18 @@ const buildNodeSelectionInputType = ({ typeName, propertyInputValues, generatedTypeMap, + typeExtensionDefinitionMap, config }) => { const mutationTypeName = OperationType.MUTATION; const mutationTypeNameLower = mutationTypeName.toLowerCase(); if (shouldAugmentType(config, mutationTypeNameLower, typeName)) { - const primaryKey = getPrimaryKey(definition); + const fields = getTypeFields({ + typeName, + definition, + typeExtensionDefinitionMap + }); + const primaryKey = getPrimaryKey({ fields }); const propertyInputName = `_${typeName}Input`; if (primaryKey) { const primaryKeyName = primaryKey.name.value; diff --git a/src/augment/types/node/selection.js b/src/augment/types/node/selection.js new file mode 100644 index 00000000..5f8a1c8a --- /dev/null +++ b/src/augment/types/node/selection.js @@ -0,0 +1,114 @@ +import { GraphQLID } from 'graphql'; +import { isNeo4jPropertyType } from '../types'; +import { + isNeo4jTypeField, + isNeo4jIDField, + unwrapNamedType, + TypeWrappers +} from '../../fields'; +import { + isCypherField, + isIgnoredField, + isRelationField, + isPrimaryKeyField, + isUniqueField, + isIndexedField, + validateFieldDirectives +} from '../../directives'; + +/** + * Gets a single field for use as a primary key + */ +export const getPrimaryKey = ({ fields = [] }) => { + // Get all scalar fields that can be used as keys + const keyFields = getKeyFields({ fields }); + // Try getting an @id field + let pk = getPrimaryKeyField(keyFields); + if (!pk) { + // Try getting a single key from @unique fields + const uniqueFields = getUniqueFields(keyFields); + pk = inferPrimaryKey(uniqueFields); + } + if (!pk) { + // Try getting a single key from @index fields + const indexedFields = getIndexedFields(keyFields); + pk = inferPrimaryKey(indexedFields); + } + if (!pk) { + // Try getting a single key from all fields + pk = inferPrimaryKey(keyFields); + } + return pk; +}; + +/** + * Gets all fields for which is it possible to set + * unique property constraint or indexes in Neo4j. + */ +export const getKeyFields = ({ fields = [] }) => { + return fields.filter(field => { + const { name, type, directives } = field; + const unwrappedType = unwrapNamedType({ type }); + validateFieldDirectives({ directives }); + // Prevent ignored, relationship, computed, temporal, + // and spatial fields from being indexable + return ( + !isCypherField({ directives }) && + !isIgnoredField({ directives }) && + !isRelationField({ directives }) && + !isNeo4jIDField({ name: name.value }) && + !isNeo4jPropertyType({ type: unwrappedType.name }) && + !isNeo4jTypeField({ type: unwrappedType.name }) + ); + }); +}; + +// Finds an @id field +const getPrimaryKeyField = fields => + fields.find(({ directives }) => isPrimaryKeyField({ directives })); + +// Gets all @unique fields +const getUniqueFields = fields => + fields.filter(({ directives }) => isUniqueField({ directives })); + +// Gets all @index fields +const getIndexedFields = fields => + fields.filter(({ directives }) => isIndexedField({ directives })); + +/** + * Attempts to select a default primary key by assessing field + * type predecence. Ideally, a default primary keyis an ID type + * and non-nullable. With neither an ID, nor a non-nullable field, + * the first scalar field is used. + */ +const inferPrimaryKey = (fields = []) => { + let pk = undefined; + if (!fields.length) return pk; + // Try to use the first `ID!` field. + pk = fields.find(({ type }) => { + const unwrappedType = unwrapNamedType({ type }); + return ( + unwrappedType.wrappers[TypeWrappers.NON_NULL_NAMED_TYPE] && + unwrappedType.name === GraphQLID.name + ); + }); + if (!pk) { + // Try to use the first `ID` type field. + pk = fields.find(({ type }) => { + return unwrapNamedType({ type }).name === GraphQLID.name; + }); + } + if (!pk) { + // Try to use the first `!` scalar field. + pk = fields.find(({ type }) => { + return unwrapNamedType({ type }).wrappers[ + TypeWrappers.NON_NULL_NAMED_TYPE + ]; + }); + } + if (!pk) { + // Try to use the first field. + pk = fields[0]; + } + return pk; +}; diff --git a/src/augment/types/relationship/mutation.js b/src/augment/types/relationship/mutation.js index 82c7671a..dcf4afce 100644 --- a/src/augment/types/relationship/mutation.js +++ b/src/augment/types/relationship/mutation.js @@ -2,7 +2,12 @@ import { RelationshipDirectionField } from './relationship'; import { buildNodeOutputFields } from './query'; import { shouldAugmentRelationshipField } from '../../augment'; import { OperationType } from '../../types/types'; -import { TypeWrappers, getFieldDefinition, isNeo4jIDField } from '../../fields'; +import { + TypeWrappers, + getFieldDefinition, + isNeo4jIDField, + getTypeFields +} from '../../fields'; import { DirectiveDefinition, buildAuthScopeDirective, @@ -20,7 +25,7 @@ import { buildObjectType, buildInputObjectType } from '../../ast'; -import { getPrimaryKey } from '../../../utils'; +import { getPrimaryKey } from '../node/selection'; import { isExternalTypeExtension } from '../../../federation'; /** @@ -86,8 +91,21 @@ export const augmentRelationshipMutationAPI = ({ typeDefinitionMap, typeExtensionDefinitionMap }); - const fromTypePk = getPrimaryKey(fromTypeDefinition); - const toTypePk = getPrimaryKey(toTypeDefinition); + + const fromFields = getTypeFields({ + typeName: fromType, + definition: fromTypeDefinition, + typeExtensionDefinitionMap + }); + const fromTypePk = getPrimaryKey({ fields: fromFields }); + + const toFields = getTypeFields({ + typeName: toType, + definition: toTypeDefinition, + typeExtensionDefinitionMap + }); + const toTypePk = getPrimaryKey({ fields: toFields }); + if ( !getFieldDefinition({ fields: mutationType.fields, diff --git a/src/augment/types/relationship/relationship.js b/src/augment/types/relationship/relationship.js index 4e6e4cb0..dcf52652 100644 --- a/src/augment/types/relationship/relationship.js +++ b/src/augment/types/relationship/relationship.js @@ -17,9 +17,13 @@ import { getDirective, isIgnoredField, isCypherField, + isPrimaryKeyField, + isUniqueField, + isIndexedField, getDirectiveArgument } from '../../directives'; import { isOperationTypeDefinition } from '../../types/types'; +import { ApolloError } from 'apollo-server-errors'; // An enum for the semantics of the directed fields of a relationship type export const RelationshipDirectionField = { @@ -49,6 +53,21 @@ export const augmentRelationshipTypeField = ({ outputTypeWrappers }) => { if (!isOperationTypeDefinition({ definition, operationTypeMap })) { + const isPrimaryKey = isPrimaryKeyField({ directives: fieldDirectives }); + const isIndex = isIndexedField({ directives: fieldDirectives }); + const isUnique = isUniqueField({ directives: fieldDirectives }); + if (isPrimaryKey) + throw new ApolloError( + `The @id directive cannot be used on @relation type fields.` + ); + if (isUnique) + throw new ApolloError( + `The @unique directive cannot be used on @relation type fields.` + ); + if (isIndex) + throw new ApolloError( + `The @index directive cannot be used on @relation type fields.` + ); if (!isCypherField({ directives: fieldDirectives })) { const relationshipTypeDirective = getDirective({ directives: outputDefinition.directives, @@ -185,6 +204,21 @@ const augmentRelationshipTypeFields = ({ type: outputType }) ) { + const isPrimaryKey = isPrimaryKeyField({ directives: fieldDirectives }); + const isIndex = isIndexedField({ directives: fieldDirectives }); + const isUnique = isUniqueField({ directives: fieldDirectives }); + if (isPrimaryKey) + throw new ApolloError( + `The @id directive cannot be used on @relation types.` + ); + if (isUnique) + throw new ApolloError( + `The @unique directive cannot be used on @relation types.` + ); + if (isIndex) + throw new ApolloError( + `The @index directive cannot be used on @relation types.` + ); relationshipInputTypeMap = augmentInputTypePropertyFields({ inputTypeMap: relationshipInputTypeMap, fieldName, diff --git a/src/augment/types/types.js b/src/augment/types/types.js index 29e0b37d..11e7a1c6 100644 --- a/src/augment/types/types.js +++ b/src/augment/types/types.js @@ -36,7 +36,6 @@ import { getFieldDefinition, isTemporalField } from '../fields'; - import { augmentNodeType, augmentNodeTypeFields } from './node/node'; import { RelationshipDirectionField } from '../types/relationship/relationship'; diff --git a/src/index.js b/src/index.js index 567236b0..e684fa38 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ import { import { buildDocument } from './augment/ast'; import { augmentDirectiveDefinitions } from './augment/directives'; import { isFederatedOperation, executeFederatedOperation } from './federation'; +import { schemaAssert } from './schemaAssert'; const neo4jGraphQLVersion = require('../package.json').version; @@ -306,3 +307,29 @@ export const cypher = (statement, ...substitutions) => { composed.push(literals[literals.length - 1]); return `statement: """${composed.join('')}"""`; }; + +export const assertSchema = ({ + driver, + schema, + dropExisting = true, + debug = false +}) => { + const statement = schemaAssert({ schema, dropExisting }); + const executeQuery = driver => { + const session = driver.session(); + return session + .writeTransaction(tx => tx.run(statement)) + .then(result => { + if (debug === true) { + const recordsJSON = result.records.map(record => record.toObject()); + recordsJSON.sort((lhs, rhs) => lhs.label < rhs.label); + console.table(recordsJSON); + } + return result; + }) + .finally(() => session.close()); + }; + return executeQuery(driver).catch(error => { + console.error(error); + }); +}; diff --git a/src/schemaAssert.js b/src/schemaAssert.js new file mode 100644 index 00000000..f1f05980 --- /dev/null +++ b/src/schemaAssert.js @@ -0,0 +1,79 @@ +import { getFieldDirective } from './utils'; +import { DirectiveDefinition } from './augment/directives'; +import { isNodeType, isUnionTypeDefinition } from './augment/types/types'; +import { getKeyFields } from './augment/types/node/selection'; + +export const schemaAssert = ({ + schema, + indexLabels, + constraintLabels, + dropExisting = true +}) => { + if (!indexLabels) indexLabels = `{}`; + if (!constraintLabels) constraintLabels = `{}`; + if (schema) { + const indexFieldTypeMap = buildKeyTypeMap({ + schema, + directives: [DirectiveDefinition.INDEX] + }); + indexLabels = cypherMap({ + typeMap: indexFieldTypeMap + }); + const uniqueFieldTypeMap = buildKeyTypeMap({ + schema, + directives: [DirectiveDefinition.ID, DirectiveDefinition.UNIQUE] + }); + constraintLabels = cypherMap({ + typeMap: uniqueFieldTypeMap + }); + } + return `CALL apoc.schema.assert(${indexLabels}, ${constraintLabels}${ + dropExisting === false ? `, ${dropExisting}` : '' + })`; +}; + +const buildKeyTypeMap = ({ schema, directives = [] }) => { + const typeMap = schema ? schema.getTypeMap() : {}; + return Object.entries(typeMap).reduce( + (mapped, [typeName, { astNode: definition }]) => { + if ( + isNodeType({ definition }) && + !isUnionTypeDefinition({ definition }) + ) { + const type = schema.getType(typeName); + const fieldMap = type.getFields(); + const fields = Object.values(fieldMap).map(field => field.astNode); + const keyFields = getKeyFields({ fields }); + if (keyFields.length && directives.length) { + const directiveFields = keyFields.filter(field => { + // there exists at least one directive on this field + // matching a directive we want to map + return directives.some(directive => + getFieldDirective(field, directive) + ); + }); + if (directiveFields.length) { + mapped[typeName] = { + ...definition, + fields: directiveFields + }; + } + } + } + return mapped; + }, + {} + ); +}; + +const cypherMap = ({ typeMap = {} }) => { + // The format of a Cypher map is close to JSON but does not quote keys + const cypherMapFormat = Object.entries(typeMap).map(([typeName, astNode]) => { + const fields = astNode.fields || []; + const fieldNames = fields.map(field => field.name.value); + return `${typeName}:${cypherList({ values: fieldNames })}`; + }); + return `{${cypherMapFormat}}`; +}; + +const cypherList = ({ values = [] }) => JSON.stringify(values); diff --git a/src/selections.js b/src/selections.js index 9783299e..c07d0b9f 100644 --- a/src/selections.js +++ b/src/selections.js @@ -13,7 +13,8 @@ import { decideNestedVariableName, safeVar, isNeo4jType, - isNeo4jTypeField, + isTemporalField, + isSpatialField, getNeo4jTypeArguments, removeIgnoredFields, getInterfaceDerivedTypeNames @@ -564,7 +565,10 @@ const translateScalarTypeField = ({ )}}, false)${commaIfTail}`, ...tailParams }; - } else if (isNeo4jTypeField(schemaType, fieldName)) { + } else if ( + isTemporalField(schemaType, fieldName) || + isSpatialField(schemaType, fieldName) + ) { return neo4jTypeField({ initial, fieldName, diff --git a/src/translate.js b/src/translate.js index eab079a0..6d0f2a8c 100644 --- a/src/translate.js +++ b/src/translate.js @@ -18,7 +18,7 @@ import { getOuterSkipLimit, getQueryCypherDirective, getMutationArguments, - possiblySetFirstId, + setPrimaryKeyValue, buildCypherParameters, getQueryArguments, initializeMutationParams, @@ -46,6 +46,7 @@ import { getPayloadSelections, isGraphqlObjectType } from './utils'; +import { getPrimaryKey } from './augment/types/node/selection'; import { getNamedType, isScalarType, @@ -1657,10 +1658,14 @@ const nodeCreate = ({ const safeLabelName = safeLabel([typeName, ...additionalLabels]); let statements = []; const args = getMutationArguments(resolveInfo); - statements = possiblySetFirstId({ + const fieldMap = schemaType.getFields(); + const fields = Object.values(fieldMap).map(field => field.astNode); + const primaryKey = getPrimaryKey({ fields }); + statements = setPrimaryKeyValue({ args, statements, - params: params.params + params: params.params, + primaryKey }); const [preparedParams, paramStatements] = buildCypherParameters({ args, @@ -1696,8 +1701,10 @@ const nodeDelete = ({ const safeVariableName = safeVar(variableName); const safeLabelName = safeLabel(typeName); const args = getMutationArguments(resolveInfo); - const primaryKeyArg = args[0]; - const primaryKeyArgName = primaryKeyArg.name.value; + const fieldMap = schemaType.getFields(); + const fields = Object.values(fieldMap).map(field => field.astNode); + const primaryKey = getPrimaryKey({ fields }); + const primaryKeyArgName = primaryKey.name.value; const neo4jTypeArgs = getNeo4jTypeArguments(args); const [primaryKeyParam] = splitSelectionParameters(params, primaryKeyArgName); const neo4jTypeClauses = neo4jTypePredicateClauses( @@ -2134,8 +2141,10 @@ const nodeMergeOrUpdate = ({ }) => { const safeVariableName = safeVar(variableName); const args = getMutationArguments(resolveInfo); - const primaryKeyArg = args[0]; - const primaryKeyArgName = primaryKeyArg.name.value; + const fieldMap = schemaType.getFields(); + const fields = Object.values(fieldMap).map(field => field.astNode); + const primaryKey = getPrimaryKey({ fields }); + const primaryKeyArgName = primaryKey.name.value; const neo4jTypeArgs = getNeo4jTypeArguments(args); const [primaryKeyParam, updateParams] = splitSelectionParameters( params, diff --git a/src/utils.js b/src/utils.js index 318073f9..903ee4dc 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,11 +1,9 @@ import { parse, GraphQLInt, Kind } from 'graphql'; -import neo4j from 'neo4j-driver'; -import _ from 'lodash'; -import { Neo4jTypeName } from './augment/types/types'; -import { SpatialType } from './augment/types/spatial'; import { unwrapNamedType } from './augment/fields'; import { Neo4jTypeFormatted } from './augment/types/types'; import { getFederatedOperationData } from './federation'; +import neo4j from 'neo4j-driver'; +import _ from 'lodash'; function parseArg(arg, variableValues) { switch (arg.value.kind) { @@ -378,12 +376,23 @@ export const computeOrderBy = (resolveInfo, schemaType) => { }; }; -export const possiblySetFirstId = ({ args, statements = [], params }) => { - const arg = args.find(e => _getNamedType(e).name.value === 'ID'); - // arg is the first ID field if it exists, and we set the value - // if no value is provided for the field name (arg.name.value) in params - if (arg && arg.name.value && params[arg.name.value] === undefined) { - statements.push(`${arg.name.value}: apoc.create.uuid()`); +export const setPrimaryKeyValue = ({ + args = [], + statements = [], + params, + primaryKey +}) => { + if (primaryKey) { + const fieldName = primaryKey.name.value; + const primaryKeyArgument = args.find(arg => arg.name.value === fieldName); + if (primaryKeyArgument) { + const type = primaryKeyArgument.type; + const unwrappedType = unwrapNamedType({ type }); + const isIDTypePrimaryKey = unwrappedType.name === 'ID'; + if (isIDTypePrimaryKey && params[fieldName] === undefined) { + statements.push(`${fieldName}: apoc.create.uuid()`); + } + } } return statements; }; @@ -429,8 +438,7 @@ export const buildCypherParameters = ({ // Get the AST definition for the argument matching this param name const fieldAst = args.find(arg => arg.name.value === paramName); if (fieldAst) { - const fieldType = _getNamedType(fieldAst.type); - const fieldTypeName = fieldType.name.value; + const fieldTypeName = unwrapNamedType({ type: fieldAst.type }).name; if (isNeo4jTypeInput(fieldTypeName)) { paramStatements = buildNeo4jTypeCypherParameters({ paramStatements, @@ -668,74 +676,6 @@ export const getRelationTypeDirective = relationshipType => { : undefined; }; -const firstNonNullAndIdField = fields => { - let valueTypeName = ''; - return fields.find(e => { - valueTypeName = _getNamedType(e).name.value; - return ( - e.name.value !== '_id' && - e.type.kind === 'NonNullType' && - valueTypeName === 'ID' - ); - }); -}; - -const firstIdField = fields => { - let valueTypeName = ''; - return fields.find(e => { - valueTypeName = _getNamedType(e).name.value; - return e.name.value !== '_id' && valueTypeName === 'ID'; - }); -}; - -const firstNonNullField = fields => { - let valueTypeName = ''; - return fields.find(e => { - valueTypeName = _getNamedType(e).name.value; - return valueTypeName === 'NonNullType'; - }); -}; - -const firstField = fields => { - return fields.find(e => { - return e.name.value !== '_id'; - }); -}; - -export const getPrimaryKey = astNode => { - let fields = astNode.fields; - let pk = undefined; - // prevent ignored, relation, and computed fields - // from being used as primary keys - fields = fields.filter( - field => - !getFieldDirective(field, 'neo4j_ignore') && - !getFieldDirective(field, 'relation') && - !getFieldDirective(field, 'cypher') - ); - if (!fields.length) return pk; - pk = firstNonNullAndIdField(fields); - if (!pk) { - pk = firstIdField(fields); - } - if (!pk) { - pk = firstNonNullField(fields); - } - if (!pk) { - pk = firstField(fields); - } - // Do not allow Point primary key - if (pk) { - const type = pk.type; - const unwrappedType = unwrapNamedType({ type }); - const typeName = unwrappedType.name; - if (isSpatialType(typeName) || typeName === SpatialType.POINT) { - pk = undefined; - } - } - return pk; -}; - /** * Render safe a variable name according to cypher rules * @param {String} i input variable name @@ -900,10 +840,6 @@ export const splitSelectionParameters = ( export const isNeo4jType = name => isTemporalType(name) || isSpatialType(name); -export const isNeo4jTypeField = (schemaType, fieldName) => - isTemporalField(schemaType, fieldName) || - isSpatialField(schemaType, fieldName); - export const isNeo4jTypeInput = name => isTemporalInputType(name) || isSpatialInputType(name) || @@ -1018,7 +954,7 @@ export const neo4jTypePredicateClauses = ( if (paramValue) neo4jTypeParam = paramValue; if (neo4jTypeParam[Neo4jTypeFormatted.FORMATTED]) { // Only the dedicated 'formatted' arg is used if it is provided - const type = t ? _getNamedType(t.type).name.value : ''; + const type = unwrapNamedType({ type: t.type }).name; acc.push( `${variableName}.${argName} = ${decideNeo4jTypeConstructor(type)}($${ // use index if provided, for nested arguments @@ -1053,7 +989,7 @@ export const getNeo4jTypeArguments = args => { if (!t) { return acc; } - const fieldType = _getNamedType(t.type).name.value; + const fieldType = unwrapNamedType({ type: t.type }).name; if (isNeo4jTypeInput(fieldType)) acc.push(t); return acc; }, []) @@ -1084,13 +1020,6 @@ export const removeIgnoredFields = (schemaType, selections) => { return selections; }; -const _getNamedType = type => { - if (type.kind !== 'NamedType') { - return _getNamedType(type.type); - } - return type; -}; - export const getInterfaceDerivedTypeNames = (schema, interfaceName) => { const implementingTypeMap = schema._implementations ? schema._implementations[interfaceName] diff --git a/test/helpers/cypherTestHelpers.js b/test/helpers/cypherTestHelpers.js index 3a6ee475..2ab3feee 100644 --- a/test/helpers/cypherTestHelpers.js +++ b/test/helpers/cypherTestHelpers.js @@ -22,7 +22,10 @@ export function cypherTestRunner( type Mutation { CreateGenre(name: String): Genre @cypher(statement: "CREATE (g:Genre) SET g.name = $name RETURN g") CreateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, released: DateTime): Movie - CreateState(name: String!): State + CreateState(name: String!, id: ID): State + CreateUniqueNode(string: String, id: ID, anotherId: ID): UniqueNode @hasScope(scopes: ["UniqueNode: Create"]) + MergeUniqueStringNode(id: ID, uniqueString: String!): UniqueStringNode @hasScope(scopes: ["UniqueStringNode: Merge"]) + DeleteUniqueStringNode(uniqueString: String!): UniqueStringNode @hasScope(scopes: ["UniqueStringNode: Delete"]) UpdateMovie(movieId: ID!, title: String, year: Int, plot: String, poster: String, imdbRating: Float): Movie DeleteMovie(movieId: ID!): Movie MergeUser(userId: ID!, name: String): User @@ -86,11 +89,14 @@ type Mutation { CreateMovie: checkCypherMutation, CreateActor: checkCypherMutation, CreateState: checkCypherMutation, + CreateUniqueNode: checkCypherMutation, + DeleteUniqueStringNode: checkCypherMutation, UpdateMovie: checkCypherMutation, DeleteMovie: checkCypherMutation, MergeUser: checkCypherMutation, MergeBook: checkCypherMutation, MergeNodeTypeMutationTest: checkCypherMutation, + MergeUniqueStringNode: checkCypherMutation, currentUserId: checkCypherMutation, computedObjectWithCypherParams: checkCypherMutation, computedStringList: checkCypherMutation, @@ -216,8 +222,14 @@ export function augmentedSchemaCypherTestRunner( CreateMovie: checkCypherMutation, CreateActor: checkCypherMutation, CreateState: checkCypherMutation, + CreateUniqueNode: checkCypherMutation, + DeleteUniqueStringNode: checkCypherMutation, + AddUniqueNodeTestRelation: checkCypherMutation, + MergeUniqueNodeTestRelation: checkCypherMutation, + RemoveUniqueNodeTestRelation: checkCypherMutation, MergeBook: checkCypherMutation, MergeNodeTypeMutationTest: checkCypherMutation, + MergeUniqueStringNode: checkCypherMutation, CreateTemporalNode: checkCypherMutation, UpdateTemporalNode: checkCypherMutation, DeleteTemporalNode: checkCypherMutation, diff --git a/test/helpers/testSchema.js b/test/helpers/testSchema.js index 10c9afbb..e81f7e7e 100644 --- a/test/helpers/testSchema.js +++ b/test/helpers/testSchema.js @@ -15,7 +15,7 @@ export const testSchema = ` ) { _id: String "Field line description" - movieId: ID! + movieId: ID! @id """ Field block @@ -102,7 +102,8 @@ export const testSchema = ` type State { customField: String @neo4j_ignore - name: String! + name: String! @index + id: ID } """ @@ -110,9 +111,9 @@ export const testSchema = ` block description """ interface Person { - userId: ID! name: String interfacedRelationshipType: [InterfacedRelationshipType] + userId: ID! @id reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] } @@ -644,8 +645,8 @@ export const testSchema = ` } interface Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int operators( @@ -671,8 +672,8 @@ export const testSchema = ` } type OldCamera implements Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int smell: String @@ -688,8 +689,8 @@ export const testSchema = ` } type NewCamera implements Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int features: [String] @@ -727,6 +728,25 @@ export const testSchema = ` reflexiveInterfacedRelationshipType: [ReflexiveInterfacedRelationshipType] } + # Normal primary key field selection applied to use the id field + type UniqueNode { + string: String @unique + id: ID @id + anotherId: ID @index + testRelation: [UniqueStringNode] @relation(name: "TEST_RELATION", direction: OUT) + } + + # Priority applied for @unique uniqueString field as primary + # key, independent of ordering of non-unique fields + type UniqueStringNode { + id: ID! + } + + extend type UniqueStringNode { + uniqueString: String @unique + testRelation: [UniqueNode] @relation(name: "TEST_RELATION", direction: IN) + } + type SubscriptionC { testSubscribe: Boolean } diff --git a/test/unit/assertSchema.test.js b/test/unit/assertSchema.test.js new file mode 100644 index 00000000..00718126 --- /dev/null +++ b/test/unit/assertSchema.test.js @@ -0,0 +1,437 @@ +import test from 'ava'; +import { testSchema } from '../helpers/testSchema'; +import { makeAugmentedSchema } from '../../src/index'; +import { schemaAssert } from '../../src/schemaAssert'; +import { gql } from 'apollo-server'; +import { ApolloError } from 'apollo-server-errors'; + +test('Call assertSchema for @id, @unique, and @index fields on node types', t => { + t.plan(1); + const schema = makeAugmentedSchema({ + typeDefs: testSchema, + config: { + auth: true + } + }); + const expected = `CALL apoc.schema.assert({State:["name"],UniqueNode:["anotherId"]}, {Movie:["movieId"],Person:["userId"],OldCamera:["id"],Camera:["id"],NewCamera:["id"],UniqueNode:["string","id"],UniqueStringNode:["uniqueString"]})`; + const schemaAssertCypher = schemaAssert({ schema }); + t.is(schemaAssertCypher, expected); +}); + +test('Throws error if node type field uses @id more than once', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String @id + movieId: ID! @id + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is(error.message, `The @id directive can only be used once per node type.`); +}); + +test('Throws error if node type field uses @id with @unique', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @id @unique + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @id and @unique directive combined are redunant. The @id directive already sets a unique property constraint and an index.` + ); +}); + +test('Throws error if node type field uses @id with @index', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @id @index + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @id and @index directive combined are redundant. The @id directive already sets a unique property constraint and an index.` + ); +}); + +test('Throws error if node type field uses @unique with @index', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @unique @index + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @unique and @index directive combined are redunant. The @unique directive sets both a unique property constraint and an index.` + ); +}); + +test('Throws error if node type field uses @id with @cypher', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @id @cypher(statement: "") + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @id directive cannot be used with the @cypher directive because computed fields are not stored as properties.` + ); +}); + +test('Throws error if node type field uses @unique with @cypher', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @unique @cypher(statement: "") + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @unique directive cannot be used with the @cypher directive because computed fields are not stored as properties.` + ); +}); + +test('Throws error if node type field uses @index with @cypher', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! @index @cypher(statement: "") + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @index directive cannot used with the @cypher directive because computed fields are not stored as properties.` + ); +}); + +test('Throws error if @id is used on @relation field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + watched: [Movie] @id @relation(name: "WATCHED", direction: OUT) + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is(error.message, `The @id directive cannot be used on @relation fields.`); +}); + +test('Throws error if @unique is used on @relation field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + watched: [Movie] @unique @relation(name: "WATCHED", direction: OUT) + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @unique directive cannot be used on @relation fields.` + ); +}); + +test('Throws error if @index is used on @relation field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + watched: [Movie] @index @relation(name: "WATCHED", direction: OUT) + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @index directive cannot be used on @relation fields.` + ); +}); + +test('Throws error if @id is used on @relation type field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] @id + } + type Rated @relation(name: "RATED") { + from: User + rating: Int + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @id directive cannot be used on @relation type fields.` + ); +}); + +test('Throws error if @unique is used on @relation type field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] @unique + } + type Rated @relation(name: "RATED") { + from: User + rating: Int + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @unique directive cannot be used on @relation type fields.` + ); +}); + +test('Throws error if @index is used on @relation type field', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] @index + } + type Rated @relation(name: "RATED") { + from: User + rating: Int + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @index directive cannot be used on @relation type fields.` + ); +}); + +test('Throws error if @id is used on @relation type', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] + } + type Rated @relation(name: "RATED") { + from: User + rating: Int @id + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is(error.message, `The @id directive cannot be used on @relation types.`); +}); + +test('Throws error if @unique is used on @relation type', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] + } + type Rated @relation(name: "RATED") { + from: User + rating: Int @unique + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @unique directive cannot be used on @relation types.` + ); +}); + +test('Throws error if @index is used on @relation type', t => { + const error = t.throws( + () => { + makeAugmentedSchema({ + typeDefs: gql` + type Movie { + title: String + movieId: ID! + } + type User { + id: ID! + name: String + rated: [Rated] + } + type Rated @relation(name: "RATED") { + from: User + rating: Int @index + to: Movie + } + ` + }); + }, + { + instanceOf: ApolloError + } + ); + t.is( + error.message, + `The @index directive cannot be used on @relation types.` + ); +}); diff --git a/test/unit/augmentSchemaTest.test.js b/test/unit/augmentSchemaTest.test.js index 11101d4b..4726c877 100644 --- a/test/unit/augmentSchemaTest.test.js +++ b/test/unit/augmentSchemaTest.test.js @@ -44,6 +44,12 @@ test.cb('Test augmented schema', t => { directive @neo4j_ignore on FIELD_DEFINITION + directive @id on FIELD_DEFINITION + + directive @unique on FIELD_DEFINITION + + directive @index on FIELD_DEFINITION + directive @isAuthenticated on OBJECT | FIELD_DEFINITION directive @hasRole(roles: [Role]) on OBJECT | FIELD_DEFINITION @@ -236,8 +242,8 @@ test.cb('Test augmented schema', t => { filter: _SpatialNodeFilter ): [SpatialNode] @hasScope(scopes: ["SpatialNode: Read"]) OldCamera( - id: ID type: String + id: ID make: String weight: Int smell: String @@ -248,8 +254,8 @@ test.cb('Test augmented schema', t => { filter: _OldCameraFilter ): [OldCamera] @hasScope(scopes: ["OldCamera: Read"]) NewCamera( - id: ID type: String + id: ID make: String weight: Int features: String @@ -269,6 +275,25 @@ test.cb('Test augmented schema', t => { orderBy: [_CameraManOrdering] filter: _CameraManFilter ): [CameraMan] @hasScope(scopes: ["CameraMan: Read"]) + UniqueNode( + string: String + id: ID + anotherId: ID + _id: String + first: Int + offset: Int + orderBy: [_UniqueNodeOrdering] + filter: _UniqueNodeFilter + ): [UniqueNode] @hasScope(scopes: ["UniqueNode: Read"]) + UniqueStringNode( + id: ID + uniqueString: String + _id: String + first: Int + offset: Int + orderBy: [_UniqueStringNodeOrdering] + filter: _UniqueStringNodeFilter + ): [UniqueStringNode] @hasScope(scopes: ["UniqueStringNode: Read"]) } extend type QueryA { @@ -647,6 +672,16 @@ test.cb('Test augmented schema', t => { name_not_starts_with: String name_ends_with: String name_not_ends_with: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID } input _MovieRatedFilter { @@ -932,7 +967,7 @@ test.cb('Test augmented schema', t => { ) { _id: String "Field line description" - movieId: ID! + movieId: ID! @id """ Field block @@ -1156,7 +1191,6 @@ test.cb('Test augmented schema', t => { block description """ interface Person { - userId: ID! name: String interfacedRelationshipType( first: Int @@ -1164,6 +1198,7 @@ test.cb('Test augmented schema', t => { orderBy: [_InterfacedRelationshipTypeOrdering] filter: _PersonInterfacedRelationshipTypeFilter ): [_PersonInterfacedRelationshipType] + userId: ID! @id reflexiveInterfacedRelationshipType: _PersonReflexiveInterfacedRelationshipTypeDirections } @@ -1188,7 +1223,8 @@ test.cb('Test augmented schema', t => { type State { customField: String @neo4j_ignore - name: String! + name: String! @index + id: ID _id: String } @@ -1549,6 +1585,8 @@ test.cb('Test augmented schema', t => { enum _StateOrdering { name_asc name_desc + id_asc + id_desc _id_asc _id_desc } @@ -1711,6 +1749,28 @@ test.cb('Test augmented schema', t => { extensionScalar_not_ends_with: String } + type _AddTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode + } + + type _RemoveTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode + } + + type _MergeTemporalNodeTemporalNodesPayload + @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { + from: TemporalNode + to: TemporalNode + } + + input _TemporalNodeInput { + name: String! + } + enum _TemporalNodeOrdering { datetime_asc datetime_desc @@ -1900,8 +1960,8 @@ test.cb('Test augmented schema', t => { } interface Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int operators( @@ -1927,10 +1987,10 @@ test.cb('Test augmented schema', t => { } enum _OldCameraOrdering { - id_asc - id_desc type_asc type_desc + id_asc + id_desc make_asc make_desc weight_asc @@ -1944,16 +2004,6 @@ test.cb('Test augmented schema', t => { input _OldCameraFilter { AND: [_OldCameraFilter!] OR: [_OldCameraFilter!] - id: ID - id_not: ID - id_in: [ID!] - id_not_in: [ID!] - id_contains: ID - id_not_contains: ID - id_starts_with: ID - id_not_starts_with: ID - id_ends_with: ID - id_not_ends_with: ID type: String type_not: String type_in: [String!] @@ -1964,6 +2014,16 @@ test.cb('Test augmented schema', t => { type_not_starts_with: String type_ends_with: String type_not_ends_with: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID make: String make_not: String make_in: [String!] @@ -2011,8 +2071,8 @@ test.cb('Test augmented schema', t => { } type OldCamera implements Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int smell: String @@ -2040,10 +2100,10 @@ test.cb('Test augmented schema', t => { } enum _NewCameraOrdering { - id_asc - id_desc type_asc type_desc + id_asc + id_desc make_asc make_desc weight_asc @@ -2055,16 +2115,6 @@ test.cb('Test augmented schema', t => { input _NewCameraFilter { AND: [_NewCameraFilter!] OR: [_NewCameraFilter!] - id: ID - id_not: ID - id_in: [ID!] - id_not_in: [ID!] - id_contains: ID - id_not_contains: ID - id_starts_with: ID - id_not_starts_with: ID - id_ends_with: ID - id_not_ends_with: ID type: String type_not: String type_in: [String!] @@ -2075,6 +2125,16 @@ test.cb('Test augmented schema', t => { type_not_starts_with: String type_ends_with: String type_not_ends_with: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID make: String make_not: String make_in: [String!] @@ -2112,8 +2172,8 @@ test.cb('Test augmented schema', t => { } type NewCamera implements Camera { - id: ID! type: String + id: ID! @unique make: String weight: Int features: [String] @@ -2541,9 +2601,13 @@ test.cb('Test augmented schema', t => { CreateGenre(name: String): Genre @hasScope(scopes: ["Genre: Create"]) DeleteGenre(name: String!): Genre @hasScope(scopes: ["Genre: Delete"]) MergeGenre(name: String!): Genre @hasScope(scopes: ["Genre: Merge"]) - CreateState(name: String!): State @hasScope(scopes: ["State: Create"]) + CreateState(name: String!, id: ID): State + @hasScope(scopes: ["State: Create"]) + UpdateState(name: String!, id: ID): State + @hasScope(scopes: ["State: Update"]) DeleteState(name: String!): State @hasScope(scopes: ["State: Delete"]) - MergeState(name: String!): State @hasScope(scopes: ["State: Merge"]) + MergeState(name: String!, id: ID): State + @hasScope(scopes: ["State: Merge"]) AddPersonInterfacedRelationshipType( from: _PersonInput! to: _GenreInput! @@ -2984,19 +3048,19 @@ test.cb('Test augmented schema', t => { localdatetimes: [_Neo4jLocalDateTimeInput] ): TemporalNode @hasScope(scopes: ["TemporalNode: Create"]) UpdateTemporalNode( - datetime: _Neo4jDateTimeInput! - name: String + datetime: _Neo4jDateTimeInput + name: String! time: _Neo4jTimeInput date: _Neo4jDateInput localtime: _Neo4jLocalTimeInput localdatetime: _Neo4jLocalDateTimeInput localdatetimes: [_Neo4jLocalDateTimeInput] ): TemporalNode @hasScope(scopes: ["TemporalNode: Update"]) - DeleteTemporalNode(datetime: _Neo4jDateTimeInput!): TemporalNode + DeleteTemporalNode(name: String!): TemporalNode @hasScope(scopes: ["TemporalNode: Delete"]) MergeTemporalNode( - datetime: _Neo4jDateTimeInput! - name: String + datetime: _Neo4jDateTimeInput + name: String! time: _Neo4jTimeInput date: _Neo4jDateInput localtime: _Neo4jLocalTimeInput @@ -3162,15 +3226,15 @@ test.cb('Test augmented schema', t => { ) @hasScope(scopes: ["OldCamera: Merge", "Camera: Merge"]) CreateOldCamera( - id: ID type: String + id: ID make: String weight: Int smell: String ): OldCamera @hasScope(scopes: ["OldCamera: Create"]) UpdateOldCamera( - id: ID! type: String + id: ID! make: String weight: Int smell: String @@ -3178,8 +3242,8 @@ test.cb('Test augmented schema', t => { DeleteOldCamera(id: ID!): OldCamera @hasScope(scopes: ["OldCamera: Delete"]) MergeOldCamera( - id: ID! type: String + id: ID! make: String weight: Int smell: String @@ -3233,15 +3297,15 @@ test.cb('Test augmented schema', t => { ) @hasScope(scopes: ["NewCamera: Merge", "Camera: Merge"]) CreateNewCamera( - id: ID type: String + id: ID make: String weight: Int features: [String] ): NewCamera @hasScope(scopes: ["NewCamera: Create"]) UpdateNewCamera( - id: ID! type: String + id: ID! make: String weight: Int features: [String] @@ -3249,8 +3313,8 @@ test.cb('Test augmented schema', t => { DeleteNewCamera(id: ID!): NewCamera @hasScope(scopes: ["NewCamera: Delete"]) MergeNewCamera( - id: ID! type: String + id: ID! make: String weight: Int features: [String] @@ -3436,6 +3500,82 @@ test.cb('Test augmented schema', t => { name: String extensionScalar: String ): CameraMan @hasScope(scopes: ["CameraMan: Merge"]) + AddUniqueNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _AddUniqueNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Create", "UniqueStringNode: Create"]) + RemoveUniqueNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _RemoveUniqueNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Delete", "UniqueStringNode: Delete"]) + MergeUniqueNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _MergeUniqueNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Merge", "UniqueStringNode: Merge"]) + CreateUniqueNode(string: String, id: ID, anotherId: ID): UniqueNode + @hasScope(scopes: ["UniqueNode: Create"]) + UpdateUniqueNode(string: String, id: ID!, anotherId: ID): UniqueNode + @hasScope(scopes: ["UniqueNode: Update"]) + DeleteUniqueNode(id: ID!): UniqueNode + @hasScope(scopes: ["UniqueNode: Delete"]) + MergeUniqueNode(string: String, id: ID!, anotherId: ID): UniqueNode + @hasScope(scopes: ["UniqueNode: Merge"]) + AddUniqueStringNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _AddUniqueStringNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Create", "UniqueStringNode: Create"]) + RemoveUniqueStringNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _RemoveUniqueStringNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Delete", "UniqueStringNode: Delete"]) + MergeUniqueStringNodeTestRelation( + from: _UniqueNodeInput! + to: _UniqueStringNodeInput! + ): _MergeUniqueStringNodeTestRelationPayload + @MutationMeta( + relationship: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) + @hasScope(scopes: ["UniqueNode: Merge", "UniqueStringNode: Merge"]) + CreateUniqueStringNode(id: ID!, uniqueString: String): UniqueStringNode + @hasScope(scopes: ["UniqueStringNode: Create"]) + UpdateUniqueStringNode(id: ID, uniqueString: String!): UniqueStringNode + @hasScope(scopes: ["UniqueStringNode: Update"]) + DeleteUniqueStringNode(uniqueString: String!): UniqueStringNode + @hasScope(scopes: ["UniqueStringNode: Delete"]) + MergeUniqueStringNode(id: ID, uniqueString: String!): UniqueStringNode + @hasScope(scopes: ["UniqueStringNode: Merge"]) } extend type Mutation { @@ -3963,28 +4103,6 @@ test.cb('Test augmented schema', t => { to: Movie } - input _TemporalNodeInput { - datetime: _Neo4jDateTimeInput! - } - - type _AddTemporalNodeTemporalNodesPayload - @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { - from: TemporalNode - to: TemporalNode - } - - type _RemoveTemporalNodeTemporalNodesPayload - @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { - from: TemporalNode - to: TemporalNode - } - - type _MergeTemporalNodeTemporalNodesPayload - @relation(name: "TEMPORAL", from: "TemporalNode", to: "TemporalNode") { - from: TemporalNode - to: TemporalNode - } - input _SpatialNodeInput { id: ID! } @@ -4394,6 +4512,198 @@ test.cb('Test augmented schema', t => { to: Person } + type _AddUniqueNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + type _RemoveUniqueNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + type _MergeUniqueNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + input _UniqueNodeInput { + id: ID! + } + + enum _UniqueNodeOrdering { + string_asc + string_desc + id_asc + id_desc + anotherId_asc + anotherId_desc + _id_asc + _id_desc + } + + input _UniqueNodeFilter { + AND: [_UniqueNodeFilter!] + OR: [_UniqueNodeFilter!] + string: String + string_not: String + string_in: [String!] + string_not_in: [String!] + string_contains: String + string_not_contains: String + string_starts_with: String + string_not_starts_with: String + string_ends_with: String + string_not_ends_with: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID + anotherId: ID + anotherId_not: ID + anotherId_in: [ID!] + anotherId_not_in: [ID!] + anotherId_contains: ID + anotherId_not_contains: ID + anotherId_starts_with: ID + anotherId_not_starts_with: ID + anotherId_ends_with: ID + anotherId_not_ends_with: ID + testRelation: _UniqueStringNodeFilter + testRelation_not: _UniqueStringNodeFilter + testRelation_in: [_UniqueStringNodeFilter!] + testRelation_not_in: [_UniqueStringNodeFilter!] + testRelation_some: _UniqueStringNodeFilter + testRelation_none: _UniqueStringNodeFilter + testRelation_single: _UniqueStringNodeFilter + testRelation_every: _UniqueStringNodeFilter + } + + type UniqueNode { + string: String @unique + id: ID @id + anotherId: ID @index + testRelation( + first: Int + offset: Int + orderBy: [_UniqueStringNodeOrdering] + filter: _UniqueStringNodeFilter + ): [UniqueStringNode] @relation(name: "TEST_RELATION", direction: OUT) + _id: String + } + + type _AddUniqueStringNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + type _RemoveUniqueStringNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + type _MergeUniqueStringNodeTestRelationPayload + @relation( + name: "TEST_RELATION" + from: "UniqueNode" + to: "UniqueStringNode" + ) { + from: UniqueNode + to: UniqueStringNode + } + + input _UniqueStringNodeInput { + uniqueString: String! + } + + enum _UniqueStringNodeOrdering { + id_asc + id_desc + uniqueString_asc + uniqueString_desc + _id_asc + _id_desc + } + + input _UniqueStringNodeFilter { + AND: [_UniqueStringNodeFilter!] + OR: [_UniqueStringNodeFilter!] + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID + uniqueString: String + uniqueString_not: String + uniqueString_in: [String!] + uniqueString_not_in: [String!] + uniqueString_contains: String + uniqueString_not_contains: String + uniqueString_starts_with: String + uniqueString_not_starts_with: String + uniqueString_ends_with: String + uniqueString_not_ends_with: String + testRelation: _UniqueNodeFilter + testRelation_not: _UniqueNodeFilter + testRelation_in: [_UniqueNodeFilter!] + testRelation_not_in: [_UniqueNodeFilter!] + testRelation_some: _UniqueNodeFilter + testRelation_none: _UniqueNodeFilter + testRelation_single: _UniqueNodeFilter + testRelation_every: _UniqueNodeFilter + } + + type UniqueStringNode { + id: ID! + _id: String + } + + extend type UniqueStringNode { + uniqueString: String @unique + testRelation( + first: Int + offset: Int + orderBy: [_UniqueNodeOrdering] + filter: _UniqueNodeFilter + ): [UniqueNode] @relation(name: "TEST_RELATION", direction: IN) + } + type SubscriptionC { testSubscribe: Boolean } diff --git a/test/unit/cypherTest.test.js b/test/unit/cypherTest.test.js index bfa968cb..630f280b 100644 --- a/test/unit/cypherTest.test.js +++ b/test/unit/cypherTest.test.js @@ -3887,571 +3887,25 @@ test('Nested Query with spatial property arguments', t => { ); }); -test('Update temporal and non-temporal properties on node using temporal property node selection', t => { - const graphQLQuery = `mutation { - UpdateTemporalNode( - datetime: { - year: 2020 - month: 11 - day: 23 - hour: 10 - minute: 30 - second: 1 - millisecond: 2 - microsecond: 2003 - nanosecond: 2003004 - timezone: "America/Los_Angeles" - }, - localdatetime: { - year: 2034 - }, - name: "Neo4j" - ) { - _id - name - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - }`, - 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\` += {name:$params.name,localdatetime: localdatetime($params.localdatetime)} RETURN \`temporalNode\` {_id: ID(\`temporalNode\`), .name ,time: { hour: \`temporalNode\`.time.hour , minute: \`temporalNode\`.time.minute , second: \`temporalNode\`.time.second , millisecond: \`temporalNode\`.time.millisecond , microsecond: \`temporalNode\`.time.microsecond , nanosecond: \`temporalNode\`.time.nanosecond , timezone: \`temporalNode\`.time.timezone , formatted: toString(\`temporalNode\`.time) },date: { year: \`temporalNode\`.date.year , month: \`temporalNode\`.date.month , day: \`temporalNode\`.date.day , formatted: toString(\`temporalNode\`.date) },datetime: { year: \`temporalNode\`.datetime.year , month: \`temporalNode\`.datetime.month , day: \`temporalNode\`.datetime.day , hour: \`temporalNode\`.datetime.hour , minute: \`temporalNode\`.datetime.minute , second: \`temporalNode\`.datetime.second , millisecond: \`temporalNode\`.datetime.millisecond , microsecond: \`temporalNode\`.datetime.microsecond , nanosecond: \`temporalNode\`.datetime.nanosecond , timezone: \`temporalNode\`.datetime.timezone , formatted: toString(\`temporalNode\`.datetime) },localtime: { hour: \`temporalNode\`.localtime.hour , minute: \`temporalNode\`.localtime.minute , second: \`temporalNode\`.localtime.second , millisecond: \`temporalNode\`.localtime.millisecond , microsecond: \`temporalNode\`.localtime.microsecond , nanosecond: \`temporalNode\`.localtime.nanosecond , formatted: toString(\`temporalNode\`.localtime) },localdatetime: { year: \`temporalNode\`.localdatetime.year , month: \`temporalNode\`.localdatetime.month , day: \`temporalNode\`.localdatetime.day , hour: \`temporalNode\`.localdatetime.hour , minute: \`temporalNode\`.localdatetime.minute , second: \`temporalNode\`.localdatetime.second , millisecond: \`temporalNode\`.localdatetime.millisecond , microsecond: \`temporalNode\`.localdatetime.microsecond , nanosecond: \`temporalNode\`.localdatetime.nanosecond , formatted: toString(\`temporalNode\`.localdatetime) }} AS \`temporalNode\``; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - -test('Update node spatial property', t => { - const graphQLQuery = `mutation { - UpdateSpatialNode( - id: "xyz", - point: { - longitude: 100, - latitude: 200, - height: 300 - } - ) { - point { - longitude - latitude - height - } - } - }`, - expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`{id: $params.id}) - SET \`spatialNode\` += {point: point($params.point)} RETURN \`spatialNode\` {point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height }} 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( - datetime: { - year: 2020 - month: 11 - day: 23 - hour: 10 - minute: 30 - second: 1 - millisecond: 2 - microsecond: 2003 - nanosecond: 2003004 - timezone: "America/Los_Angeles" - }, - localdatetimes: [ - { - year: 3000 - }, - { - year: 4000 - } - ] - ) { - _id - name - localdatetimes { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - }`, - 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 = [], 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); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - -test('Delete node using temporal property node selection', t => { - const graphQLQuery = `mutation { - DeleteTemporalNode( - datetime: { - year: 2020 - month: 11 - day: 23 - hour: 10 - minute: 30 - second: 1 - millisecond: 2 - microsecond: 2003 - nanosecond: 2003004 - timezone: "America/Los_Angeles" - } - ) { - _id - name - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - }`, - expectedCypherQuery = `MATCH (\`temporalNode\`:\`TemporalNode\`) WHERE \`temporalNode\`.datetime.year = $datetime.year AND \`temporalNode\`.datetime.month = $datetime.month AND \`temporalNode\`.datetime.day = $datetime.day AND \`temporalNode\`.datetime.hour = $datetime.hour AND \`temporalNode\`.datetime.minute = $datetime.minute AND \`temporalNode\`.datetime.second = $datetime.second AND \`temporalNode\`.datetime.millisecond = $datetime.millisecond AND \`temporalNode\`.datetime.microsecond = $datetime.microsecond AND \`temporalNode\`.datetime.nanosecond = $datetime.nanosecond AND \`temporalNode\`.datetime.timezone = $datetime.timezone -WITH \`temporalNode\` AS \`temporalNode_toDelete\`, \`temporalNode\` {_id: ID(\`temporalNode\`), .name ,time: { hour: \`temporalNode\`.time.hour , minute: \`temporalNode\`.time.minute , second: \`temporalNode\`.time.second , millisecond: \`temporalNode\`.time.millisecond , microsecond: \`temporalNode\`.time.microsecond , nanosecond: \`temporalNode\`.time.nanosecond , timezone: \`temporalNode\`.time.timezone , formatted: toString(\`temporalNode\`.time) },date: { year: \`temporalNode\`.date.year , month: \`temporalNode\`.date.month , day: \`temporalNode\`.date.day , formatted: toString(\`temporalNode\`.date) },datetime: { year: \`temporalNode\`.datetime.year , month: \`temporalNode\`.datetime.month , day: \`temporalNode\`.datetime.day , hour: \`temporalNode\`.datetime.hour , minute: \`temporalNode\`.datetime.minute , second: \`temporalNode\`.datetime.second , millisecond: \`temporalNode\`.datetime.millisecond , microsecond: \`temporalNode\`.datetime.microsecond , nanosecond: \`temporalNode\`.datetime.nanosecond , timezone: \`temporalNode\`.datetime.timezone , formatted: toString(\`temporalNode\`.datetime) },localtime: { hour: \`temporalNode\`.localtime.hour , minute: \`temporalNode\`.localtime.minute , second: \`temporalNode\`.localtime.second , millisecond: \`temporalNode\`.localtime.millisecond , microsecond: \`temporalNode\`.localtime.microsecond , nanosecond: \`temporalNode\`.localtime.nanosecond , formatted: toString(\`temporalNode\`.localtime) },localdatetime: { year: \`temporalNode\`.localdatetime.year , month: \`temporalNode\`.localdatetime.month , day: \`temporalNode\`.localdatetime.day , hour: \`temporalNode\`.localdatetime.hour , minute: \`temporalNode\`.localdatetime.minute , second: \`temporalNode\`.localdatetime.second , millisecond: \`temporalNode\`.localdatetime.millisecond , microsecond: \`temporalNode\`.localdatetime.microsecond , nanosecond: \`temporalNode\`.localdatetime.nanosecond , formatted: toString(\`temporalNode\`.localdatetime) }} AS \`temporalNode\` -DETACH DELETE \`temporalNode_toDelete\` -RETURN \`temporalNode\``; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - -test('Add relationship mutation using temporal property node selection', t => { - const graphQLQuery = `mutation { - AddTemporalNodeTemporalNodes( - from: { - datetime: { - year: 2018, - month: 11, - day: 23, - hour: 10, - minute: 30, - second: 1, - millisecond: 2, - microsecond: 2003, - nanosecond: 2003004, - timezone: "America/Los_Angeles" - } - }, - to: { - datetime: { - year: 2020, - month: 11, - day: 23, - hour: 10, - minute: 30, - second: 1, - millisecond: 2, - microsecond: 2003, - nanosecond: 2003004, - timezone: "America/Los_Angeles" - } - } - ) { - from { - _id - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - to { - _id - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - } - }`, - expectedCypherQuery = ` - MATCH (\`temporalNode_from\`:\`TemporalNode\`) WHERE \`temporalNode_from\`.datetime.year = $from.datetime.year AND \`temporalNode_from\`.datetime.month = $from.datetime.month AND \`temporalNode_from\`.datetime.day = $from.datetime.day AND \`temporalNode_from\`.datetime.hour = $from.datetime.hour AND \`temporalNode_from\`.datetime.minute = $from.datetime.minute AND \`temporalNode_from\`.datetime.second = $from.datetime.second AND \`temporalNode_from\`.datetime.millisecond = $from.datetime.millisecond AND \`temporalNode_from\`.datetime.microsecond = $from.datetime.microsecond AND \`temporalNode_from\`.datetime.nanosecond = $from.datetime.nanosecond AND \`temporalNode_from\`.datetime.timezone = $from.datetime.timezone - MATCH (\`temporalNode_to\`:\`TemporalNode\`) WHERE \`temporalNode_to\`.datetime.year = $to.datetime.year AND \`temporalNode_to\`.datetime.month = $to.datetime.month AND \`temporalNode_to\`.datetime.day = $to.datetime.day AND \`temporalNode_to\`.datetime.hour = $to.datetime.hour AND \`temporalNode_to\`.datetime.minute = $to.datetime.minute AND \`temporalNode_to\`.datetime.second = $to.datetime.second AND \`temporalNode_to\`.datetime.millisecond = $to.datetime.millisecond AND \`temporalNode_to\`.datetime.microsecond = $to.datetime.microsecond AND \`temporalNode_to\`.datetime.nanosecond = $to.datetime.nanosecond AND \`temporalNode_to\`.datetime.timezone = $to.datetime.timezone - CREATE (\`temporalNode_from\`)-[\`temporal_relation\`:\`TEMPORAL\`]->(\`temporalNode_to\`) - RETURN \`temporal_relation\` { from: \`temporalNode_from\` {_id: ID(\`temporalNode_from\`),time: { hour: \`temporalNode_from\`.time.hour , minute: \`temporalNode_from\`.time.minute , second: \`temporalNode_from\`.time.second , millisecond: \`temporalNode_from\`.time.millisecond , microsecond: \`temporalNode_from\`.time.microsecond , nanosecond: \`temporalNode_from\`.time.nanosecond , timezone: \`temporalNode_from\`.time.timezone , formatted: toString(\`temporalNode_from\`.time) },date: { year: \`temporalNode_from\`.date.year , month: \`temporalNode_from\`.date.month , day: \`temporalNode_from\`.date.day , formatted: toString(\`temporalNode_from\`.date) },datetime: { year: \`temporalNode_from\`.datetime.year , month: \`temporalNode_from\`.datetime.month , day: \`temporalNode_from\`.datetime.day , hour: \`temporalNode_from\`.datetime.hour , minute: \`temporalNode_from\`.datetime.minute , second: \`temporalNode_from\`.datetime.second , millisecond: \`temporalNode_from\`.datetime.millisecond , microsecond: \`temporalNode_from\`.datetime.microsecond , nanosecond: \`temporalNode_from\`.datetime.nanosecond , timezone: \`temporalNode_from\`.datetime.timezone , formatted: toString(\`temporalNode_from\`.datetime) },localtime: { hour: \`temporalNode_from\`.localtime.hour , minute: \`temporalNode_from\`.localtime.minute , second: \`temporalNode_from\`.localtime.second , millisecond: \`temporalNode_from\`.localtime.millisecond , microsecond: \`temporalNode_from\`.localtime.microsecond , nanosecond: \`temporalNode_from\`.localtime.nanosecond , formatted: toString(\`temporalNode_from\`.localtime) },localdatetime: { year: \`temporalNode_from\`.localdatetime.year , month: \`temporalNode_from\`.localdatetime.month , day: \`temporalNode_from\`.localdatetime.day , hour: \`temporalNode_from\`.localdatetime.hour , minute: \`temporalNode_from\`.localdatetime.minute , second: \`temporalNode_from\`.localdatetime.second , millisecond: \`temporalNode_from\`.localdatetime.millisecond , microsecond: \`temporalNode_from\`.localdatetime.microsecond , nanosecond: \`temporalNode_from\`.localdatetime.nanosecond , formatted: toString(\`temporalNode_from\`.localdatetime) }} ,to: \`temporalNode_to\` {_id: ID(\`temporalNode_to\`),time: { hour: \`temporalNode_to\`.time.hour , minute: \`temporalNode_to\`.time.minute , second: \`temporalNode_to\`.time.second , millisecond: \`temporalNode_to\`.time.millisecond , microsecond: \`temporalNode_to\`.time.microsecond , nanosecond: \`temporalNode_to\`.time.nanosecond , timezone: \`temporalNode_to\`.time.timezone , formatted: toString(\`temporalNode_to\`.time) },date: { year: \`temporalNode_to\`.date.year , month: \`temporalNode_to\`.date.month , day: \`temporalNode_to\`.date.day , formatted: toString(\`temporalNode_to\`.date) },datetime: { year: \`temporalNode_to\`.datetime.year , month: \`temporalNode_to\`.datetime.month , day: \`temporalNode_to\`.datetime.day , hour: \`temporalNode_to\`.datetime.hour , minute: \`temporalNode_to\`.datetime.minute , second: \`temporalNode_to\`.datetime.second , millisecond: \`temporalNode_to\`.datetime.millisecond , microsecond: \`temporalNode_to\`.datetime.microsecond , nanosecond: \`temporalNode_to\`.datetime.nanosecond , timezone: \`temporalNode_to\`.datetime.timezone , formatted: toString(\`temporalNode_to\`.datetime) },localtime: { hour: \`temporalNode_to\`.localtime.hour , minute: \`temporalNode_to\`.localtime.minute , second: \`temporalNode_to\`.localtime.second , millisecond: \`temporalNode_to\`.localtime.millisecond , microsecond: \`temporalNode_to\`.localtime.microsecond , nanosecond: \`temporalNode_to\`.localtime.nanosecond , formatted: toString(\`temporalNode_to\`.localtime) },localdatetime: { year: \`temporalNode_to\`.localdatetime.year , month: \`temporalNode_to\`.localdatetime.month , day: \`temporalNode_to\`.localdatetime.day , hour: \`temporalNode_to\`.localdatetime.hour , minute: \`temporalNode_to\`.localdatetime.minute , second: \`temporalNode_to\`.localdatetime.second , millisecond: \`temporalNode_to\`.localdatetime.millisecond , microsecond: \`temporalNode_to\`.localdatetime.microsecond , nanosecond: \`temporalNode_to\`.localdatetime.nanosecond , formatted: toString(\`temporalNode_to\`.localdatetime) }} } AS \`_AddTemporalNodeTemporalNodesPayload\`; - `; - - t.plan(1); - - return augmentedSchemaCypherTestRunner( - t, - graphQLQuery, - {}, - expectedCypherQuery, - {} - ); -}); - -test('Remove relationship mutation using temporal property node selection', t => { - const graphQLQuery = `mutation { - RemoveTemporalNodeTemporalNodes( - from: { - datetime: { - year: 2018, - month: 11, - day: 23, - hour: 10, - minute: 30, - second: 1, - millisecond: 2, - microsecond: 2003, - nanosecond: 2003004, - timezone: "America/Los_Angeles" - } - }, - to: { - datetime: { - year: 2020, - month: 11, - day: 23, - hour: 10, - minute: 30, - second: 1, - millisecond: 2, - microsecond: 2003, - nanosecond: 2003004, - timezone: "America/Los_Angeles" - } - } - ) { - from { - _id - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - } - to { - _id - time { - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - date { - year - month - day - formatted - } - datetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - timezone - formatted - } - localtime { - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } - localdatetime { - year - month - day - hour - minute - second - millisecond - microsecond - nanosecond - formatted - } +test('Update node spatial property', t => { + const graphQLQuery = `mutation { + UpdateSpatialNode( + id: "xyz", + point: { + longitude: 100, + latitude: 200, + height: 300 + } + ) { + point { + longitude + latitude + height } } }`, - expectedCypherQuery = ` - MATCH (\`temporalNode_from\`:\`TemporalNode\`) WHERE \`temporalNode_from\`.datetime.year = $from.datetime.year AND \`temporalNode_from\`.datetime.month = $from.datetime.month AND \`temporalNode_from\`.datetime.day = $from.datetime.day AND \`temporalNode_from\`.datetime.hour = $from.datetime.hour AND \`temporalNode_from\`.datetime.minute = $from.datetime.minute AND \`temporalNode_from\`.datetime.second = $from.datetime.second AND \`temporalNode_from\`.datetime.millisecond = $from.datetime.millisecond AND \`temporalNode_from\`.datetime.microsecond = $from.datetime.microsecond AND \`temporalNode_from\`.datetime.nanosecond = $from.datetime.nanosecond AND \`temporalNode_from\`.datetime.timezone = $from.datetime.timezone - MATCH (\`temporalNode_to\`:\`TemporalNode\`) WHERE \`temporalNode_to\`.datetime.year = $to.datetime.year AND \`temporalNode_to\`.datetime.month = $to.datetime.month AND \`temporalNode_to\`.datetime.day = $to.datetime.day AND \`temporalNode_to\`.datetime.hour = $to.datetime.hour AND \`temporalNode_to\`.datetime.minute = $to.datetime.minute AND \`temporalNode_to\`.datetime.second = $to.datetime.second AND \`temporalNode_to\`.datetime.millisecond = $to.datetime.millisecond AND \`temporalNode_to\`.datetime.microsecond = $to.datetime.microsecond AND \`temporalNode_to\`.datetime.nanosecond = $to.datetime.nanosecond AND \`temporalNode_to\`.datetime.timezone = $to.datetime.timezone - OPTIONAL MATCH (\`temporalNode_from\`)-[\`temporalNode_fromtemporalNode_to\`:\`TEMPORAL\`]->(\`temporalNode_to\`) - DELETE \`temporalNode_fromtemporalNode_to\` - WITH COUNT(*) AS scope, \`temporalNode_from\` AS \`_temporalNode_from\`, \`temporalNode_to\` AS \`_temporalNode_to\` - RETURN {from: \`_temporalNode_from\` {_id: ID(\`_temporalNode_from\`),time: { hour: \`_temporalNode_from\`.time.hour , minute: \`_temporalNode_from\`.time.minute , second: \`_temporalNode_from\`.time.second , millisecond: \`_temporalNode_from\`.time.millisecond , microsecond: \`_temporalNode_from\`.time.microsecond , nanosecond: \`_temporalNode_from\`.time.nanosecond , timezone: \`_temporalNode_from\`.time.timezone , formatted: toString(\`_temporalNode_from\`.time) },date: { year: \`_temporalNode_from\`.date.year , month: \`_temporalNode_from\`.date.month , day: \`_temporalNode_from\`.date.day , formatted: toString(\`_temporalNode_from\`.date) },datetime: { year: \`_temporalNode_from\`.datetime.year , month: \`_temporalNode_from\`.datetime.month , day: \`_temporalNode_from\`.datetime.day , hour: \`_temporalNode_from\`.datetime.hour , minute: \`_temporalNode_from\`.datetime.minute , second: \`_temporalNode_from\`.datetime.second , millisecond: \`_temporalNode_from\`.datetime.millisecond , microsecond: \`_temporalNode_from\`.datetime.microsecond , nanosecond: \`_temporalNode_from\`.datetime.nanosecond , timezone: \`_temporalNode_from\`.datetime.timezone , formatted: toString(\`_temporalNode_from\`.datetime) },localtime: { hour: \`_temporalNode_from\`.localtime.hour , minute: \`_temporalNode_from\`.localtime.minute , second: \`_temporalNode_from\`.localtime.second , millisecond: \`_temporalNode_from\`.localtime.millisecond , microsecond: \`_temporalNode_from\`.localtime.microsecond , nanosecond: \`_temporalNode_from\`.localtime.nanosecond , formatted: toString(\`_temporalNode_from\`.localtime) },localdatetime: { year: \`_temporalNode_from\`.localdatetime.year , month: \`_temporalNode_from\`.localdatetime.month , day: \`_temporalNode_from\`.localdatetime.day , hour: \`_temporalNode_from\`.localdatetime.hour , minute: \`_temporalNode_from\`.localdatetime.minute , second: \`_temporalNode_from\`.localdatetime.second , millisecond: \`_temporalNode_from\`.localdatetime.millisecond , microsecond: \`_temporalNode_from\`.localdatetime.microsecond , nanosecond: \`_temporalNode_from\`.localdatetime.nanosecond , formatted: toString(\`_temporalNode_from\`.localdatetime) }} ,to: \`_temporalNode_to\` {_id: ID(\`_temporalNode_to\`),time: { hour: \`_temporalNode_to\`.time.hour , minute: \`_temporalNode_to\`.time.minute , second: \`_temporalNode_to\`.time.second , millisecond: \`_temporalNode_to\`.time.millisecond , microsecond: \`_temporalNode_to\`.time.microsecond , nanosecond: \`_temporalNode_to\`.time.nanosecond , timezone: \`_temporalNode_to\`.time.timezone , formatted: toString(\`_temporalNode_to\`.time) },date: { year: \`_temporalNode_to\`.date.year , month: \`_temporalNode_to\`.date.month , day: \`_temporalNode_to\`.date.day , formatted: toString(\`_temporalNode_to\`.date) },datetime: { year: \`_temporalNode_to\`.datetime.year , month: \`_temporalNode_to\`.datetime.month , day: \`_temporalNode_to\`.datetime.day , hour: \`_temporalNode_to\`.datetime.hour , minute: \`_temporalNode_to\`.datetime.minute , second: \`_temporalNode_to\`.datetime.second , millisecond: \`_temporalNode_to\`.datetime.millisecond , microsecond: \`_temporalNode_to\`.datetime.microsecond , nanosecond: \`_temporalNode_to\`.datetime.nanosecond , timezone: \`_temporalNode_to\`.datetime.timezone , formatted: toString(\`_temporalNode_to\`.datetime) },localtime: { hour: \`_temporalNode_to\`.localtime.hour , minute: \`_temporalNode_to\`.localtime.minute , second: \`_temporalNode_to\`.localtime.second , millisecond: \`_temporalNode_to\`.localtime.millisecond , microsecond: \`_temporalNode_to\`.localtime.microsecond , nanosecond: \`_temporalNode_to\`.localtime.nanosecond , formatted: toString(\`_temporalNode_to\`.localtime) },localdatetime: { year: \`_temporalNode_to\`.localdatetime.year , month: \`_temporalNode_to\`.localdatetime.month , day: \`_temporalNode_to\`.localdatetime.day , hour: \`_temporalNode_to\`.localdatetime.hour , minute: \`_temporalNode_to\`.localdatetime.minute , second: \`_temporalNode_to\`.localdatetime.second , millisecond: \`_temporalNode_to\`.localdatetime.millisecond , microsecond: \`_temporalNode_to\`.localdatetime.microsecond , nanosecond: \`_temporalNode_to\`.localdatetime.nanosecond , formatted: toString(\`_temporalNode_to\`.localdatetime) }} } AS \`_RemoveTemporalNodeTemporalNodesPayload\`; - `; + expectedCypherQuery = `MATCH (\`spatialNode\`:\`SpatialNode\`{id: $params.id}) + SET \`spatialNode\` += {point: point($params.point)} RETURN \`spatialNode\` {point: { longitude: \`spatialNode\`.point.longitude , latitude: \`spatialNode\`.point.latitude , height: \`spatialNode\`.point.height }} AS \`spatialNode\``; t.plan(1); @@ -7776,6 +7230,336 @@ test('Create interfaced object type node with additional union label', t => { ]); }); +test('Create object type node with @id field', t => { + const graphQLQuery = `mutation someMutation { + CreateMovie( + title: "My Super Awesome Movie" + year: 2018 + plot: "An unending saga" + poster: "www.movieposter.com/img.png" + imdbRating: 1.0 + ) { + _id + movieId + title + genres { + name + } + } + }`, + expectedCypherQuery = ` + CREATE (\`movie\`:\`Movie\`${ADDITIONAL_MOVIE_LABELS}:\`MovieSearch\` {movieId: apoc.create.uuid(),title:$params.title,year:$params.year,plot:$params.plot,poster:$params.poster,imdbRating:$params.imdbRating}) + RETURN \`movie\` {_id: ID(\`movie\`), .movieId , .title ,genres: [(\`movie\`)-[:\`IN_GENRE\`]->(\`movie_genres\`:\`Genre\`) | \`movie_genres\` { .name }] } AS \`movie\` + `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + title: 'My Super Awesome Movie', + year: 2018, + plot: 'An unending saga', + poster: 'www.movieposter.com/img.png', + imdbRating: 1 + }, + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + title: 'My Super Awesome Movie', + year: 2018, + plot: 'An unending saga', + poster: 'www.movieposter.com/img.png', + imdbRating: 1 + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Create interfaced object type node with @unique field', t => { + const graphQLQuery = `mutation { + CreateNewCamera( + type: "floating" + features: ["selfie", "zoom"] + ) { + id + type + features + } + } + `, + expectedCypherQuery = ` + CREATE (\`newCamera\`:\`NewCamera\`:\`Camera\` {id: apoc.create.uuid(),type:$params.type,features:$params.features}) + RETURN \`newCamera\` { .id , .type , .features } AS \`newCamera\` + `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + type: 'floating', + features: ['selfie', 'zoom'] + }, + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + type: 'floating', + features: ['selfie', 'zoom'] + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Create object type node with @index field', t => { + const graphQLQuery = `mutation { + CreateState(name: "California", id: "123") { + name + } + } + `, + expectedCypherQuery = ` + CREATE (\`state\`:\`State\` {name:$params.name,id:$params.id}) + RETURN \`state\` { .name } AS \`state\` + `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + name: 'California', + id: '123' + }, + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + name: 'California', + id: '123' + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Create object type node with multiple @unique ID type fields', t => { + const graphQLQuery = `mutation { + CreateUniqueNode(string: "hello world", anotherId: "123") { + string + id + anotherId + } + }`, + expectedCypherQuery = ` + CREATE (\`uniqueNode\`:\`UniqueNode\` {id: apoc.create.uuid(),string:$params.string,anotherId:$params.anotherId}) + RETURN \`uniqueNode\` { .string , .id , .anotherId } AS \`uniqueNode\` + `; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + string: 'hello world', + anotherId: '123' + }, + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + string: 'hello world', + anotherId: '123' + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Merge object type node with @unique field', t => { + const graphQLQuery = `mutation { + MergeUniqueStringNode(id: "123", uniqueString: "abc") { + id + uniqueString + } + } +`, + expectedCypherQuery = `MERGE (\`uniqueStringNode\`:\`UniqueStringNode\`{uniqueString: $params.uniqueString}) + SET \`uniqueStringNode\` += {id:$params.id} RETURN \`uniqueStringNode\` { .id , .uniqueString } AS \`uniqueStringNode\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + id: '123', + uniqueString: 'abc' + }, + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + params: { + id: '123', + uniqueString: 'abc' + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Delete object type node with @unique field', t => { + const graphQLQuery = `mutation { + DeleteUniqueStringNode(uniqueString: "abc") { + id + uniqueString + } + }`, + expectedCypherQuery = `MATCH (\`uniqueStringNode\`:\`UniqueStringNode\` {uniqueString: $uniqueString}) +WITH \`uniqueStringNode\` AS \`uniqueStringNode_toDelete\`, \`uniqueStringNode\` { .id , .uniqueString } AS \`uniqueStringNode\` +DETACH DELETE \`uniqueStringNode_toDelete\` +RETURN \`uniqueStringNode\``; + + t.plan(3); + return Promise.all([ + cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + uniqueString: 'abc', + offset: 0, + first: -1 + }), + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + uniqueString: 'abc', + offset: 0, + first: -1 + }) + ]); +}); + +test('Add relationship using @id and @unique node type for node selection', t => { + const graphQLQuery = `mutation { + AddUniqueNodeTestRelation( + from: { id: "123" } + to: { uniqueString: "abc" } + ) { + from { + string + id + anotherId + } + to { + uniqueString + } + } + }`, + expectedCypherQuery = ` + MATCH (\`uniqueNode_from\`:\`UniqueNode\` {id: $from.id}) + MATCH (\`uniqueStringNode_to\`:\`UniqueStringNode\` {uniqueString: $to.uniqueString}) + CREATE (\`uniqueNode_from\`)-[\`test_relation_relation\`:\`TEST_RELATION\`]->(\`uniqueStringNode_to\`) + RETURN \`test_relation_relation\` { from: \`uniqueNode_from\` { .string , .id , .anotherId } ,to: \`uniqueStringNode_to\` { .uniqueString } } AS \`_AddUniqueNodeTestRelationPayload\`; + `; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + from: { + id: '123' + }, + to: { + uniqueString: 'abc' + }, + offset: 0, + first: -1 + }) + ]); +}); + +test('Merge relationship using @id and @unique node type fields for node selection', t => { + const graphQLQuery = `mutation { + MergeUniqueNodeTestRelation( + from: { id: "123" } + to: { uniqueString: "abc" } + ) { + from { + string + id + anotherId + } + to { + uniqueString + } + } + }`, + expectedCypherQuery = ` + MATCH (\`uniqueNode_from\`:\`UniqueNode\` {id: $from.id}) + MATCH (\`uniqueStringNode_to\`:\`UniqueStringNode\` {uniqueString: $to.uniqueString}) + MERGE (\`uniqueNode_from\`)-[\`test_relation_relation\`:\`TEST_RELATION\`]->(\`uniqueStringNode_to\`) + RETURN \`test_relation_relation\` { from: \`uniqueNode_from\` { .string , .id , .anotherId } ,to: \`uniqueStringNode_to\` { .uniqueString } } AS \`_MergeUniqueNodeTestRelationPayload\`; + `; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + from: { + id: '123' + }, + to: { + uniqueString: 'abc' + }, + first: -1, + offset: 0 + }) + ]); +}); + +test('Remove relationship using @id and @unique node type fields for node selection', t => { + const graphQLQuery = `mutation { + RemoveUniqueNodeTestRelation( + from: { id: "123" } + to: { uniqueString: "abc" } + ) { + from { + string + id + anotherId + } + to { + uniqueString + } + } + } + `, + expectedCypherQuery = ` + MATCH (\`uniqueNode_from\`:\`UniqueNode\` {id: $from.id}) + MATCH (\`uniqueStringNode_to\`:\`UniqueStringNode\` {uniqueString: $to.uniqueString}) + OPTIONAL MATCH (\`uniqueNode_from\`)-[\`uniqueNode_fromuniqueStringNode_to\`:\`TEST_RELATION\`]->(\`uniqueStringNode_to\`) + DELETE \`uniqueNode_fromuniqueStringNode_to\` + WITH COUNT(*) AS scope, \`uniqueNode_from\` AS \`_uniqueNode_from\`, \`uniqueStringNode_to\` AS \`_uniqueStringNode_to\` + RETURN {from: \`_uniqueNode_from\` { .string , .id , .anotherId } ,to: \`_uniqueStringNode_to\` { .uniqueString } } AS \`_RemoveUniqueNodeTestRelationPayload\`; + `; + + t.plan(1); + return Promise.all([ + augmentedSchemaCypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, { + from: { + id: '123' + }, + to: { + uniqueString: 'abc' + }, + first: -1, + offset: 0 + }) + ]); +}); + test('query only __typename field on union type', t => { const graphQLQuery = `query { MovieSearch {