UNPKG

@neo4j/graphql

Version:

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

414 lines 20.4 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 = translateUpdate; const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder")); const debug_1 = __importDefault(require("debug")); const CallbackBucketDeprecated_1 = require("../classes/CallbackBucketDeprecated"); const constants_1 = require("../constants"); const compile_cypher_1 = require("../utils/compile-cypher"); const get_relationship_type_1 = require("../utils/get-relationship-type"); 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 create_update_and_params_1 = __importDefault(require("./create-update-and-params")); const QueryASTContext_1 = require("./queryAST/ast/QueryASTContext"); const QueryASTFactory_1 = require("./queryAST/factory/QueryASTFactory"); const callback_bucket_1 = require("./queryAST/utils/callback-bucket"); const translate_top_level_match_1 = require("./translate-top-level-match"); const build_clause_1 = require("./utils/build-clause"); const get_authorization_statements_1 = require("./utils/get-authorization-statements"); const debug = (0, debug_1.default)(constants_1.DEBUG_TRANSLATE); async function translateUpdate({ node, context, }) { const { resolveTree } = context; const updateInput = resolveTree.args.update; const connectInput = resolveTree.args.connect; const disconnectInput = resolveTree.args.disconnect; const createInput = resolveTree.args.create; const deleteInput = resolveTree.args.delete; const varName = "this"; const callbackBucket = new CallbackBucketDeprecated_1.CallbackBucketDeprecated(context); const withVars = [varName]; let matchAndWhereStr = ""; let updateStr = ""; const connectStrs = []; const disconnectStrs = []; const createStrs = []; let deleteStr = ""; const matchNode = new cypher_builder_1.default.NamedNode(varName); const where = resolveTree.args.where; const matchPattern = new cypher_builder_1.default.Pattern(matchNode, { labels: node.getLabels(context) }); const topLevelMatch = (0, translate_top_level_match_1.translateTopLevelMatch)({ matchNode, matchPattern, node, context, operation: "UPDATE", where, }); matchAndWhereStr = topLevelMatch.cypher; let cypherParams = topLevelMatch.params; const connectionStrs = []; const interfaceStrs = []; let updateArgs = {}; if (deleteInput) { const deleteAndParams = (0, create_delete_and_params_1.default)({ context, node, deleteInput, varName: `${varName}_delete`, parentVar: varName, withVars, parameterPrefix: `${resolveTree.name}.args.delete`, }); [deleteStr] = deleteAndParams; cypherParams = { ...cypherParams, ...deleteAndParams[1], }; updateArgs = { ...updateArgs, ...(deleteStr.includes(resolveTree.name) ? { delete: deleteInput } : {}), }; } if (disconnectInput) { Object.entries(disconnectInput).forEach((entry) => { const relationField = node.relationFields.find((x) => x.fieldName === entry[0]); const refNodes = []; if (relationField.union) { Object.keys(entry[1]).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)); } if (relationField.interface) { const disconnectAndParams = (0, create_disconnect_and_params_1.default)({ context, parentVar: varName, refNodes, relationField, value: entry[1], varName: `${varName}_disconnect_${entry[0]}`, withVars, parentNode: node, parameterPrefix: `${resolveTree.name}.args.disconnect.${entry[0]}`, labelOverride: "", }); disconnectStrs.push(disconnectAndParams[0]); cypherParams = { ...cypherParams, ...disconnectAndParams[1] }; } else { refNodes.forEach((refNode) => { const disconnectAndParams = (0, create_disconnect_and_params_1.default)({ context, parentVar: varName, refNodes: [refNode], relationField, value: relationField.union ? entry[1][refNode.name] : entry[1], varName: `${varName}_disconnect_${entry[0]}${relationField.union ? `_${refNode.name}` : ""}`, withVars, parentNode: node, parameterPrefix: `${resolveTree.name}.args.disconnect.${entry[0]}${relationField.union ? `.${refNode.name}` : ""}`, labelOverride: relationField.union ? refNode.name : "", }); disconnectStrs.push(disconnectAndParams[0]); cypherParams = { ...cypherParams, ...disconnectAndParams[1] }; }); } }); updateArgs = { ...updateArgs, disconnect: disconnectInput, }; } if (updateInput) { const updateAndParams = (0, create_update_and_params_1.default)({ context, callbackBucket, node, updateInput, varName, parentVar: varName, withVars, parameterPrefix: `${resolveTree.name}.args.update`, }); [updateStr] = updateAndParams; cypherParams = { ...cypherParams, ...updateAndParams[1], }; updateArgs = { ...updateArgs, ...(updateStr.includes(resolveTree.name) ? { update: updateInput } : {}), }; } if (connectInput) { Object.entries(connectInput).forEach((entry) => { const relationField = node.relationFields.find((x) => entry[0] === x.fieldName); const relationFieldType = (0, get_relationship_type_1.getRelationshipType)(relationField, context.features); const refNodes = []; if (relationField.union) { Object.keys(entry[1]).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)); } 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) { connectStrs.push("WITH *"); connectStrs.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, parentVar: varName, refNodes, relationField, value: entry[1], varName: `${varName}_connect_${entry[0]}`, withVars, parentNode: node, labelOverride: "", source: "UPDATE", }); connectStrs.push(connectAndParams[0]); cypherParams = { ...cypherParams, ...connectAndParams[1] }; } else { refNodes.forEach((refNode) => { const connectAndParams = (0, create_connect_and_params_1.default)({ context, callbackBucket, parentVar: varName, refNodes: [refNode], relationField, value: relationField.union ? entry[1][refNode.name] : entry[1], varName: `${varName}_connect_${entry[0]}${relationField.union ? `_${refNode.name}` : ""}`, withVars, parentNode: node, labelOverride: relationField.union ? refNode.name : "", source: "UPDATE", }); connectStrs.push(connectAndParams[0]); cypherParams = { ...cypherParams, ...connectAndParams[1] }; }); } }); } if (createInput) { Object.entries(createInput).forEach((entry) => { const relationField = node.relationFields.find((x) => entry[0] === x.fieldName); const relationFieldType = (0, get_relationship_type_1.getRelationshipType)(relationField, context.features); const refNodes = []; if (relationField.union) { Object.keys(entry[1]).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 = relationField.direction === "IN" ? "<-" : "-"; const outStr = relationField.direction === "OUT" ? "->" : "-"; refNodes.forEach((refNode) => { let v = relationField.union ? entry[1][refNode.name] : entry[1]; if (relationField.interface) { if (relationField.typeMeta.array) { v = entry[1] .filter((c) => Object.keys(c.node).includes(refNode.name)) .map((c) => ({ edge: c.edge, node: c.node[refNode.name] })); if (!v.length) { return; } } else { if (!entry[1].node[refNode.name]) { return; } v = { edge: entry[1].edge, node: entry[1].node[refNode.name] }; } } const creates = relationField.typeMeta.array ? v : [v]; creates.forEach((create, index) => { const baseName = `${varName}_create_${entry[0]}${relationField.union || relationField.interface ? `_${refNode.name}` : ""}${index}`; const nodeName = `${baseName}_node${relationField.interface ? `_${refNode.name}` : ""}`; const propertiesName = `${baseName}_relationship`; const relationVarName = relationField.properties ? propertiesName : ""; const relTypeStr = `[${relationVarName}:${relationFieldType}]`; if (!relationField.typeMeta.array) { createStrs.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); createStrs.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); } else if (relationField.interface && relationField.interface.implementations) { const validateRelationshipExistence = relationField.interface.implementations.map(singleCardinalityValidationTemplate); createStrs.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); } else { const validateRelationshipExistence = singleCardinalityValidationTemplate(refNode.name); createStrs.push(validatePredicateTemplate(validateRelationshipExistence)); } } const { create: nestedCreate, params, authorizationPredicates, authorizationSubqueries, } = (0, create_create_and_params_1.default)({ context, callbackBucket, node: refNode, input: create.node, varName: nodeName, withVars: [...withVars, nodeName], }); createStrs.push(nestedCreate); cypherParams = { ...cypherParams, ...params }; createStrs.push(`MERGE (${varName})${inStr}${relTypeStr}${outStr}(${nodeName})`); if (relationField.properties) { const relationship = context.relationships.find((x) => x.properties === relationField.properties); const setA = (0, create_set_relationship_properties_1.createSetRelationshipProperties)({ properties: create.edge ?? {}, varName: propertiesName, withVars, relationship, operation: "CREATE", callbackBucket, parameterPrefix: "", parameterNotation: ".", }); if (setA) { createStrs.push(setA[0]); cypherParams = { ...cypherParams, ...setA[1] }; } } creates.push(...(0, get_authorization_statements_1.getAuthorizationStatements)(authorizationPredicates, authorizationSubqueries)); }); }); }); } const entityAdapter = context.schemaModel.getConcreteEntityAdapter(node.name); if (!entityAdapter) { throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); } const queryAST = new QueryASTFactory_1.QueryASTFactory(context.schemaModel).createMutationAST({ resolveTree, entityAdapter, context, callbackBucket: new callback_bucket_1.CallbackBucket(context), }); const queryASTEnv = new QueryASTContext_1.QueryASTEnv(); const queryASTContext = new QueryASTContext_1.QueryASTContext({ target: new cypher_builder_1.default.NamedNode(varName), env: queryASTEnv, neo4jGraphQLContext: context, returnVariable: new cypher_builder_1.default.NamedVariable("data"), shouldCollect: true, shouldDistinct: true, }); debug(queryAST.print()); const queryASTResult = queryAST.transpile(queryASTContext); const projectionStatements = queryASTResult.clauses.length ? cypher_builder_1.default.utils.concat(...queryASTResult.clauses) : new cypher_builder_1.default.Return(new cypher_builder_1.default.Literal("Query cannot conclude with CALL")); const updateQuery = new cypher_builder_1.default.Raw((env) => { const cypher = [ matchAndWhereStr, deleteStr, disconnectStrs.join("\n"), updateStr, connectStrs.join("\n"), createStrs.join("\n"), ...(deleteStr.length || connectStrs.length || disconnectStrs.length || createStrs.length || connectionStrs.length || isFollowedByASubquery(projectionStatements) ? [`WITH *`] : []), // When FOREACH is the last line of update 'Neo4jError: WITH is required between FOREACH and CALL' ...connectionStrs, ...interfaceStrs, (0, compile_cypher_1.compileCypher)(projectionStatements, env), ] .filter(Boolean) .join("\n"); return [ cypher, { ...cypherParams, ...(Object.keys(updateArgs).length ? { [resolveTree.name]: { args: updateArgs } } : {}), }, ]; }); const cypherResult = (0, build_clause_1.buildClause)(updateQuery, { context, prefix: "update_" }); const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({ cypher: cypherResult.cypher, }); const result = [cypher, { ...cypherResult.params, resolvedCallbacks }]; return result; } /** * Temporary helper to keep consistency with the old code where if a subquery was present, it would be followed by a WITH *. * The recursion is needed because the subquery can be wrapped inside a Cypher.Composite. **/ function isFollowedByASubquery(clause) { if (clause.children?.length) { if (clause.children[0] instanceof cypher_builder_1.default.Call) { return true; } if (clause.children[0]?.children?.length) { return isFollowedByASubquery(clause.children[0]); } } return false; } //# sourceMappingURL=translate-update.js.map