UNPKG

@neo4j/graphql

Version:

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations

483 lines 29 kB
"use strict"; /* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = createUpdateAndParams; const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder")); const pluralize_1 = __importDefault(require("pluralize")); const case_where_1 = require("../utils/case-where"); const get_relationship_type_1 = require("../utils/get-relationship-type"); const wrap_string_in_apostrophes_1 = require("../utils/wrap-string-in-apostrophes"); const check_authentication_1 = require("./authorization/check-authentication"); const create_authorization_after_and_params_1 = require("./authorization/compatibility/create-authorization-after-and-params"); const create_authorization_before_and_params_1 = require("./authorization/compatibility/create-authorization-before-and-params"); const create_connect_and_params_1 = __importDefault(require("./create-connect-and-params")); const create_create_and_params_1 = __importDefault(require("./create-create-and-params")); const create_delete_and_params_1 = __importDefault(require("./create-delete-and-params")); const create_disconnect_and_params_1 = __importDefault(require("./create-disconnect-and-params")); const create_set_relationship_properties_1 = require("./create-set-relationship-properties"); const assert_non_ambiguous_update_1 = require("./utils/assert-non-ambiguous-update"); const build_clause_1 = require("./utils/build-clause"); const callback_utils_1 = require("./utils/callback-utils"); const get_authorization_statements_1 = require("./utils/get-authorization-statements"); const get_mutation_field_statements_1 = require("./utils/get-mutation-field-statements"); const get_relationship_direction_1 = require("./utils/get-relationship-direction"); const indent_block_1 = require("./utils/indent-block"); const parse_mutable_field_1 = require("./utils/parse-mutable-field"); const create_connection_where_and_params_1 = __importDefault(require("./where/create-connection-where-and-params")); function createUpdateAndParams({ updateInput, varName, node, parentVar, chainStr, withVars, context, callbackBucket, parameterPrefix, }) { let hasAppliedTimeStamps = false; (0, assert_non_ambiguous_update_1.assertNonAmbiguousUpdate)(node, updateInput); (0, check_authentication_1.checkAuthentication)({ context, node, targetOperations: ["UPDATE"] }); function reducer(res, [key, value]) { let param; if (chainStr) { param = `${chainStr}_${key}`; } else { param = `${parentVar}_update_${key}`; } const relationField = node.relationFields.find((x) => key === x.fieldName); if (relationField) { const relationFieldType = (0, get_relationship_type_1.getRelationshipType)(relationField, context.features); const refNodes = []; const relationship = context.relationships.find((x) => x.properties === relationField.properties); if (relationField.union) { Object.keys(value).forEach((unionTypeName) => { refNodes.push(context.nodes.find((x) => x.name === unionTypeName)); }); } else if (relationField.interface) { relationField.interface?.implementations?.forEach((implementationName) => { refNodes.push(context.nodes.find((x) => x.name === implementationName)); }); } else { refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name)); } const { inStr, outStr } = (0, get_relationship_direction_1.getRelationshipDirection)(relationField); const subqueries = []; const intermediateWithMetaStatements = []; refNodes.forEach((refNode) => { const v = relationField.union ? value[refNode.name] : value; const updates = relationField.typeMeta.array ? v : [v]; const subquery = []; updates.forEach((update, index) => { const relationshipVariable = `${varName}_${relationField.typeUnescaped.toLowerCase()}${index}_relationship`; const relTypeStr = `[${relationshipVariable}:${relationFieldType}]`; const variableName = `${varName}_${key}${relationField.union ? `_${refNode.name}` : ""}${index}`; if (update.delete) { const innerVarName = `${variableName}_delete`; let deleteInput = { [key]: update.delete }; if (relationField.union) { deleteInput = { [key]: { [refNode.name]: update.delete } }; } const deleteAndParams = (0, create_delete_and_params_1.default)({ context, node, deleteInput: deleteInput, varName: innerVarName, chainStr: innerVarName, parentVar, withVars, parameterPrefix: `${parameterPrefix}.${key}${relationField.typeMeta.array ? `[${index}]` : ``}.delete`, // its use here recursing: true, }); subquery.push(deleteAndParams[0]); res.params = { ...res.params, ...deleteAndParams[1] }; } if (update.disconnect) { const disconnectAndParams = (0, create_disconnect_and_params_1.default)({ context, refNodes: [refNode], value: update.disconnect, varName: `${variableName}_disconnect`, withVars, parentVar, relationField, labelOverride: relationField.union ? refNode.name : "", parentNode: node, parameterPrefix: `${parameterPrefix}.${key}${relationField.union ? `.${refNode.name}` : ""}${relationField.typeMeta.array ? `[${index}]` : ""}.disconnect`, }); subquery.push(disconnectAndParams[0]); res.params = { ...res.params, ...disconnectAndParams[1] }; } if (update.update) { const whereStrs = []; const delayedSubquery = []; let aggregationWhere = false; if (update.update.where || update.where) { try { const { cypher: whereClause, subquery: preComputedSubqueries, params: whereParams, } = (0, create_connection_where_and_params_1.default)({ whereInput: update.update.where || update.where, node: refNode, nodeVariable: variableName, relationship, relationshipVariable, context, parameterPrefix: `${parameterPrefix}.${key}${relationField.union ? `.${refNode.name}` : ""}${relationField.typeMeta.array ? `[${index}]` : ``}.where`, }); if (whereClause) { whereStrs.push(whereClause); res.params = { ...res.params, ...whereParams }; if (preComputedSubqueries) { delayedSubquery.push(preComputedSubqueries); aggregationWhere = true; } } } catch { return; } } const innerUpdate = []; if (withVars) { innerUpdate.push(`WITH ${withVars.join(", ")}`); } const labels = refNode.getLabelString(context); innerUpdate.push(`MATCH (${parentVar})${inStr}${relTypeStr}${outStr}(${variableName}${labels})`); innerUpdate.push(...delayedSubquery); const authorizationBeforeAndParams = (0, create_authorization_before_and_params_1.createAuthorizationBeforeAndParams)({ context, nodes: [{ node: refNode, variable: variableName }], operations: ["UPDATE"], indexPrefix: "update", }); if (authorizationBeforeAndParams) { const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; whereStrs.push(cypher); res.params = { ...res.params, ...authWhereParams }; if (subqueries) { innerUpdate.push(subqueries); if (whereStrs.length) { innerUpdate.push("WITH *"); } } } if (whereStrs.length) { const predicate = `${whereStrs.join(" AND ")}`; if (aggregationWhere) { const columns = [ new cypher_builder_1.default.NamedVariable(relationshipVariable), new cypher_builder_1.default.NamedVariable(variableName), ]; const caseWhereClause = (0, case_where_1.caseWhere)(new cypher_builder_1.default.Raw(predicate), columns); const { cypher } = (0, build_clause_1.buildClause)(caseWhereClause, { context, prefix: "aggregateWhereFilter", }); innerUpdate.push(cypher); } else { innerUpdate.push(`WHERE ${predicate}`); } } if (update.update.edge) { const entity = context.schemaModel.getConcreteEntityAdapter(node.name); const relationshipAdapter = entity ? entity.findRelationship(relationField.fieldName) : undefined; const res = (0, create_set_relationship_properties_1.createSetRelationshipProperties)({ properties: update.update.edge, varName: relationshipVariable, withVars: withVars, relationship, relationshipAdapter, callbackBucket, operation: "UPDATE", parameterPrefix: `${parameterPrefix}.${key}${relationField.union ? `.${refNode.name}` : ""}${relationField.typeMeta.array ? `[${index}]` : ``}.update.edge`, parameterNotation: ".", isUpdateOperation: true, }); let setProperties; if (res) { setProperties = res[0]; } if (setProperties) { innerUpdate.push(setProperties); } } if (update.update.node) { const nestedWithVars = [...withVars, variableName]; const nestedUpdateInput = Object.entries(update.update.node).reduce((d1, [k1, v1]) => ({ ...d1, [k1]: v1 }), {}); const updateAndParams = createUpdateAndParams({ context, callbackBucket, node: refNode, updateInput: nestedUpdateInput, varName: variableName, withVars: nestedWithVars, parentVar: variableName, chainStr: `${param}${relationField.union ? `_${refNode.name}` : ""}${index}`, parameterPrefix: `${parameterPrefix}.${key}${relationField.union ? `.${refNode.name}` : ""}${relationField.typeMeta.array ? `[${index}]` : ``}.update.node`, }); res.params = { ...res.params, ...updateAndParams[1] }; innerUpdate.push(updateAndParams[0]); } innerUpdate.push(`RETURN count(*) AS update_${variableName}`); subquery.push(`WITH ${withVars.join(", ")}`, "CALL(*) {", (0, indent_block_1.indentBlock)(innerUpdate.join("\n")), "}"); } if (update.connect) { if (relationField.interface) { if (!relationField.typeMeta.array) { const inStr = relationField.direction === "IN" ? "<-" : "-"; const outStr = relationField.direction === "OUT" ? "->" : "-"; const validatePredicates = []; refNodes.forEach((refNode) => { const validateRelationshipExistence = `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${refNode.name}))`; validatePredicates.push(validateRelationshipExistence); }); if (validatePredicates.length) { subquery.push("WITH *"); subquery.push(`WHERE apoc.util.validatePredicate(${validatePredicates.join(" OR ")},'Relationship field "%s.%s" cannot have more than one node linked',["${relationField.connectionPrefix}","${relationField.fieldName}"])`); } } } const connectAndParams = (0, create_connect_and_params_1.default)({ context, callbackBucket, refNodes: [refNode], value: update.connect, varName: `${variableName}_connect`, withVars, parentVar, relationField, labelOverride: relationField.union ? refNode.name : "", parentNode: node, source: "UPDATE", }); subquery.push(connectAndParams[0]); res.params = { ...res.params, ...connectAndParams[1] }; } if (update.create) { if (withVars) { subquery.push(`WITH ${withVars.join(", ")}`); } const creates = relationField.typeMeta.array ? update.create : [update.create]; creates.forEach((create, i) => { const baseName = `${variableName}_create${i}`; const nodeName = `${baseName}_node`; const propertiesName = `${baseName}_relationship`; let createNodeInput = { input: create.node, }; if (relationField.interface) { const nodeFields = create.node[refNode.name]; if (!nodeFields) return; // Interface specific type not defined createNodeInput = { input: nodeFields, }; } if (!relationField.typeMeta.array) { subquery.push("WITH *"); const validatePredicateTemplate = (condition) => `WHERE apoc.util.validatePredicate(${condition},'Relationship field "%s.%s" cannot have more than one node linked',["${relationField.connectionPrefix}","${relationField.fieldName}"])`; const singleCardinalityValidationTemplate = (nodeName) => `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${nodeName}))`; if (relationField.union && relationField.union.nodes) { const validateRelationshipExistence = relationField.union.nodes.map(singleCardinalityValidationTemplate); subquery.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); } else if (relationField.interface && relationField.interface.implementations) { const validateRelationshipExistence = relationField.interface.implementations.map(singleCardinalityValidationTemplate); subquery.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); } else { const validateRelationshipExistence = singleCardinalityValidationTemplate(refNode.name); subquery.push(validatePredicateTemplate(validateRelationshipExistence)); } } const { create: nestedCreate, params, authorizationPredicates, authorizationSubqueries, } = (0, create_create_and_params_1.default)({ context, node: refNode, callbackBucket, varName: nodeName, withVars: [...withVars, nodeName], ...createNodeInput, }); subquery.push(nestedCreate); res.params = { ...res.params, ...params }; const entity = context.schemaModel.getConcreteEntityAdapter(node.name); const relationshipAdapter = entity ? entity.findRelationship(relationField.fieldName) : undefined; const setA = (0, create_set_relationship_properties_1.createSetRelationshipProperties)({ properties: create.edge ?? {}, varName: propertiesName, withVars, relationship, relationshipAdapter, callbackBucket, operation: "CREATE", parameterPrefix: `${parameterPrefix}.${key}${relationField.union ? `.${refNode.name}` : ""}[${index}].create[${i}].edge`, parameterNotation: ".", }); const relationVarName = setA ? propertiesName : ""; subquery.push(`MERGE (${parentVar})${inStr}[${relationVarName}:${relationFieldType}]${outStr}(${nodeName})`); if (setA) { subquery.push(setA[0]); } subquery.push(...(0, get_authorization_statements_1.getAuthorizationStatements)(authorizationPredicates, authorizationSubqueries)); }); } if (relationField.interface) { const returnStatement = `RETURN count(*) AS update_${varName}_${refNode.name}`; subquery.push(returnStatement); } }); if (subquery.length) { subqueries.push(subquery.join("\n")); } }); if (relationField.interface) { res.strs.push(`WITH ${withVars.join(", ")}`); res.strs.push(`CALL (${withVars.join(", ")}) {\n\t`); const subqueriesWithMetaPassedOn = subqueries.map((each, i) => each + `\n}\n${intermediateWithMetaStatements[i] || ""}`); res.strs.push(subqueriesWithMetaPassedOn.join(`\nCALL (${withVars.join(", ")}){\n\t`)); } else { res.strs.push(subqueries.join("\n")); } return res; } if (!hasAppliedTimeStamps) { const timestampedFields = node.temporalFields.filter((temporalField) => ["DateTime", "Time"].includes(temporalField.typeMeta.name) && temporalField.timestamps?.includes("UPDATE")); timestampedFields.forEach((field) => { // DateTime -> datetime(); Time -> time() res.strs.push(`SET ${varName}.${field.dbPropertyName} = ${field.typeMeta.name.toLowerCase()}()`); }); hasAppliedTimeStamps = true; } [...node.primitiveFields, ...node.temporalFields].forEach((field) => (0, callback_utils_1.addCallbackAndSetParam)(field, varName, updateInput, callbackBucket, res.strs, "UPDATE")); const { settableField, operator } = (0, parse_mutable_field_1.parseMutableField)(node, key); if (settableField) { if (settableField.typeMeta.required && value === null) { throw new Error(`Cannot set non-nullable field ${node.name}.${settableField.fieldName} to null`); } (0, check_authentication_1.checkAuthentication)({ context, node, targetOperations: ["UPDATE"], field: settableField.fieldName }); if (operator === "PUSH" || operator === "POP") { validateNonNullProperty(res, varName, settableField); } const mutationFieldStatements = (0, get_mutation_field_statements_1.getMutationFieldStatements)({ nodeOrRel: node, param, key, varName, value, withVars, isUpdateOperation: true, }); res.strs.push(mutationFieldStatements); res.params[param] = value; const authorizationBeforeAndParams = (0, create_authorization_before_and_params_1.createAuthorizationBeforeAndParamsField)({ context, nodes: [{ node: node, variable: varName, fieldName: settableField.fieldName }], operations: ["UPDATE"], }); if (authorizationBeforeAndParams) { const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; res.meta.authorizationBeforePredicates.push(cypher); if (subqueries) { res.meta.authorizationBeforeSubqueries.push(subqueries); } res.params = { ...res.params, ...authWhereParams }; } const authorizationAfterAndParams = (0, create_authorization_after_and_params_1.createAuthorizationAfterAndParamsField)({ context, nodes: [{ node: node, variable: varName, fieldName: settableField.fieldName }], operations: ["UPDATE"], }); if (authorizationAfterAndParams) { const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; res.meta.authorizationAfterPredicates.push(cypher); if (subqueries) { res.meta.authorizationAfterSubqueries.push(subqueries); } res.params = { ...res.params, ...authWhereParams }; } } return res; } const reducedUpdate = Object.entries(updateInput).reduce(reducer, { strs: [], meta: { preArrayMethodValidationStrs: [], authorizationBeforeSubqueries: [], authorizationBeforePredicates: [], authorizationAfterSubqueries: [], authorizationAfterPredicates: [], }, params: {}, }); const { strs, meta } = reducedUpdate; let params = reducedUpdate.params; const authorizationBeforeStrs = meta.authorizationBeforePredicates; const authorizationBeforeSubqueries = meta.authorizationBeforeSubqueries; const authorizationAfterStrs = meta.authorizationAfterPredicates; const authorizationAfterSubqueries = meta.authorizationAfterSubqueries; const withStr = `WITH ${withVars.join(", ")}`; const authorizationAfterAndParams = (0, create_authorization_after_and_params_1.createAuthorizationAfterAndParams)({ context, nodes: [{ node, variable: varName }], operations: ["UPDATE"], }); if (authorizationAfterAndParams) { const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; if (cypher) { if (subqueries) { authorizationAfterSubqueries.push(subqueries); } authorizationAfterStrs.push(cypher); params = { ...params, ...authWhereParams }; } } const preUpdatePredicates = authorizationBeforeStrs; if (meta.preArrayMethodValidationStrs.length) { const nullChecks = meta.preArrayMethodValidationStrs.map((validationStr) => `${validationStr[0]} IS NULL`); const propertyNames = meta.preArrayMethodValidationStrs.map((validationStr) => validationStr[1]); preUpdatePredicates.push(`apoc.util.validatePredicate(${nullChecks.join(" OR ")}, "${(0, pluralize_1.default)("Property", propertyNames.length)} ${propertyNames.map(() => "%s").join(", ")} cannot be NULL", [${(0, wrap_string_in_apostrophes_1.wrapStringInApostrophes)(propertyNames).join(", ")}])`); } let preUpdatePredicatesStr = ""; let authorizationAfterStr = ""; if (preUpdatePredicates.length) { if (authorizationBeforeSubqueries.length) { preUpdatePredicatesStr = `${withStr}\n${authorizationBeforeSubqueries.join("\n")}\nWITH *\nWHERE ${preUpdatePredicates.join(" AND ")}`; } else { preUpdatePredicatesStr = `${withStr}\nWHERE ${preUpdatePredicates.join(" AND ")}`; } } if (authorizationAfterStrs.length) { if (authorizationAfterSubqueries.length) { authorizationAfterStr = `${withStr}\n${authorizationAfterSubqueries.join("\n")}\nWITH *\nWHERE ${authorizationAfterStrs.join(" AND ")}`; } else { authorizationAfterStr = `${withStr}\nWHERE ${authorizationAfterStrs.join(" AND ")}`; } } const statements = strs; return [[preUpdatePredicatesStr, ...statements, authorizationAfterStr].join("\n"), params]; } function validateNonNullProperty(res, varName, field) { res.meta.preArrayMethodValidationStrs.push([`${varName}.${field.dbPropertyName}`, `${field.dbPropertyName}`]); } //# sourceMappingURL=create-update-and-params.js.map