From 7f5761bb5a6a1a10fed42bb9b77ced7f2448c939 Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Sat, 3 Mar 2018 17:01:46 +0100 Subject: [PATCH] graphql: support un-nesting objects into parent; resolves #686 --- docs/GraphQL.md | 44 +++++++++++++++++++ query/graphql/graphql.go | 81 ++++++++++++++++++++++------------- query/graphql/graphql_test.go | 29 ++++++++++++- 3 files changed, 124 insertions(+), 30 deletions(-) diff --git a/docs/GraphQL.md b/docs/GraphQL.md index 425b1080c..abe21d7b3 100644 --- a/docs/GraphQL.md +++ b/docs/GraphQL.md @@ -233,4 +233,48 @@ To expand all properties of an object, `*` can be used instead of property name: follows {*} } } +``` + +### Un-nest objects + +The following query will return objects with `{id: x, status: {name: y}}` structure: + +```graphql +{ + nodes{ + id + status { + name + } + } +} +``` + +It is possible to un-nest `status` field object into parent: + +```graphql +{ + nodes{ + id + status @unnest { + status: name + } + } +} +``` + +Resulted objects will have a flat structure: `{id: x, status: y}`. + +Arrays fields cannot be un-nested. You can still un-nest such fields by +providing a limit directive (will select the first value from array): + +```graphql +{ + nodes{ + id + statuses(first: 1) @unnest { + status: name + } + } +} ``` \ No newline at end of file diff --git a/query/graphql/graphql.go b/query/graphql/graphql.go index 9bcb1cdf8..ec0f31bfb 100644 --- a/query/graphql/graphql.go +++ b/query/graphql/graphql.go @@ -117,13 +117,14 @@ type field struct { Has []has Fields []field AllFields bool // fetch all fields + UnNest bool // all fields will be saved to parent object } func (f field) isSave() bool { return len(f.Has)+len(f.Fields) == 0 && !f.AllFields } type object struct { id graph.Value - fields map[string][]graph.Value + fields map[string]interface{} } func buildIterator(qs graph.QuadStore, p *path.Path) graph.Iterator { @@ -221,7 +222,11 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa } return out, it.Err() } + unnest := make(map[string]bool) for _, f2 := range f.Fields { + if f2.UnNest { + unnest[f2.Alias] = true + } if !f2.isSave() { continue } @@ -251,7 +256,7 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa } tail() - // load object ids and flat keys + // first, collect result node ids and any tags associated with it (flat values) it := buildIterator(qs, p) defer it.Close() @@ -265,14 +270,12 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa if !it.Next(ctx) { break } + fields := make(map[string][]graph.Value) + tags := make(map[string]graph.Value) it.TagResults(tags) - obj := object{id: it.Result()} - if len(tags) > 0 { - obj.fields = make(map[string][]graph.Value) - } for k, v := range tags { - obj.fields[k] = []graph.Value{v} + fields[k] = []graph.Value{v} } for it.NextPath(ctx) { select { @@ -280,17 +283,32 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa return out, ctx.Err() default: } - tags := make(map[string]graph.Value) + tags = make(map[string]graph.Value) it.TagResults(tags) dedup: for k, v := range tags { - vals := obj.fields[k] + vals := fields[k] for _, v2 := range vals { if graph.ToKey(v) == graph.ToKey(v2) { continue dedup } } - obj.fields[k] = append(vals, v) + fields[k] = append(vals, v) + } + } + obj := object{id: it.Result()} + if len(fields) > 0 { + obj.fields = make(map[string]interface{}, len(fields)) + for k, arr := range fields { + vals, err := graph.ValuesOf(ctx, qs, arr) + if err != nil { + return nil, err + } + if len(vals) == 1 { + obj.fields[k] = vals[0] + } else { + obj.fields[k] = vals + } } } results = append(results, obj) @@ -299,24 +317,17 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa return out, err } - // load values and complex keys + // next, load complex objects inside fields for _, r := range results { - obj := make(map[string]interface{}) - for k, arr := range r.fields { - vals, err := graph.ValuesOf(ctx, qs, arr) - if err != nil { - return nil, err - } - if len(vals) == 1 { - obj[k] = vals[0] - } else { - obj[k] = vals - } + obj := r.fields + if obj == nil { + obj = make(map[string]interface{}) } for _, f2 := range f.Fields { if f2.isSave() { - continue + continue // skip flat values } + // start from saved id for a field node p2 := path.StartPathNodes(qs, r.id) if len(f2.Labels) != 0 { p2 = p2.LabelContext(f2.Labels) @@ -333,13 +344,23 @@ func iterateObject(ctx context.Context, qs graph.QuadStore, f *field, p *path.Pa if err != nil { return out, err } - var v interface{} - if len(arr) == 1 { - v = arr[0] - } else if len(arr) > 1 { - v = arr + if f2.UnNest { + if len(arr) > 1 { + return nil, fmt.Errorf("cannot unnest more than one object on %q; use (%s: 1) to force", + f2.Alias, LimitKey) + } + for k, v := range arr[0] { + obj[k] = v + } + } else { + var v interface{} + if len(arr) == 1 { + v = arr[0] + } else if len(arr) > 1 { + v = arr + } + obj[f2.Alias] = v } - obj[f2.Alias] = v } out = append(out, obj) } @@ -494,6 +515,8 @@ func convField(fld *ast.Field, labels []quad.Value) (out field, err error) { out.Opt = true case "label": // already processed + case "unnest": + out.UnNest = true default: return out, fmt.Errorf("unknown directive: %q", d.Name.Value) } diff --git a/query/graphql/graphql_test.go b/query/graphql/graphql_test.go index dd8102486..ea7000c4c 100644 --- a/query/graphql/graphql_test.go +++ b/query/graphql/graphql_test.go @@ -38,7 +38,7 @@ var casesParse = []struct { sname @label } isViewerFriend, - profilePicture(size: 50) { + profilePicture(size: 50) @unnest { uri, width @opt, height @rev @@ -75,6 +75,7 @@ var casesParse = []struct { {Via: "width", Alias: "width", Opt: true}, {Via: "height", Alias: "height", Rev: true}, }, + UnNest: true, }, {Via: "sub", Alias: "sub", AllFields: true}, }, @@ -220,6 +221,32 @@ var casesExecute = []struct { }, }, }, + { + "unnest object", + `{ + me(id: fred) { + id: ` + ValueKey + ` + follows @unnest { + friend: ` + ValueKey + ` + friend_status: status + followed: follows(` + LimitKey + `: 1) @rev @unnest { + fof: ` + ValueKey + ` + } + } + } +}`, + map[string]interface{}{ + "me": map[string]interface{}{ + "id": quad.IRI("fred"), + "fof": quad.IRI("dani"), + "friend": quad.IRI("greg"), + "friend_status": []quad.Value{ + quad.String("cool_person"), + quad.String("smart_person"), + }, + }, + }, + }, } func toJson(o interface{}) string {