@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
301 lines • 15.3 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 });
const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder"));
const InterfaceEntity_1 = require("../schema-model/entity/InterfaceEntity");
const InterfaceEntityAdapter_1 = require("../schema-model/entity/model-adapters/InterfaceEntityAdapter");
const case_where_1 = require("../utils/case-where");
const compile_cypher_1 = require("../utils/compile-cypher");
const get_entity_adapter_from_node_1 = require("../utils/get-entity-adapter-from-node");
const get_relationship_type_1 = require("../utils/get-relationship-type");
const utils_1 = require("../utils/utils");
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_set_relationship_properties_1 = require("./create-set-relationship-properties");
const filter_meta_variable_1 = require("./subscriptions/filter-meta-variable");
const build_clause_1 = require("./utils/build-clause");
const create_where_predicate_1 = require("./where/create-where-predicate");
function createConnectAndParams({ withVars, value, varName, relationField, parentVar, refNodes, context, callbackBucket, labelOverride, parentNode, isFirstLevel = true, source, indexPrefix, }) {
(0, check_authentication_1.checkAuthentication)({ context, node: parentNode, targetOperations: ["CREATE_RELATIONSHIP"] });
function createSubqueryContents(relatedNode, connect, index, filters) {
(0, check_authentication_1.checkAuthentication)({ context, node: relatedNode, targetOperations: ["CREATE_RELATIONSHIP"] });
let params = {};
const nodeName = getConnectNodeName(varName, index);
const relationshipName = getConnectEdgeName(varName, index);
const inStr = relationField.direction === "IN" ? "<-" : "-";
const outStr = relationField.direction === "OUT" ? "->" : "-";
const relationTypeStr = (0, get_relationship_type_1.getRelationshipType)(relationField, context.features);
const relTypeStr = `[${relationField.properties ? relationshipName : ""}:${relationTypeStr}]`;
const subquery = [];
const labels = relatedNode.getLabelString(context);
const label = labelOverride ? `:${labelOverride}` : labels;
subquery.push(`\tWITH ${(0, filter_meta_variable_1.filterMetaVariable)(withVars).join(", ")}`);
subquery.push(`\tOPTIONAL MATCH (${nodeName}${label})`);
const whereStrs = [];
if (filters) {
whereStrs.push(filters.predicate[0]);
params = { ...filters.predicate[1] };
if (filters.preComputedSubqueries) {
subquery.push(filters.preComputedSubqueries);
}
}
const authorizationNodes = [{ node: relatedNode, variable: nodeName }];
// If the source is a create operation, it is likely that authorization
// rules are not satisfied until connect operation is complete
if (source !== "CREATE") {
authorizationNodes.push({ node: parentNode, variable: parentVar });
}
const authorizationBeforeAndParams = (0, create_authorization_before_and_params_1.createAuthorizationBeforeAndParams)({
context,
nodes: authorizationNodes,
operations: ["CREATE_RELATIONSHIP"],
indexPrefix,
});
if (authorizationBeforeAndParams) {
const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams;
whereStrs.push(cypher);
params = { ...params, ...authWhereParams };
if (subqueries) {
subquery.push(subqueries);
if (whereStrs.length) {
subquery.push("WITH *");
}
}
}
if (whereStrs.length) {
const predicate = `${whereStrs.join(" AND ")}`;
if (filters?.preComputedSubqueries?.length) {
const columns = [new cypher_builder_1.default.NamedVariable(nodeName)];
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" });
subquery.push(cypher);
}
else {
subquery.push(`\tWHERE ${predicate}`);
}
}
/*
TODO
Replace with subclauses https://neo4j.com/developer/kb/conditional-cypher-execution/
https://neo4j.slack.com/archives/C02PUHA7C/p1603458561099100
*/
subquery.push("\tCALL(*) {");
const withVarsInner = [
...withVars.filter((v) => v !== parentVar),
`collect(${nodeName}) as connectedNodes`,
`collect(${parentVar}) as parentNodes`,
];
subquery.push(`\t\tWITH ${(0, filter_meta_variable_1.filterMetaVariable)(withVarsInner).join(", ")}`);
subquery.push("\t\tCALL(connectedNodes, parentNodes) {");
subquery.push(`\t\t\tUNWIND parentNodes as ${parentVar}`);
subquery.push(`\t\t\tUNWIND connectedNodes as ${nodeName}`);
subquery.push(`\t\t\tCREATE (${parentVar})${inStr}${relTypeStr}${outStr}(${nodeName})`);
if (relationField.properties) {
const relationship = context.relationships.find((x) => x.properties === relationField.properties);
const sourceAdapter = context.schemaModel.getConcreteEntityAdapter(relationship.source);
if (!sourceAdapter) {
throw new Error(`Transpile error: Entity with name ${relationship.source} not found`);
}
const relationshipAdapter = sourceAdapter.relationships.get(relationship.relationshipFieldName);
if (!relationshipAdapter) {
throw new Error(`Transpile error: Relationship with name ${relationship.relationshipFieldName} not found`);
}
const setA = (0, create_set_relationship_properties_1.createSetRelationshipProperties)({
properties: connect.edge ?? {},
varName: relationshipName,
relationship,
relationshipAdapter: relationshipAdapter,
operation: "CREATE",
callbackBucket,
withVars,
parameterPrefix: relationshipName,
parameterNotation: "_",
});
if (setA) {
subquery.push(`\t\t\t${setA[0]}`);
params = { ...params, ...setA[1] };
}
}
subquery.push("\t\t}");
subquery.push("\t}");
const innerMetaStr = "";
subquery.push(`WITH ${[...(0, filter_meta_variable_1.filterMetaVariable)(withVars), nodeName].join(", ")}${innerMetaStr}`);
if (connect.connect) {
const connects = (Array.isArray(connect.connect) ? connect.connect : [connect.connect]);
connects.forEach((c) => {
const reduced = Object.entries(c).reduce((r, [k, v]) => {
const relField = relatedNode.relationFields.find((x) => k === x.fieldName);
const newRefNodes = [];
if (relField.union) {
Object.keys(v).forEach((modelName) => {
newRefNodes.push(context.nodes.find((x) => x.name === modelName));
});
}
else if (relField.interface) {
relField.interface.implementations.forEach((modelName) => {
newRefNodes.push(context.nodes.find((x) => x.name === modelName));
});
}
else {
newRefNodes.push(context.nodes.find((x) => x.name === relField.typeMeta.name));
}
newRefNodes.forEach((newRefNode) => {
const recurse = createConnectAndParams({
withVars: [...withVars, nodeName],
value: relField.union ? v[newRefNode.name] : v,
varName: `${nodeName}_${k}${relField.union ? `_${newRefNode.name}` : ""}`,
relationField: relField,
parentVar: nodeName,
context,
callbackBucket,
refNodes: [newRefNode],
parentNode: relatedNode,
labelOverride: relField.union ? newRefNode.name : "",
isFirstLevel: false,
source: "CONNECT",
});
r.connects.push(recurse[0]);
r.params = { ...r.params, ...recurse[1] };
});
return r;
}, { connects: [], params: {} });
subquery.push(reduced.connects.join("\n"));
params = { ...params, ...reduced.params };
});
}
const authorizationAfterAndParams = (0, create_authorization_after_and_params_1.createAuthorizationAfterAndParams)({
context,
nodes: [
{ node: parentNode, variable: parentVar },
{ node: relatedNode, variable: nodeName },
],
operations: ["CREATE_RELATIONSHIP"],
indexPrefix,
});
if (authorizationAfterAndParams) {
const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams;
if (cypher) {
if (subqueries) {
subquery.push(`WITH *`);
subquery.push(`${subqueries}`);
subquery.push(`WITH *`);
}
else {
subquery.push(`WITH ${[...withVars, nodeName].join(", ")}`);
}
subquery.push(`WHERE ${cypher}`);
params = { ...params, ...authWhereParams };
}
}
subquery.push(`\tRETURN count(*) AS connect_${varName}_${relatedNode.name}${index}`);
return { subquery: subquery.join("\n"), params };
}
function reducer(res, connect, index) {
if (isFirstLevel) {
res.connects.push(`WITH *`);
}
const inner = [];
if (relationField.interface) {
const subqueries = [];
const targetInterface = context.schemaModel.compositeEntities.find((x) => x.name === relationField.typeMeta.name);
if (!targetInterface || !(targetInterface instanceof InterfaceEntity_1.InterfaceEntity)) {
throw new Error(`Target with name ${relationField.typeMeta.name} not found`);
}
const entity = new InterfaceEntityAdapter_1.InterfaceEntityAdapter(targetInterface);
refNodes.forEach((refNode, i) => {
const nodeName = getConnectNodeName(varName, i);
const targetEntity = entity.concreteEntities.find((x) => x.name === refNode.name);
const filters = getFilters({ connect, context, entity, nodeName, targetEntity });
const subquery = createSubqueryContents(refNode, connect, i, filters);
if (subquery.subquery) {
subqueries.push(subquery.subquery);
res.params = { ...res.params, ...subquery.params };
}
});
if (subqueries.length > 0) {
inner.push(subqueries.join("\n}\nCALL(*) {\n\t"));
}
}
else {
const targetNode = refNodes[0];
if (!targetNode) {
throw new Error("No refNodes found");
}
const entity = (0, get_entity_adapter_from_node_1.getEntityAdapterFromNode)(targetNode, context);
const nodeName = getConnectNodeName(varName, index);
const filters = getFilters({ connect, context, entity, nodeName });
const subquery = createSubqueryContents(targetNode, connect, index, filters);
inner.push(subquery.subquery);
res.params = { ...res.params, ...subquery.params };
}
if (inner.length > 0) {
res.connects.push("CALL(*) {");
res.connects.push(...inner);
res.connects.push("}");
}
return res;
}
const { connects, params } = (0, utils_1.asArray)(value).reduce(reducer, {
connects: [],
params: {},
});
return [connects.join("\n"), params];
}
// function to have a single source of truth for the node name of a connect operation, until the refactor to use CypherBuilder.
function getConnectNodeName(varName, index) {
return `${varName}${index}_node`;
}
// function to have a single source of truth for the edge name of a connect operation, until the refactor to use CypherBuilder.
function getConnectEdgeName(varName, index) {
return `${varName}${index}_relationship`;
}
function getFilters({ connect, entity, targetEntity, context, nodeName, }) {
if (!connect.where) {
return;
}
const targetElement = new cypher_builder_1.default.NamedNode(nodeName);
const whereInput = connect.where.node ?? {};
const { predicate: wherePredicate, preComputedSubqueries } = (0, create_where_predicate_1.createWhereNodePredicate)({
entity,
context,
whereInput,
targetElement,
targetEntity,
});
let preComputedWhereFieldsResult = "";
const whereCypher = new cypher_builder_1.default.Raw((env) => {
preComputedWhereFieldsResult = (0, compile_cypher_1.compileCypherIfExists)(preComputedSubqueries, env);
const cypher = wherePredicate ? env.compile(wherePredicate) : "";
return [cypher, {}];
});
const result = (0, build_clause_1.buildClause)(whereCypher, { context, prefix: `${nodeName}_` });
if (result.cypher) {
return {
predicate: [result.cypher, result.params],
preComputedSubqueries: preComputedWhereFieldsResult,
};
}
}
exports.default = createConnectAndParams;
//# sourceMappingURL=create-connect-and-params.js.map