Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data): new API to search Readings by multiple resource names #3766

Merged
merged 1 commit into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/edgexfoundry/edgex-go
require (
bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690
github.com/edgexfoundry/go-mod-bootstrap/v2 v2.0.0
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.23
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.25
github.com/edgexfoundry/go-mod-messaging/v2 v2.0.1
github.com/edgexfoundry/go-mod-registry/v2 v2.0.0
github.com/edgexfoundry/go-mod-secrets/v2 v2.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ github.com/edgexfoundry/go-mod-bootstrap/v2 v2.0.0/go.mod h1:E5KLeFEwTuIbjrKMCIn
github.com/edgexfoundry/go-mod-configuration/v2 v2.0.0 h1:S1rUyJPJWSvlkNHLbHLFWBvqzH+ShO1xPp4HH7Pvn9I=
github.com/edgexfoundry/go-mod-configuration/v2 v2.0.0/go.mod h1:NZnjQtCdQtPH/eoOuTKA+8+FsoUU3WSlZ5JpXWuiYQQ=
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.0/go.mod h1:pfXURRetgIto0GR0sCjDrfa71hqJ1wxmQWi/mOzWfWU=
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.23 h1:kVg8RVx2MaOOctd3I6XLy0N3exOZ3muyK4IHRyD9Dzs=
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.23/go.mod h1:I6UhBPCREubcU0ouIGBdZlNG5Xx4NijUVN5rvEtD03k=
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.25 h1:AFQD5sbxpAfwESF/SXApyq7piSDgoioWLL5D3GY8qvw=
github.com/edgexfoundry/go-mod-core-contracts/v2 v2.0.1-dev.25/go.mod h1:I6UhBPCREubcU0ouIGBdZlNG5Xx4NijUVN5rvEtD03k=
github.com/edgexfoundry/go-mod-messaging/v2 v2.0.1 h1:8nT3CiPLIft5RmR+vbmXBW9Kbz7TqPZ6C8QuQ6TTn6w=
github.com/edgexfoundry/go-mod-messaging/v2 v2.0.1/go.mod h1:bLKWB9yeOHLZoQtHLZlGwz8MjsMJIvHDFce7CcUb4fE=
github.com/edgexfoundry/go-mod-registry/v2 v2.0.0 h1:FCodcfCo3EqgINbGa9Rn6LqbiwkdT2FPKgCnk81GbFs=
Expand Down
36 changes: 29 additions & 7 deletions internal/core/data/application/reading.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ func ReadingsByTimeRange(start int, end int, offset int, limit int, dic *di.Cont

if err != nil {
return readings, totalCount, errors.NewCommonEdgeXWrapper(err)
} else {
return readings, totalCount, nil
}
return readings, totalCount, nil
}

func convertReadingModelsToDTOs(readingModels []models.Reading) (readings []dtos.BaseReading, err errors.EdgeX) {
Expand Down Expand Up @@ -140,9 +139,8 @@ func ReadingsByResourceNameAndTimeRange(resourceName string, start int, end int,

if err != nil {
return readings, totalCount, errors.NewCommonEdgeXWrapper(err)
} else {
return readings, totalCount, nil
}
return readings, totalCount, nil
}

// ReadingsByDeviceNameAndResourceName query readings with offset, limit, device name and its associated resource name
Expand All @@ -165,9 +163,8 @@ func ReadingsByDeviceNameAndResourceName(deviceName string, resourceName string,

if err != nil {
return readings, totalCount, errors.NewCommonEdgeXWrapper(err)
} else {
return readings, totalCount, nil
}
return readings, totalCount, nil
}

// ReadingsByDeviceNameAndResourceNameAndTimeRange query readings with offset, limit, device name, its associated resource name and specified time range
Expand All @@ -190,7 +187,32 @@ func ReadingsByDeviceNameAndResourceNameAndTimeRange(deviceName string, resource

if err != nil {
return readings, totalCount, errors.NewCommonEdgeXWrapper(err)
}
return readings, totalCount, nil
}

// ReadingsByDeviceNameAndResourceNamesAndTimeRange query readings with offset, limit, device name, its associated resource name and specified time range
func ReadingsByDeviceNameAndResourceNamesAndTimeRange(deviceName string, resourceNames []string, start, end, offset, limit int, dic *di.Container) (readings []dtos.BaseReading, totalCount uint32, err errors.EdgeX) {
if deviceName == "" {
return readings, totalCount, errors.NewCommonEdgeX(errors.KindContractInvalid, "device name is empty", nil)
}

dbClient := container.DBClientFrom(dic.Get)
var readingModels []models.Reading
if len(resourceNames) > 0 {
readingModels, totalCount, err = dbClient.ReadingsByDeviceNameAndResourceNamesAndTimeRange(deviceName, resourceNames, start, end, offset, limit)
} else {
return readings, totalCount, nil
readingModels, err = dbClient.ReadingsByDeviceNameAndTimeRange(deviceName, start, end, offset, limit)
if err == nil {
totalCount, err = dbClient.ReadingCountByDeviceNameAndTimeRange(deviceName, start, end)
}
}

if err == nil {
readings, err = convertReadingModelsToDTOs(readingModels)
}
if err != nil {
return readings, totalCount, errors.NewCommonEdgeXWrapper(err)
}
return readings, totalCount, nil
}
72 changes: 63 additions & 9 deletions internal/core/data/controller/http/reading.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,34 @@
package http

import (
"fmt"
"math"
"net/http"

"github.com/edgexfoundry/edgex-go/internal/core/data/application"
dataContainer "github.com/edgexfoundry/edgex-go/internal/core/data/container"
"github.com/edgexfoundry/edgex-go/internal/io"
"github.com/edgexfoundry/edgex-go/internal/pkg"
"github.com/edgexfoundry/edgex-go/internal/pkg/utils"
"github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container"
"github.com/edgexfoundry/go-mod-bootstrap/v2/di"
"github.com/gorilla/mux"

"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common"
responseDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses"

"github.com/edgexfoundry/edgex-go/internal/core/data/application"
dataContainer "github.com/edgexfoundry/edgex-go/internal/core/data/container"
"github.com/edgexfoundry/edgex-go/internal/pkg"
"github.com/edgexfoundry/edgex-go/internal/pkg/utils"
"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
"github.com/gorilla/mux"
)

type ReadingController struct {
dic *di.Container
reader io.DtoReader
dic *di.Container
}

// NewReadingController creates and initializes a ReadingController
func NewReadingController(dic *di.Container) *ReadingController {
return &ReadingController{
dic: dic,
reader: io.NewJsonDtoReader(),
dic: dic,
}
}

Expand Down Expand Up @@ -246,3 +249,54 @@ func (rc *ReadingController) ReadingsByDeviceNameAndResourceNameAndTimeRange(w h
utils.WriteHttpHeader(w, ctx, http.StatusOK)
pkg.Encode(response, w, lc)
}

func (rc *ReadingController) ReadingsByDeviceNameAndResourceNamesAndTimeRange(w http.ResponseWriter, r *http.Request) {
lc := container.LoggingClientFrom(rc.dic.Get)
ctx := r.Context()
config := dataContainer.ConfigurationFrom(rc.dic.Get)

vars := mux.Vars(r)
deviceName := vars[common.Name]

// parse time range (start, end), offset, and limit from incoming request
start, end, offset, limit, err := utils.ParseTimeRangeOffsetLimit(r, 0, math.MaxInt32, -1, config.Service.MaxResultCount)
if err != nil {
utils.WriteErrorResponse(w, ctx, lc, err, "")
return
}

var queryPayload map[string]interface{}
if r.Body != http.NoBody { //only parse request body when there are contents provided
err = rc.reader.Read(r.Body, &queryPayload)
if err != nil {
utils.WriteErrorResponse(w, ctx, lc, err, "")
return
}
}

var resourceNames []string
if val, ok := queryPayload[common.ResourceNames]; ok { //look for
switch t := val.(type) {
case []interface{}:
for _, v := range t {
if strVal, ok := v.(string); ok {
resourceNames = append(resourceNames, strVal)
}
}
default:
err = errors.NewCommonEdgeX(errors.KindContractInvalid, fmt.Sprintf("query criteria [%v] not in expected format", common.ResourceNames), nil)
utils.WriteErrorResponse(w, ctx, lc, err, "")
return
}
}

readings, totalCount, err := application.ReadingsByDeviceNameAndResourceNamesAndTimeRange(deviceName, resourceNames, start, end, offset, limit, rc.dic)
if err != nil {
utils.WriteErrorResponse(w, ctx, lc, err, "")
return
}

response := responseDTO.NewMultiReadingsResponse("", "", http.StatusOK, totalCount, readings)
utils.WriteHttpHeader(w, ctx, http.StatusOK)
pkg.Encode(response, w, lc)
}
93 changes: 93 additions & 0 deletions internal/core/data/controller/http/reading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package http

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/edgexfoundry/go-mod-bootstrap/v2/di"
Expand Down Expand Up @@ -589,3 +591,94 @@ func TestReadingsByDeviceNameAndResourceNameAndTimeRange(t *testing.T) {
})
}
}

