Skip to content

Commit

Permalink
Merge branch 'dev' into 4.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
angrykoala authored Jan 24, 2023
2 parents 9c8f83c + 48d4ec5 commit 1e4e5e7
Show file tree
Hide file tree
Showing 62 changed files with 3,755 additions and 717 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-cows-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fixed #2713 - missing checks for connection not NONE when filtering by connection_ALL
5 changes: 5 additions & 0 deletions .changeset/hip-coats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fixed #2708 - invalid cypher when using an aggregation filter within a relationship filter
5 changes: 5 additions & 0 deletions .changeset/new-games-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fixes #2670 - invalid cypher when using an aggregation filter within a connection filter
11 changes: 0 additions & 11 deletions .github/workflows/reusable-toolbox-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,6 @@ jobs:
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Update system (Ubuntu)
run: |
# use apt-spy2 to select closest apt mirror,
# which helps avoid connectivity issues in Azure;
# see https://github.com/actions/virtual-environments/issues/675
sudo gem install apt-spy2
sudo apt-spy2 check
sudo apt-spy2 fix --commit
# need to run apt-get update after running apt-spy2 fix
sudo apt-get -o Acquire::Retries=3 update
# end of apt-spy2 workaround
- uses: actions/setup-node@v3
with:
node-version: lts/*
Expand Down
3 changes: 1 addition & 2 deletions packages/graphql/src/schema/resolvers/query/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";
import { fulltextArgDeprecationMessage } from "../../../schema/augment/fulltext";

export function findResolver({ node }: { node: Node }) {

async function resolve(_root: any, args: any, _context: unknown, info: GraphQLResolveInfo) {
const context = _context as Context;
context.resolveTree = getNeo4jResolveTree(info, { args });
Expand All @@ -41,7 +40,7 @@ export function findResolver({ node }: { node: Node }) {

return executeResult.records.map((x) => x.this);
}

return {
type: `[${node.name}!]!`,
resolve,
Expand Down
125 changes: 93 additions & 32 deletions packages/graphql/src/translate/create-aggregate-where-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

import Cypher from "@neo4j/cypher-builder";
import type { Node, Relationship } from "../classes";
import type { RelationField, Context, GraphQLWhereArg } from "../types";
import { aggregationFieldRegEx, AggregationFieldRegexGroups, whereRegEx } from "./where/utils";
import type { RelationField, Context, GraphQLWhereArg, PredicateReturn } from "../types";
import { aggregationFieldRegEx, AggregationFieldRegexGroups, ListPredicate, whereRegEx } from "./where/utils";
import { createBaseOperation } from "./where/property-operations/create-comparison-operation";
import { NODE_OR_EDGE_KEYS, LOGICAL_OPERATORS, AGGREGATION_AGGREGATE_COUNT_OPERATORS } from "../constants";
import mapToDbProperty from "../utils/map-to-db-property";
Expand All @@ -42,18 +42,17 @@ export type AggregateWhereInput = {
type AggregateWhereReturn = {
returnProjections: ("*" | Cypher.ProjectionColumn)[];
predicates: Cypher.Predicate[];
returnVariables: Cypher.Variable[];
};

export function aggregatePreComputedWhereFields(
value: GraphQLWhereArg,
relationField: RelationField,
relationship: Relationship | undefined,
context: Context,
matchNode: Cypher.Variable
): {
predicate: Cypher.Predicate | undefined;
preComputedSubquery: Cypher.Call;
} {
matchNode: Cypher.Variable,
listPredicateStr?: ListPredicate
): PredicateReturn {
const refNode = context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node;
const direction = relationField.direction;
const aggregationTarget = new Cypher.Node({ labels: refNode.getLabels(context) });
Expand All @@ -62,22 +61,40 @@ export function aggregatePreComputedWhereFields(
target: aggregationTarget,
type: relationField.type,
});
let matchPattern = cypherRelation.pattern({ source: { labels: false } });
if (direction === "IN") {
cypherRelation.reverse();
matchPattern = cypherRelation.pattern({ target: { labels: false } });
}
const matchQuery = new Cypher.Match(cypherRelation);
const { returnProjections, predicates } = aggregateWhere(
const matchQuery = new Cypher.Match(matchPattern);
const { returnProjections, predicates, returnVariables } = aggregateWhere(
value as AggregateWhereInput,
refNode,
relationship,
aggregationTarget,
cypherRelation
cypherRelation,
listPredicateStr
);
matchQuery.return(...returnProjections);
const subquery = new Cypher.Call(matchQuery).innerWith(matchNode);

// The return values are needed when performing SOME/NONE/ALL/SINGLE operations as they need to be aggregated to perform comparisons
if (listPredicateStr) {
return {
predicate: Cypher.and(...predicates),
// Cypher.concat is used because this is passed to createWherePredicate which expects a Cypher.CompositeClause
preComputedSubqueries: Cypher.concat(subquery),
requiredVariables: [],
aggregatingVariables: returnVariables,
};
}

return {
predicate: Cypher.and(...predicates),
preComputedSubquery: subquery,
// Cypher.concat is used because this is passed to createWherePredicate which expects a Cypher.CompositeClause
preComputedSubqueries: Cypher.concat(subquery),
requiredVariables: returnVariables,
aggregatingVariables: [],
};
}

Expand All @@ -86,57 +103,73 @@ export function aggregateWhere(
refNode: Node,
relationship: Relationship | undefined,
aggregationTarget: Cypher.Node,
cypherRelation: Cypher.Relationship
cypherRelation: Cypher.Relationship,
listPredicateStr?: ListPredicate
): AggregateWhereReturn {
const returnProjections: ("*" | Cypher.ProjectionColumn)[] = [];
const predicates: Cypher.Predicate[] = [];
const returnVariables: Cypher.Variable[] = [];
Object.entries(aggregateWhereInput).forEach(([key, value]) => {
if (AGGREGATION_AGGREGATE_COUNT_OPERATORS.includes(key)) {
const { returnProjection: innerReturnProjection, predicate: innerPredicate } =
createCountPredicateAndProjection(aggregationTarget, key, value);
const {
returnProjection: innerReturnProjection,
predicate: innerPredicate,
returnVariable: innerReturnVariable,
} = createCountPredicateAndProjection(aggregationTarget, key, value, listPredicateStr);
returnProjections.push(innerReturnProjection);
predicates.push(innerPredicate);
if (innerPredicate) predicates.push(innerPredicate);
returnVariables.push(innerReturnVariable);
} else if (NODE_OR_EDGE_KEYS.includes(key)) {
const target = key === "edge" ? cypherRelation : aggregationTarget;
const refNodeOrRelation = key === "edge" ? relationship : refNode;
if (!refNodeOrRelation) throw new Error(`Edge filter ${key} on undefined relationship`);
const { returnProjections: innerReturnProjections, predicates: innerPredicates } = aggregateEntityWhere(
value,
refNodeOrRelation,
target
);
const {
returnProjections: innerReturnProjections,
predicates: innerPredicates,
returnVariables: innerReturnVariables,
} = aggregateEntityWhere(value, refNodeOrRelation, target, listPredicateStr);
returnProjections.push(...innerReturnProjections);
predicates.push(...innerPredicates);
returnVariables.push(...innerReturnVariables);
} else if (LOGICAL_OPERATORS.includes(key)) {
const logicalOperator = key === "AND" ? Cypher.and : Cypher.or;
const logicalPredicates: Cypher.Predicate[] = [];
value.forEach((whereInput) => {
const { returnProjections: innerReturnProjections, predicates: innerPredicates } = aggregateWhere(
const {
returnProjections: innerReturnProjections,
predicates: innerPredicates,
returnVariables: innerReturnVariables,
} = aggregateWhere(
whereInput,
refNode,
relationship,
aggregationTarget,
cypherRelation
cypherRelation,
listPredicateStr
);
returnProjections.push(...innerReturnProjections);
logicalPredicates.push(...innerPredicates);
returnVariables.push(...innerReturnVariables);
});
predicates.push(logicalOperator(...logicalPredicates));
}
});
return {
returnProjections,
predicates,
returnVariables,
};
}

function createCountPredicateAndProjection(
aggregationTarget: Cypher.Node,
filterKey: string,
filterValue: number
filterValue: number,
listPredicateStr?: ListPredicate
): {
returnProjection: "*" | Cypher.ProjectionColumn;
predicate: Cypher.Predicate;
predicate: Cypher.Predicate | undefined;
returnVariable: Cypher.Variable;
} {
const paramName = new Cypher.Param(filterValue);
const count = Cypher.count(aggregationTarget);
Expand All @@ -147,43 +180,50 @@ function createCountPredicateAndProjection(
param: paramName,
});
const operationVar = new Cypher.Variable();

return {
returnProjection: [operation, operationVar],
predicate: Cypher.eq(operationVar, new Cypher.Literal(true)),
predicate: getReturnValuePredicate(operationVar, listPredicateStr),
returnVariable: operationVar,
};
}

function aggregateEntityWhere(
aggregateEntityWhereInput: WhereFilter,
refNodeOrRelation: Node | Relationship,
target: Cypher.Node | Cypher.Relationship
target: Cypher.Node | Cypher.Relationship,
listPredicateStr?: ListPredicate
): AggregateWhereReturn {
const returnProjections: ("*" | Cypher.ProjectionColumn)[] = [];
const predicates: Cypher.Predicate[] = [];
const returnVariables: Cypher.Variable[] = [];
Object.entries(aggregateEntityWhereInput).forEach(([key, value]) => {
if (LOGICAL_OPERATORS.includes(key)) {
const logicalOperator = key === "AND" ? Cypher.and : Cypher.or;
const logicalPredicates: Cypher.Predicate[] = [];
value.forEach((whereInput) => {
const { returnProjections: innerReturnProjections, predicates: innerPredicates } = aggregateEntityWhere(
whereInput,
refNodeOrRelation,
target
);
const {
returnProjections: innerReturnProjections,
predicates: innerPredicates,
returnVariables: innerReturnVariables,
} = aggregateEntityWhere(whereInput, refNodeOrRelation, target, listPredicateStr);
returnProjections.push(...innerReturnProjections);
logicalPredicates.push(...innerPredicates);
returnVariables.push(...innerReturnVariables);
});
predicates.push(logicalOperator(...logicalPredicates));
} else {
const operation = createEntityOperation(refNodeOrRelation, target, key, value);
const operationVar = new Cypher.Variable();
returnProjections.push([operation, operationVar]);
predicates.push(Cypher.eq(operationVar, new Cypher.Literal(true)));
predicates.push(getReturnValuePredicate(operationVar, listPredicateStr));
returnVariables.push(operationVar);
}
});
return {
returnProjections,
predicates,
returnVariables,
};
}

Expand Down Expand Up @@ -247,3 +287,24 @@ function getAggregateOperation(
throw new Error(`Invalid operator ${aggregationOperator}`);
}
}

function getReturnValuePredicate(operationVar: Cypher.Variable, listPredicateStr?: ListPredicate) {
switch (listPredicateStr) {
case "all": {
const listVar = new Cypher.Variable();
return Cypher.all(listVar, operationVar, Cypher.eq(listVar, new Cypher.Literal(true)));
}
case "single": {
const listVar = new Cypher.Variable();
return Cypher.single(listVar, operationVar, Cypher.eq(listVar, new Cypher.Literal(true)));
}
case "not":
case "none":
case "any": {
return Cypher.in(new Cypher.Literal(true), operationVar);
}
default: {
return Cypher.eq(operationVar, new Cypher.Literal(true));
}
}
}
15 changes: 0 additions & 15 deletions packages/graphql/src/translate/create-connect-and-params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,21 +126,6 @@ describe("createConnectAndParams", () => {
}
RETURN count(*) AS _
}
WITH this, this0_node, this0_node_similarMovies0_node
CALL {
WITH this0_node
MATCH (this0_node)-[this0_node_similarMovies_Movie_unique:SIMILAR]->(other:Movie)
WITH count(this0_node_similarMovies_Movie_unique) as c, other
CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.similarMovies required exactly once for a specific Movie', [0])
RETURN collect(c) AS this0_node_similarMovies_Movie_unique_ignored
}
CALL {
WITH this0_node_similarMovies0_node
MATCH (this0_node_similarMovies0_node)-[this0_node_similarMovies0_node_similarMovies_Movie_unique:SIMILAR]->(other:Movie)
WITH count(this0_node_similarMovies0_node_similarMovies_Movie_unique) as c, other
CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.similarMovies required exactly once for a specific Movie', [0])
RETURN collect(c) AS this0_node_similarMovies0_node_similarMovies_Movie_unique_ignored
}
WITH this, this0_node, this0_node_similarMovies0_node
RETURN count(*) AS connect_this0_node_similarMovies_Movie
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function createConnectAndParams({
node: mi[0],
context,
varName: mi[1],
relationshipFieldNotOverwritable: relationField.fieldName,
...(isOverwriteNotAllowed && { relationshipFieldNotOverwritable: relationField.fieldName }),
});
if (relValidationStr) {
relValidationStrs.push(relValidationStr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default function createWhereAndParams({
const whereCypher = new Cypher.RawCypher((env: Cypher.Environment) => {
preComputedWhereFieldsResult = preComputedSubqueries?.getCypher(env) || "";
const cypher = wherePredicate?.getCypher(env) || "";

return [cypher, {}];
});

Expand Down
Loading

0 comments on commit 1e4e5e7

Please sign in to comment.