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

graphql: support un-nesting objects into parent #699

Merged
merged 1 commit into from
Mar 3, 2018
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
44 changes: 44 additions & 0 deletions docs/GraphQL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
```
81 changes: 52 additions & 29 deletions query/graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()

Expand All @@ -265,32 +270,45 @@ 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 {
case <-ctx.Done():
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)
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
29 changes: 28 additions & 1 deletion query/graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var casesParse = []struct {
sname @label
}
isViewerFriend,
profilePicture(size: 50) {
profilePicture(size: 50) @unnest {
uri,
width @opt,
height @rev
Expand Down Expand Up @@ -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},
},
Expand Down Expand Up @@ -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 {
Expand Down