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

MF-1061 - Add PageMetadata to readers (#1333)

* MF-1061 - Add PageMetadata to readers

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix merge conflicts

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv Total to MessagesPage

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix readers mock and add filters tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add Total check and allow combinations of query parameters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use slices length as Total

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Simplify readers mock

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add empty lines

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
This commit is contained in:
Manuel Imperiale 2021-01-26 12:23:15 +01:00 committed by GitHub
parent 31f5bf714a
commit 4619576e94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 950 additions and 477 deletions

View File

@ -18,16 +18,15 @@ func listMessagesEndpoint(svc readers.MessageRepository) endpoint.Endpoint {
return nil, err
}
page, err := svc.ReadAll(req.chanID, req.offset, req.limit, req.query)
page, err := svc.ReadAll(req.chanID, req.pageMeta)
if err != nil {
return nil, err
}
return pageRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
Messages: page.Messages,
PageMetadata: page.PageMetadata,
Total: page.Total,
Messages: page.Messages,
}, nil
}
}

View File

@ -4,26 +4,34 @@
package api_test
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/transformers/senml"
"github.com/mainflux/mainflux/pkg/uuid"
"github.com/mainflux/mainflux/readers"
"github.com/mainflux/mainflux/readers/api"
"github.com/mainflux/mainflux/readers/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
svcName = "test-service"
token = "1"
invalid = "invalid"
numOfMessages = 42
chanID = "1"
numOfMessages = 100
valueFields = 5
subtopic = "topic"
mqttProt = "mqtt"
httpProt = "http"
msgName = "temperature"
)
var (
@ -32,40 +40,10 @@ var (
vb = true
vd = "dataValue"
sum float64 = 42
idProvider = uuid.New()
)
func newService() readers.MessageRepository {
var messages []readers.Message
for i := 0; i < numOfMessages; i++ {
msg := senml.Message{
Channel: chanID,
Publisher: "1",
Protocol: "mqtt",
}
// Mix possible values as well as value sum.
count := i % valueFields
switch count {
case 0:
msg.Value = &v
case 1:
msg.BoolValue = &vb
case 2:
msg.StringValue = &vs
case 3:
msg.DataValue = &vd
case 4:
msg.Sum = &sum
}
messages = append(messages, msg)
}
return mocks.NewMessageRepository(map[string][]readers.Message{
chanID: messages,
})
}
func newServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient) *httptest.Server {
mux := api.MakeHandler(repo, tc, svcName)
return httptest.NewServer(mux)
@ -76,6 +54,7 @@ type testRequest struct {
method string
url string
token string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
@ -91,114 +70,300 @@ func (tr testRequest) make() (*http.Response, error) {
}
func TestReadAll(t *testing.T) {
svc := newService()
tc := mocks.NewThingsService()
ts := newServer(svc, tc)
chanID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
now := time.Now().Unix()
var messages []senml.Message
var queryMsgs []senml.Message
var valueMsgs []senml.Message
var boolMsgs []senml.Message
var stringMsgs []senml.Message
var dataMsgs []senml.Message
for i := 0; i < numOfMessages; i++ {
// Mix possible values as well as value sum.
msg := senml.Message{
Channel: chanID,
Publisher: pubID,
Protocol: mqttProt,
Time: float64(now - int64(i)),
Name: "name",
}
count := i % valueFields
switch count {
case 0:
msg.Value = &v
valueMsgs = append(valueMsgs, msg)
case 1:
msg.BoolValue = &vb
boolMsgs = append(boolMsgs, msg)
case 2:
msg.StringValue = &vs
stringMsgs = append(stringMsgs, msg)
case 3:
msg.DataValue = &vd
dataMsgs = append(dataMsgs, msg)
case 4:
msg.Sum = &sum
msg.Subtopic = subtopic
msg.Protocol = httpProt
msg.Publisher = pubID2
msg.Name = msgName
queryMsgs = append(queryMsgs, msg)
}
messages = append(messages, msg)
}
svc := mocks.NewThingsService()
repo := mocks.NewMessageRepository(chanID, fromSenml(messages))
ts := newServer(repo, svc)
defer ts.Close()
cases := map[string]struct {
cases := []struct {
desc string
req string
url string
token string
status int
res pageRes
}{
"read page with valid offset and limit": {
{
desc: "read page with valid offset and limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=10", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(messages)),
Messages: messages[0:10],
},
},
"read page with negative offset": {
{
desc: "read page with negative offset",
url: fmt.Sprintf("%s/channels/%s/messages?offset=-1&limit=10", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with negative limit": {
{
desc: "read page with negative limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=-10", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with zero limit": {
{
desc: "read page with zero limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=0", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with non-integer offset": {
{
desc: "read page with non-integer offset",
url: fmt.Sprintf("%s/channels/%s/messages?offset=abc&limit=10", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with non-integer limit": {
{
desc: "read page with non-integer limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=abc", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with invalid channel id": {
{
desc: "read page with invalid channel id",
url: fmt.Sprintf("%s/channels//messages?offset=0&limit=10", ts.URL),
token: token,
status: http.StatusBadRequest,
},
"read page with invalid token": {
{
desc: "read page with invalid token",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=10", ts.URL, chanID),
token: invalid,
status: http.StatusForbidden,
},
"read page with multiple offset": {
{
desc: "read page with multiple offset",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&offset=1&limit=10", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with multiple limit": {
{
desc: "read page with multiple limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=20&limit=10", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
"read page with empty token": {
{
desc: "read page with empty token",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0&limit=10", ts.URL, chanID),
token: "",
status: http.StatusForbidden,
},
"read page with default offset": {
{
desc: "read page with default offset",
url: fmt.Sprintf("%s/channels/%s/messages?limit=10", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(messages)),
Messages: messages[0:10],
},
},
"read page with default limit": {
{
desc: "read page with default limit",
url: fmt.Sprintf("%s/channels/%s/messages?offset=0", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(messages)),
Messages: messages[0:10],
},
},
"read page with value": {
{
desc: "read page with senml fornat",
url: fmt.Sprintf("%s/channels/%s/messages?format=messages", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(messages)),
Messages: messages[0:10],
},
},
{
desc: "read page with subtopic",
url: fmt.Sprintf("%s/channels/%s/messages?subtopic=%s&protocol=%s", ts.URL, chanID, subtopic, httpProt),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(queryMsgs)),
Messages: queryMsgs[0:10],
},
},
{
desc: "read page with subtopic and protocol",
url: fmt.Sprintf("%s/channels/%s/messages?subtopic=%s&protocol=%s", ts.URL, chanID, subtopic, httpProt),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(queryMsgs)),
Messages: queryMsgs[0:10],
},
},
{
desc: "read page with publisher",
url: fmt.Sprintf("%s/channels/%s/messages?publisher=%s", ts.URL, chanID, pubID2),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(queryMsgs)),
Messages: queryMsgs[0:10],
},
},
{
desc: "read page with protocol",
url: fmt.Sprintf("%s/channels/%s/messages?protocol=http", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(queryMsgs)),
Messages: queryMsgs[0:10],
},
},
{
desc: "read page with name",
url: fmt.Sprintf("%s/channels/%s/messages?name=%s", ts.URL, chanID, msgName),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(queryMsgs)),
Messages: queryMsgs[0:10],
},
},
{
desc: "read page with value",
url: fmt.Sprintf("%s/channels/%s/messages?v=%f", ts.URL, chanID, v),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(valueMsgs)),
Messages: valueMsgs[0:10],
},
},
"read page with boolean value": {
url: fmt.Sprintf("%s/channels/%s/messages?vb=%t", ts.URL, chanID, vb),
{
desc: "read page with non-float value",
url: fmt.Sprintf("%s/channels/%s/messages?v=ab01", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
{
desc: "read page with boolean value",
url: fmt.Sprintf("%s/channels/%s/messages?vb=true", ts.URL, chanID),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(boolMsgs)),
Messages: boolMsgs[0:10],
},
},
"read page with string value": {
url: fmt.Sprintf("%s/channels/%s/messages?vs=%s", ts.URL, chanID, vd),
{
desc: "read page with non-boolean value",
url: fmt.Sprintf("%s/channels/%s/messages?vb=yes", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
{
desc: "read page with string value",
url: fmt.Sprintf("%s/channels/%s/messages?vs=%s", ts.URL, chanID, vs),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(stringMsgs)),
Messages: stringMsgs[0:10],
},
},
"read page with data value": {
{
desc: "read page with data value",
url: fmt.Sprintf("%s/channels/%s/messages?vd=%s", ts.URL, chanID, vd),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(dataMsgs)),
Messages: dataMsgs[0:10],
},
},
"read page with from": {
url: fmt.Sprintf("%s/channels/%s/messages?from=1608651539.673909", ts.URL, chanID),
token: token,
status: http.StatusOK,
},
"read page with to": {
url: fmt.Sprintf("%s/channels/%s/messages?to=1508651539.673909", ts.URL, chanID),
{
desc: "read page with non-float from",
url: fmt.Sprintf("%s/channels/%s/messages?from=ABCD", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
{
desc: "read page with non-float to",
url: fmt.Sprintf("%s/channels/%s/messages?to=ABCD", ts.URL, chanID),
token: token,
status: http.StatusBadRequest,
},
{
desc: "read page with from/to",
url: fmt.Sprintf("%s/channels/%s/messages?from=%f&to=%f", ts.URL, chanID, messages[19].Time, messages[4].Time),
token: token,
status: http.StatusOK,
res: pageRes{
Total: uint64(len(messages[5:20])),
Messages: messages[5:15],
},
},
}
for desc, tc := range cases {
for _, tc := range cases {
req := testRequest{
client: ts.Client(),
method: http.MethodGet,
@ -206,7 +371,27 @@ func TestReadAll(t *testing.T) {
token: tc.token,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected %d got %d", desc, tc.status, res.StatusCode))
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var page pageRes
json.NewDecoder(res.Body).Decode(&page)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res.Total, page.Total, fmt.Sprintf("%s: expected %d got %d", tc.desc, tc.res.Total, page.Total))
assert.ElementsMatch(t, tc.res.Messages, page.Messages, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res.Messages, page.Messages))
}
}
type pageRes struct {
readers.PageMetadata
Total uint64 `json:"total"`
Messages []senml.Message `json:"messages,omitempty"`
}
func fromSenml(in []senml.Message) []readers.Message {
var ret []readers.Message
for _, m := range in {
ret = append(ret, m)
}
return ret
}

View File

@ -28,9 +28,9 @@ func LoggingMiddleware(svc readers.MessageRepository, logger logger.Logger) read
}
}
func (lm *loggingMiddleware) ReadAll(chanID string, offset, limit uint64, query map[string]string) (page readers.MessagesPage, err error) {
func (lm *loggingMiddleware) ReadAll(chanID string, rpm readers.PageMetadata) (page readers.MessagesPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method read_all for channel %s with offset %d and limit %d took %s to complete", chanID, offset, limit, time.Since(begin))
message := fmt.Sprintf("Method read_all for channel %s with query %v took %s to complete", chanID, rpm, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
@ -38,5 +38,5 @@ func (lm *loggingMiddleware) ReadAll(chanID string, offset, limit uint64, query
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ReadAll(chanID, offset, limit, query)
return lm.svc.ReadAll(chanID, rpm)
}

View File

@ -29,11 +29,11 @@ func MetricsMiddleware(svc readers.MessageRepository, counter metrics.Counter, l
}
}
func (mm *metricsMiddleware) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
func (mm *metricsMiddleware) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
defer func(begin time.Time) {
mm.counter.With("method", "read_all").Add(1)
mm.latency.With("method", "read_all").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ReadAll(chanID, offset, limit, query)
return mm.svc.ReadAll(chanID, rpm)
}

View File

@ -3,19 +3,19 @@
package api
import "github.com/mainflux/mainflux/readers"
type apiReq interface {
validate() error
}
type listMessagesReq struct {
chanID string
offset uint64
limit uint64
query map[string]string
chanID string
pageMeta readers.PageMetadata
}
func (req listMessagesReq) validate() error {
if req.limit < 1 {
if req.pageMeta.Limit < 1 {
return errInvalidRequest
}

View File

@ -13,9 +13,8 @@ import (
var _ mainflux.Response = (*pageRes)(nil)
type pageRes struct {
readers.PageMetadata
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Messages []readers.Message `json:"messages,omitempty"`
}

View File

@ -31,8 +31,8 @@ const (
var (
errInvalidRequest = errors.New("received invalid request")
errUnauthorizedAccess = errors.New("missing or invalid credentials provided")
errNotInQuery = errors.New("parameter missing in the query")
auth mainflux.ThingsServiceClient
queryFields = []string{"format", "subtopic", "publisher", "protocol", "name", "v", "vs", "vb", "vd", "from", "to"}
)
// MakeHandler returns a HTTP handler for API endpoints.
@ -67,31 +67,94 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
return nil, err
}
offset, err := getQuery(r, "offset", defOffset)
offset, err := readUintQuery(r, "offset", defOffset)
if err != nil {
return nil, err
}
limit, err := getQuery(r, "limit", defLimit)
limit, err := readUintQuery(r, "limit", defLimit)
if err != nil {
return nil, err
}
query := map[string]string{}
for _, name := range queryFields {
if value := bone.GetQuery(r, name); len(value) == 1 {
query[name] = value[0]
}
format, err := readStringQuery(r, "format")
if err != nil {
return nil, err
}
if query[format] == "" {
query[format] = defFormat
if format != "" {
format = defFormat
}
subtopic, err := readStringQuery(r, "subtopic")
if err != nil {
return nil, err
}
publisher, err := readStringQuery(r, "publisher")
if err != nil {
return nil, err
}
protocol, err := readStringQuery(r, "protocol")
if err != nil {
return nil, err
}
name, err := readStringQuery(r, "name")
if err != nil {
return nil, err
}
v, err := readFloatQuery(r, "v")
if err != nil {
return nil, err
}
vs, err := readStringQuery(r, "vs")
if err != nil {
return nil, err
}
vd, err := readStringQuery(r, "vd")
if err != nil {
return nil, err
}
from, err := readFloatQuery(r, "from")
if err != nil {
return nil, err
}
to, err := readFloatQuery(r, "to")
if err != nil {
return nil, err
}
req := listMessagesReq{
chanID: chanID,
offset: offset,
limit: limit,
query: query,
pageMeta: readers.PageMetadata{
Offset: offset,
Limit: limit,
Format: format,
Subtopic: subtopic,
Publisher: publisher,
Protocol: protocol,
Name: name,
Value: v,
StringValue: vs,
DataValue: vd,
From: from,
To: to,
},
}
vb, err := readBoolQuery(r, "vb")
// Check if vb is in the query
if err != nil && err != errNotInQuery {
return nil, err
}
if err == nil {
req.pageMeta.BoolValue = vb
}
return req, nil
@ -155,20 +218,71 @@ func authorize(r *http.Request, chanID string) error {
return nil
}
func getQuery(req *http.Request, name string, fallback uint64) (uint64, error) {
vals := bone.GetQuery(req, name)
if len(vals) == 0 {
return fallback, nil
}
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return 0, errInvalidRequest
}
val, err := strconv.ParseUint(vals[0], 10, 64)
if len(vals) == 0 {
return def, nil
}
strval := vals[0]
val, err := strconv.ParseUint(strval, 10, 64)
if err != nil {
return 0, errInvalidRequest
}
return uint64(val), nil
return val, nil
}
func readFloatQuery(r *http.Request, key string) (float64, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return 0, errInvalidRequest
}
if len(vals) == 0 {
return 0, nil
}
fval := vals[0]
val, err := strconv.ParseFloat(fval, 64)
if err != nil {
return 0, errInvalidRequest
}
return val, nil
}
func readStringQuery(r *http.Request, key string) (string, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return "", errInvalidRequest
}
if len(vals) == 0 {
return "", nil
}
return vals[0], nil
}
func readBoolQuery(r *http.Request, key string) (bool, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return false, errInvalidRequest
}
if len(vals) == 0 {
return false, errNotInQuery
}
b, err := strconv.ParseBool(vals[0])
if err != nil {
return false, errInvalidRequest
}
return b, nil
}

View File

@ -6,7 +6,6 @@ package cassandra
import (
"encoding/json"
"fmt"
"strconv"
"github.com/gocql/gocql"
"github.com/mainflux/mainflux/pkg/errors"
@ -36,26 +35,23 @@ func New(session *gocql.Session) readers.MessageRepository {
}
}
func (cr cassandraRepository) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
table, ok := query[format]
if !ok {
table = defTable
func (cr cassandraRepository) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
if rpm.Format == "" {
rpm.Format = defTable
}
// Remove format filter and format the rest properly.
delete(query, format)
q, vals := buildQuery(chanID, offset, limit, query)
q, vals := buildQuery(chanID, rpm)
selectCQL := fmt.Sprintf(`SELECT channel, subtopic, publisher, protocol, name, unit,
value, string_value, bool_value, data_value, sum, time,
update_time FROM messages WHERE channel = ? %s LIMIT ?
ALLOW FILTERING`, q)
countCQL := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE channel = ? %s ALLOW FILTERING`, defTable, q)
countCQL := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE channel = ? %s ALLOW FILTERING`, rpm.Format, q)
if table != defTable {
if rpm.Format != defTable {
selectCQL = fmt.Sprintf(`SELECT channel, subtopic, publisher, protocol, created, payload FROM %s WHERE channel = ? %s LIMIT ?
ALLOW FILTERING`, table, q)
countCQL = fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE channel = ? %s ALLOW FILTERING`, table, q)
ALLOW FILTERING`, rpm.Format, q)
countCQL = fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE channel = ? %s ALLOW FILTERING`, rpm.Format, q)
}
iter := cr.session.Query(selectCQL, vals...).Iter()
@ -63,19 +59,18 @@ func (cr cassandraRepository) ReadAll(chanID string, offset, limit uint64, query
scanner := iter.Scanner()
// skip first OFFSET rows
for i := uint64(0); i < offset; i++ {
for i := uint64(0); i < rpm.Offset; i++ {
if !scanner.Next() {
break
}
}
page := readers.MessagesPage{
Offset: offset,
Limit: limit,
Messages: []readers.Message{},
PageMetadata: rpm,
Messages: []readers.Message{},
}
switch table {
switch rpm.Format {
case defTable:
for scanner.Next() {
var msg senml.Message
@ -110,10 +105,17 @@ func (cr cassandraRepository) ReadAll(chanID string, offset, limit uint64, query
return page, nil
}
func buildQuery(chanID string, offset, limit uint64, query map[string]string) (string, []interface{}) {
func buildQuery(chanID string, rpm readers.PageMetadata) (string, []interface{}) {
var condCQL string
vals := []interface{}{chanID}
var query map[string]interface{}
meta, err := json.Marshal(rpm)
if err != nil {
return condCQL, vals
}
json.Unmarshal(meta, &query)
for name, val := range query {
switch name {
case
@ -125,18 +127,10 @@ func buildQuery(chanID string, offset, limit uint64, query map[string]string) (s
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND %s = ?`, condCQL, name)
case "v":
fVal, err := strconv.ParseFloat(val, 64)
if err != nil {
continue
}
vals = append(vals, fVal)
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND value = ?`, condCQL)
case "vb":
bVal, err := strconv.ParseBool(val)
if err != nil {
continue
}
vals = append(vals, bVal)
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND bool_value = ?`, condCQL)
case "vs":
vals = append(vals, val)
@ -145,22 +139,14 @@ func buildQuery(chanID string, offset, limit uint64, query map[string]string) (s
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND data_value = ?`, condCQL)
case "from":
fVal, err := strconv.ParseFloat(val, 64)
if err != nil {
continue
}
vals = append(vals, fVal)
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND time >= ?`, condCQL)
case "to":
fVal, err := strconv.ParseFloat(val, 64)
if err != nil {
continue
}
vals = append(vals, fVal)
vals = append(vals, val)
condCQL = fmt.Sprintf(`%s AND time < ?`, condCQL)
}
}
vals = append(vals, offset+limit)
vals = append(vals, rpm.Offset+rpm.Limit)
return condCQL, vals
}

View File

@ -55,6 +55,8 @@ func TestReadSenml(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := senml.Message{
Channel: chanID,
@ -110,168 +112,190 @@ func TestReadSenml(t *testing.T) {
// cases that return subset of messages are only
// checking data result set size, but not content.
cases := map[string]struct {
chanID string
query map[string]string
page readers.MessagesPage
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 0,
Limit: msgsNum,
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
chanID: "2",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: 0,
Offset: 0,
Limit: msgsNum,
Messages: []readers.Message{},
},
},
"read message last page": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 95,
Limit: limit,
Messages: fromSenml(messages[95:msgsNum]),
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": "not-present"},
page: readers.MessagesPage{
Total: 0,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
Subtopic: "not-present",
},
page: readers.MessagesPage{
Messages: []readers.Message{},
},
},
"read message with subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": subtopic},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Subtopic: subtopic,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 5,
Limit: msgsNum,
Messages: fromSenml(queryMsgs[5:]),
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
chanID: chanID,
query: map[string]string{"publisher": pubID2},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Publisher: pubID2,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with protocol": {
chanID: chanID,
query: map[string]string{"protocol": httpProt},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Protocol: httpProt,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
chanID: chanID,
query: map[string]string{"name": msgName},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Name: msgName,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
chanID: chanID,
query: map[string]string{"v": fmt.Sprintf("%f", v)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Value: v,
},
page: readers.MessagesPage{
Total: uint64(len(valueMsgs)),
Offset: 0,
Limit: limit,
Total: uint64(len(queryMsgs)),
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
chanID: chanID,
query: map[string]string{"vb": fmt.Sprintf("%t", vb)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
BoolValue: vb,
},
page: readers.MessagesPage{
Total: uint64(len(boolMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
chanID: chanID,
query: map[string]string{"vs": vs},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
StringValue: vs,
},
page: readers.MessagesPage{
Total: uint64(len(stringMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
chanID: chanID,
query: map[string]string{"vd": vd},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
DataValue: vd,
},
page: readers.MessagesPage{
Total: uint64(len(dataMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[0:21])),
From: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[0:21])),
Offset: 0,
Limit: uint64(len(messages[0:21])),
Messages: fromSenml(messages[0:21]),
},
},
"read message with to": {
chanID: chanID,
query: map[string]string{
"to": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[21:])),
To: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[21:])),
Offset: 0,
Limit: uint64(len(messages[21:])),
Messages: fromSenml(messages[21:]),
},
},
"read message with from/to": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[5].Time),
"to": fmt.Sprintf("%f", messages[0].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
From: messages[5].Time,
To: messages[0].Time,
},
page: readers.MessagesPage{
Total: 5,
Offset: 0,
Limit: limit,
Messages: fromSenml(messages[1:6]),
},
},
}
for desc, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.page.Offset, tc.page.Limit, tc.query)
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Total, result.Total))