func TestReadingsByDeviceNameAndResourceNamesAndTimeRange(t *testing.T) {
totalCount := uint32(0)
testResourceNames := []string{"resource01", "resource02"}
emptyPayload := make(map[string]interface{})
testResourceNamesPayload := emptyPayload
testResourceNamesPayload[common.ResourceNames] = testResourceNames
dic := mocks.NewMockDIC()
dbClientMock := &dbMock.DBClient{}
dbClientMock.On("ReadingCountByDeviceNameAndTimeRange", TestDeviceName, 0, 100).Return(totalCount, nil)
dbClientMock.On("ReadingsByDeviceNameAndTimeRange", TestDeviceName, 0, 100, 0, 10).Return([]models.Reading{}, nil)
dbClientMock.On("ReadingsByDeviceNameAndResourceNamesAndTimeRange", TestDeviceName, testResourceNames, 0, 100, 0, 10).Return([]models.Reading{}, totalCount, nil)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
return dbClientMock
},
})
rc := NewReadingController(dic)
assert.NotNil(t, rc)

tests := []struct {
name string
deviceName string
payload map[string]interface{}
start string
end string
offset string
limit string
errorExpected bool
expectedTotalCount uint32
expectedStatusCode int
}{
{"Valid - provide deviceName and nil resourceNames", TestDeviceName, nil, "0", "100", "0", "10", false, totalCount, http.StatusOK},
{"Valid - provide deviceName and empty resourceNames", TestDeviceName, emptyPayload, "0", "100", "0", "10", false, totalCount, http.StatusOK},
{"Valid - provide deviceName and resourceNames", TestDeviceName, testResourceNamesPayload, "0", "100", "0", "10", false, totalCount, http.StatusOK},
{"Invalid - empty deviceName", "", testResourceNamesPayload, "0", "100", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - invalid start format", TestDeviceName, testResourceNamesPayload, "aaa", "100", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - invalid end format", TestDeviceName, testResourceNamesPayload, "0", "bbb", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - empty start", TestDeviceName, testResourceNamesPayload, "", "100", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - empty end", TestDeviceName, testResourceNamesPayload, "0", "", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - end before start", TestDeviceName, testResourceNamesPayload, "10", "0", "0", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - invalid offset format", TestDeviceName, testResourceNamesPayload, "0", "100", "aaa", "10", true, totalCount, http.StatusBadRequest},
{"Invalid - invalid limit format", TestDeviceName, testResourceNamesPayload, "0", "100", "0", "aaa", true, totalCount, http.StatusBadRequest},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
var reader io.Reader
if testCase.payload != nil {
byteData, err := toByteArray(common.ContentTypeJSON, testCase.payload)
require.NoError(t, err)
reader = strings.NewReader(string(byteData))
} else {
reader = http.NoBody
}
req, err := http.NewRequest(http.MethodGet, common.ApiReadingByDeviceNameAndTimeRangeRoute, reader)
req.Header.Set(common.ContentType, common.ContentTypeJSON)
require.NoError(t, err)
query := req.URL.Query()
query.Add(common.Offset, testCase.offset)
query.Add(common.Limit, testCase.limit)
req.URL.RawQuery = query.Encode()
req = mux.SetURLVars(req, map[string]string{common.Name: testCase.deviceName, common.Start: testCase.start, common.End: testCase.end})
require.NoError(t, err)

// Act
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(rc.ReadingsByDeviceNameAndResourceNamesAndTimeRange)
handler.ServeHTTP(recorder, req)

// Assert
if testCase.errorExpected {
var res commonDTO.BaseResponse
err = json.Unmarshal(recorder.Body.Bytes(), &res)
require.NoError(t, err)
assert.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected")
assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected")
assert.Equal(t, testCase.expectedStatusCode, res.StatusCode, "Response status code not as expected")
assert.NotEmpty(t, res.Message, "Response message doesn't contain the error message")
} else {
var res responseDTO.MultiReadingsResponse
err = json.Unmarshal(recorder.Body.Bytes(), &res)
require.NoError(t, err)
assert.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected")
assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected")
assert.Equal(t, testCase.expectedStatusCode, res.StatusCode, "Response status code not as expected")
assert.Empty(t, res.Message, "Message should be empty when it is successful")
assert.Equal(t, testCase.expectedTotalCount, res.TotalCount, "Total count not as expected")
}
})
}
}
3 changes: 3 additions & 0 deletions internal/core/data/infrastructure/interfaces/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ type DBClient interface {
ReadingCountByDeviceNameAndResourceNameAndTimeRange(deviceName string, resourceName string, start int, end int) (uint32, errors.EdgeX)
ReadingCountByTimeRange(start int, end int) (uint32, errors.EdgeX)
ReadingsByResourceNameAndTimeRange(resourceName string, start int, end int, offset int, limit int) ([]model.Reading, errors.EdgeX)
ReadingsByDeviceNameAndResourceNamesAndTimeRange(deviceName string, resourceNames []string, start, end, offset, limit int) ([]model.Reading, uint32, errors.EdgeX)
ReadingsByDeviceNameAndTimeRange(deviceName string, start int, end int, offset int, limit int) ([]model.Reading, errors.EdgeX)
ReadingCountByDeviceNameAndTimeRange(deviceName string, start int, end int) (uint32, errors.EdgeX)
}
80 changes: 80 additions & 0 deletions internal/core/data/infrastructure/interfaces/mocks/DBClient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading