Skip to content

Commit db6ef5b

Browse files
committed
Merge branch 'omani-master'
2 parents e5aeb52 + 2b3fe10 commit db6ef5b

30 files changed

+185
-126
lines changed

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,6 @@ Content-Length: 257
413413
Content-Type: application/json
414414
Date: Mon, 27 Jul 2015 19:51:46 GMT
415415
Vary: Origin
416-
X-Page: 1
417416
X-Total: 1
418417
419418
[
@@ -439,7 +438,6 @@ Content-Length: 257
439438
Content-Type: application/json
440439
Date: Mon, 27 Jul 2015 19:51:46 GMT
441440
Vary: Origin
442-
X-Page: 1
443441
X-Total: 1
444442
445443
[
@@ -848,14 +846,29 @@ You can invert the operator by passing `false`.
848846

849847
## Sorting
850848

851-
Sorting is of resource items is defined throught the `sort` query-string parameter. The `sort` value is a list of resource's fields separated by comas (`,`). To invert a field's sort, you can prefix its name with a minus (`-`) character.
849+
Sorting of resource items is defined through the `sort` query-string parameter. The `sort` value is a list of resource's fields separated by comas (`,`). To invert a field's sort, you can prefix its name with a minus (`-`) character.
852850

853851
To use a resource field with the `sort` parameter, the field must be defined on the resource and the `Sortable` field property must be set to `true`. You may want to ensure the backend database has this field indexed when enabled.
854852

855853
Here we sort the result by ascending quantity and descending date:
856854

857855
/posts?sort=quantity,-created
858856

857+
## Skipping
858+
859+
Skipping of resource items is defined through the `skip` query-string parameter. The `skip` value is a positive integer defining the number of items to skip when querying for items.
860+
861+
To use a resource field with the `skip` parameter, the field must be defined on the resource.
862+
863+
Skip the first 10 items of the result:
864+
865+
/posts?skip=10
866+
867+
Return the first 2 items after skipping the first 10 of the result:
868+
869+
/posts?skip=10&limit=2
870+
Note that `skip` can't be used with pagination.
871+
859872
## Field Selection
860873

861874
REST APIs tend to grow over time. Resources get more and more fields to fulfill the needs for new features. But each time fields are added, all existing API clients automatically gets the additional cost. This tend to lead to huge wast of bandwidth and added latency due to the transfer of unnecessary data.
@@ -999,13 +1012,13 @@ $ http -b :8080/api/users/ar6eimekj5lfktka9mt0/posts \
9991012

10001013
In the above example, the `user` field is a reference on the `users` resource. REST Layer did fetch the user referenced by the post and embedded the requested sub-fields (`id` and `name`). Same for `comments`: `comments` is set as a sub-resource of the `posts` resource. With this syntax, it's easy to get the last 10 comments on the post in the same REST request. For each of those comment, we asked to embed the `user` field referenced resource with `id` and `name` fields again.
10011014

1002-
Notice the `sort` and `limit` parameters passed to the `comments` field. Those are field parameter automatically exposed by connections to let you control the embedded list order, filter and pagination. You can use `sort`, `filter`, `page` and `limit` parameters with those field with the same syntax as their top level query-string parameter counterpart.
1015+
Notice the `sort` and `limit` parameters passed to the `comments` field. Those are field parameter automatically exposed by connections to let you control the embedded list order, filter and pagination. You can use `sort`, `filter`, `skip`, `page` and `limit` parameters with those field with the same syntax as their top level query-string parameter counterpart.
10031016

10041017
Such request can quickly generate a lot of queries on the storage handler. To ensure a fast response time, REST layer tries to coalesce those storage requests and to execute them concurrently whenever possible.
10051018

10061019
## Pagination
10071020

1008-
Pagination is supported on collection URLs using `page` and `limit` query-string parameters. If you don't define a default pagination limit using `PaginationDefaultLimit` resource configuration parameter, the resource won't be paginated until you provide the `limit` query-string parameter.
1021+
Pagination is supported on collection URLs using `skip`, `page` and `limit` query-string parameters. If you don't define a default pagination limit using `PaginationDefaultLimit` resource configuration parameter, the resource won't be paginated until you provide the `limit` query-string parameter.
10091022

10101023
If your collections are large enough, failing to define a reasonable `PaginationDefaultLimit` parameter may quickly render your API unusable.
10111024

@@ -1289,7 +1302,7 @@ A resource storage handler is easy to write though. Some handlers for [popular d
12891302

12901303
```go
12911304
type Storer interface {
1292-
Find(ctx context.Context, lookup *resource.Lookup, page, perPage int) (*resource.ItemList, error)
1305+
Find(ctx context.Context, lookup *resource.Lookup, offset, limit int) (*resource.ItemList, error)
12931306
Insert(ctx context.Context, items []*resource.Item) error
12941307
Update(ctx context.Context, item *resource.Item, original *resource.Item) error
12951308
Delete(ctx context.Context, item *resource.Item) error
@@ -1349,11 +1362,11 @@ type myResponseFormatter struct {
13491362

13501363
// Add a wrapper around the list with pagination info
13511364
func (r myResponseFormatter) FormatList(ctx context.Context, headers http.Header, l *resource.ItemList, skipBody bool) (context.Context, interface{}) {
1352-
ctx, data := r.DefaultResponseSender.FormatList(ctx, headers, l, skipBody)
1365+
ctx, data := r.DefaultResponseFormatter.FormatList(ctx, headers, l, skipBody)
13531366
return ctx, map[string]interface{}{
13541367
"meta": map[string]int{
1355-
"total": l.Total,
1356-
"page": l.Page,
1368+
"offset": l.Offset,
1369+
"total": l.Total,
13571370
},
13581371
"list": data,
13591372
}
@@ -1364,7 +1377,7 @@ func (r myResponseFormatter) FormatList(ctx context.Context, headers http.Header
13641377

13651378
In parallel of the REST API handler, REST Layer is also able to handle GraphQL queries (mutation will come later). GraphQL is a query language created by Facebook which provides a common interface fetch and manipulate data. REST Layer's GraphQL handler is able to read a [resource.Index](https://godoc.org/github.com/rs/rest-layer/resource#Index) and create a corresponding GraphQL schema.
13661379

1367-
GraphQL doesn't expose resources directly, but queries. REST Layer take all the resources defined at the root of the `resource.Index` and create two GraphQL queries for each one. On query is just the name of the endpoint, so `/users` would result in `users` and another is the name of the endpoint suffixed with `List`, as `usersList`. The item queries takes an `id` parameter and the list queries take `page`, `limit`, `filter` and `sort` parameters. All sub-resources are accessible using GraphQL sub-selection syntax.
1380+
GraphQL doesn't expose resources directly, but queries. REST Layer take all the resources defined at the root of the `resource.Index` and create two GraphQL queries for each one. On query is just the name of the endpoint, so `/users` would result in `users` and another is the name of the endpoint suffixed with `List`, as `usersList`. The item queries takes an `id` parameter and the list queries take `skip`, `page`, `limit`, `filter` and `sort` parameters. All sub-resources are accessible using GraphQL sub-selection syntax.
13681381

13691382
If you resource defines aliases, some additional GraphQL queris are exposes with their name constructed as the name of the resource suffixed with the name of the alias with a capital. So for `users` with an alias `admin`, the query would be `usersAdmin`.
13701383

examples/auth-jwt/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ type AuthResourceHook struct {
9393
}
9494

9595
// OnFind implements resource.FindEventHandler interface
96-
func (a AuthResourceHook) OnFind(ctx context.Context, lookup *resource.Lookup, page, perPage int) error {
96+
func (a AuthResourceHook) OnFind(ctx context.Context, lookup *resource.Lookup, offset, limit int) error {
9797
// Reject unauthorized users
9898
user, found := UserFromContext(ctx)
9999
if !found {

examples/auth/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type AuthResourceHook struct {
7777
}
7878

7979
// OnFind implements resource.FindEventHandler interface
80-
func (a AuthResourceHook) OnFind(ctx context.Context, lookup *resource.Lookup, page, perPage int) error {
80+
func (a AuthResourceHook) OnFind(ctx context.Context, lookup *resource.Lookup, offset, limit int) error {
8181
// Reject unauthorized users
8282
user, found := UserFromContext(ctx)
8383
if !found {

graphql/functional_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func TestHandler(t *testing.T) {
196196
r, _ = http.NewRequest("GET", "/?query={posts(id:\"ar5qrgukj5l7a6eq2ps0\"){id,meta{title},followers(limit:2){user{id,name}}}}", nil)
197197
s, b = performRequest(gql, r)
198198
assert.Equal(t, 200, s)
199-
assert.Equal(t, "{\"data\":{\"posts\":{\"followers\":[{\"user\":{\"id\":\"fan1\",\"name\":\"Fan 1\"}},{\"user\":{\"id\":\"fan2\",\"name\":\"Fan 2\"}},{\"user\":{\"id\":\"fan3\",\"name\":\"Fan 3\"}}],\"id\":\"ar5qrgukj5l7a6eq2ps0\",\"meta\":{\"title\":\"First Post\"}}}}\n", b)
199+
assert.Equal(t, "{\"data\":{\"posts\":{\"followers\":[{\"user\":{\"id\":\"fan1\",\"name\":\"Fan 1\"}},{\"user\":{\"id\":\"fan2\",\"name\":\"Fan 2\"}}],\"id\":\"ar5qrgukj5l7a6eq2ps0\",\"meta\":{\"title\":\"First Post\"}}}}\n", b)
200200

201201
r, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{postsList{id,thumb_s_url:thumbnail_url(height:80)}}"))
202202
s, b = performRequest(gql, r)

graphql/query.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log"
77
"net/url"
8-
"strconv"
98
"strings"
109

1110
"github.com/graphql-go/graphql"
@@ -63,6 +62,9 @@ func (t types) getGetQuery(idx resource.Index, r *resource.Resource) *graphql.Fi
6362
}
6463

6564
var listArgs = graphql.FieldConfigArgument{
65+
"skip": &graphql.ArgumentConfig{
66+
Type: graphql.Int,
67+
},
6668
"page": &graphql.ArgumentConfig{
6769
Type: graphql.Int,
6870
},
@@ -77,30 +79,28 @@ var listArgs = graphql.FieldConfigArgument{
7779
},
7880
}
7981

80-
func listParamResolver(r *resource.Resource, p graphql.ResolveParams, params url.Values) (lookup *resource.Lookup, page int, perPage int, err error) {
81-
page = 1
82-
// Default value on non HEAD request for perPage is -1 (pagination disabled)
83-
perPage = -1
82+
func listParamResolver(r *resource.Resource, p graphql.ResolveParams, params url.Values) (lookup *resource.Lookup, offset int, limit int, err error) {
83+
skip := 0
84+
page := 1
85+
// Default value on non HEAD request for limit is -1 (pagination disabled)
86+
limit = -1
87+
8488
if l := r.Conf().PaginationDefaultLimit; l > 0 {
85-
perPage = l
89+
limit = l
8690
}
87-
if p, ok := p.Args["page"].(string); ok && p != "" {
88-
i, err := strconv.ParseUint(p, 10, 32)
89-
if err != nil {
90-
return nil, 0, 0, errors.New("invalid `limit` parameter")
91-
}
92-
page = int(i)
91+
if i, ok := p.Args["skip"].(int); ok && i >= 0 {
92+
skip = i
9393
}
94-
if l, ok := p.Args["limit"].(string); ok && l != "" {
95-
i, err := strconv.ParseUint(l, 10, 32)
96-
if err != nil {
97-
return nil, 0, 0, errors.New("invalid `limit` parameter")
98-
}
99-
perPage = int(i)
94+
if i, ok := p.Args["page"].(int); ok && i > 0 && i < 1000 {
95+
page = i
96+
}
97+
if i, ok := p.Args["limit"].(int); ok && i >= 0 && i < 1000 {
98+
limit = i
10099
}
101-
if perPage == -1 && page != 1 {
100+
if page != 1 && limit == -1 {
102101
return nil, 0, 0, errors.New("cannot use `page' parameter with no `limit' paramter on a resource with no default pagination size")
103102
}
103+
offset = (page-1)*limit + skip
104104
lookup = resource.NewLookup()
105105
if sort, ok := p.Args["sort"].(string); ok && sort != "" {
106106
if err := lookup.SetSort(sort, r.Validator()); err != nil {
@@ -128,11 +128,11 @@ func (t types) getListQuery(idx resource.Index, r *resource.Resource, params url
128128
Type: graphql.NewList(t.getObjectType(idx, r)),
129129
Args: listArgs,
130130
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
131-
lookup, page, perPage, err := listParamResolver(r, p, params)
131+
lookup, offset, limit, err := listParamResolver(r, p, params)
132132
if err != nil {
133133
return nil, err
134134
}
135-
list, err := r.Find(p.Context, lookup, page, perPage)
135+
list, err := r.Find(p.Context, lookup, offset, limit)
136136
if err != nil {
137137
return nil, err
138138
}

graphql/types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func getSubResourceResolver(r *resource.Resource) graphql.FieldResolveFn {
8787
if !ok {
8888
return nil, nil
8989
}
90-
lookup, page, perPage, err := listParamResolver(r, p, nil)
90+
lookup, offset, limit, err := listParamResolver(r, p, nil)
9191
if err != nil {
9292
return nil, err
9393
}
@@ -98,7 +98,7 @@ func getSubResourceResolver(r *resource.Resource) graphql.FieldResolveFn {
9898
Value: parent["id"],
9999
},
100100
})
101-
list, err := r.Find(p.Context, lookup, page, perPage)
101+
list, err := r.Find(p.Context, lookup, offset, limit)
102102
if err != nil {
103103
return nil, err
104104
}

resource/hook.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import (
99
// want to be called before a find is performed on a resource. This interface is
1010
// to be used with resource.Use() method.
1111
type FindEventHandler interface {
12-
OnFind(ctx context.Context, lookup *Lookup, page, perPage int) error
12+
OnFind(ctx context.Context, lookup *Lookup, offset, limit int) error
1313
}
1414

1515
// FindEventHandlerFunc converts a function into a FindEventHandler.
16-
type FindEventHandlerFunc func(ctx context.Context, lookup *Lookup, page, perPage int) error
16+
type FindEventHandlerFunc func(ctx context.Context, lookup *Lookup, offset, limit int) error
1717

1818
// OnFind implements FindEventHandler
19-
func (e FindEventHandlerFunc) OnFind(ctx context.Context, lookup *Lookup, page, perPage int) error {
20-
return e(ctx, lookup, page, perPage)
19+
func (e FindEventHandlerFunc) OnFind(ctx context.Context, lookup *Lookup, offset, limit int) error {
20+
return e(ctx, lookup, offset, limit)
2121
}
2222

2323
// FoundEventHandler is an interface to be implemented by an event handler that
@@ -256,9 +256,9 @@ func (h *eventHandler) use(e interface{}) error {
256256
return nil
257257
}
258258

259-
func (h *eventHandler) onFind(ctx context.Context, lookup *Lookup, page, perPage int) error {
259+
func (h *eventHandler) onFind(ctx context.Context, lookup *Lookup, offset, limit int) error {
260260
for _, e := range h.onFindH {
261-
if err := e.OnFind(ctx, lookup, page, perPage); err != nil {
261+
if err := e.OnFind(ctx, lookup, offset, limit); err != nil {
262262
return err
263263
}
264264
}

resource/hook_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func TestHookUseFind(t *testing.T) {
1212
h := eventHandler{}
1313
called := false
14-
err := h.use(FindEventHandlerFunc(func(ctx context.Context, lookup *Lookup, page, perPage int) error {
14+
err := h.use(FindEventHandlerFunc(func(ctx context.Context, lookup *Lookup, offset, limit int) error {
1515
called = true
1616
return nil
1717
}))
@@ -31,7 +31,7 @@ func TestHookUseFind(t *testing.T) {
3131
err = h.onFind(nil, nil, 0, 0)
3232
assert.True(t, called)
3333

34-
err = h.use(FindEventHandlerFunc(func(ctx context.Context, lookup *Lookup, page, perPage int) error {
34+
err = h.use(FindEventHandlerFunc(func(ctx context.Context, lookup *Lookup, offset, limit int) error {
3535
return errors.New("error")
3636
}))
3737
assert.NoError(t, err)

resource/item.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ type ItemList struct {
2727
// Total defines the total number of items in the collection matching the current
2828
// context. If the storage handler cannot compute this value, -1 is set.
2929
Total int
30-
// Page is the current page represented by this ItemList.
31-
Page int
30+
// Offset is the index of the first item of the list in the global collection.
31+
Offset int
32+
// Limit is the max number of items requested.
33+
Limit int
3234
// Items is the list of items contained in the current page given the current
3335
// context.
3436
Items []*Item

resource/resource.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ func (r *Resource) Bind(name, field string, s schema.Schema, h Storer, c Conf) *
156156
path: "." + name,
157157
},
158158
Params: schema.Params{
159+
"skip": schema.Param{
160+
Description: "The number of items to skip",
161+
Validator: schema.Integer{
162+
Boundaries: &schema.Boundaries{Min: 0},
163+
},
164+
},
159165
"page": schema.Param{
160166
Description: "The page number",
161167
Validator: schema.Integer{
@@ -305,22 +311,22 @@ func (r *Resource) MultiGet(ctx context.Context, ids []interface{}) (items []*It
305311
}
306312

307313
// Find implements Storer interface
308-
func (r *Resource) Find(ctx context.Context, lookup *Lookup, page, perPage int) (list *ItemList, err error) {
314+
func (r *Resource) Find(ctx context.Context, lookup *Lookup, offset, limit int) (list *ItemList, err error) {
309315
if LoggerLevel <= LogLevelDebug && Logger != nil {
310316
defer func(t time.Time) {
311317
found := -1
312318
if list != nil {
313319
found = len(list.Items)
314320
}
315-
Logger(ctx, LogLevelDebug, fmt.Sprintf("%s.Find(..., %d, %d)", r.path, page, perPage), map[string]interface{}{
321+
Logger(ctx, LogLevelDebug, fmt.Sprintf("%s.Find(..., %d, %d)", r.path, offset, limit), map[string]interface{}{
316322
"duration": time.Since(t),
317323
"found": found,
318324
"error": err,
319325
})
320326
}(time.Now())
321327
}
322-
if err = r.hooks.onFind(ctx, lookup, page, perPage); err == nil {
323-
list, err = r.storage.Find(ctx, lookup, page, perPage)
328+
if err = r.hooks.onFind(ctx, lookup, offset, limit); err == nil {
329+
list, err = r.storage.Find(ctx, lookup, offset, limit)
324330
}
325331
r.hooks.onFound(ctx, lookup, &list, &err)
326332
return

0 commit comments

Comments
 (0)