diff --git a/glide.lock b/glide.lock index 915ccf5d09..ef8cf2cf46 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 0ed5871e7b062bd45449c789a58a5b8d2df91c275af984b344b109b232ca1bb3 -updated: 2016-09-24T21:21:01.057881898-04:00 +hash: 18323336ff6ef09b5aa4c35766dea76ad4986f955c3f0b2598772322a8f72f7f +updated: 2016-10-14T15:44:13.911342299-07:00 imports: - name: github.com/alecthomas/template version: a0175ee3bccc567396460bf5acd36800cb10c49c @@ -17,8 +17,6 @@ imports: - spew - name: github.com/dgrijalva/jwt-go version: 01aeca54ebda6e0fbfafd0a524d234159c05ec20 - subpackages: - - request - name: github.com/elazarl/go-bindata-assetfs version: 9a6736ed45b44bf3835afeebb3034b57ed329f3e - name: github.com/fsnotify/fsnotify @@ -97,6 +95,8 @@ imports: - pbutil - name: github.com/mitchellh/mapstructure version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1 +- name: github.com/patrickmn/sortutil + version: abeda66eb583fac2d8d98d3d2e6fbd5c67af7947 - name: github.com/pelletier/go-buffruneio version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d - name: github.com/pelletier/go-toml diff --git a/glide.yaml b/glide.yaml index 2549d461aa..361eddf3fa 100644 --- a/glide.yaml +++ b/glide.yaml @@ -83,3 +83,4 @@ import: version: ~0.8.0 subpackages: - prometheus +- package: github.com/patrickmn/sortutil diff --git a/server/datastore/gorm.go b/server/datastore/gorm.go index 1604fb6cdc..1fd35353b8 100644 --- a/server/datastore/gorm.go +++ b/server/datastore/gorm.go @@ -88,11 +88,23 @@ func openGORM(driver, conn string, maxAttempts int) (*gorm.DB, error) { // applyLimitOffset applies the appropriate limit and offset parameters to the // gorm.DB instance, returning a DB that can be chained as usual with *gorm.DB. func (orm *gormDB) applyListOptions(opt kolide.ListOptions) *gorm.DB { - if opt.PerPage == 0 { + db := orm.DB + if opt.PerPage != 0 { // PerPage value of 0 indicates unlimited - return orm.DB + offset := opt.Page * opt.PerPage + db = db.Limit(opt.PerPage).Offset(offset) } - offset := opt.Page * opt.PerPage - return orm.DB.Limit(opt.PerPage).Offset(offset) + if opt.OrderKey != "" { + var dir string + if opt.OrderDirection == kolide.OrderDescending { + dir = "DESC" + } else { + dir = "ASC" + } + + db = db.Order(opt.OrderKey + " " + dir) + } + + return db } diff --git a/server/datastore/inmem.go b/server/datastore/inmem.go index 2aefc685a8..891378f8d5 100644 --- a/server/datastore/inmem.go +++ b/server/datastore/inmem.go @@ -1,10 +1,12 @@ package datastore import ( + "errors" "reflect" "sync" "github.com/kolide/kolide-ose/server/kolide" + "github.com/patrickmn/sortutil" ) type inmem struct { @@ -70,6 +72,21 @@ func (orm *inmem) getLimitOffsetSliceBounds(opt kolide.ListOptions, length int) return offset, max } +func sortResults(slice interface{}, opt kolide.ListOptions, fields map[string]string) error { + field, ok := fields[opt.OrderKey] + if !ok { + return errors.New("cannot sort on unknown key: " + opt.OrderKey) + } + + if opt.OrderDirection == kolide.OrderDescending { + sortutil.DescByField(slice, field) + } else { + sortutil.AscByField(slice, field) + } + + return nil +} + // nextID returns the next ID value that should be used for a struct of the // given type func (orm *inmem) nextID(val interface{}) uint { diff --git a/server/datastore/inmem_hosts.go b/server/datastore/inmem_hosts.go index 5bac1a5c07..7f5948a8a1 100644 --- a/server/datastore/inmem_hosts.go +++ b/server/datastore/inmem_hosts.go @@ -76,6 +76,28 @@ func (orm *inmem) ListHosts(opt kolide.ListOptions) ([]*kolide.Host, error) { hosts = append(hosts, orm.hosts[uint(k)]) } + // Apply ordering + if opt.OrderKey != "" { + var fields = map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "detail_update_time": "DetailUpdateTime", + "hostname": "HostName", + "uuid": "UUID", + "platform": "Platform", + "osquery_version": "OsqueryVersion", + "os_version": "OSVersion", + "uptime": "Uptime", + "memory": "PhysicalMemory", + "mac": "PrimaryMAC", + "ip": "PrimaryIP", + } + if err := sortResults(hosts, opt, fields); err != nil { + return nil, err + } + } + // Apply limit/offset low, high := orm.getLimitOffsetSliceBounds(opt, len(hosts)) hosts = hosts[low:high] diff --git a/server/datastore/inmem_invites.go b/server/datastore/inmem_invites.go index 7a08eb0726..e3ee626613 100644 --- a/server/datastore/inmem_invites.go +++ b/server/datastore/inmem_invites.go @@ -17,7 +17,7 @@ func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) { } } - invite.ID = orm.nextID(invite) + invite.ID = uint(len(orm.invites) + 1) orm.invites[invite.ID] = invite return invite, nil } @@ -39,6 +39,23 @@ func (orm *inmem) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error) invites = append(invites, orm.invites[uint(k)]) } + // Apply ordering + if opt.OrderKey != "" { + var fields = map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "detail_update_time": "DetailUpdateTime", + "email": "Email", + "admin": "Admin", + "name": "Name", + "position": "Position", + } + if err := sortResults(invites, opt, fields); err != nil { + return nil, err + } + } + // Apply limit/offset low, high := orm.getLimitOffsetSliceBounds(opt, len(invites)) invites = invites[low:high] diff --git a/server/datastore/inmem_packs.go b/server/datastore/inmem_packs.go index 3949032ce6..ce2e08e2f0 100644 --- a/server/datastore/inmem_packs.go +++ b/server/datastore/inmem_packs.go @@ -76,6 +76,20 @@ func (orm *inmem) ListPacks(opt kolide.ListOptions) ([]*kolide.Pack, error) { packs = append(packs, orm.packs[uint(k)]) } + // Apply ordering + if opt.OrderKey != "" { + var fields = map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "name": "Name", + "platform": "Platform", + } + if err := sortResults(packs, opt, fields); err != nil { + return nil, err + } + } + // Apply limit/offset low, high := orm.getLimitOffsetSliceBounds(opt, len(packs)) packs = packs[low:high] diff --git a/server/datastore/inmem_queries.go b/server/datastore/inmem_queries.go index bad2766930..75b56ebd68 100644 --- a/server/datastore/inmem_queries.go +++ b/server/datastore/inmem_queries.go @@ -76,6 +76,25 @@ func (orm *inmem) ListQueries(opt kolide.ListOptions) ([]*kolide.Query, error) { queries = append(queries, orm.queries[uint(k)]) } + // Apply ordering + if opt.OrderKey != "" { + var fields = map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "name": "Name", + "query": "Query", + "interval": "Interval", + "snapshot": "Snapshot", + "differential": "Differential", + "platform": "Platform", + "version": "Version", + } + if err := sortResults(queries, opt, fields); err != nil { + return nil, err + } + } + // Apply limit/offset low, high := orm.getLimitOffsetSliceBounds(opt, len(queries)) queries = queries[low:high] diff --git a/server/datastore/inmem_users.go b/server/datastore/inmem_users.go index f62d1f0901..560609c5b4 100644 --- a/server/datastore/inmem_users.go +++ b/server/datastore/inmem_users.go @@ -51,6 +51,24 @@ func (orm *inmem) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) { users = append(users, orm.users[uint(k)]) } + // Apply ordering + if opt.OrderKey != "" { + var fields = map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "username": "Username", + "name": "Name", + "email": "Email", + "admin": "Admin", + "enabled": "Enabled", + "position": "Position", + } + if err := sortResults(users, opt, fields); err != nil { + return nil, err + } + } + // Apply limit/offset low, high := orm.getLimitOffsetSliceBounds(opt, len(users)) users = users[low:high] diff --git a/server/kolide/app.go b/server/kolide/app.go index b7f320ba53..00a447bdbb 100644 --- a/server/kolide/app.go +++ b/server/kolide/app.go @@ -33,6 +33,13 @@ type OrgInfoPayload struct { OrgLogoURL *string `json:"org_logo_url"` } +type OrderDirection int + +const ( + OrderAscending OrderDirection = iota + OrderDescending +) + // ListOptions defines options related to paging and ordering to be used when // listing objects type ListOptions struct { @@ -41,4 +48,8 @@ type ListOptions struct { // How many results per page (must be positive integer, 0 indicates // unlimited) PerPage uint + // Key to use for ordering + OrderKey string + // Direction of ordering + OrderDirection OrderDirection } diff --git a/server/service/transport.go b/server/service/transport.go index 0e0f239101..52d91aae86 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -62,6 +62,8 @@ func listOptionsFromRequest(r *http.Request) (kolide.ListOptions, error) { pageString := r.URL.Query().Get("page") perPageString := r.URL.Query().Get("per_page") + orderKey := r.URL.Query().Get("order_key") + orderDirectionString := r.URL.Query().Get("order_direction") var page int = 0 if pageString != "" { @@ -94,7 +96,31 @@ func listOptionsFromRequest(r *http.Request) (kolide.ListOptions, error) { perPage = defaultPerPage } - return kolide.ListOptions{Page: uint(page), PerPage: uint(perPage)}, nil + if orderKey == "" && orderDirectionString != "" { + return kolide.ListOptions{}, + errors.New("order_key must be specified with order_direction") + } + + var orderDirection kolide.OrderDirection + switch orderDirectionString { + case "desc": + orderDirection = kolide.OrderDescending + case "asc": + orderDirection = kolide.OrderAscending + case "": + orderDirection = kolide.OrderAscending + default: + return kolide.ListOptions{}, + errors.New("unknown order_direction: " + orderDirectionString) + + } + + return kolide.ListOptions{ + Page: uint(page), + PerPage: uint(perPage), + OrderKey: orderKey, + OrderDirection: orderDirection, + }, nil } func decodeNoParamsRequest(ctx context.Context, r *http.Request) (interface{}, error) { diff --git a/server/service/transport_test.go b/server/service/transport_test.go index 4c2ca5b0fc..9c7ec0d8b1 100644 --- a/server/service/transport_test.go +++ b/server/service/transport_test.go @@ -40,6 +40,33 @@ func TestListOptionsFromRequest(t *testing.T) { listOptions: kolide.ListOptions{}, }, + // Both order params provided + { + url: "/foo?order_key=foo&order_direction=desc", + listOptions: kolide.ListOptions{OrderKey: "foo", OrderDirection: kolide.OrderDescending}, + }, + // Both order params provided (asc) + { + url: "/foo?order_key=bar&order_direction=asc", + listOptions: kolide.ListOptions{OrderKey: "bar", OrderDirection: kolide.OrderAscending}, + }, + // Default order direction + { + url: "/foo?order_key=foo", + listOptions: kolide.ListOptions{OrderKey: "foo", OrderDirection: kolide.OrderAscending}, + }, + + // All params defined + { + url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100", + listOptions: kolide.ListOptions{ + OrderKey: "foo", + OrderDirection: kolide.OrderDescending, + Page: 1, + PerPage: 100, + }, + }, + // various error cases { url: "/foo?page=foo&per_page=10", @@ -57,6 +84,14 @@ func TestListOptionsFromRequest(t *testing.T) { url: "/foo?page=-1&per_page=-10", shouldErr: true, }, + { + url: "/foo?page=1&order_direction=desc", + shouldErr: true, + }, + { + url: "/foo?&order_direction=foo&order_key=", + shouldErr: true, + }, } for _, tt := range listOptionsTests {