Skip to content

Commit

Permalink
feat: add property filter to search API (#456)
Browse files Browse the repository at this point in the history
* feat: add property filter to search API

* chore: generate SDK
  • Loading branch information
eric-nguyen-cs authored Mar 27, 2024
1 parent 4119aed commit 0b26030
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 3 deletions.
128 changes: 127 additions & 1 deletion backend/editor/models/search_models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Annotated, Literal

from pydantic import Field, StringConstraints, TypeAdapter, computed_field
from pydantic import Field, StringConstraints, TypeAdapter, computed_field, field_validator

from .base_models import BaseModel
from .node_models import EntryNode
Expand Down Expand Up @@ -115,6 +116,130 @@ def build_cypher_query(self, param_name: str) -> CypherQuery:
)


class PropertyFilterSearchTerm(AbstractFilterSearchTerm):
filter_type: Literal["property"]
filter_value: str

@field_validator("filter_value")
@classmethod
def validate_filter_value(cls, filter_value: str):
"""
The filter value is in the format `not:inherited:property_name:property_value`
where `not:` and `inherited:` and `:property_value` are optional.
Note that a property_name is always of format `name:lc`.
"""
parsed_value = filter_value
if parsed_value.startswith("not:"):
parsed_value = parsed_value[4:]
if parsed_value.startswith("inherited:"):
parsed_value = parsed_value[10:]

assert ":" in parsed_value, "A property_name is mandatory and must contain a colon"

terms = parsed_value.split(":")
property_name = terms[0] + ":" + terms[1]

if not re.match(r"^[^:\\]+:[a-z]{2}$", property_name):
raise ValueError("Invalid property_name")

return filter_value

@computed_field
def negated(self) -> bool:
return self.filter_value.startswith("not:")

@computed_field
def inherited(self) -> bool:
filter_value = self.get_parsed_filter_value(self.negated)
return filter_value.startswith("inherited:")

@computed_field
def property_name(self) -> str:
filter_value = self.get_parsed_filter_value(self.negated, self.inherited)
terms = filter_value.split(":")
return terms[0] + "_" + terms[1]

@computed_field
def property_value(self) -> str | None:
filter_value = self.get_parsed_filter_value(self.negated, self.inherited)
terms = filter_value.split(":")
return ":".join(terms[2:]) if len(terms) > 2 else None

def get_parsed_filter_value(self, negated=False, inherited=False):
filter_value = self.filter_value
if negated:
filter_value = filter_value[4:]
if inherited:
filter_value = filter_value[10:]
return filter_value

def build_cypher_query(self, param_name: str) -> CypherQuery:
branches = {
"negated": self.negated,
"inherited": self.inherited,
"with_value": self.property_value is not None,
}
match branches:
case {"negated": False, "inherited": False, "with_value": False}:
return CypherQuery(f"n.prop_{self.property_name} IS NOT NULL")
case {"negated": True, "inherited": False, "with_value": False}:
return CypherQuery(f"n.prop_{self.property_name} IS NULL")
case {"negated": False, "inherited": False, "with_value": True}:
return CypherQuery(
f"n.prop_{self.property_name} = ${param_name}",
{param_name: self.property_value},
)
case {"negated": True, "inherited": False, "with_value": True}:
return CypherQuery(
f"n.prop_{self.property_name} <> ${param_name}",
{param_name: self.property_value},
)
case {"negated": False, "inherited": True, "with_value": False}:
return CypherQuery(
f"""(n.prop_{self.property_name} IS NOT NULL OR
any(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NOT NULL)
)""",
)
case {"negated": True, "inherited": True, "with_value": False}:
return CypherQuery(
f"""(n.prop_{self.property_name} IS NULL AND
all(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NULL)
)""",
)
case {"negated": False, "inherited": True, "with_value": True}:
return CypherQuery(
f"""
[
property IN
[n.prop_{self.property_name}] +
[(n)<-[:is_child_of*]-(p:ENTRY) | p.prop_{self.property_name}]
WHERE property IS NOT NULL
][0]
= ${param_name}""",
{param_name: self.property_value},
)
case {"negated": True, "inherited": True, "with_value": True}:
return CypherQuery(
f"""((n.prop_{self.property_name} IS NULL AND
all(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NULL)
) OR
[
property IN
[n.prop_{self.property_name}] +
[(n)<-[:is_child_of*]-(p:ENTRY) | p.prop_{self.property_name}]
WHERE property IS NOT NULL
][0]
<> ${param_name})""",
{param_name: self.property_value},
)


