Skip to content

Commit

Permalink
fix optimizations, add to docs
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewpeterkort committed Jan 7, 2025
1 parent aba2d71 commit e0c8e9b
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 50 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:
# Login to Quay.io and build image
docker login quay.io
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t $REPO:$BRANCH . --push
docker build -t $REPO:$BRANCH .
# Add 'latest' tag to 'main' image
if [[ $BRANCH == 'main' ]]; then
docker buildx build --platform linux/amd64,linux/arm64 -t $REPO:latest . --push
docker image tag $REPO:main $REPO:latest
fi
# Push the tagged image to Quay.io
Expand Down
50 changes: 49 additions & 1 deletion gql-gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Filters only currently supported on the first node that is queried. Ex: specimen

```
query($filter: JSON){
specimen(filter: $filter first:100){
specimen(filter: $filter first:100 offset: 5){
id
subject{
... on PatientType{
Expand Down Expand Up @@ -67,3 +67,51 @@ query($filter: JSON){
}
}
```

## Query Filters

Queries follow the general structure of the FHIR data model except in places where references exist, the graph DB collects all of the vertex data on that specified reference edge and returns it in one dict.

Filters follow the general structure of including "and" and "or" logical operations with comparators as keys

Current supported comparator statements:

| Operator | Explanation |
| ----------- | ------------------------------ |
| "eq", "=", | equal |
| "neq", "!=" | not equal |
| "lt", "<" | less than |
| "gt", ">" | greater than |
| "gte", ">=" | greater than or equal |
| "lte", "<=" | less than or equal |
| "in" | value is in the list of values |

Example query using random SNOMED codings:

```
{
"filter": {
"and": [
{
"or": [
{
"=": {
"processing.method.coding.display": "Brief intervention"
}
},
{
"=": {
"processing.method.coding.display": "Cuboid syndrome"
}
}
]
},
{
">": {
"collection.bodySite.concept.coding.code": "261665006"
}
}
]
}
}
```
69 changes: 23 additions & 46 deletions gql-gen/graph/collectFields.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import (
"context"
"fmt"
"os"
"reflect"

"strconv"
"strings"

"github.com/99designs/gqlgen/graphql"
"github.com/bmeg/grip/gripql"
"github.com/bmeg/grip/gripql/inspect"
"github.com/vektah/gqlparser/v2/ast"
)

Expand Down Expand Up @@ -38,7 +37,7 @@ func (rt *renderTree) NewElement() string {
}

