@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
483 lines • 29 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 = 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