UNPKG

@neo4j/graphql

Version:

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

331 lines 15.7 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.ConnectionReadOperation = void 0; const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder")); const utils_1 = require("../../../../utils/utils"); const wrap_subquery_in_calls_1 = require("../../utils/wrap-subquery-in-calls"); const OperationField_1 = require("../fields/OperationField"); const CypherPropertySort_1 = require("../sort/CypherPropertySort"); const CypherAttributeOperation_1 = require("./CypherAttributeOperation"); const operations_1 = require("./operations"); class ConnectionReadOperation extends operations_1.Operation { constructor({ relationship, target, selection, }) { super(); this.nodeFields = []; this.edgeFields = []; // TODO: merge with attachedTo? this.filters = []; this.skipConnection = false; // If set to true, skips the connection (use for aggregation only queries optimisation) this.sortFields = []; this.authFilters = []; this.relationship = relationship; this.target = target; this.selection = selection; } setNodeFields(fields) { this.nodeFields = fields; } addFilters(...filters) { this.filters.push(...filters); } setEdgeFields(fields) { this.edgeFields = fields; } addAuthFilters(...filter) { this.authFilters.push(...filter); } addSort(sortElement) { this.sortFields.push(sortElement); } addPagination(pagination) { this.pagination = pagination; } /** Sets the aggregation field and adds the needed filters */ setAggregationField(aggregationField) { this.aggregationField = aggregationField; } getChildren() { const sortFields = this.sortFields.flatMap((s) => { return [...s.edge, ...s.node]; }); return (0, utils_1.filterTruthy)([ this.selection, ...this.nodeFields, ...this.edgeFields, this.aggregationField, ...this.filters, ...this.authFilters, this.pagination, ...sortFields, ]); } getWithCollectEdgesAndTotalCount(nestedContext, edgesVar, totalCount, extraColumns = []) { const nodeAndRelationshipMap = new cypher_builder_1.default.Map({ node: nestedContext.target, }); if (nestedContext.relationship) { nodeAndRelationshipMap.set("relationship", nestedContext.relationship); } const extraColumnsVariables = extraColumns.map((c) => c[1]); return new cypher_builder_1.default.With([cypher_builder_1.default.collect(nodeAndRelationshipMap), edgesVar], ...extraColumns).with(edgesVar, [cypher_builder_1.default.size(edgesVar), totalCount], ...extraColumnsVariables); } transpile(context) { if (!context.hasTarget()) { throw new Error("Error generating query: contxt has no target in ConnectionReadOperation. This is likely a bug with the @neo4j/graphql library"); } // eslint-disable-next-line prefer-const let { selection: selectionClause, nestedContext } = this.selection.apply(context); let extraMatches = this.getChildren().flatMap((f) => { return f.getSelection(nestedContext); }); if (extraMatches.length > 0) { extraMatches = [selectionClause, ...extraMatches]; selectionClause = new cypher_builder_1.default.With("*"); } const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => { return new cypher_builder_1.default.Call(sq, [nestedContext.target]); }); const normalFilterSubqueries = this.getFilterSubqueries(nestedContext).map((sq) => { return new cypher_builder_1.default.Call(sq, [nestedContext.target]); }); const filtersSubqueries = [...authFilterSubqueries, ...normalFilterSubqueries]; // Only add the import if it is nested const isTopLevel = !this.relationship; const aggregationSubqueries = (this.aggregationField?.getSubqueries(context) ?? []).map((sq) => { if (!isTopLevel) { return new cypher_builder_1.default.Call(sq, [context.target]); } else { return new cypher_builder_1.default.Call(sq); } }); const aggregationProjection = this.aggregationField?.getProjectionField() ?? {}; const edgesVar = new cypher_builder_1.default.NamedVariable("edges"); const totalCount = new cypher_builder_1.default.NamedVariable("totalCount"); const edgesProjectionVar = new cypher_builder_1.default.Variable(); if (this.skipConnection) { const returnClause = new cypher_builder_1.default.Return([ new cypher_builder_1.default.Map({ ...aggregationProjection, }), context.returnVariable, ]); return { clauses: [cypher_builder_1.default.utils.concat(...aggregationSubqueries, returnClause)], projectionExpr: context.returnVariable, }; } const unwindAndProjectionSubquery = this.createUnwindAndProjectionSubquery(nestedContext, edgesVar, edgesProjectionVar); let withWhere; if (filtersSubqueries.length > 0) { withWhere = new cypher_builder_1.default.With("*"); this.addFiltersToClause(withWhere, nestedContext); } else { this.addFiltersToClause(selectionClause, nestedContext); } const withCollectEdgesAndTotalCount = this.getWithCollectEdgesAndTotalCount(nestedContext, edgesVar, totalCount); const returnClause = new cypher_builder_1.default.Return([ new cypher_builder_1.default.Map({ edges: edgesProjectionVar, totalCount: totalCount, ...aggregationProjection, }), context.returnVariable, ]); const validations = this.getValidations(nestedContext); let connectionClauses = cypher_builder_1.default.utils.concat(...extraMatches, selectionClause, ...filtersSubqueries, withWhere, ...validations, withCollectEdgesAndTotalCount, unwindAndProjectionSubquery); if (aggregationSubqueries.length > 0) { connectionClauses = new cypher_builder_1.default.Call(// NOTE: this call is only needed when aggregate is used cypher_builder_1.default.utils.concat(connectionClauses, new cypher_builder_1.default.Return(edgesProjectionVar, totalCount)), "*"); } return { clauses: [cypher_builder_1.default.utils.concat(...aggregationSubqueries, connectionClauses, returnClause)], projectionExpr: context.returnVariable, }; } getAuthFilterSubqueries(context) { return this.authFilters.flatMap((f) => f.getSubqueries(context)); } getFilterSubqueries(context) { return this.filters.flatMap((f) => f.getSubqueries(context)); } getAuthFilterPredicate(context) { return (0, utils_1.filterTruthy)(this.authFilters.map((f) => f.getPredicate(context))); } getValidations(context) { return (0, utils_1.filterTruthy)(this.authFilters.flatMap((f) => f.getValidation(context))); } getUnwindClause(context, edgeVar, edgesVar) { let unwindClause; if (context.relationship) { unwindClause = new cypher_builder_1.default.Unwind([edgesVar, edgeVar]).with([edgeVar.property("node"), context.target], [edgeVar.property("relationship"), context.relationship]); } else { unwindClause = new cypher_builder_1.default.Unwind([edgesVar, edgeVar]).with([edgeVar.property("node"), context.target]); } return unwindClause; } createUnwindAndProjectionSubquery(context, edgesVar, returnVar) { const edgeVar = new cypher_builder_1.default.NamedVariable("edge"); const { prePaginationSubqueries, postPaginationSubqueries } = this.getPreAndPostSubqueries(context); const unwindClause = this.getUnwindClause(context, edgeVar, edgesVar); const edgeProjectionMap = this.createProjectionMapForEdge(context); const paginationWith = this.generateSortAndPaginationClause(context); return new cypher_builder_1.default.Call(cypher_builder_1.default.utils.concat(unwindClause, ...prePaginationSubqueries, paginationWith, ...postPaginationSubqueries, new cypher_builder_1.default.Return([cypher_builder_1.default.collect(edgeProjectionMap), returnVar])), [edgesVar]); } createProjectionMapForNode(context) { const projectionMap = this.generateProjectionMapForFields(this.nodeFields, context.target); if (projectionMap.size === 0) { projectionMap.set({ __id: cypher_builder_1.default.id(context.target), }); } projectionMap.set({ __resolveType: new cypher_builder_1.default.Literal(this.target.name), }); return projectionMap; } addProjectionMapForRelationshipProperties(context, edgeProjectionMap) { if (context.relationship) { const propertiesProjectionMap = this.generateProjectionMapForFields(this.edgeFields, context.relationship); if (propertiesProjectionMap.size) { if (this.relationship?.propertiesTypeName) { // should be true if getting here but just in case.. propertiesProjectionMap.set("__resolveType", new cypher_builder_1.default.Literal(this.relationship.propertiesTypeName)); } edgeProjectionMap.set("properties", propertiesProjectionMap); } } } createProjectionMapForEdge(context) { const edgeProjectionMap = new cypher_builder_1.default.Map(); this.addProjectionMapForRelationshipProperties(context, edgeProjectionMap); edgeProjectionMap.set("node", this.createProjectionMapForNode(context)); return edgeProjectionMap; } generateProjectionMapForFields(fields, target) { const projectionMap = new cypher_builder_1.default.Map(); fields .map((f) => f.getProjectionField(target)) .forEach((p) => { if (typeof p === "string") { projectionMap.set(p, target.property(p)); } else { projectionMap.set(p); } }); return projectionMap; } generateSortAndPaginationClause(context) { const shouldGenerateSortWith = this.pagination || this.sortFields.length > 0; if (!shouldGenerateSortWith) { return undefined; } const paginationWith = new cypher_builder_1.default.With("*"); this.addPaginationSubclauses(paginationWith); this.addSortSubclause(paginationWith, context); return paginationWith; } addPaginationSubclauses(clause) { const paginationField = this.pagination && this.pagination.getPagination(); if (paginationField?.limit) { clause.limit(paginationField.limit); } if (paginationField?.skip) { clause.skip(paginationField.skip); } } addSortSubclause(clause, context) { if (this.sortFields.length > 0) { const sortFields = this.getSortFields({ context: context, nodeVar: context.target, edgeVar: context.relationship, }); clause.orderBy(...sortFields); } } addFiltersToClause(clause, context) { const predicates = this.filters.map((f) => f.getPredicate(context)); const authPredicate = this.getAuthFilterPredicate(context); const predicate = cypher_builder_1.default.and(...predicates, ...authPredicate); if (predicate) { clause.where(predicate); } } getSortFields({ context, nodeVar, edgeVar, }) { const aliasSort = true; return this.sortFields.flatMap(({ node, edge }) => { const nodeFields = node.flatMap((s) => s.getSortFields(context, nodeVar, aliasSort)); if (edgeVar) { const edgeFields = edge.flatMap((s) => s.getSortFields(context, edgeVar, aliasSort)); return [...nodeFields, ...edgeFields]; } return nodeFields; }); } /** * This method resolves all the subqueries for each field and splits them into separate fields: `prePaginationSubqueries` and `postPaginationSubqueries`, * in the `prePaginationSubqueries` are present all the subqueries required for the pagination purpose. **/ getPreAndPostSubqueries(context) { if (!context.hasTarget()) { throw new Error("No parent node found!"); } const sortNodeFields = this.sortFields.flatMap((sf) => sf.node); /** * cypherSortFieldsFlagMap is a Record<string, boolean> that holds the name of the sort field as key * and a boolean flag defined as true when the field is a `@cypher` field. **/ const cypherSortFieldsFlagMap = sortNodeFields.reduce((sortFieldsFlagMap, sortField) => { if (sortField instanceof CypherPropertySort_1.CypherPropertySort) { sortFieldsFlagMap[sortField.getFieldName()] = true; } return sortFieldsFlagMap; }, {}); const preAndPostFields = this.nodeFields.reduce((acc, nodeField) => { if (nodeField instanceof OperationField_1.OperationField && nodeField.isCypherField() && nodeField.operation instanceof CypherAttributeOperation_1.CypherAttributeOperation) { const cypherFieldName = nodeField.operation.cypherAttributeField.name; if (cypherSortFieldsFlagMap[cypherFieldName]) { acc.Pre.push(nodeField); return acc; } } acc.Post.push(nodeField); return acc; }, { Pre: [], Post: [] }); const preNodeSubqueries = (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(context, preAndPostFields.Pre, [context.target]); const postNodeSubqueries = (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(context, preAndPostFields.Post, [context.target]); const sortSubqueries = (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(context, sortNodeFields, [context.target]); return { prePaginationSubqueries: [...sortSubqueries, ...preNodeSubqueries], postPaginationSubqueries: postNodeSubqueries, }; } } exports.ConnectionReadOperation = ConnectionReadOperation; //# sourceMappingURL=ConnectionReadOperation.js.map