Skip to content

Commit

Permalink
Adds nested filters
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewpeterkort committed Jan 17, 2025
1 parent 118e0c9 commit 08a8e6d
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ENV PATH="/go/bin:${PATH}"
ADD ./ /go/src/github.com/bmeg/grip-graphql
WORKDIR /go/src/github.com/bmeg/grip-graphql

RUN go install github.com/bmeg/grip@c8560233d049d28b1d3e296fbba1d144715d411e
RUN go install github.com/bmeg/grip@0fc1119cf92d230b1ec8abc9824763198b1fabbf
RUN make all

#FROM alpine
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22.5

require (
github.com/99designs/gqlgen v0.17.60
github.com/bmeg/grip v0.0.0-20250114170030-20e7f63fb720
github.com/bmeg/grip v0.0.0-20250117212733-0fc1119cf92d
github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5
github.com/gin-gonic/gin v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/g
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bmeg/grip v0.0.0-20250114170030-20e7f63fb720 h1:v/ESPTr9vb6uijv/VswPCAfvimtF/bCbEDPC/skzI0E=
github.com/bmeg/grip v0.0.0-20250114170030-20e7f63fb720/go.mod h1:xBPjee1i7ZnwR9/UsHtblHyf7X7vAr5tFWTNtmtqmWg=
github.com/bmeg/grip v0.0.0-20250117212733-0fc1119cf92d h1:cNqgSq6pwN7AxUiAlPSLAhXrPC3+HZrcps4VLKQWAUk=
github.com/bmeg/grip v0.0.0-20250117212733-0fc1119cf92d/go.mod h1:xBPjee1i7ZnwR9/UsHtblHyf7X7vAr5tFWTNtmtqmWg=
github.com/bmeg/jsonpath v0.0.0-20210207014051-cca5355553ad h1:ICgBexeLB7iv/IQz4rsP+MimOXFZUwWSPojEypuOaQ8=
github.com/bmeg/jsonpath v0.0.0-20210207014051-cca5355553ad/go.mod h1:ft96Irkp72C7ZrUWRenG7LrF0NKMxXdRvsypo5Njhm4=
github.com/bufbuild/protovalidate-go v0.4.0 h1:ModSkCLEW07fiyGtdtMXKY+Gz3oPFKSfiaSCgL+FtpU=
Expand Down
11 changes: 8 additions & 3 deletions gql-gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ Current supported comparator statements:
| "lte", "<=" | less than or equal |
| "in" | value is in the list of values |

Filter keys must be of form TYPE.jsonPath where TYPE is the vertex name without the 'Type' suffix and jsonpath is a '.' delimited path to
the field that will be filtered on. So for example in 'Specimen.processing.method.coding.display' Specimen is short for SpecimenType which is the vertex that is being filtered and 'processing.method.coding.display' is the path to the field that is filtered.

Nested filtering is supported and can be done by specifying nested queried node types instead of the root node type.

Example query using random SNOMED codings:

```
Expand All @@ -96,19 +101,19 @@ Example query using random SNOMED codings:
"or": [
{
"=": {
"processing.method.coding.display": "Brief intervention"
"Specimen.processing.method.coding.display": "Brief intervention"
}
},
{
"=": {
"processing.method.coding.display": "Cuboid syndrome"
"Specimen.processing.method.coding.display": "Cuboid syndrome"
}
}
]
},
{
">": {
"collection.bodySite.concept.coding.code": "261665006"
"Specimen.collection.bodySite.concept.coding.code": "261665006"
}
}
]
Expand Down
49 changes: 22 additions & 27 deletions gql-gen/graph/collectFields.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ type Resolver struct {
}

type renderTree struct {
prevName string
moved bool
rFieldPaths map[string][]string
rTree map[string]interface{}
rPotentialUnwinds []string
rActualUnwinds map[string][]string
prevName string
moved bool
fLookup map[string]string
rFieldPaths map[string][]string
rTree map[string]interface{}
rUnwinds map[string][]string
}

func (rt *renderTree) NewElement() string {
Expand All @@ -35,6 +35,8 @@ func (rt *renderTree) NewElement() string {
}

func containedinSubstr(pUnwinds []string, path string) bool {
/* Compares list of potential unwind filter paths to currenet path to determine wether
current path needs to be unwound or not*/
paths := strings.Split(path, ".")

for _, unwind := range pUnwinds {
Expand All @@ -53,7 +55,7 @@ func containedinSubstr(pUnwinds []string, path string) bool {
}

func queryBuild(query **gripql.Query, selSet ast.SelectionSet, curElement string, rt *renderTree, parentPath string, currentTree map[string]any) {
// Recursively traverses AST and build grip query, renders field tree
// Recursively traverses AST and build grip query and populate many fields in renderTree object
for _, s := range selSet {
switch sel := s.(type) {
case *ast.Field:
Expand All @@ -80,18 +82,13 @@ func queryBuild(query **gripql.Query, selSet ast.SelectionSet, curElement string
}
}
if !exists {
rPath := rt.rFieldPaths[curElement]
rPath = append(rPath, firstTerm)
rt.rFieldPaths[curElement] = rPath
rt.rFieldPaths[curElement] = append(rt.rFieldPaths[curElement], firstTerm)
}
currentTree[curElement] = rt.rFieldPaths[curElement]
} else {
fmt.Println("UNWINDS: ", rt.rPotentialUnwinds, "NEWPPATH", newParentPath, "NODE: ", len(rt.rFieldPaths))
if sel.Definition.Type.Elem != nil && containedinSubstr(rt.rPotentialUnwinds, newParentPath) {
if sel.Definition.Type.Elem != nil && containedinSubstr(rt.rUnwinds["potential"], newParentPath) {
fval := fmt.Sprintf("f%d", len(rt.rFieldPaths)-1)
aunwinds := rt.rActualUnwinds[fval]
aunwinds = append(aunwinds, newParentPath)
rt.rActualUnwinds[fval] = aunwinds
rt.rUnwinds[fval] = append(rt.rUnwinds[fval], newParentPath)
*query = (*query).Unwind(newParentPath)
*query = (*query).As(fval)
}
Expand All @@ -100,6 +97,7 @@ func queryBuild(query **gripql.Query, selSet ast.SelectionSet, curElement string
case *ast.InlineFragment:
elem := rt.NewElement()
if _, exists := currentTree[rt.prevName]; !exists {
rt.fLookup[sel.TypeCondition[:len(sel.TypeCondition)-4]] = elem
currentTree[rt.prevName] = map[string]any{"__typename": sel.TypeCondition}
}
fragmentTree := currentTree[rt.prevName].(map[string]interface{})
Expand All @@ -122,25 +120,25 @@ func (r *queryResolver) GetSelectedFieldsAst(ctx context.Context, sourceType str
return nil, err
}
}

sourceRoot := sourceType[:len(sourceType)-4]
rt := &renderTree{
rActualUnwinds: map[string][]string{},
rPotentialUnwinds: unwinds,
rFieldPaths: map[string][]string{},
rTree: map[string]any{},
fLookup: map[string]string{sourceRoot: "f0"},
rUnwinds: map[string][]string{"potential": unwinds},
rFieldPaths: map[string][]string{},
rTree: map[string]any{},
}
q := gripql.V().HasLabel(sourceType[:len(sourceType)-4]).As("f0")
q := gripql.V().HasLabel(sourceRoot).As("f0")
queryBuild(&q, resctx.Field.Selections, "f0", rt, "", rt.rTree)
fmt.Println("ACTUAL UNWINDS: ", rt.rActualUnwinds)
delete(rt.rUnwinds, "potential")

log.Infof("RNAME TREE: %#v\n", rt.rFieldPaths)
log.Infof("R TREE: %#v\n", rt.rTree)

renderTree := make(map[string]any, len(rt.rTree))
buildRenderTree(renderTree, rt.rTree)

log.Infof("ARGS: %#v\n", resctx.Args)
log.Infof("RENDER: \n", renderTree)
q = q.Select("f0")

if filter, ok := resctx.Args["filter"]; ok {
if filter != nil && len(filter.(map[string]any)) > 0 {
Expand Down Expand Up @@ -170,15 +168,12 @@ func (r *queryResolver) GetSelectedFieldsAst(ctx context.Context, sourceType str
return nil, fmt.Errorf("Traversal Error: %s", err)
}

log.Infoln("QUERY AFTER TRAVERSAL")

out := []any{}
for r := range result {
out = append(out, r.GetRender().GetStructValue().AsMap())
}

log.Infoln("QUERY AFTER RESULT")

return out, nil
}

Expand All @@ -203,7 +198,7 @@ func buildRenderTree(output map[string]any, renderTree map[string]any) {
case string:
output[key] = val
default:
fmt.Printf("Unexpected type: %T\n", val)
log.Infof("Unexpected type: %T\n", val)
}
}
}
39 changes: 31 additions & 8 deletions gql-gen/graph/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (

"github.com/bmeg/grip/gripql"
"github.com/bmeg/grip/gripql/inspect"
"github.com/bmeg/grip/log"
)

func applyAuthFilters(q **gripql.Query, authList []any) {
/* Applies gen3 RBAC auth filters to the query */
Has_Statement := &gripql.GraphStatement{Statement: &gripql.GraphStatement_Has{gripql.Within("auth_resource_path", authList...)}}
steps := inspect.PipelineSteps((*q).Statements)
FilteredGS := []*gripql.GraphStatement{}
Expand Down Expand Up @@ -45,7 +47,8 @@ func applyDefaultFilters(q **gripql.Query, args map[string]any) {
}

func (rt *renderTree) applyRewinds(query **gripql.Query) {
for f, paths := range rt.rActualUnwinds {
/* Applies rewinds negating unwinds so that output data list structures are preserved */
for f, paths := range rt.rUnwinds {
*query = (*query).Select(f)
// sort fields so that toType operations are done first then groups
sort.Slice(paths, func(i, j int) bool {
Expand All @@ -64,11 +67,12 @@ func (rt *renderTree) applyRewinds(query **gripql.Query) {
}

func (rt *renderTree) applyFilters(query **gripql.Query, filter map[string]any) error {
//Todo: support "sort" operations
chainedFilter, err := applyJsonFilter(filter)
/* Applies json filters to query as one Has statment at the end of the traversal */
chainedFilter, err := rt.makeJsonFilter(filter)
if err != nil {
return err
}
log.Infof("Has Filter: %v\n", chainedFilter)

*query = (*query).Has(chainedFilter)
rt.applyRewinds(query)
Expand Down Expand Up @@ -115,7 +119,8 @@ func getUnwinds(filter map[string]any) ([]string, error) {
}
}

func applyJsonFilter(filter map[string]any) (*gripql.HasExpression, error) {
func (rt *renderTree) makeJsonFilter(filter map[string]any) (*gripql.HasExpression, error) {
/* Constructs the Grip Has expression used to do json filters */
topLevelOp := ""
for key := range filter {
topLevelOp = key
Expand All @@ -131,7 +136,7 @@ func applyJsonFilter(filter map[string]any) (*gripql.HasExpression, error) {
if !ok {
return nil, fmt.Errorf("invalid nested filter structure")
}
subExpr, err := applyJsonFilter(itemObj)
subExpr, err := rt.makeJsonFilter(itemObj)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -162,8 +167,14 @@ func applyJsonFilter(filter map[string]any) (*gripql.HasExpression, error) {
field = key
break
}

hasExpr, err := mapGraphQLOperatorToGrip(field, topFilter[field], topLevelOp)
if !strings.Contains(field, ".") {
return nil, fmt.Errorf("filter key '%s' must be of format TYPE.property", field)
}
formattedField, err := rt.formatField(field)
if err != nil {
return nil, err
}
hasExpr, err := mapGraphQLOperatorToGrip(formattedField, topFilter[field], topLevelOp)
if err != nil {
return nil, err
}
Expand All @@ -172,7 +183,19 @@ func applyJsonFilter(filter map[string]any) (*gripql.HasExpression, error) {
}
}

func (rt *renderTree) formatField(field string) (string, error) {
/* Swaps input field like Observation into traversal state '$fn'*/
filestrs := strings.Split(field, ".")
f, ok := rt.fLookup[filestrs[0]]
if !ok {
return "", fmt.Errorf("node type %s not found in traversal types %s", filestrs[0], rt.fLookup)
}
filestrs[0] = "$" + f
return strings.Join(filestrs, "."), nil
}

func mapGraphQLOperatorToGrip(field string, value any, op string) (*gripql.HasExpression, error) {
/* Translates query operator into Grip Has Expression filter operation */
switch strings.ToLower(op) {
case "eq", "=":
return gripql.Eq(field, value), nil
Expand All @@ -189,6 +212,6 @@ func mapGraphQLOperatorToGrip(field string, value any, op string) (*gripql.HasEx
case "in":
return gripql.Within(field, value), nil
default:
return nil, fmt.Errorf("Operator %s does not match any of the known operators\n", op)
return nil, fmt.Errorf("Operator %s does not match any of the known operators", op)
}
}
63 changes: 58 additions & 5 deletions tests/grip-graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,12 +664,12 @@ func Test_Graphql_Bad_Filter_Logical_Operator(t *testing.T) {
"orr": [
{
"!=": {
"processing.method.coding.display": "some code"
"Specimen.processing.method.coding.display": "some code"
}
},
{
">": {
"collection.bodySite.concept.coding.code": "123456"
"Specimen.collection.bodySite.concept.coding.code": "123456"
}
}
]
Expand Down Expand Up @@ -720,19 +720,19 @@ func Test_Graphql_Filter_Ok(t *testing.T) {
"and": [
{
">": {
"component.valueInteger": "364"
"Observation.component.valueInteger": "364"
}
},
{
"=": {
"component.code.coding.code": "indexed_collection_date"
"Observation.component.code.coding.code": "indexed_collection_date"
}
}
]
},
{
"!=": {
"id": "21f3411d-89a4-4bcc-9ce7-b76edb1c745f"
"Observation.id": "21f3411d-89a4-4bcc-9ce7-b76edb1c745f"
}
}
]
Expand Down Expand Up @@ -932,6 +932,59 @@ func Test_Nested_Edge_Traversal_Ok(t *testing.T) {
}
}

func Test_Nested_Edge_Traversal_Filter_Ok(t *testing.T) {
payload := strings.ReplaceAll(`{
"query": "query($filter: JSON){observation(filter: $filter first:10){id component{ valueInteger code{ coding{ code}}} focus {... on DocumentReferenceType {id auth_resource_path content { attachment { extension { url valueString valueUrl}}}}}}}",
"variables": {
"filter": {
"or": [
{
"and": [
{
"=": {
"Observation.component.code.coding.code": "indexed_collection_date"
}
}
]
},
{
"!=": {
"DocumentReference.content.attachment.extension.valueString": "227f0a5379362d42eaa1814cfc0101b8"
}
}
]
}
}}`, "\t", "")
req := &Request{
url: "http://localhost:8201/graphql/query",
method: "POST",
headers: map[string]any{
"Authorization": createToken(false, false, true),
"Content-Type": "application/json"},
body: []byte(payload),
}

response, status := TemplateRequest(req, t)
t.Log("RESP: ", response)
if !status {
t.Error("Status returned false: ", response)
}

data, ok := response["data"].(map[string]any)["observation"].([]any)
if !ok {
t.Error("observation index not found in data")
}
for _, row := range data {
valueString, ok := row.(map[string]any)["focus"].(map[string]any)["content"].([]any)[0].(map[string]any)["attachment"].(map[string]any)["extension"].([]any)[0].(map[string]any)["valueString"]
if !ok {
t.Error("error in traversal, docref doesn't contain valuestring")
}
if valueString == "227f0a5379362d42eaa1814cfc0101b8" {
t.Error("filter asks for value string to not be 227f0a5379362d42eaa1814cfc0101b8 yet valuestring is 227f0a5379362d42eaa1814cfc0101b8")
}
}
}

func Test_Delete_Proj(t *testing.T) {
/* Delete Everything from test graph project ohsu-test. Should return 200 */
req := &Request{
Expand Down

0 comments on commit 08a8e6d

Please sign in to comment.