1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-27 13:48:49 +08:00

MF-1357 - Add new endpoint for searching things (#1383)

* Add new enpoint for thing search

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Rename endpoint to /search
Use same request as list endpoint

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add optional parameters in body (offset, limit)
Add swagger file

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* move all parameters into body

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix swagger

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix error description

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* Add tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove dead code
fix tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* remove unused var

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* fix sdk tests

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* add url endpoint for search test

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* description in swagger
fix tracer string
change test offset

Signed-off-by: Ivan Milosevic <iva@blokovi.com>

* rename in tests searchThReq to searchThingReq

Signed-off-by: Ivan Milosevic <iva@blokovi.com>
This commit is contained in:
Ivan Milošević 2021-03-11 10:28:44 +01:00 committed by GitHub
parent a1e18a770a
commit 30ba38c919
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 386 additions and 76 deletions

View File

@ -6,7 +6,6 @@ package sdk_test
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -16,7 +15,7 @@ import (
) )
var ( var (
channel = sdk.Channel{ID: "1", Name: "test"} channel = sdk.Channel{ID: "001", Name: "test"}
emptyChannel = sdk.Channel{} emptyChannel = sdk.Channel{}
) )
@ -99,8 +98,8 @@ func TestCreateChannels(t *testing.T) {
mainfluxSDK := sdk.NewSDK(sdkConf) mainfluxSDK := sdk.NewSDK(sdkConf)
channels := []sdk.Channel{ channels := []sdk.Channel{
sdk.Channel{ID: "1", Name: "1"}, sdk.Channel{ID: "001", Name: "1"},
sdk.Channel{ID: "2", Name: "2"}, sdk.Channel{ID: "002", Name: "2"},
} }
cases := []struct { cases := []struct {
@ -221,7 +220,7 @@ func TestChannels(t *testing.T) {
var channels []sdk.Channel var channels []sdk.Channel
mainfluxSDK := sdk.NewSDK(sdkConf) mainfluxSDK := sdk.NewSDK(sdkConf)
for i := 1; i < 101; i++ { for i := 1; i < 101; i++ {
ch := sdk.Channel{ID: strconv.Itoa(i), Name: "test"} ch := sdk.Channel{ID: fmt.Sprintf("%03d", i), Name: "test"}
mainfluxSDK.CreateChannel(ch, token) mainfluxSDK.CreateChannel(ch, token)
channels = append(channels, ch) channels = append(channels, ch)
} }
@ -260,12 +259,12 @@ func TestChannels(t *testing.T) {
response: nil, response: nil,
}, },
{ {
desc: "get a list of channels with zero limit", desc: "get a list of channels without limit, default 10",
token: token, token: token,
offset: 0, offset: 0,
limit: 0, limit: 0,
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest), err: nil,
response: nil, response: channels[0:10],
}, },
{ {
desc: "get a list of channels with limit greater than max", desc: "get a list of channels with limit greater than max",
@ -283,14 +282,6 @@ func TestChannels(t *testing.T) {
err: nil, err: nil,
response: []sdk.Channel{}, response: []sdk.Channel{},
}, },
{
desc: "get a list of channels with invalid args (zero limit) and invalid token",
token: wrongValue,
offset: 0,
limit: 0,
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest),
response: nil,
},
} }
for _, tc := range cases { for _, tc := range cases {
page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit, tc.name) page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit, tc.name)
@ -323,7 +314,7 @@ func TestChannelsByThing(t *testing.T) {
var channels []sdk.Channel var channels []sdk.Channel
for i := 1; i < n+1; i++ { for i := 1; i < n+1; i++ {
ch := sdk.Channel{ ch := sdk.Channel{
ID: strconv.Itoa(i), ID: fmt.Sprintf("%03d", i),
Name: "test", Name: "test",
} }
cid, err := mainfluxSDK.CreateChannel(ch, token) cid, err := mainfluxSDK.CreateChannel(ch, token)

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
sdk "github.com/mainflux/mainflux/pkg/sdk/go" sdk "github.com/mainflux/mainflux/pkg/sdk/go"
@ -36,7 +35,7 @@ const (
var ( var (
metadata = map[string]interface{}{"meta": "data"} metadata = map[string]interface{}{"meta": "data"}
metadata2 = map[string]interface{}{"meta": "data2"} metadata2 = map[string]interface{}{"meta": "data2"}
thing = sdk.Thing{ID: "1", Name: "test_device", Metadata: metadata} thing = sdk.Thing{ID: "001", Name: "test_device", Metadata: metadata}
emptyThing = sdk.Thing{} emptyThing = sdk.Thing{}
) )
@ -86,14 +85,14 @@ func TestCreateThing(t *testing.T) {
thing: thing, thing: thing,
token: token, token: token,
err: nil, err: nil,
location: "1", location: "001",
}, },
{ {
desc: "create new empty thing", desc: "create new empty thing",
thing: emptyThing, thing: emptyThing,
token: token, token: token,
err: nil, err: nil,
location: "2", location: "002",
}, },
{ {
desc: "create new thing with empty token", desc: "create new thing with empty token",
@ -136,8 +135,8 @@ func TestCreateThings(t *testing.T) {
mainfluxSDK := sdk.NewSDK(sdkConf) mainfluxSDK := sdk.NewSDK(sdkConf)
things := []sdk.Thing{ things := []sdk.Thing{
sdk.Thing{ID: "1", Name: "1", Key: "1"}, sdk.Thing{ID: "001", Name: "1", Key: "1"},
sdk.Thing{ID: "2", Name: "2", Key: "2"}, sdk.Thing{ID: "002", Name: "2", Key: "2"},
} }
cases := []struct { cases := []struct {
@ -262,7 +261,7 @@ func TestThings(t *testing.T) {
mainfluxSDK := sdk.NewSDK(sdkConf) mainfluxSDK := sdk.NewSDK(sdkConf)
for i := 1; i < 101; i++ { for i := 1; i < 101; i++ {
th := sdk.Thing{ID: strconv.Itoa(i), Name: "test_device", Metadata: metadata} th := sdk.Thing{ID: fmt.Sprintf("%03d", i), Name: "test_device", Metadata: metadata}
mainfluxSDK.CreateThing(th, token) mainfluxSDK.CreateThing(th, token)
th.Key = fmt.Sprintf("%s%012d", keyPrefix, 2*i) th.Key = fmt.Sprintf("%s%012d", keyPrefix, 2*i)
things = append(things, th) things = append(things, th)
@ -306,8 +305,8 @@ func TestThings(t *testing.T) {
token: token, token: token,
offset: 0, offset: 0,
limit: 0, limit: 0,
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest), err: nil,
response: nil, response: things[0:10],
}, },
{ {
desc: "get a list of things with limit greater than max", desc: "get a list of things with limit greater than max",
@ -325,14 +324,6 @@ func TestThings(t *testing.T) {
err: nil, err: nil,
response: []sdk.Thing{}, response: []sdk.Thing{},
}, },
{
desc: "get a list of things with invalid args (zero limit) and invalid token",
token: wrongValue,
offset: 0,
limit: 0,
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest),
response: nil,
},
} }
for _, tc := range cases { for _, tc := range cases {
page, err := mainfluxSDK.Things(tc.token, tc.offset, tc.limit, tc.name) page, err := mainfluxSDK.Things(tc.token, tc.offset, tc.limit, tc.name)
@ -366,7 +357,7 @@ func TestThingsByChannel(t *testing.T) {
var things []sdk.Thing var things []sdk.Thing
for i := 1; i < n+1; i++ { for i := 1; i < n+1; i++ {
th := sdk.Thing{ th := sdk.Thing{
ID: strconv.Itoa(i), ID: fmt.Sprintf("%03d", i),
Name: "test_device", Name: "test_device",
Metadata: metadata, Metadata: metadata,
Key: fmt.Sprintf("%s%012d", keyPrefix, 2*i+1), Key: fmt.Sprintf("%s%012d", keyPrefix, 2*i+1),

View File

@ -46,9 +46,13 @@ var (
Name: "test", Name: "test",
Metadata: map[string]interface{}{"test": "data"}, Metadata: map[string]interface{}{"test": "data"},
} }
invalidName = strings.Repeat("m", maxNameSize+1) invalidName = strings.Repeat("m", maxNameSize+1)
notFoundRes = toJSON(errorRes{things.ErrNotFound.Error()}) notFoundRes = toJSON(errorRes{things.ErrNotFound.Error()})
unauthRes = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()}) unauthRes = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()})
searchThingReq = things.PageMetadata{
Limit: 5,
Offset: 0,
}
) )
type testRequest struct { type testRequest struct {
@ -122,7 +126,7 @@ func TestCreateThing(t *testing.T) {
contentType: contentType, contentType: contentType,
auth: token, auth: token,
status: http.StatusCreated, status: http.StatusCreated,
location: "/things/1", location: "/things/001",
}, },
{ {
desc: "add thing with existing key", desc: "add thing with existing key",
@ -138,7 +142,7 @@ func TestCreateThing(t *testing.T) {
contentType: contentType, contentType: contentType,
auth: token, auth: token,
status: http.StatusCreated, status: http.StatusCreated,
location: "/things/2", location: "/things/002",
}, },
{ {
desc: "add thing with invalid auth token", desc: "add thing with invalid auth token",
@ -723,11 +727,11 @@ func TestListThings(t *testing.T) {
res: nil, res: nil,
}, },
{ {
desc: "get a list of things with zero limit", desc: "get a list of things with zero limit and offset 1",
auth: token, auth: token,
status: http.StatusBadRequest, status: http.StatusOK,
url: fmt.Sprintf("%s?offset=%d&limit=%d", thingURL, 1, 0), url: fmt.Sprintf("%s?offset=%d&limit=%d", thingURL, 1, 0),
res: nil, res: data[1:11],
}, },
{ {
desc: "get a list of things without offset", desc: "get a list of things without offset",
@ -838,6 +842,201 @@ func TestListThings(t *testing.T) {
} }
} }
func TestSearchThings(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
th := searchThingReq
validData := toJSON(th)
th.Dir = "desc"
th.Order = "name"
descData := toJSON(th)
th.Dir = "asc"
ascData := toJSON(th)
th.Order = "wrong"
invalidOrderData := toJSON(th)
th = searchThingReq
th.Dir = "wrong"
invalidDirData := toJSON(th)
th = searchThingReq
th.Limit = 110
limitMaxData := toJSON(th)
th.Limit = 0
zeroLimitData := toJSON(th)
th = searchThingReq
th.Name = invalidName
invalidNameData := toJSON(th)
th.Name = invalidName
invalidData := toJSON(th)
data := []thingRes{}
for i := 0; i < 100; i++ {
name := "name_" + fmt.Sprintf("%03d", i+1)
ths, err := svc.CreateThings(context.Background(), token, things.Thing{Name: name, Metadata: map[string]interface{}{"test": name}})
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
th := ths[0]
data = append(data, thingRes{
ID: th.ID,
Name: th.Name,
Key: th.Key,
Metadata: th.Metadata,
})
}
cases := []struct {
desc string
auth string
status int
req string
res []thingRes
}{
{
desc: "search things",
auth: token,
status: http.StatusOK,
req: validData,
res: data[0:5],
},
{
desc: "search things ordered by name descendent",
auth: token,
status: http.StatusOK,
req: descData,
res: data[0:5],
},
{
desc: "search things ordered by name ascendent",
auth: token,
status: http.StatusOK,
req: ascData,
res: data[0:5],
},
{
desc: "search things with invalid order",
auth: token,
status: http.StatusBadRequest,
req: invalidOrderData,
res: nil,
},
{
desc: "search things with invalid dir",
auth: token,
status: http.StatusBadRequest,
req: invalidDirData,
res: nil,
},
{
desc: "search things with invalid token",
auth: wrongValue,
status: http.StatusUnauthorized,
req: validData,
res: nil,
},
{
desc: "search things with invalid data",
auth: token,
status: http.StatusBadRequest,
req: invalidData,
res: nil,
},
{
desc: "search things with empty token",
auth: "",
status: http.StatusUnauthorized,
req: validData,
res: nil,
},
{
desc: "search things with zero limit",
auth: token,
status: http.StatusOK,
req: zeroLimitData,
res: data[0:10],
},
{
desc: "search things without offset",
auth: token,
status: http.StatusOK,
req: validData,
res: data[0:5],
},
{
desc: "search things with limit greater than max",
auth: token,
status: http.StatusBadRequest,
req: limitMaxData,
res: nil,
},
{
desc: "search things with default URL",
auth: token,
status: http.StatusOK,
req: validData,
res: data[0:5],
},
{
desc: "search things filtering with invalid name",
auth: token,
status: http.StatusBadRequest,
req: invalidNameData,
res: nil,
},
{
desc: "search things sorted by name ascendent",
auth: token,
status: http.StatusOK,
req: validData,
res: data[0:5],
},
{
desc: "search things sorted by name descendent",
auth: token,
status: http.StatusOK,
req: validData,
res: data[0:5],
},
{
desc: "search things sorted with invalid order",
auth: token,
status: http.StatusBadRequest,
req: invalidOrderData,
res: nil,
},
{
desc: "search things sorted by name with invalid direction",
auth: token,
status: http.StatusBadRequest,
req: invalidDirData,
res: nil,
},
}
for _, tc := range cases {
req := testRequest{
client: ts.Client(),
method: http.MethodPost,
url: fmt.Sprintf("%s/things/search", ts.URL),
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var data thingsPageRes
json.NewDecoder(res.Body).Decode(&data)
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.ElementsMatch(t, tc.res, data.Things, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res, data.Things))
}
}
func TestListThingsByChannel(t *testing.T) { func TestListThingsByChannel(t *testing.T) {
svc := newService(map[string]string{token: email}) svc := newService(map[string]string{token: email})
ts := newServer(svc) ts := newServer(svc)
@ -1097,7 +1296,7 @@ func TestCreateChannel(t *testing.T) {
contentType: contentType, contentType: contentType,
auth: token, auth: token,
status: http.StatusCreated, status: http.StatusCreated,
location: "/channels/1", location: "/channels/001",
}, },
{ {
desc: "create new channel with invalid token", desc: "create new channel with invalid token",
@ -1129,7 +1328,7 @@ func TestCreateChannel(t *testing.T) {
contentType: contentType, contentType: contentType,
auth: token, auth: token,
status: http.StatusCreated, status: http.StatusCreated,
location: "/channels/2", location: "/channels/002",
}, },
{ {
desc: "create new channel with empty request", desc: "create new channel with empty request",
@ -1482,7 +1681,12 @@ func TestListChannels(t *testing.T) {
channels := []channelRes{} channels := []channelRes{}
for i := 0; i < 101; i++ { for i := 0; i < 101; i++ {
chs, err := svc.CreateChannels(context.Background(), token, channel) name := "name_" + fmt.Sprintf("%03d", i+1)
chs, err := svc.CreateChannels(context.Background(), token,
things.Channel{
Name: name,
Metadata: map[string]interface{}{"test": "data"},
})
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
ch := chs[0] ch := chs[0]
ths, err := svc.CreateThings(context.Background(), token, thing) ths, err := svc.CreateThings(context.Background(), token, thing)
@ -1513,17 +1717,17 @@ func TestListChannels(t *testing.T) {
res: channels[0:6], res: channels[0:6],
}, },
{ {
desc: "get a list of channels ordered by name descendent", desc: "get a list of channels ordered by id descendent",
auth: token, auth: token,
status: http.StatusOK, status: http.StatusOK,
url: fmt.Sprintf("%s?offset=%d&limit=%d&order=name&dir=desc", channelURL, 0, 6), url: fmt.Sprintf("%s?offset=%d&limit=%d&order=id&dir=desc", channelURL, 0, 6),
res: channels[0:6], res: channels[len(channels)-6:],
}, },
{ {
desc: "get a list of channels ordered by name ascendent", desc: "get a list of channels ordered by id ascendent",
auth: token, auth: token,
status: http.StatusOK, status: http.StatusOK,
url: fmt.Sprintf("%s?offset=%d&limit=%d&order=name&dir=asc", channelURL, 0, 6), url: fmt.Sprintf("%s?offset=%d&limit=%d&order=id&dir=asc", channelURL, 0, 6),
res: channels[0:6], res: channels[0:6],
}, },
{ {
@ -1569,11 +1773,11 @@ func TestListChannels(t *testing.T) {
res: nil, res: nil,
}, },
{ {
desc: "get a list of channels with zero limit", desc: "get a list of channels with zero limit and offset 1",
auth: token, auth: token,
status: http.StatusBadRequest, status: http.StatusOK,
url: fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), url: fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0),
res: nil, res: channels[1:11],
}, },
{ {
desc: "get a list of channels with no offset provided", desc: "get a list of channels with no offset provided",
@ -1650,7 +1854,7 @@ func TestListChannels(t *testing.T) {
auth: token, auth: token,
status: http.StatusOK, status: http.StatusOK,
url: fmt.Sprintf("%s?offset=%d&limit=%d&order=%s&dir=%s", channelURL, 0, 6, nameKey, descKey), url: fmt.Sprintf("%s?offset=%d&limit=%d&order=%s&dir=%s", channelURL, 0, 6, nameKey, descKey),
res: channels[0:6], res: channels[len(channels)-6:],
}, },
{ {
desc: "get a list of channels sorted with invalid order", desc: "get a list of channels sorted with invalid order",

View File

@ -185,7 +185,11 @@ func (req *listResourcesReq) validate() error {
return things.ErrUnauthorizedAccess return things.ErrUnauthorizedAccess
} }
if req.pageMetadata.Limit == 0 || req.pageMetadata.Limit > maxLimitSize { if req.pageMetadata.Limit == 0 {
req.pageMetadata.Limit = defLimit
}
if req.pageMetadata.Limit > maxLimitSize {
return things.ErrMalformedEntity return things.ErrMalformedEntity
} }

View File

@ -105,6 +105,13 @@ func MakeHandler(tracer opentracing.Tracer, svc things.Service) http.Handler {
opts..., opts...,
)) ))
r.Post("/things/search", kithttp.NewServer(
kitot.TraceServer(tracer, "search_things")(listThingsEndpoint(svc)),
decodeListByMetadata,
encodeResponse,
opts...,
))
r.Post("/channels", kithttp.NewServer( r.Post("/channels", kithttp.NewServer(
kitot.TraceServer(tracer, "create_channel")(createChannelEndpoint(svc)), kitot.TraceServer(tracer, "create_channel")(createChannelEndpoint(svc)),
decodeChannelCreation, decodeChannelCreation,
@ -344,6 +351,15 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
return req, nil return req, nil
} }
func decodeListByMetadata(_ context.Context, r *http.Request) (interface{}, error) {
req := listResourcesReq{token: r.Header.Get("Authorization")}
if err := json.NewDecoder(r.Body).Decode(&req.pageMetadata); err != nil {
return nil, errors.Wrap(things.ErrMalformedEntity, err)
}
return req, nil
}
func decodeListByConnection(_ context.Context, r *http.Request) (interface{}, error) { func decodeListByConnection(_ context.Context, r *http.Request) (interface{}, error) {
o, err := readUintQuery(r, offsetKey, defOffset) o, err := readUintQuery(r, offsetKey, defOffset)
if err != nil { if err != nil {

View File

@ -48,7 +48,7 @@ func (crm *channelRepositoryMock) Save(_ context.Context, channels ...things.Cha
for i := range channels { for i := range channels {
crm.counter++ crm.counter++
channels[i].ID = strconv.FormatUint(crm.counter, 10) channels[i].ID = fmt.Sprintf("%03d", crm.counter)
crm.channels[key(channels[i].Owner, channels[i].ID)] = channels[i] crm.channels[key(channels[i].Owner, channels[i].ID)] = channels[i]
} }
@ -78,12 +78,15 @@ func (crm *channelRepositoryMock) RetrieveByID(_ context.Context, owner, id stri
} }
func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, pm things.PageMetadata) (things.ChannelsPage, error) { func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, pm things.PageMetadata) (things.ChannelsPage, error) {
if pm.Limit <= 0 { if pm.Limit < 0 {
return things.ChannelsPage{}, nil return things.ChannelsPage{}, nil
} }
if pm.Limit == 0 {
pm.Limit = 10
}
first := uint64(pm.Offset) + 1 first := int(pm.Offset)
last := first + uint64(pm.Limit) last := first + int(pm.Limit)
var chs []things.Channel var chs []things.Channel
@ -91,8 +94,7 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
// itself (see mocks/commons.go). // itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner) prefix := fmt.Sprintf("%s-", owner)
for k, v := range crm.channels { for k, v := range crm.channels {
id, _ := strconv.ParseUint(v.ID, 10, 64) if strings.HasPrefix(k, prefix) {
if strings.HasPrefix(k, prefix) && id >= first && id < last {
chs = append(chs, v) chs = append(chs, v)
} }
} }
@ -100,8 +102,16 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
// Sort Channels list // Sort Channels list
chs = sortChannels(pm, chs) chs = sortChannels(pm, chs)
if last > len(chs) {
last = len(chs)
}
if first > last {
return things.ChannelsPage{}, nil
}
page := things.ChannelsPage{ page := things.ChannelsPage{
Channels: chs, Channels: chs[first:last],
PageMetadata: things.PageMetadata{ PageMetadata: things.PageMetadata{
Total: crm.counter, Total: crm.counter,
Offset: pm.Offset, Offset: pm.Offset,

View File

@ -31,6 +31,17 @@ func sortThings(pm things.PageMetadata, ths []things.Thing) []things.Thing {
return ths[i].Name > ths[j].Name return ths[i].Name > ths[j].Name
}) })
} }
case "id":
if pm.Dir == "asc" {
sort.SliceStable(ths, func(i, j int) bool {
return ths[i].ID < ths[j].ID
})
}
if pm.Dir == "desc" {
sort.SliceStable(ths, func(i, j int) bool {
return ths[i].ID > ths[j].ID
})
}
default: default:
sort.SliceStable(ths, func(i, j int) bool { sort.SliceStable(ths, func(i, j int) bool {
return ths[i].ID < ths[j].ID return ths[i].ID < ths[j].ID
@ -53,6 +64,17 @@ func sortChannels(pm things.PageMetadata, chs []things.Channel) []things.Channel
return chs[i].Name > chs[j].Name return chs[i].Name > chs[j].Name
}) })
} }
case "id":
if pm.Dir == "asc" {
sort.SliceStable(chs, func(i, j int) bool {
return chs[i].ID < chs[j].ID
})
}
if pm.Dir == "desc" {
sort.SliceStable(chs, func(i, j int) bool {
return chs[i].ID > chs[j].ID
})
}
default: default:
sort.SliceStable(chs, func(i, j int) bool { sort.SliceStable(chs, func(i, j int) bool {
return chs[i].ID < chs[j].ID return chs[i].ID < chs[j].ID

View File

@ -55,7 +55,7 @@ func (trm *thingRepositoryMock) Save(_ context.Context, ths ...things.Thing) ([]
} }
trm.counter++ trm.counter++
ths[i].ID = strconv.FormatUint(trm.counter, 10) ths[i].ID = fmt.Sprintf("%03d", trm.counter)
trm.things[key(ths[i].Owner, ths[i].ID)] = ths[i] trm.things[key(ths[i].Owner, ths[i].ID)] = ths[i]
} }
@ -115,7 +115,7 @@ func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, pm
trm.mu.Lock() trm.mu.Lock()
defer trm.mu.Unlock() defer trm.mu.Unlock()
if pm.Limit <= 0 { if pm.Limit < 0 {
return things.Page{}, nil return things.Page{}, nil
} }

View File

@ -62,6 +62,34 @@ paths:
description: Database can't process request. description: Database can't process request.
'500': '500':
$ref: "#/components/responses/ServiceError" $ref: "#/components/responses/ServiceError"
/things/search:
post:
summary: Search and retrieves things
description: |
Retrieves a list of things with name and metadata filtering.
Due to performance concerns, data is retrieved in subsets.
The API things must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- things
parameters:
- $ref: "#/components/parameters/Authorization"
requestBody:
$ref: "#/components/requestBodies/ThingsSearchReq"
responses:
'200':
$ref: "#/components/responses/ThingsPageRes"
'400':
description: Failed due to malformed query parameters.
'401':
description: Missing or invalid access token provided.
'404':
description: A non-existent entity request.
'422':
description: Unprocessable Entity
'500':
$ref: "#/components/responses/ServiceError"
/things/bulk: /things/bulk:
post: post:
summary: Bulk provisions new things summary: Bulk provisions new things
@ -569,6 +597,43 @@ components:
metadata: metadata:
type: object type: object
description: Arbitrary, object-encoded thing's data. description: Arbitrary, object-encoded thing's data.
ThingsReqSchema:
type: object
properties:
name:
type: string
description: Name filter. Filtering is performed as a case-insensitive partial match.
metadata:
type: object
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
total:
type: integer
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
default: 0
minimum: 0
limit:
type: integer
description: Size of the subset to retrieve.
default: 10
maximum: 100
minimum: 1
order:
type: string
description: Order type.
default: id
enum:
- name
- id
dir:
type: string
description: Order direction.
default: desc
enum:
- asc
- desc
ThingResSchema: ThingResSchema:
type: object type: object
properties: properties:
@ -800,6 +865,13 @@ components:
description: Free-form thing name. description: Free-form thing name.
metadata: metadata:
type: object type: object
ThingsSearchReq:
description: JSON-formatted document describing search parameters.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ThingsReqSchema"
KeyUpdateReq: KeyUpdateReq:
required: true required: true
description: JSON containing thing. description: JSON containing thing.

View File

@ -72,7 +72,7 @@ func TestCreateThings(t *testing.T) {
key: token, key: token,
err: nil, err: nil,
event: map[string]interface{}{ event: map[string]interface{}{
"id": "1", "id": "001",
"name": "a", "name": "a",
"owner": email, "owner": email,
"metadata": "{\"test\":\"test\"}", "metadata": "{\"test\":\"test\"}",
@ -298,7 +298,7 @@ func TestCreateChannels(t *testing.T) {
key: token, key: token,
err: nil, err: nil,
event: map[string]interface{}{ event: map[string]interface{}{
"id": "1", "id": "001",
"name": "a", "name": "a",
"metadata": "{\"test\":\"test\"}", "metadata": "{\"test\":\"test\"}",
"owner": email, "owner": email,

View File

@ -126,13 +126,13 @@ type Service interface {
// PageMetadata contains page metadata that helps navigation. // PageMetadata contains page metadata that helps navigation.
type PageMetadata struct { type PageMetadata struct {
Total uint64 Total uint64
Offset uint64 Offset uint64 `json:"offset,omitempty"`
Limit uint64 Limit uint64 `json:"limit,omitempty"`
Name string Name string `json:"name,omitempty"`
Order string Order string `json:"order,omitempty"`
Dir string Dir string `json:"dir,omitempty"`
Metadata map[string]interface{} Metadata map[string]interface{} `json:"metadata,omitempty"`
Connected bool // Used for connected or disconnected lists Connected bool // Used for connected or disconnected lists
} }
var _ Service = (*thingsService)(nil) var _ Service = (*thingsService)(nil)

View File

@ -711,13 +711,13 @@ func TestListChannels(t *testing.T) {
size: 0, size: 0,
err: nil, err: nil,
}, },
"list with zero limit": { "list with zero limit and offset 1": {
token: token, token: token,
pageMetadata: things.PageMetadata{ pageMetadata: things.PageMetadata{
Offset: 1, Offset: 1,
Limit: 0, Limit: 0,
}, },
size: 0, size: n - 1,
err: nil, err: nil,
}, },
"list with wrong credentials": { "list with wrong credentials": {