@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
239 lines • 11.4 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.ReadOperation = void 0;
const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder"));
const utils_1 = require("../../../../utils/utils");
const context_has_target_1 = require("../../utils/context-has-target");
const wrap_subquery_in_calls_1 = require("../../utils/wrap-subquery-in-calls");
const CypherPropertySort_1 = require("../sort/CypherPropertySort");
const operations_1 = require("./operations");
class ReadOperation extends operations_1.Operation {
constructor({ target, relationship, selection, }) {
super();
this.fields = [];
this.filters = [];
this.authFilters = [];
this.sortFields = [];
this.target = target;
this.relationship = relationship;
this.selection = selection;
}
setFields(fields) {
this.fields = fields;
}
addSort(...sort) {
this.sortFields.push(...sort);
}
addPagination(pagination) {
this.pagination = pagination;
}
addFilters(...filters) {
this.filters.push(...filters);
}
addAuthFilters(...filter) {
this.authFilters.push(...filter);
}
getAuthFilterSubqueries(context) {
return this.authFilters.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.map((f) => f.getValidation(context)));
}
getProjectionClause(context, returnVariable) {
if (!(0, context_has_target_1.hasTarget)(context)) {
throw new Error("No parent node found!");
}
const projection = this.getProjectionMap(context);
const aggregationExpr = cypher_builder_1.default.collect(context.target);
const withClause = new cypher_builder_1.default.With([projection, context.target]);
if (this.sortFields.length > 0 || this.pagination) {
this.addSortToClause(context, context.target, withClause);
}
return withClause.return([aggregationExpr, returnVariable]);
}
getPredicates(queryASTContext) {
return cypher_builder_1.default.and(...this.filters.map((f) => f.getPredicate(queryASTContext)));
}
transpile(context) {
// eslint-disable-next-line prefer-const
let { selection: matchClause, nestedContext } = this.selection.apply(context);
const topLevelOperationName = (this.relationship ? context : nestedContext).env.topLevelOperationName;
const isCreateSelection = topLevelOperationName === "CREATE";
const isUpdateSelection = topLevelOperationName === "UPDATE";
if (isCreateSelection || isUpdateSelection) {
if (!context.hasTarget()) {
throw new Error("Invalid target for create operation");
}
if (!this.relationship) {
// Match is not applied on mutations (last concat ignores the top level match) so we revert the context apply
nestedContext = context;
}
}
const filterSubqueries = (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(nestedContext, this.filters, [nestedContext.target]);
const filterPredicates = this.getPredicates(nestedContext);
const fieldSubqueries = this.getFieldsSubqueries(nestedContext);
const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext);
const sortSubqueries = (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(nestedContext, this.sortFields, [nestedContext.target]);
const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => new cypher_builder_1.default.Call(sq, [nestedContext.target]));
const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext);
const ret = this.relationship
? this.getProjectionClause(nestedContext, context.returnVariable)
: this.getReturnStatement(isCreateSelection || isUpdateSelection ? context : nestedContext, nestedContext.returnVariable);
let filterSubqueryWith;
let filterSubqueriesClause;
const validations = this.getValidations(nestedContext);
// WITH is required between SET and CALL
const shouldAddWithForAuth = authFilterSubqueries.length || authFiltersPredicate.length || validations.length;
if (filterSubqueries.length || shouldAddWithForAuth) {
filterSubqueriesClause = cypher_builder_1.default.utils.concat(...filterSubqueries);
if (!isCreateSelection || authFilterSubqueries.length) {
filterSubqueryWith = new cypher_builder_1.default.With("*");
}
}
let sortAndLimitBlock;
let subqueries;
if (this.relationship) {
subqueries = cypher_builder_1.default.utils.concat(...fieldSubqueries, ...cypherFieldSubqueries, ...sortSubqueries);
}
else {
subqueries = cypher_builder_1.default.utils.concat(...fieldSubqueries);
let sortClause;
if (this.sortFields.length || this.pagination) {
sortClause = new cypher_builder_1.default.With("*");
this.addSortToClause(nestedContext, nestedContext.target, sortClause);
}
const sortBlock = cypher_builder_1.default.utils.concat(...sortSubqueries, sortClause);
sortAndLimitBlock = this.hasCypherSort()
? cypher_builder_1.default.utils.concat(...cypherFieldSubqueries, sortBlock)
: cypher_builder_1.default.utils.concat(sortBlock, ...cypherFieldSubqueries);
}
let clause;
if (isCreateSelection && !this.relationship) {
// Top-level read part of a mutation does not contain the MATCH clause as it's implicit in the mutation.
clause = cypher_builder_1.default.utils.concat(filterSubqueriesClause, filterSubqueryWith, sortAndLimitBlock, subqueries, ret);
}
else {
const extraMatches = this.getChildren().flatMap((f) => f.getSelection(nestedContext));
let extraMatchesWith;
const wherePredicate = cypher_builder_1.default.and(filterPredicates, ...authFiltersPredicate);
if (wherePredicate) {
if (filterSubqueryWith) {
filterSubqueryWith.where(wherePredicate); // TODO: should this only be for aggregation filters?
}
else if (extraMatches.length) {
extraMatchesWith = new cypher_builder_1.default.With("*").where(wherePredicate);
}
else {
matchClause.where(wherePredicate);
}
}
const matchBlock = [];
if (!isUpdateSelection || this.relationship) {
matchBlock.push(matchClause);
}
matchBlock.push(...extraMatches, extraMatchesWith);
clause = cypher_builder_1.default.utils.concat(...matchBlock, this.relationship ? new cypher_builder_1.default.With(nestedContext.target).distinct() : undefined, ...authFilterSubqueries, filterSubqueriesClause, filterSubqueryWith, ...validations, sortAndLimitBlock, subqueries, ret);
}
return {
clauses: [clause],
projectionExpr: context.returnVariable,
};
}
getReturnStatement(context, returnVariable) {
const projection = this.getProjectionMap(context);
if (context.shouldCollect) {
const collectProj = cypher_builder_1.default.collect(projection);
if (context.shouldDistinct) {
collectProj.distinct();
}
return new cypher_builder_1.default.Return([collectProj, returnVariable]);
}
return new cypher_builder_1.default.Return([projection, returnVariable]);
}
hasCypherSort() {
return this.sortFields.some((s) => s instanceof CypherPropertySort_1.CypherPropertySort);
}
getChildren() {
return (0, utils_1.filterTruthy)([
this.selection,
...this.filters,
...this.authFilters,
...this.fields,
this.pagination,
...this.sortFields,
]);
}
getFieldsSubqueries(context) {
const nonCypherFields = this.fields.filter((f) => !f.isCypherField());
if (!context.hasTarget()) {
throw new Error("No parent node found!");
}
return (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(context, nonCypherFields, [context.target]);
}
getCypherFieldsSubqueries(context) {
if (!context.hasTarget()) {
throw new Error("No parent node found!");
}
return (0, wrap_subquery_in_calls_1.wrapSubqueriesInCypherCalls)(context, this.getCypherFields(), [context.target]);
}
getCypherFields() {
return this.fields.filter((f) => f.isCypherField());
}
getProjectionMap(context) {
if (!context.hasTarget()) {
throw new Error("No parent node found!");
}
const projectionFields = this.fields.map((f) => f.getProjectionField(context.target));
const sortProjectionFields = this.sortFields.map((f) => f.getProjectionField(context));
const uniqueProjectionFields = Array.from(new Set([...projectionFields, ...sortProjectionFields])); // TODO remove duplicates with alias
const stringFields = [];
let otherFields = {};
for (const field of uniqueProjectionFields) {
if (typeof field === "string") {
stringFields.push(field);
}
else {
otherFields = { ...otherFields, ...field };
}
}
return new cypher_builder_1.default.MapProjection(context.target, stringFields, otherFields);
}
addSortToClause(context, node, clause) {
const isNested = Boolean(context.source); // This is to keep Cypher compatibility
const orderByFields = this.sortFields.flatMap((f) => f.getSortFields(context, node, !isNested));
const pagination = this.pagination ? this.pagination.getPagination() : undefined;
clause.orderBy(...orderByFields);
if (pagination?.skip) {
clause.skip(pagination.skip);
}
if (pagination?.limit) {
clause.limit(pagination.limit);
}
}
}
exports.ReadOperation = ReadOperation;
//# sourceMappingURL=ReadOperation.js.map