@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
414 lines • 20.4 kB
JavaScript
;
/*
* 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