Skip to content

Commit

Permalink
Merge pull request #5553 from mjfwebb/feat/cypher-field-filtering
Browse files Browse the repository at this point in the history
Custom cypher scalar field filtering
  • Loading branch information
mjfwebb authored Sep 12, 2024
2 parents 935cf24 + 3334cb7 commit 35fc0ed
Show file tree
Hide file tree
Showing 22 changed files with 3,175 additions and 185 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
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
4 changes: 2 additions & 2 deletions packages/graphql/src/schema/get-where-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function getWhereFieldsForAttributes({
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 @@ -247,7 +247,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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";

export function coalesceValueIfNeeded(attribute: AttributeAdapter, expr: Cypher.Expr): Cypher.Expr {
if (attribute.annotations.coalesce) {
const value = attribute.annotations.coalesce.value;
const literal = new Cypher.Literal(value);
return Cypher.coalesce(expr, literal);
}
return expr;
}
Loading

0 comments on commit 35fc0ed

Please sign in to comment.