Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom cypher scalar field filtering #5553

Merged
merged 17 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading