UNPKG

@neo4j/graphql

Version:

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

301 lines 15.3 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 }); 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