Skip to content

Commit

Permalink
merge with dev, remove implicit _EQ from Cypher filters
Browse files Browse the repository at this point in the history
  • Loading branch information
MacondoExpress committed Sep 13, 2024
2 parents 0e5dc07 + a2713e8 commit 00d2b7c
Show file tree
Hide file tree
Showing 28 changed files with 3,218 additions and 257 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-beans-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": minor
---

Add filtering on scalar custom cypher fields
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
"eslint-plugin-import": "2.30.0",
"eslint-plugin-jest": "28.8.3",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-react": "7.35.2",
"eslint-plugin-react": "7.36.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"graphql": "16.9.0",
"husky": "9.1.5",
"husky": "9.1.6",
"jest": "29.7.0",
"lint-staged": "15.2.10",
"neo4j-driver": "5.24.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-amqp-subscriptions-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@types/body-parser": "1.19.5",
"@types/cors": "2.8.17",
"@types/debug": "4.1.12",
"@types/jest": "29.5.12",
"@types/jest": "29.5.13",
"@types/node": "20.16.5",
"camelcase": "6.3.0",
"graphql-ws": "5.16.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@apollo/server": "4.11.0",
"@types/deep-equal": "1.0.4",
"@types/is-uuid": "1.0.2",
"@types/jest": "29.5.12",
"@types/jest": "29.5.13",
"@types/jsonwebtoken": "9.0.6",
"@types/node": "20.16.5",
"@types/pluralize": "0.0.33",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,7 @@ export class AttributeAdapter {
return (
(this.typeHelper.isEnum() || this.typeHelper.isSpatial() || this.typeHelper.isScalar()) &&
this.isFilterable() &&
!this.isCustomResolvable() &&
!this.isCypher()
!this.isCustomResolvable()
);
}

