mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-26 13:48:53 +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:
parent
a1e18a770a
commit
30ba38c919
@ -6,7 +6,6 @@ package sdk_test
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -16,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
channel = sdk.Channel{ID: "1", Name: "test"}
|
||||
channel = sdk.Channel{ID: "001", Name: "test"}
|
||||
emptyChannel = sdk.Channel{}
|
||||
)
|
||||
|
||||
@ -99,8 +98,8 @@ func TestCreateChannels(t *testing.T) {
|
||||
mainfluxSDK := sdk.NewSDK(sdkConf)
|
||||
|
||||
channels := []sdk.Channel{
|
||||
sdk.Channel{ID: "1", Name: "1"},
|
||||
sdk.Channel{ID: "2", Name: "2"},
|
||||
sdk.Channel{ID: "001", Name: "1"},
|
||||
sdk.Channel{ID: "002", Name: "2"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
@ -221,7 +220,7 @@ func TestChannels(t *testing.T) {
|
||||
var channels []sdk.Channel
|
||||
mainfluxSDK := sdk.NewSDK(sdkConf)
|
||||
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)
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
@ -260,12 +259,12 @@ func TestChannels(t *testing.T) {
|
||||
response: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of channels with zero limit",
|
||||
desc: "get a list of channels without limit, default 10",
|
||||
token: token,
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest),
|
||||
response: nil,
|
||||
err: nil,
|
||||
response: channels[0:10],
|
||||
},
|
||||
{
|
||||
desc: "get a list of channels with limit greater than max",
|
||||
@ -283,14 +282,6 @@ func TestChannels(t *testing.T) {
|
||||
err: nil,
|
||||
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 {
|
||||
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
|
||||
for i := 1; i < n+1; i++ {
|
||||
ch := sdk.Channel{
|
||||
ID: strconv.Itoa(i),
|
||||
ID: fmt.Sprintf("%03d", i),
|
||||
Name: "test",
|
||||
}
|
||||
cid, err := mainfluxSDK.CreateChannel(ch, token)
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
@ -36,7 +35,7 @@ const (
|
||||
var (
|
||||
metadata = map[string]interface{}{"meta": "data"}
|
||||
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{}
|
||||
)
|
||||
|
||||
@ -86,14 +85,14 @@ func TestCreateThing(t *testing.T) {
|
||||
thing: thing,
|
||||
token: token,
|
||||
err: nil,
|
||||
location: "1",
|
||||
location: "001",
|
||||
},
|
||||
{
|
||||
desc: "create new empty thing",
|
||||
thing: emptyThing,
|
||||
token: token,
|
||||
err: nil,
|
||||
location: "2",
|
||||
location: "002",
|
||||
},
|
||||
{
|
||||
desc: "create new thing with empty token",
|
||||
@ -136,8 +135,8 @@ func TestCreateThings(t *testing.T) {
|
||||
mainfluxSDK := sdk.NewSDK(sdkConf)
|
||||
|
||||
things := []sdk.Thing{
|
||||
sdk.Thing{ID: "1", Name: "1", Key: "1"},
|
||||
sdk.Thing{ID: "2", Name: "2", Key: "2"},
|
||||
sdk.Thing{ID: "001", Name: "1", Key: "1"},
|
||||
sdk.Thing{ID: "002", Name: "2", Key: "2"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
@ -262,7 +261,7 @@ func TestThings(t *testing.T) {
|
||||
mainfluxSDK := sdk.NewSDK(sdkConf)
|
||||
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)
|
||||
th.Key = fmt.Sprintf("%s%012d", keyPrefix, 2*i)
|
||||
things = append(things, th)
|
||||
@ -306,8 +305,8 @@ func TestThings(t *testing.T) {
|
||||
token: token,
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: createError(sdk.ErrFailedFetch, http.StatusBadRequest),
|
||||
response: nil,
|
||||
err: nil,
|
||||
response: things[0:10],
|
||||
},
|
||||
{
|
||||
desc: "get a list of things with limit greater than max",
|
||||
@ -325,14 +324,6 @@ func TestThings(t *testing.T) {
|
||||
err: nil,
|
||||
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 {
|
||||
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
|
||||
for i := 1; i < n+1; i++ {
|
||||
th := sdk.Thing{
|
||||
ID: strconv.Itoa(i),
|
||||
ID: fmt.Sprintf("%03d", i),
|
||||
Name: "test_device",
|
||||
Metadata: metadata,
|
||||
Key: fmt.Sprintf("%s%012d", keyPrefix, 2*i+1),
|
||||
|
@ -46,9 +46,13 @@ var (
|
||||
Name: "test",
|
||||
Metadata: map[string]interface{}{"test": "data"},
|
||||
}
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
notFoundRes = toJSON(errorRes{things.ErrNotFound.Error()})
|
||||
unauthRes = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()})
|
||||
invalidName = strings.Repeat("m", maxNameSize+1)
|
||||
notFoundRes = toJSON(errorRes{things.ErrNotFound.Error()})
|
||||
unauthRes = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()})
|
||||
searchThingReq = things.PageMetadata{
|
||||
Limit: 5,
|
||||
Offset: 0,
|
||||
}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@ -122,7 +126,7 @@ func TestCreateThing(t *testing.T) {
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusCreated,
|
||||
location: "/things/1",
|
||||
location: "/things/001",
|
||||
},
|
||||
{
|
||||
desc: "add thing with existing key",
|
||||
@ -138,7 +142,7 @@ func TestCreateThing(t *testing.T) {
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusCreated,
|
||||
location: "/things/2",
|
||||
location: "/things/002",
|
||||
},
|
||||
{
|
||||
desc: "add thing with invalid auth token",
|
||||
@ -723,11 +727,11 @@ func TestListThings(t *testing.T) {
|
||||
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,
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusOK,
|
||||
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",
|
||||
@ -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) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
@ -1097,7 +1296,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusCreated,
|
||||
location: "/channels/1",
|
||||
location: "/channels/001",
|
||||
},
|
||||
{
|
||||
desc: "create new channel with invalid token",
|
||||
@ -1129,7 +1328,7 @@ func TestCreateChannel(t *testing.T) {
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusCreated,
|
||||
location: "/channels/2",
|
||||
location: "/channels/002",
|
||||
},
|
||||
{
|
||||
desc: "create new channel with empty request",
|
||||
@ -1482,7 +1681,12 @@ func TestListChannels(t *testing.T) {
|
||||
|
||||
channels := []channelRes{}
|
||||
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))
|
||||
ch := chs[0]
|
||||
ths, err := svc.CreateThings(context.Background(), token, thing)
|
||||
@ -1513,17 +1717,17 @@ func TestListChannels(t *testing.T) {
|
||||
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,
|
||||
status: http.StatusOK,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&order=name&dir=desc", channelURL, 0, 6),
|
||||
res: channels[0:6],
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&order=id&dir=desc", channelURL, 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,
|
||||
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],
|
||||
},
|
||||
{
|
||||
@ -1569,11 +1773,11 @@ func TestListChannels(t *testing.T) {
|
||||
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,
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusOK,
|
||||
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",
|
||||
@ -1650,7 +1854,7 @@ func TestListChannels(t *testing.T) {
|
||||
auth: token,
|
||||
status: http.StatusOK,
|
||||
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",
|
||||
|
@ -185,7 +185,11 @@ func (req *listResourcesReq) validate() error {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -105,6 +105,13 @@ func MakeHandler(tracer opentracing.Tracer, svc things.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/things/search", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "search_things")(listThingsEndpoint(svc)),
|
||||
decodeListByMetadata,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/channels", kithttp.NewServer(
|
||||
kitot.TraceServer(tracer, "create_channel")(createChannelEndpoint(svc)),
|
||||
decodeChannelCreation,
|
||||
@ -344,6 +351,15 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
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) {
|
||||
o, err := readUintQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
|
@ -48,7 +48,7 @@ func (crm *channelRepositoryMock) Save(_ context.Context, channels ...things.Cha
|
||||
|
||||
for i := range channels {
|
||||
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]
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
if pm.Limit <= 0 {
|
||||
if pm.Limit < 0 {
|
||||
return things.ChannelsPage{}, nil
|
||||
}
|
||||
if pm.Limit == 0 {
|
||||
pm.Limit = 10
|
||||
}
|
||||
|
||||
first := uint64(pm.Offset) + 1
|
||||
last := first + uint64(pm.Limit)
|
||||
first := int(pm.Offset)
|
||||
last := first + int(pm.Limit)
|
||||
|
||||
var chs []things.Channel
|
||||
|
||||
@ -91,8 +94,7 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
for k, v := range crm.channels {
|
||||
id, _ := strconv.ParseUint(v.ID, 10, 64)
|
||||
if strings.HasPrefix(k, prefix) && id >= first && id < last {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
chs = append(chs, v)
|
||||
}
|
||||
}
|
||||
@ -100,8 +102,16 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
|
||||
// Sort Channels list
|
||||
chs = sortChannels(pm, chs)
|
||||
|
||||
if last > len(chs) {
|
||||
last = len(chs)
|
||||
}
|
||||
|
||||
if first > last {
|
||||
return things.ChannelsPage{}, nil
|
||||
}
|
||||
|
||||
page := things.ChannelsPage{
|
||||
Channels: chs,
|
||||
Channels: chs[first:last],
|
||||
PageMetadata: things.PageMetadata{
|
||||
Total: crm.counter,
|
||||
Offset: pm.Offset,
|
||||
|
@ -31,6 +31,17 @@ func sortThings(pm things.PageMetadata, ths []things.Thing) []things.Thing {
|
||||
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:
|
||||
sort.SliceStable(ths, func(i, j int) bool {
|
||||
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
|
||||
})
|
||||
}
|
||||
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:
|
||||
sort.SliceStable(chs, func(i, j int) bool {
|
||||
return chs[i].ID < chs[j].ID
|
||||
|
@ -55,7 +55,7 @@ func (trm *thingRepositoryMock) Save(_ context.Context, ths ...things.Thing) ([]
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, pm
|
||||
trm.mu.Lock()
|
||||
defer trm.mu.Unlock()
|
||||
|
||||
if pm.Limit <= 0 {
|
||||
if pm.Limit < 0 {
|
||||
return things.Page{}, nil
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,34 @@ paths:
|
||||
description: Database can't process request.
|
||||
'500':
|
||||
$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:
|
||||
post:
|
||||
summary: Bulk provisions new things
|
||||
@ -569,6 +597,43 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
@ -800,6 +865,13 @@ components:
|
||||
description: Free-form thing name.
|
||||
metadata:
|
||||
type: object
|
||||
ThingsSearchReq:
|
||||
description: JSON-formatted document describing search parameters.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ThingsReqSchema"
|
||||
KeyUpdateReq:
|
||||
required: true
|
||||
description: JSON containing thing.
|
||||
|
@ -72,7 +72,7 @@ func TestCreateThings(t *testing.T) {
|
||||
key: token,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"id": "1",
|
||||
"id": "001",
|
||||
"name": "a",
|
||||
"owner": email,
|
||||
"metadata": "{\"test\":\"test\"}",
|
||||
@ -298,7 +298,7 @@ func TestCreateChannels(t *testing.T) {
|
||||
key: token,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"id": "1",
|
||||
"id": "001",
|
||||
"name": "a",
|
||||
"metadata": "{\"test\":\"test\"}",
|
||||
"owner": email,
|
||||
|
@ -126,13 +126,13 @@ type Service interface {
|
||||
// PageMetadata contains page metadata that helps navigation.
|
||||
type PageMetadata struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Name string
|
||||
Order string
|
||||
Dir string
|
||||
Metadata map[string]interface{}
|
||||
Connected bool // Used for connected or disconnected lists
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Order string `json:"order,omitempty"`
|
||||
Dir string `json:"dir,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
Connected bool // Used for connected or disconnected lists
|
||||
}
|
||||
|
||||
var _ Service = (*thingsService)(nil)
|
||||
|
@ -711,13 +711,13 @@ func TestListChannels(t *testing.T) {
|
||||
size: 0,
|
||||
err: nil,
|
||||
},
|
||||
"list with zero limit": {
|
||||
"list with zero limit and offset 1": {
|
||||
token: token,
|
||||
pageMetadata: things.PageMetadata{
|
||||
Offset: 1,
|
||||
Limit: 0,
|
||||
},
|
||||
size: 0,
|
||||
size: n - 1,
|
||||
err: nil,
|
||||
},
|
||||
"list with wrong credentials": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user