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