Expand Down
7 changes: 4 additions & 3 deletions packages/graphql/src/schema/get-where-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export function getWhereFieldsForAttributes({
const deprecatedDirectives = graphqlDirectivesToCompose(
(userDefinedDirectivesOnField ?? []).filter((directive) => directive.name.value === DEPRECATED)
);
if (shouldAddDeprecatedFields(features, "implicitEqualFilters")) {
if (shouldAddDeprecatedFields(features, "implicitEqualFilters") && !field.isCypher()) {
result[field.name] = {
type: field.getInputTypeNames().where.pretty,
directives: deprecatedDirectives.length ? deprecatedDirectives : [DEPRECATE_EQUAL_FILTERS],
Expand All @@ -218,7 +218,8 @@ export function getWhereFieldsForAttributes({
type: field.getInputTypeNames().where.pretty,
directives: deprecatedDirectives,
};
if (shouldAddDeprecatedFields(features, "negationFilters")) {

if (shouldAddDeprecatedFields(features, "negationFilters") && !field.isCypher()) {
result[`${field.name}_NOT`] = {
type: field.getInputTypeNames().where.pretty,
directives: deprecatedDirectives.length ? deprecatedDirectives : [DEPRECATE_NOT],
Expand Down Expand Up @@ -251,7 +252,7 @@ export function getWhereFieldsForAttributes({
type: field.getFilterableInputTypeName(),
directives: deprecatedDirectives,
};
if (shouldAddDeprecatedFields(features, "negationFilters")) {
if (shouldAddDeprecatedFields(features, "negationFilters") && !field.isCypher()) {
result[`${field.name}_NOT_IN`] = {
type: field.getFilterableInputTypeName(),
directives: deprecatedDirectives.length ? deprecatedDirectives : [DEPRECATE_NOT],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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.
*/

import Cypher from "@neo4j/cypher-builder";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import { createComparisonOperation } from "../../../utils/create-comparison-operator";
import type { QueryASTContext } from "../../QueryASTContext";
import type { QueryASTNode } from "../../QueryASTNode";
import type { CustomCypherSelection } from "../../selection/CustomCypherSelection";
import type { FilterOperator } from "../Filter";
import { Filter } from "../Filter";
import { coalesceValueIfNeeded } from "../utils/coalesce-if-needed";
import { createDurationOperation } from "../utils/create-duration-operation";
import { createPointOperation } from "../utils/create-point-operation";

/** A property which comparison has already been parsed into a Param */
export class CypherFilter extends Filter {
private returnVariable: Cypher.Variable = new Cypher.Variable();
private attribute: AttributeAdapter;
private selection: CustomCypherSelection;
private operator: FilterOperator;
protected comparisonValue: unknown;

constructor({
selection,
attribute,
operator,
comparisonValue,
}: {
selection: CustomCypherSelection;
attribute: AttributeAdapter;
operator: FilterOperator;
comparisonValue: unknown;
}) {
super();
this.selection = selection;
this.attribute = attribute;
this.operator = operator;
this.comparisonValue = comparisonValue;
}

public getChildren(): QueryASTNode[] {
return [this.selection];
}

public print(): string {
return `${super.print()} [${this.attribute.name}] <${this.operator}>`;
}

public getSubqueries(context: QueryASTContext): Cypher.Clause[] {
const { selection: cypherSubquery, nestedContext } = this.selection.apply(context);

const clause = Cypher.concat(
cypherSubquery,
new Cypher.Return([nestedContext.returnVariable, this.returnVariable])
);

return [clause];
}

public getPredicate(_queryASTContext: QueryASTContext): Cypher.Predicate {
const operation = this.createBaseOperation({
operator: this.operator,
property: this.returnVariable,
param: new Cypher.Param(this.comparisonValue),
});

return operation;
}

/** Returns the default operation for a given filter */
private createBaseOperation({
operator,
property,
param,
}: {
operator: FilterOperator;
property: Cypher.Expr;
param: Cypher.Expr;
}): Cypher.ComparisonOp {
const coalesceProperty = coalesceValueIfNeeded(this.attribute, property);

// This could be solved with specific a specific CypherDurationFilter but
// we need to use the return variable for the cypher subquery.
// To allow us to extend the DurationFilter class with a CypherDurationFilter class
// we would need to have a way to provide the return variable
// to the PropertyFilter's getPropertyRef method.
if (this.attribute.typeHelper.isDuration()) {
return createDurationOperation({
operator,
property: coalesceProperty,
param: new Cypher.Param(this.comparisonValue),
});
}

if (this.attribute.typeHelper.isSpatial()) {
return createPointOperation({
operator,
property: coalesceProperty,
param: new Cypher.Param(this.comparisonValue),
attribute: this.attribute,
});
}

return createComparisonOperation({ operator, property: coalesceProperty, param });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,18 @@
*/

import Cypher from "@neo4j/cypher-builder";
import type { WhereOperator } from "../Filter";
import { coalesceValueIfNeeded } from "../utils/coalesce-if-needed";
import { createDurationOperation } from "../utils/create-duration-operation";
import { PropertyFilter } from "./PropertyFilter";

export class DurationFilter extends PropertyFilter {
protected getOperation(prop: Cypher.Property): Cypher.ComparisonOp {
// NOTE: this may not be needed
if (this.operator === "EQ") {
return Cypher.eq(prop, new Cypher.Param(this.comparisonValue));
}
return this.createDurationOperation({
protected getOperation(prop: Cypher.Expr): Cypher.ComparisonOp {
const coalesceProperty = coalesceValueIfNeeded(this.attribute, prop);

return createDurationOperation({
operator: this.operator,
property: prop,
property: coalesceProperty,
param: new Cypher.Param(this.comparisonValue),
});
}

private createDurationOperation({
operator,
property,
param,
}: {
operator: WhereOperator | "EQ";
property: Cypher.Expr;
param: Cypher.Expr;
}) {
const variable = Cypher.plus(Cypher.datetime(), param);
const propertyRef = Cypher.plus(Cypher.datetime(), property);

return this.createBaseOperation({
operator,
property: propertyRef,
param: variable,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,72 +18,16 @@
*/

import Cypher from "@neo4j/cypher-builder";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import type { WhereOperator } from "../Filter";
import { createPointOperation } from "../utils/create-point-operation";
import { PropertyFilter } from "./PropertyFilter";

export class PointFilter extends PropertyFilter {
protected getOperation(prop: Cypher.Property): Cypher.ComparisonOp {
return this.createPointOperation({
return createPointOperation({
operator: this.operator || "EQ",
property: prop,
param: new Cypher.Param(this.comparisonValue),
attribute: this.attribute,
});
}

private createPointOperation({
operator,
property,
param,
attribute,
}: {
operator: WhereOperator | "EQ";
property: Cypher.Expr;
param: Cypher.Param;
attribute: AttributeAdapter;
}): Cypher.ComparisonOp {
const pointDistance = this.createPointDistanceExpression(property, param);
const distanceRef = param.property("distance");

switch (operator) {
case "LT":
return Cypher.lt(pointDistance, distanceRef);
case "LTE":
return Cypher.lte(pointDistance, distanceRef);
case "GT":
return Cypher.gt(pointDistance, distanceRef);
case "GTE":
return Cypher.gte(pointDistance, distanceRef);
case "DISTANCE":
return Cypher.eq(pointDistance, distanceRef);
case "EQ": {
if (attribute.typeHelper.isList()) {
const pointList = this.createPointListComprehension(param);
return Cypher.eq(property, pointList);
}

return Cypher.eq(property, Cypher.point(param));
}
case "IN": {
const pointList = this.createPointListComprehension(param);
return Cypher.in(property, pointList);
}
case "INCLUDES":
return Cypher.in(Cypher.point(param), property);
default:
throw new Error(`Invalid operator ${operator}`);
}
}

private createPointListComprehension(param: Cypher.Param): Cypher.ListComprehension {
const comprehensionVar = new Cypher.Variable();
const mapPoint = Cypher.point(comprehensionVar);
return new Cypher.ListComprehension(comprehensionVar, param).map(mapPoint);
}

private createPointDistanceExpression(property: Cypher.Expr, param: Cypher.Param): Cypher.Function {
const nestedPointRef = param.property("point");
return Cypher.point.distance(property, Cypher.point(nestedPointRef));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@

import Cypher from "@neo4j/cypher-builder";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { hasTarget } from "../../../utils/context-has-target";
import { createComparisonOperation } from "../../../utils/create-comparison-operator";
import type { QueryASTContext } from "../../QueryASTContext";
import type { QueryASTNode } from "../../QueryASTNode";
import type { FilterOperator } from "../Filter";
import { Filter } from "../Filter";
import { hasTarget } from "../../../utils/context-has-target";
import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import { coalesceValueIfNeeded } from "../utils/coalesce-if-needed";

export class PropertyFilter extends Filter {
protected attribute: AttributeAdapter;
Expand Down Expand Up @@ -130,7 +131,7 @@ export class PropertyFilter extends Filter {
/** Returns the operation for a given filter.
* To be overridden by subclasses
*/
protected getOperation(prop: Cypher.Property | Cypher.Case): Cypher.ComparisonOp {
protected getOperation(prop: Cypher.Expr): Cypher.ComparisonOp {
return this.createBaseOperation({
operator: this.operator,
property: prop,
Expand All @@ -148,20 +149,11 @@ export class PropertyFilter extends Filter {
property: Cypher.Expr;
param: Cypher.Expr;
}): Cypher.ComparisonOp {
const coalesceProperty = this.coalesceValueIfNeeded(property);
const coalesceProperty = coalesceValueIfNeeded(this.attribute, property);

return createComparisonOperation({ operator, property: coalesceProperty, param });
}

protected coalesceValueIfNeeded(expr: Cypher.Expr): Cypher.Expr {
if (this.attribute.annotations.coalesce) {
const value = this.attribute.annotations.coalesce.value;
const literal = new Cypher.Literal(value);
return Cypher.coalesce(expr, literal);
}
return expr;
}

private getNullPredicate(propertyRef: Cypher.Property | Cypher.Case): Cypher.Predicate {
if (this.isNot) {
return Cypher.isNotNull(propertyRef);
Expand Down
Loading

0 comments on commit 00d2b7c

Please sign in to comment.