View File

@ -40,16 +40,14 @@ func New(client influxdata.Client, database string) readers.MessageRepository {
}
}
func (repo *influxRepository) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
measurement, ok := query[format]
if !ok {
measurement = defMeasurement
func (repo *influxRepository) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
if rpm.Format == "" {
rpm.Format = defMeasurement
}
// Remove format filter and format the rest properly.
delete(query, format)
condition := fmtCondition(chanID, query)
cmd := fmt.Sprintf(`SELECT * FROM %s WHERE %s ORDER BY time DESC LIMIT %d OFFSET %d`, measurement, condition, limit, offset)
condition := fmtCondition(chanID, rpm)
cmd := fmt.Sprintf(`SELECT * FROM %s WHERE %s ORDER BY time DESC LIMIT %d OFFSET %d`, rpm.Format, condition, rpm.Limit, rpm.Offset)
q := influxdata.Query{
Command: cmd,
Database: repo.database,
@ -71,20 +69,21 @@ func (repo *influxRepository) ReadAll(chanID string, offset, limit uint64, query
result := resp.Results[0].Series[0]
for _, v := range result.Values {
ret = append(ret, parseMessage(measurement, result.Columns, v))
ret = append(ret, parseMessage(rpm.Format, result.Columns, v))
}
total, err := repo.count(measurement, condition)
total, err := repo.count(rpm.Format, condition)
if err != nil {
return readers.MessagesPage{}, errors.Wrap(errReadMessages, err)
}
return readers.MessagesPage{
Total: total,
Offset: offset,
Limit: limit,
Messages: ret,
}, nil
page := readers.MessagesPage{
PageMetadata: rpm,
Total: total,
Messages: ret,
}
return page, nil
}
func (repo *influxRepository) count(measurement, condition string) (uint64, error) {
@ -129,8 +128,16 @@ func (repo *influxRepository) count(measurement, condition string) (uint64, erro
return strconv.ParseUint(count.String(), 10, 64)
}
func fmtCondition(chanID string, query map[string]string) string {
func fmtCondition(chanID string, rpm readers.PageMetadata) string {
condition := fmt.Sprintf(`channel='%s'`, chanID)
var query map[string]interface{}
meta, err := json.Marshal(rpm)
if err != nil {
return condition
}
json.Unmarshal(meta, &query)
for name, value := range query {
switch name {
case
@ -141,26 +148,18 @@ func fmtCondition(chanID string, query map[string]string) string {
"protocol":
condition = fmt.Sprintf(`%s AND "%s"='%s'`, condition, name, value)
case "v":
condition = fmt.Sprintf(`%s AND value = %s`, condition, value)
condition = fmt.Sprintf(`%s AND value = %f`, condition, value)
case "vb":
condition = fmt.Sprintf(`%s AND boolValue = %s`, condition, value)
condition = fmt.Sprintf(`%s AND boolValue = %t`, condition, value)
case "vs":
condition = fmt.Sprintf(`%s AND stringValue = '%s'`, condition, value)
case "vd":
condition = fmt.Sprintf(`%s AND dataValue = '%s'`, condition, value)
case "from":
fVal, err := strconv.ParseFloat(value, 64)
if err != nil {
continue
}
iVal := int64(fVal * 1e9)
iVal := int64(value.(float64) * 1e9)
condition = fmt.Sprintf(`%s AND time >= %d`, condition, iVal)
case "to":
fVal, err := strconv.ParseFloat(value, 64)
if err != nil {
continue
}
iVal := int64(fVal * 1e9)
iVal := int64(value.(float64) * 1e9)
condition = fmt.Sprintf(`%s AND time < %d`, condition, iVal)
}
}

View File

@ -47,6 +47,8 @@ func TestReadAll(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := senml.Message{
Channel: chanID,
@ -104,168 +106,190 @@ func TestReadAll(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("Creating new InfluxDB reader expected to succeed: %s.\n", err))
cases := map[string]struct {
chanID string
query map[string]string
page readers.MessagesPage
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 0,
Limit: limit,
Messages: fromSenml(messages[0:limit]),
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
chanID: "wrong",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: 0,
Offset: 0,
Limit: limit,
Messages: []readers.Message{},
},
},
"read message last page": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 95,
Limit: limit,
Messages: fromSenml(messages[95:msgsNum]),
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": "not-present"},
page: readers.MessagesPage{
Total: 0,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
Subtopic: "not-present",
},
page: readers.MessagesPage{
Messages: []readers.Message{},
},
},
"read message with subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": subtopic},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Subtopic: subtopic,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
chanID: chanID,
query: map[string]string{"publisher": pubID2},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Publisher: pubID2,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with protocol": {
chanID: chanID,
query: map[string]string{"protocol": httpProt},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Protocol: httpProt,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
chanID: chanID,
query: map[string]string{"name": msgName},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Name: msgName,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
chanID: chanID,
query: map[string]string{"v": fmt.Sprintf("%f", v)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Value: v,
},
page: readers.MessagesPage{
Total: uint64(len(valueMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
chanID: chanID,
query: map[string]string{"vb": fmt.Sprintf("%t", vb)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
BoolValue: vb,
},
page: readers.MessagesPage{
Total: uint64(len(boolMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
chanID: chanID,
query: map[string]string{"vs": vs},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
StringValue: vs,
},
page: readers.MessagesPage{
Total: uint64(len(stringMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
chanID: chanID,
query: map[string]string{"vd": vd},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
DataValue: vd,
},
page: readers.MessagesPage{
Total: uint64(len(dataMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[0:21])),
From: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[0:21])),
Offset: 0,
Limit: uint64(len(messages[0:21])),
Messages: fromSenml(messages[0:21]),
},
},
"read message with to": {
chanID: chanID,
query: map[string]string{
"to": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[21:])),
To: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[21:])),
Offset: 0,
Limit: uint64(len(messages[21:])),
Messages: fromSenml(messages[21:]),
},
},
"read message with from/to": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[5].Time),
"to": fmt.Sprintf("%f", messages[0].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
From: messages[5].Time,
To: messages[0].Time,
},
page: readers.MessagesPage{
Total: 5,
Offset: 0,
Limit: limit,
Messages: fromSenml(messages[1:6]),
},
},
}
for desc, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.page.Offset, tc.page.Limit, tc.query)
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected: %v, got: %v", desc, tc.page.Messages, result.Messages))

View File

@ -12,7 +12,7 @@ var ErrNotFound = errors.New("entity not found")
type MessageRepository interface {
// ReadAll skips given number of messages for given channel and returns next
// limited number of messages.
ReadAll(chanID string, offset, limit uint64, query map[string]string) (MessagesPage, error)
ReadAll(chanID string, pm PageMetadata) (MessagesPage, error)
}
// Message represents any message format.
@ -21,8 +21,24 @@ type Message interface{}
// MessagesPage contains page related metadata as well as list of messages that
// belong to this page.
type MessagesPage struct {
PageMetadata
Total uint64
Offset uint64
Limit uint64
Messages []Message
}
// PageMetadata represents the parameters used to create database queries
type PageMetadata struct {
Offset uint64 `json:"offset,omitempty"`
Limit uint64 `json:"limit,omitempty"`
Subtopic string `json:"subtopic,omitempty"`
Publisher string `json:"publisher,omitempty"`
Protocol string `json:"protocol,omitempty"`
Name string `json:"name,omitempty"`
Value float64 `json:"v,omitempty"`
BoolValue bool `json:"vb,omitempty"`
StringValue string `json:"vs,omitempty"`
DataValue string `json:"vd,omitempty"`
From float64 `json:"from,omitempty"`
To float64 `json:"to,omitempty"`
Format string `json:"format,omitempty"`
}

View File

@ -4,8 +4,10 @@
package mocks
import (
"encoding/json"
"sync"
"github.com/mainflux/mainflux/pkg/transformers/senml"
"github.com/mainflux/mainflux/readers"
)
@ -17,36 +19,115 @@ type messageRepositoryMock struct {
}
// NewMessageRepository returns mock implementation of message repository.
func NewMessageRepository(messages map[string][]readers.Message) readers.MessageRepository {
func NewMessageRepository(chanID string, messages []readers.Message) readers.MessageRepository {
repo := map[string][]readers.Message{
chanID: messages,
}
return &messageRepositoryMock{
mutex: sync.Mutex{},
messages: messages,
messages: repo,
}
}
func (repo *messageRepositoryMock) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
func (repo *messageRepositoryMock) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
repo.mutex.Lock()
defer repo.mutex.Unlock()
end := offset + limit
numOfMessages := uint64(len(repo.messages[chanID]))
if offset < 0 || offset >= numOfMessages {
if rpm.Format != "" && rpm.Format != "messages" {
return readers.MessagesPage{}, nil
}
if limit < 1 {
var query map[string]interface{}
meta, _ := json.Marshal(rpm)
json.Unmarshal(meta, &query)
var msgs []readers.Message
for _, m := range repo.messages[chanID] {
senml := m.(senml.Message)
ok := true
for name := range query {
switch name {
case "subtopic":
if rpm.Subtopic != senml.Subtopic {
ok = false
}
case "publisher":
if rpm.Publisher != senml.Publisher {
ok = false
}
case "name":
if rpm.Name != senml.Name {
ok = false
}
case "protocol":
if rpm.Protocol != senml.Protocol {
ok = false
}
case "v":
if senml.Value == nil ||
(senml.Value != nil &&
*senml.Value != rpm.Value) {
ok = false
}
case "vb":
if senml.BoolValue == nil ||
(senml.BoolValue != nil &&
*senml.BoolValue != rpm.BoolValue) {
ok = false
}
case "vs":
if senml.StringValue == nil ||
(senml.StringValue != nil &&
*senml.StringValue != rpm.StringValue) {
ok = false
}
case "vd":
if senml.DataValue == nil ||
(senml.DataValue != nil &&
*senml.DataValue != rpm.DataValue) {
ok = false
}
case "from":
if senml.Time < rpm.From {
ok = false
}
case "to":
if senml.Time >= rpm.To {
ok = false
}
}
if !ok {
break
}
}
if ok {
msgs = append(msgs, m)
}
}
numOfMessages := uint64(len(msgs))
if rpm.Offset >= numOfMessages {
return readers.MessagesPage{}, nil
}
if offset+limit > numOfMessages {
if rpm.Limit < 1 {
return readers.MessagesPage{}, nil
}
end := rpm.Offset + rpm.Limit
if rpm.Offset+rpm.Limit > numOfMessages {
end = numOfMessages
}
return readers.MessagesPage{
Total: numOfMessages,
Limit: limit,
Offset: offset,
Messages: repo.messages[chanID][offset:end],
PageMetadata: rpm,
Total: uint64(len(msgs)),
Messages: msgs[rpm.Offset:end],
}, nil
}

View File

@ -5,7 +5,7 @@ package mongodb
import (
"context"
"strconv"
"encoding/json"
"github.com/mainflux/mainflux/pkg/errors"
jsont "github.com/mainflux/mainflux/pkg/transformers/json"
@ -37,26 +37,26 @@ func New(db *mongo.Database) readers.MessageRepository {
}
}
func (repo mongoRepository) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
collection, ok := query[format]
if !ok {
collection = defCollection
func (repo mongoRepository) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
if rpm.Format == "" {
rpm.Format = defCollection
}
col := repo.db.Collection(collection)
delete(query, format)
col := repo.db.Collection(rpm.Format)
sortMap := map[string]interface{}{
"time": -1,
}
// Remove format filter and format the rest properly.
filter := fmtCondition(chanID, query)
cursor, err := col.Find(context.Background(), filter, options.Find().SetSort(sortMap).SetLimit(int64(limit)).SetSkip(int64(offset)))
filter := fmtCondition(chanID, rpm)
cursor, err := col.Find(context.Background(), filter, options.Find().SetSort(sortMap).SetLimit(int64(rpm.Limit)).SetSkip(int64(rpm.Offset)))
if err != nil {
return readers.MessagesPage{}, errors.Wrap(errReadMessages, err)
}
defer cursor.Close(context.Background())
var messages []readers.Message
switch collection {
switch rpm.Format {
case defCollection:
for cursor.Next(context.Background()) {
var m senml.Message
@ -83,21 +83,30 @@ func (repo mongoRepository) ReadAll(chanID string, offset, limit uint64, query m
return readers.MessagesPage{}, errors.Wrap(errReadMessages, err)
}
return readers.MessagesPage{
Total: uint64(total),
Offset: offset,
Limit: limit,
Messages: messages,
}, nil
mp := readers.MessagesPage{
PageMetadata: rpm,
Total: uint64(total),
Messages: messages,
}
return mp, nil
}
func fmtCondition(chanID string, query map[string]string) bson.D {
func fmtCondition(chanID string, rpm readers.PageMetadata) bson.D {
filter := bson.D{
bson.E{
Key: "channel",
Value: chanID,
},
}
var query map[string]interface{}
meta, err := json.Marshal(rpm)
if err != nil {
return filter
}
json.Unmarshal(meta, &query)
for name, value := range query {
switch name {
case
@ -108,33 +117,17 @@ func fmtCondition(chanID string, query map[string]string) bson.D {
"protocol":
filter = append(filter, bson.E{Key: name, Value: value})
case "v":
fVal, err := strconv.ParseFloat(value, 64)
if err != nil {
continue
}
filter = append(filter, bson.E{Key: "value", Value: fVal})
filter = append(filter, bson.E{Key: "value", Value: value})
case "vb":
bVal, err := strconv.ParseBool(value)
if err != nil {
continue
}
filter = append(filter, bson.E{Key: "bool_value", Value: bVal})
filter = append(filter, bson.E{Key: "bool_value", Value: value})
case "vs":
filter = append(filter, bson.E{Key: "string_value", Value: value})
case "vd":
filter = append(filter, bson.E{Key: "data_value", Value: value})
case "from":
fVal, err := strconv.ParseFloat(value, 64)
if err != nil {
continue
}
filter = append(filter, bson.E{Key: "time", Value: bson.M{"$gte": fVal}})
filter = append(filter, bson.E{Key: "time", Value: bson.M{"$gte": value}})
case "to":
fVal, err := strconv.ParseFloat(value, 64)
if err != nil {
continue
}
filter = append(filter, bson.E{Key: "time", Value: bson.M{"$lt": fVal}})
filter = append(filter, bson.E{Key: "time", Value: bson.M{"$lt": value}})
}
}

View File

@ -42,6 +42,8 @@ var (
vb = true
vd = "dataValue"
sum float64 = 42
idProvider = uuid.New()
)
func TestReadSenml(t *testing.T) {
@ -51,11 +53,13 @@ func TestReadSenml(t *testing.T) {
db := client.Database(testDB)
writer := writer.New(db)
chanID, err := uuid.New().ID()
chanID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID, err := uuid.New().ID()
pubID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
pubID2, err := uuid.New().ID()
pubID2, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
wrongID, err := idProvider.ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
m := senml.Message{
@ -106,168 +110,190 @@ func TestReadSenml(t *testing.T) {
reader := reader.New(db)
cases := map[string]struct {
chanID string
query map[string]string
page readers.MessagesPage
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 0,
Limit: 11,
Messages: fromSenml(messages[0:11]),
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
chanID: "2",
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: 0,
Offset: 0,
Limit: 10,
Messages: []readers.Message{},
},
},
"read message last page": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 95,
Limit: limit,
Messages: fromSenml(messages[95:msgsNum]),
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": "not-present"},
page: readers.MessagesPage{
Total: 0,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
Subtopic: "not-present",
},
page: readers.MessagesPage{
Messages: []readers.Message{},
},
},
"read message with subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": subtopic},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Subtopic: subtopic,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
chanID: chanID,
query: map[string]string{"publisher": pubID2},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Publisher: pubID2,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with protocol": {
chanID: chanID,
query: map[string]string{"protocol": httpProt},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Protocol: httpProt,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
chanID: chanID,
query: map[string]string{"name": msgName},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Name: msgName,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
chanID: chanID,
query: map[string]string{"v": fmt.Sprintf("%f", v)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Value: v,
},
page: readers.MessagesPage{
Total: uint64(len(valueMsgs)),
Offset: 0,
Limit: limit,
Total: uint64(len(queryMsgs)),
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
chanID: chanID,
query: map[string]string{"vb": fmt.Sprintf("%t", vb)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
BoolValue: vb,
},
page: readers.MessagesPage{
Total: uint64(len(boolMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
chanID: chanID,
query: map[string]string{"vs": vs},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
StringValue: vs,
},
page: readers.MessagesPage{
Total: uint64(len(stringMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
chanID: chanID,
query: map[string]string{"vd": vd},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
DataValue: vd,
},
page: readers.MessagesPage{
Total: uint64(len(dataMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[0:21])),
From: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[0:21])),
Offset: 0,
Limit: uint64(len(messages[0:21])),
Messages: fromSenml(messages[0:21]),
},
},
"read message with to": {
chanID: chanID,
query: map[string]string{
"to": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[21:])),
To: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[21:])),
Offset: 0,
Limit: uint64(len(messages[21:])),
Messages: fromSenml(messages[21:]),
},
},
"read message with from/to": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[5].Time),
"to": fmt.Sprintf("%f", messages[0].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
From: messages[5].Time,
To: messages[0].Time,
},
page: readers.MessagesPage{
Total: 5,
Offset: 0,
Limit: limit,
Messages: fromSenml(messages[1:6]),
},
},
}
for desc, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.page.Offset, tc.page.Limit, tc.query)
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Messages, result.Messages))

View File

@ -37,33 +37,31 @@ func New(db *sqlx.DB) readers.MessageRepository {
}
}
func (tr postgresRepository) ReadAll(chanID string, offset, limit uint64, query map[string]string) (readers.MessagesPage, error) {
table, ok := query[format]
func (tr postgresRepository) ReadAll(chanID string, rpm readers.PageMetadata) (readers.MessagesPage, error) {
order := "created"
if !ok {
table = defTable
if rpm.Format == "" {
order = "time"
rpm.Format = defTable
}
// Remove format filter and format the rest properly.
delete(query, format)
q := fmt.Sprintf(`SELECT * FROM %s
WHERE %s ORDER BY %s DESC
LIMIT :limit OFFSET :offset;`, table, fmtCondition(chanID, query), order)
LIMIT :limit OFFSET :offset;`, rpm.Format, fmtCondition(chanID, rpm), order)
params := map[string]interface{}{
"channel": chanID,
"limit": limit,
"offset": offset,
"subtopic": query["subtopic"],
"publisher": query["publisher"],
"name": query["name"],
"protocol": query["protocol"],
"value": query["v"],
"bool_value": query["vb"],
"string_value": query["vs"],
"data_value": query["vd"],
"from": query["from"],
"to": query["to"],
"limit": rpm.Limit,
"offset": rpm.Offset,
"subtopic": rpm.Subtopic,
"publisher": rpm.Publisher,
"name": rpm.Name,
"protocol": rpm.Protocol,
"value": rpm.Value,
"bool_value": rpm.BoolValue,
"string_value": rpm.StringValue,
"data_value": rpm.DataValue,
"from": rpm.From,
"to": rpm.To,
}
rows, err := tr.db.NamedQuery(q, params)
@ -73,11 +71,10 @@ func (tr postgresRepository) ReadAll(chanID string, offset, limit uint64, query
defer rows.Close()
page := readers.MessagesPage{
Offset: offset,
Limit: limit,
Messages: []readers.Message{},
PageMetadata: rpm,
Messages: []readers.Message{},
}
switch table {
switch rpm.Format {
case defTable:
for rows.Next() {
msg := dbMessage{Message: senml.Message{}}
@ -103,7 +100,7 @@ func (tr postgresRepository) ReadAll(chanID string, offset, limit uint64, query
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s;`, table, fmtCondition(chanID, query))
q = fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s;`, rpm.Format, fmtCondition(chanID, rpm))
rows, err = tr.db.NamedQuery(q, params)
if err != nil {
return readers.MessagesPage{}, errors.Wrap(errReadMessages, err)
@ -121,8 +118,16 @@ func (tr postgresRepository) ReadAll(chanID string, offset, limit uint64, query
return page, nil
}
func fmtCondition(chanID string, query map[string]string) string {
func fmtCondition(chanID string, rpm readers.PageMetadata) string {
condition := `channel = :channel`
var query map[string]interface{}
meta, err := json.Marshal(rpm)
if err != nil {
return condition
}
json.Unmarshal(meta, &query)
for name := range query {
switch name {
case

View File

@ -103,168 +103,190 @@ func TestReadSenml(t *testing.T) {
// cases that return subset of messages are only
// checking data result set size, but not content.
cases := map[string]struct {
chanID string
query map[string]string
page readers.MessagesPage
chanID string
pageMeta readers.PageMetadata
page readers.MessagesPage
}{
"read message page for existing channel": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: 0,
Limit: msgsNum,
Messages: fromSenml(messages),
},
},
"read message page for non-existent channel": {
chanID: wrongID,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: 0,
Offset: 0,
Limit: msgsNum,
Messages: []readers.Message{},
},
},
"read message last page": {
chanID: chanID,
pageMeta: readers.PageMetadata{
Offset: msgsNum - 20,
Limit: msgsNum,
},
page: readers.MessagesPage{
Total: msgsNum,
Offset: msgsNum - 20,
Limit: msgsNum,
Messages: fromSenml(messages[msgsNum-20 : msgsNum]),
},
},
"read message with non-existent subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": "not-present"},
page: readers.MessagesPage{
Total: 0,
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: msgsNum,
Subtopic: "not-present",
},
page: readers.MessagesPage{
Messages: []readers.Message{},
},
},
"read message with subtopic": {
chanID: chanID,
query: map[string]string{"subtopic": subtopic},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Subtopic: subtopic,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Messages: fromSenml(queryMsgs),
},
},
"read message with publisher": {
chanID: chanID,
query: map[string]string{"publisher": pubID2},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Publisher: pubID2,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: uint64(len(queryMsgs)),
Messages: fromSenml(queryMsgs),
},
},
"read message with protocol": {
chanID: chanID,
query: map[string]string{"protocol": httpProt},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(queryMsgs)),
Protocol: httpProt,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Messages: fromSenml(queryMsgs),
},
},
"read message with name": {
chanID: chanID,
query: map[string]string{"name": msgName},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Name: msgName,
},
page: readers.MessagesPage{
Total: uint64(len(queryMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(queryMsgs[0:limit]),
},
},
"read message with value": {
chanID: chanID,
query: map[string]string{"v": fmt.Sprintf("%f", v)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
Value: v,
},
page: readers.MessagesPage{
Total: uint64(len(valueMsgs)),
Offset: 0,
Limit: limit,
Total: uint64(len(queryMsgs)),
Messages: fromSenml(valueMsgs[0:limit]),
},
},
"read message with boolean value": {
chanID: chanID,
query: map[string]string{"vb": fmt.Sprintf("%t", vb)},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
BoolValue: vb,
},
page: readers.MessagesPage{
Total: uint64(len(boolMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(boolMsgs[0:limit]),
},
},
"read message with string value": {
chanID: chanID,
query: map[string]string{"vs": vs},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
StringValue: vs,
},
page: readers.MessagesPage{
Total: uint64(len(stringMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(stringMsgs[0:limit]),
},
},
"read message with data value": {
chanID: chanID,
query: map[string]string{"vd": vd},
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
DataValue: vd,
},
page: readers.MessagesPage{
Total: uint64(len(dataMsgs)),
Offset: 0,
Limit: limit,
Messages: fromSenml(dataMsgs[0:limit]),
},
},
"read message with from": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[0:21])),
From: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[0:21])),
Offset: 0,
Limit: uint64(len(messages[0:21])),
Messages: fromSenml(messages[0:21]),
},
},
"read message with to": {
chanID: chanID,
query: map[string]string{
"to": fmt.Sprintf("%f", messages[20].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: uint64(len(messages[21:])),
To: messages[20].Time,
},
page: readers.MessagesPage{
Total: uint64(len(messages[21:])),
Offset: 0,
Limit: uint64(len(messages[21:])),
Messages: fromSenml(messages[21:]),
},
},
"read message with from/to": {
chanID: chanID,
query: map[string]string{
"from": fmt.Sprintf("%f", messages[5].Time),
"to": fmt.Sprintf("%f", messages[0].Time),
pageMeta: readers.PageMetadata{
Offset: 0,
Limit: limit,
From: messages[5].Time,
To: messages[0].Time,
},
page: readers.MessagesPage{
Total: 5,
Offset: 0,
Limit: limit,
Messages: fromSenml(messages[1:6]),
},
},
}
for desc, tc := range cases {
result, err := reader.ReadAll(tc.chanID, tc.page.Offset, tc.page.Limit, tc.query)
result, err := reader.ReadAll(tc.chanID, tc.pageMeta)
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %s", desc, err))
assert.ElementsMatch(t, tc.page.Messages, result.Messages, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Messages, result.Messages))
assert.Equal(t, tc.page.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", desc, tc.page.Total, result.Total))