FilterSearchTerm = Annotated[
(
IsFilterSearchTerm
Expand All @@ -123,6 +248,7 @@ def build_cypher_query(self, param_name: str) -> CypherQuery:
| ChildFilterSearchTerm
| AncestorFilterSearchTerm
| DescendantFilterSearchTerm
| PropertyFilterSearchTerm
),
Field(discriminator="filter_type"),
]
Expand Down
42 changes: 40 additions & 2 deletions backend/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,8 @@
{ "$ref": "#/components/schemas/ParentFilterSearchTerm" },
{ "$ref": "#/components/schemas/ChildFilterSearchTerm" },
{ "$ref": "#/components/schemas/AncestorFilterSearchTerm" },
{ "$ref": "#/components/schemas/DescendantFilterSearchTerm" }
{ "$ref": "#/components/schemas/DescendantFilterSearchTerm" },
{ "$ref": "#/components/schemas/PropertyFilterSearchTerm" }
],
"discriminator": {
"propertyName": "filterType",
Expand All @@ -1271,7 +1272,8 @@
"descendant": "#/components/schemas/DescendantFilterSearchTerm",
"is": "#/components/schemas/IsFilterSearchTerm",
"language": "#/components/schemas/LanguageFilterSearchTerm",
"parent": "#/components/schemas/ParentFilterSearchTerm"
"parent": "#/components/schemas/ParentFilterSearchTerm",
"property": "#/components/schemas/PropertyFilterSearchTerm"
}
}
},
Expand Down Expand Up @@ -1434,6 +1436,42 @@
"enum": ["OPEN", "EXPORTED", "LOADING", "FAILED"],
"title": "ProjectStatus"
},
"PropertyFilterSearchTerm": {
"properties": {
"filterType": { "const": "property", "title": "Filtertype" },
"filterValue": { "type": "string", "title": "Filtervalue" },
"negated": {
"type": "boolean",
"title": "Negated",
"readOnly": true
},
"inherited": {
"type": "boolean",
"title": "Inherited",
"readOnly": true
},
"propertyName": {
"type": "string",
"title": "Propertyname",
"readOnly": true
},
"propertyValue": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Propertyvalue",
"readOnly": true
}
},
"type": "object",
"required": [
"filterType",
"filterValue",
"negated",
"inherited",
"propertyName",
"propertyValue"
],
"title": "PropertyFilterSearchTerm"
},
"ValidationError": {
"properties": {
"loc": {
Expand Down
1 change: 1 addition & 0 deletions taxonomy-editor-frontend/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type { LanguageFilterSearchTerm } from "./models/LanguageFilterSearchTerm
export type { ParentFilterSearchTerm } from "./models/ParentFilterSearchTerm";
export type { Project } from "./models/Project";
export { ProjectStatus } from "./models/ProjectStatus";
export type { PropertyFilterSearchTerm } from "./models/PropertyFilterSearchTerm";
export type { ValidationError } from "./models/ValidationError";

export { DefaultService } from "./services/DefaultService";
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { EntryNode } from "./EntryNode";
import type { IsFilterSearchTerm } from "./IsFilterSearchTerm";
import type { LanguageFilterSearchTerm } from "./LanguageFilterSearchTerm";
import type { ParentFilterSearchTerm } from "./ParentFilterSearchTerm";
import type { PropertyFilterSearchTerm } from "./PropertyFilterSearchTerm";
export type EntryNodeSearchResult = {
q: string;
nodeCount: number;
Expand All @@ -20,6 +21,7 @@ export type EntryNodeSearchResult = {
| ChildFilterSearchTerm
| AncestorFilterSearchTerm
| DescendantFilterSearchTerm
| PropertyFilterSearchTerm
>;
nodes: Array<EntryNode>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PropertyFilterSearchTerm = {
filterType: "property";
filterValue: string;
readonly negated: boolean;
readonly inherited: boolean;
readonly propertyName: string;
readonly propertyValue: string | null;
};

0 comments on commit 0b26030

Please sign in to comment.