Skip to content

Commit cbf74b0

Browse files
authored
Allow user (or config) to explicitely request the list total metadata (#69)
Fix: #68
1 parent 3505194 commit cbf74b0

File tree

5 files changed

+92
-4
lines changed

5 files changed

+92
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ The `resource.Conf` type has the following customizable properties:
655655
| ------------------------ | -------------
656656
| `AllowedModes` | A list of `resource.Mode` allowed for the resource.
657657
| `PaginationDefaultLimit` | If set, pagination is enabled by default with a number of item per page defined here.
658+
| `ForceTotal` | Control the behavior of the computation of `X-Total` header and the `total` query-string parameter. See `resource.ForceTotalMode` for available options.
658659

659660
### Modes
660661

resource/conf.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,36 @@ type Conf struct {
88
// no default page size is set resulting in no pagination if no `limit` parameter
99
// is provided.
1010
PaginationDefaultLimit int
11+
// ForceTotal controls how total number of items on list request is computed.
12+
// By default (TotalOptIn), if the total cannot be computed by the storage
13+
// handler for free, no total metadata is returned until the user explicitely
14+
// request it using the total=1 query-string parameter. Note that if the
15+
// storage cannot compute the total and does not implement the resource.Counter
16+
// interface, a "not implemented" error is returned.
17+
//
18+
// The TotalAlways mode always force the computation of the total (make sure the
19+
// storage either compute the total on Find or implement the resource.Counter
20+
// interface.
21+
//
22+
// TotalDenied prevents the user from requesting the total.
23+
ForceTotal ForceTotalMode
1124
}
1225

26+
// ForceTotalMode defines Conf.ForceTotal modes
27+
type ForceTotalMode int
28+
29+
const (
30+
// TotalOptIn allows the end-user to opt-in to forcing the total count by
31+
// adding the total=1 query-string parameter.
32+
TotalOptIn ForceTotalMode = iota
33+
// TotalAlways always force the total number of items on list requests
34+
TotalAlways
35+
// TotalDenied disallows forcing of the total count, and returns an error
36+
// if total=1 is supplied, and the total count is not provided by the
37+
// Storer's Find method.
38+
TotalDenied
39+
)
40+
1341
// Mode defines CRUDL modes to be used with Conf.AllowedModes.
1442
type Mode int
1543

resource/resource.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,20 @@ func (r *Resource) MultiGet(ctx context.Context, ids []interface{}) (items []*It
310310
return
311311
}
312312

313-
// Find implements Storer interface
313+
// Find calls the Find method on the storage handler with the corresponding pre/post hooks.
314314
func (r *Resource) Find(ctx context.Context, lookup *Lookup, offset, limit int) (list *ItemList, err error) {
315+
return r.find(ctx, lookup, offset, limit, false)
316+
}
317+
318+
// FindWithTotal calls the Find method on the storage handler with the corresponding pre/post hooks.
319+
// If the storage is not able to compute the total, this method will call the Count method on the
320+
// storage. If the storage Find does not compute the total and the Counter interface is not implemented,
321+
// an ErrNotImpemented error is returned.
322+
func (r *Resource) FindWithTotal(ctx context.Context, lookup *Lookup, offset, limit int) (list *ItemList, err error) {
323+
return r.find(ctx, lookup, offset, limit, true)
324+
}
325+
326+
func (r *Resource) find(ctx context.Context, lookup *Lookup, offset, limit int, forceTotal bool) (list *ItemList, err error) {
315327
if LoggerLevel <= LogLevelDebug && Logger != nil {
316328
defer func(t time.Time) {
317329
found := -1
@@ -327,6 +339,9 @@ func (r *Resource) Find(ctx context.Context, lookup *Lookup, offset, limit int)
327339
}
328340
if err = r.hooks.onFind(ctx, lookup, offset, limit); err == nil {
329341
list, err = r.storage.Find(ctx, lookup, offset, limit)
342+
if err == nil && list.Total == -1 && forceTotal {
343+
list.Total, err = r.storage.Count(ctx, lookup)
344+
}
330345
}
331346
r.hooks.onFound(ctx, lookup, &list, &err)
332347
return

resource/storage.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ type Storer interface {
1212
// pagination argument must be respected. If no items are found, an empty list
1313
// should be returned with no error.
1414
//
15-
// If the total number of item can't be easily computed, ItemList.Total should
16-
// be set to -1. The requested page should be set to ItemList.Page.
15+
// If the total number of item can't be computed for free, ItemList.Total must
16+
// be set to -1. Your Storer may implement the Counter interface to let the user
17+
// explicitely request the total.
1718
//
1819
// The whole lookup query must be treated. If a query operation is not implemented
1920
// by the storage handler, a resource.ErrNotImplemented must be returned.
@@ -81,9 +82,20 @@ type MultiGetter interface {
8182
MultiGet(ctx context.Context, ids []interface{}) ([]*Item, error)
8283
}
8384

85+
// Counter is an optional interface a Storer can implement to provide a way to explicitely
86+
// count the total number of elements a given lookup would return with no pagination
87+
// provided. This method is called by REST Layer when the storage handler returned -1
88+
// as ItemList.Total and the user (or configuration) explicitely request the total.
89+
type Counter interface {
90+
// Count returns the total number of item in the collection given the provided
91+
// lookup filter.
92+
Count(ctx context.Context, lookup *Lookup) (int, error)
93+
}
94+
8495
type storageHandler interface {
8596
Storer
8697
MultiGetter
98+
Counter
8799
Get(ctx context.Context, id interface{}) (item *Item, err error)
88100
}
89101

@@ -228,3 +240,16 @@ func (s storageWrapper) Clear(ctx context.Context, lookup *Lookup) (deleted int,
228240
}
229241
return s.Storer.Clear(ctx, lookup)
230242
}
243+
244+
func (s storageWrapper) Count(ctx context.Context, lookup *Lookup) (total int, err error) {
245+
if s.Storer == nil {
246+
return -1, ErrNoStorage
247+
}
248+
if ctx.Err() != nil {
249+
return -1, ctx.Err()
250+
}
251+
if c, ok := s.Storer.(Counter); ok {
252+
return c.Count(ctx, lookup)
253+
}
254+
return -1, ErrNotImplemented
255+
}

rest/method_get.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"net/url"
88
"strconv"
9+
10+
"github.com/rs/rest-layer/resource"
911
)
1012

1113
// listGet handles GET resquests on a resource URL
@@ -45,11 +47,28 @@ func listGet(ctx context.Context, r *http.Request, route *RouteMatch) (status in
4547
}
4648
offset = (page-1)*limit + skip
4749
}
50+
forceTotal := false
51+
switch rsrc.Conf().ForceTotal {
52+
case resource.TotalOptIn:
53+
forceTotal = route.Params.Get("total") == "1"
54+
case resource.TotalAlways:
55+
forceTotal = true
56+
case resource.TotalDenied:
57+
if route.Params.Get("total") == "1" {
58+
return 422, nil, &Error{422, "Cannot use `total' parameter: denied by configuration", nil}
59+
}
60+
}
4861
lookup, e := route.Lookup()
4962
if e != nil {
5063
return e.Code, nil, e
5164
}
52-
list, err := rsrc.Find(ctx, lookup, offset, limit)
65+
var list *resource.ItemList
66+
var err error
67+
if forceTotal {
68+
list, err = rsrc.FindWithTotal(ctx, lookup, offset, limit)
69+
} else {
70+
list, err = rsrc.Find(ctx, lookup, offset, limit)
71+
}
5372
if err != nil {
5473
e = NewError(err)
5574
return e.Code, nil, e

0 commit comments

Comments
 (0)