func queryBuild(query **gripql.Query, selSet ast.SelectionSet, curElement string, rt *renderTree, parentPath string, currentTree map[string]any) {
// Recursively traverses AST and builds grip query, renders field tree
// Recursively traverses AST and build grip query, renders field tree
for _, s := range selSet {
switch sel := s.(type) {
case *ast.Field:
Expand Down Expand Up @@ -95,7 +94,6 @@ func queryBuild(query **gripql.Query, selSet ast.SelectionSet, curElement string

func (r *queryResolver) GetSelectedFieldsAst(ctx context.Context, sourceType string) ([]any, error) {
resctx := graphql.GetFieldContext(ctx)

rt := &renderTree{
rFieldPaths: map[string]renderTreePath{"f0": renderTreePath{path: []string{}, unwindPath: []string{}}},
rTree: map[string]any{},
Expand Down Expand Up @@ -124,48 +122,20 @@ func (r *queryResolver) GetSelectedFieldsAst(ctx context.Context, sourceType str
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("filter is specified but filter not populated in variables")
}
}

if first, ok := resctx.Args["first"]; ok {
firstPtr, _ := first.(*int)
if firstPtr == nil {
q = q.Limit(uint32(10))
} else {
q = q.Limit(uint32(*firstPtr))
}
}
if offset, ok := resctx.Args["offset"]; ok {
if offset.(*int) != nil {
q = q.Skip(uint32(*offset.(*int)))
}
}
// apply default filters after main filters so that all data can be considered in filter before apply filter statements
applyDefaultFilters(&q, resctx.Args)

if os.Getenv("AUTH_ENABLED") == "true" {
authList, ok := ctx.Value("auth_list").([]interface{})
if !ok {
return nil, fmt.Errorf("auth_list not found or invalid")
}

Has_Statement := &gripql.GraphStatement{Statement: &gripql.GraphStatement_Has{gripql.Within("auth_resource_path", authList...)}}
steps := inspect.PipelineSteps(q.Statements)
FilteredGS := []*gripql.GraphStatement{}
for i, v := range q.Statements {
steps_index, _ := strconv.Atoi(steps[i])
if i == 0 {
FilteredGS = append(FilteredGS, v)
continue
} else if i == steps_index {
FilteredGS = append(FilteredGS, v, Has_Statement)
} else {
FilteredGS = append(FilteredGS, v)
}
}

q.Statements = FilteredGS
applyAuthFilters(q, authList)
}

q = q.Render(render)
fmt.Println("QUERY AFTER RENDER: ", q)

Expand All @@ -174,19 +144,19 @@ func (r *queryResolver) GetSelectedFieldsAst(ctx context.Context, sourceType str
return nil, fmt.Errorf("Traversal Error: %s", err)
}

cachedTree := make(map[string]any, len(rt.rTree))
buildTreeStructure(cachedTree, rt.rTree)
// Build response tree once, traverse/populate it len(result) times
responseTree := make(map[string]any, len(rt.rTree))
buildResponseTree(responseTree, rt.rTree)

out := []any{}
for r := range result {
values := r.GetRender().GetStructValue().AsMap()
data := buildFilteredResponseTree(cachedTree, values)
out = append(out, data)
out = append(out, populateResponseTree(responseTree, r.GetRender().GetStructValue().AsMap()))
}
return out, nil
}

func buildTreeStructure(output map[string]any, renderTree map[string]any) {
func buildResponseTree(output map[string]any, renderTree map[string]any) {
/* Build the skeleton of the response tree without filling in the values */
for key, val := range renderTree {
switch v := val.(type) {
case renderTreePath:
Expand All @@ -208,7 +178,7 @@ func buildTreeStructure(output map[string]any, renderTree map[string]any) {
}
case map[string]any:
subTree := make(map[string]any)
buildTreeStructure(subTree, v)
buildResponseTree(subTree, v)
output[key] = subTree
case string:
if key == "__typename" {
Expand All @@ -222,17 +192,24 @@ func buildTreeStructure(output map[string]any, renderTree map[string]any) {
}
}

func buildFilteredResponseTree(cachedTree, values map[string]any) map[string]any {
func populateResponseTree(cachedTree, values map[string]any) map[string]any {
/* fill in the values of the given response tree */
output := make(map[string]any, len(cachedTree))
for key, val := range cachedTree {
switch v := val.(type) {
case map[string]any:
if filteredSubTree := buildFilteredResponseTree(v, values); len(filteredSubTree) > 0 {
if filteredSubTree := populateResponseTree(v, values); len(filteredSubTree) > 0 {
output[key] = filteredSubTree
}
case nil:
if renderedValue, exists := values[key+"_data"]; exists {
output[key] = renderedValue
if reflect.TypeOf(renderedValue) == reflect.TypeOf("") && strings.HasPrefix(renderedValue.(string), "$f") {
output[key] = nil
} else {
output[key] = renderedValue
}
} else {
output[key] = nil
}
default:
output[key] = val
Expand Down
38 changes: 38 additions & 0 deletions gql-gen/graph/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,48 @@ import (
"fmt"
"strings"

"strconv"

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

"github.com/bmeg/grip/gripql"
"google.golang.org/protobuf/types/known/structpb"
)

func applyAuthFilters(q *gripql.Query, authList []any) {
Has_Statement := &gripql.GraphStatement{Statement: &gripql.GraphStatement_Has{gripql.Within("auth_resource_path", authList...)}}
steps := inspect.PipelineSteps(q.Statements)
FilteredGS := []*gripql.GraphStatement{}
for i, v := range q.Statements {
steps_index, _ := strconv.Atoi(steps[i])
if i == 0 {
FilteredGS = append(FilteredGS, v)
continue
} else if i == steps_index {
FilteredGS = append(FilteredGS, v, Has_Statement)
} else {
FilteredGS = append(FilteredGS, v)
}
}
q.Statements = FilteredGS
}

func applyDefaultFilters(q **gripql.Query, args map[string]any) {
if first, ok := args["first"]; ok {
firstPtr, _ := first.(*int)
if firstPtr == nil {
*q = (*q).Limit(uint32(10))
} else {
*q = (*q).Limit(uint32(*firstPtr))
}
}
if offset, ok := args["offset"]; ok {
if offset.(*int) != nil {
*q = (*q).Skip(uint32(*offset.(*int)))
}
}
}

func (rt *renderTree) applyUnwinds(query **gripql.Query) {
/* Assumes query is at f0 and only applies unwinds to that node currently*/
for _, val := range rt.rFieldPaths["f0"].unwindPath {
Expand Down

0 comments on commit e0c8e9b

Please sign in to comment.