Skip to content

Commit a1c5a89

Browse files
feat(metadata): Add query parameters for device parents/children. (#5053)
* feat(metadata): Add query parameters for device parents/children. Closes: #4769 Signed-off-by: Corey Mutter <CoreyMutter@eaton.com> * fix: Repair a broken UT due to the added check in device deletion. Signed-off-by: Corey Mutter <CoreyMutter@eaton.com> * fix: Stop tree queries if they get into a loop, which could happen if a device was its own parent. Also try to keep that from happening in the first place. Signed-off-by: Corey Mutter <CoreyMutter@eaton.com> * fix: Add unit tests for tree queries and add/update/delete checks. Signed-off-by: Corey Mutter <CoreyMutter@eaton.com> * fix: gofmt Signed-off-by: Corey Mutter <CoreyMutter@eaton.com> --------- Signed-off-by: Corey Mutter <CoreyMutter@eaton.com>
1 parent f9a1cad commit a1c5a89

File tree

11 files changed

+335
-30
lines changed

11 files changed

+335
-30
lines changed

internal/core/metadata/application/device.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func AddDevice(d models.Device, ctx context.Context, dic *di.Container, bypassVa
5454
return id, errors.NewCommonEdgeX(errors.KindContractInvalid, fmt.Sprintf("device service '%s' does not exists", d.ServiceName), nil)
5555
}
5656

57-
err := validateProfileAndAutoEvent(dic, d)
57+
err := validateParentProfileAndAutoEvent(dic, d)
5858
if err != nil {
5959
return "", errors.NewCommonEdgeXWrapper(err)
6060
}
@@ -147,6 +147,13 @@ func DeleteDeviceByName(name string, ctx context.Context, dic *di.Container) err
147147
if err != nil {
148148
return errors.NewCommonEdgeXWrapper(err)
149149
}
150+
childcount, _, err := dbClient.DeviceTree(name, 1, 0, 1, nil)
151+
if err != nil {
152+
return errors.NewCommonEdgeXWrapper(err)
153+
}
154+
if childcount != 0 {
155+
return errors.NewCommonEdgeX(errors.KindStatusConflict, "cannot delete device with children", nil)
156+
}
150157
err = dbClient.DeleteDeviceByName(name)
151158
if err != nil {
152159
return errors.NewCommonEdgeXWrapper(err)
@@ -225,7 +232,7 @@ func PatchDevice(dto dtos.UpdateDevice, ctx context.Context, dic *di.Container,
225232

226233
requests.ReplaceDeviceModelFieldsWithDTO(&device, dto)
227234

228-
err = validateProfileAndAutoEvent(dic, device)
235+
err = validateParentProfileAndAutoEvent(dic, device)
229236
if err != nil {
230237
return errors.NewCommonEdgeXWrapper(err)
231238
}
@@ -295,22 +302,30 @@ func deviceByDTO(dbClient interfaces.DBClient, dto dtos.UpdateDevice) (device mo
295302
}
296303

297304
// AllDevices query the devices with offset, limit, and labels
298-
func AllDevices(offset int, limit int, labels []string, dic *di.Container) (devices []dtos.Device, totalCount uint32, err errors.EdgeX) {
305+
func AllDevices(offset int, limit int, labels []string, parent string, maxLevels int, dic *di.Container) (devices []dtos.Device, totalCount uint32, err errors.EdgeX) {
299306
dbClient := container.DBClientFrom(dic.Get)
307+
var deviceModels []models.Device
308+
if parent != "" {
309+
totalCount, deviceModels, err = dbClient.DeviceTree(parent, maxLevels, offset, limit, labels)
310+
if err != nil {
311+
return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
312+
}
313+
} else {
314+
totalCount, err = dbClient.DeviceCountByLabels(labels)
315+
if err != nil {
316+
return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
317+
}
318+
cont, err := utils.CheckCountRange(totalCount, offset, limit)
319+
if !cont {
320+
return []dtos.Device{}, totalCount, err
321+
}
300322

301-
totalCount, err = dbClient.DeviceCountByLabels(labels)
302-
if err != nil {
303-
return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
304-
}
305-
cont, err := utils.CheckCountRange(totalCount, offset, limit)
306-
if !cont {
307-
return []dtos.Device{}, totalCount, err
323+
deviceModels, err = dbClient.AllDevices(offset, limit, labels)
324+
if err != nil {
325+
return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
326+
}
308327
}
309328

310-
deviceModels, err := dbClient.AllDevices(offset, limit, labels)
311-
if err != nil {
312-
return devices, totalCount, errors.NewCommonEdgeXWrapper(err)
313-
}
314329
devices = make([]dtos.Device, len(deviceModels))
315330
for i, d := range deviceModels {
316331
devices[i] = dtos.FromDeviceModelToDTO(d)
@@ -361,11 +376,14 @@ func DevicesByProfileName(offset int, limit int, profileName string, dic *di.Con
361376

362377
var noMessagingClientError = goErrors.New("MessageBus Client not available. Please update RequireMessageBus and MessageBus configuration to enable sending System Events via the EdgeX MessageBus")
363378

364-
func validateProfileAndAutoEvent(dic *di.Container, d models.Device) errors.EdgeX {
379+
func validateParentProfileAndAutoEvent(dic *di.Container, d models.Device) errors.EdgeX {
365380
if d.ProfileName == "" {
366381
// if the profile is not set, skip the validation until we have the profile
367382
return nil
368383
}
384+
if (d.Name == d.Parent) && (d.Name != "") {
385+
return errors.NewCommonEdgeX(errors.KindContractInvalid, "a device cannot be its own parent", nil)
386+
}
369387
dbClient := container.DBClientFrom(dic.Get)
370388
dp, err := dbClient.DeviceProfileByName(d.ProfileName)
371389
if err != nil {

internal/core/metadata/application/device_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"github.com/stretchr/testify/require"
2525
)
2626

27-
func TestValidateProfileAndAutoEvents(t *testing.T) {
27+
func TestValidateParentProfileAndAutoEvents(t *testing.T) {
2828
profile := "test-profile"
2929
notFountProfileName := "notFoundProfile"
3030
source1 := "source1"
@@ -107,10 +107,18 @@ func TestValidateProfileAndAutoEvents(t *testing.T) {
107107
},
108108
false,
109109
},
110+
{"is own parent",
111+
models.Device{
112+
ProfileName: profile,
113+
Parent: "me",
114+
Name: "me",
115+
},
116+
true,
117+
},
110118
}
111119
for _, testCase := range tests {
112120
t.Run(testCase.name, func(t *testing.T) {
113-
err := validateProfileAndAutoEvent(dic, testCase.device)
121+
err := validateParentProfileAndAutoEvent(dic, testCase.device)
114122
if testCase.errorExpected {
115123
assert.Error(t, err)
116124
} else {

internal/core/metadata/controller/http/device.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,15 @@ func (dc *DeviceController) AllDevices(c echo.Context) error {
235235
if err != nil {
236236
return utils.WriteErrorResponse(w, ctx, lc, err, "")
237237
}
238-
devices, totalCount, err := application.AllDevices(offset, limit, labels, dc.dic)
238+
parent := utils.ParseQueryStringToString(r, common.DescendantsOf, "")
239+
levels, err := utils.ParseQueryStringToInt(c, common.MaxLevels, 0, -1, math.MaxInt32)
240+
if err != nil {
241+
return utils.WriteErrorResponse(w, ctx, lc, err, "")
242+
}
243+
if levels < 0 {
244+
levels = math.MaxInt32
245+
}
246+
devices, totalCount, err := application.AllDevices(offset, limit, labels, parent, levels, dc.dic)
239247
if err != nil {
240248
return utils.WriteErrorResponse(w, ctx, lc, err, "")
241249
}

internal/core/metadata/controller/http/device_test.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/json"
1010
"errors"
1111
"fmt"
12+
"math"
1213
"net/http"
1314
"net/http/httptest"
1415
"strings"
@@ -183,6 +184,8 @@ func TestAddDevice(t *testing.T) {
183184
dbClientMock.On("AddDevice", emptyProtocolsModel).Return(emptyProtocolsModel, nil)
184185
invalidProtocols := testDevice
185186
invalidProtocols.Device.Protocols = map[string]dtos.ProtocolProperties{"others": {}}
187+
ownParent := testDevice
188+
ownParent.Device.Parent = ownParent.Device.Name
186189

187190
dic.Update(di.ServiceConstructorMap{
188191
container.DBClientInterfaceName: func(get di.Get) interface{} {
@@ -217,6 +220,7 @@ func TestAddDevice(t *testing.T) {
217220
{"Invalid - not found device service", []requests.AddDeviceRequest{notFoundService}, http.StatusMultiStatus, http.StatusBadRequest, false, false, false},
218221
{"Invalid - device service unavailable", []requests.AddDeviceRequest{valid}, http.StatusMultiStatus, http.StatusServiceUnavailable, true, false, false},
219222
{"Valid - force add device", []requests.AddDeviceRequest{validForceAdd}, http.StatusMultiStatus, http.StatusCreated, false, true, true},
223+
{"Invalid - own parent", []requests.AddDeviceRequest{ownParent}, http.StatusMultiStatus, http.StatusBadRequest, false, false, false},
220224
}
221225
for _, testCase := range tests {
222226
t.Run(testCase.name, func(t *testing.T) {
@@ -319,14 +323,18 @@ func TestDeleteDeviceByName(t *testing.T) {
319323
device := dtos.ToDeviceModel(buildTestDeviceRequest().Device)
320324
noName := ""
321325
notFoundName := "notFoundName"
326+
deviceParent := device
327+
deviceParent.Name = "someOtherName"
322328

323329
dic := mockDic()
324330
dbClientMock := &dbMock.DBClient{}
331+
dbClientMock.On("DeviceTree", device.Name, 1, 0, 1, []string(nil)).Return(uint32(0), nil, nil)
325332
dbClientMock.On("DeleteDeviceByName", device.Name).Return(nil)
326333
dbClientMock.On("DeleteDeviceByName", notFoundName).Return(edgexErr.NewCommonEdgeX(edgexErr.KindEntityDoesNotExist, "device doesn't exist in the database", nil))
327334
dbClientMock.On("DeviceByName", notFoundName).Return(device, edgexErr.NewCommonEdgeX(edgexErr.KindEntityDoesNotExist, "device doesn't exist in the database", nil))
328335
dbClientMock.On("DeviceByName", device.Name).Return(device, nil)
329-
dbClientMock.On("DeviceServiceByName", device.ServiceName).Return(models.DeviceService{BaseAddress: testBaseAddress}, nil)
336+
dbClientMock.On("DeviceByName", deviceParent.Name).Return(device, nil)
337+
dbClientMock.On("DeviceTree", deviceParent.Name, 1, 0, 1, []string(nil)).Return(uint32(1), []models.Device{device}, nil)
330338
dic.Update(di.ServiceConstructorMap{
331339
container.DBClientInterfaceName: func(get di.Get) interface{} {
332340
return dbClientMock
@@ -344,6 +352,7 @@ func TestDeleteDeviceByName(t *testing.T) {
344352
{"Valid - delete device by name", device.Name, http.StatusOK},
345353
{"Invalid - name parameter is empty", noName, http.StatusBadRequest},
346354
{"Invalid - device not found by name", notFoundName, http.StatusNotFound},
355+
{"Invalid - device has children", deviceParent.Name, http.StatusConflict},
347356
}
348357
for _, testCase := range tests {
349358
t.Run(testCase.name, func(t *testing.T) {
@@ -603,14 +612,17 @@ func TestPatchDevice(t *testing.T) {
603612
notFoundService.Device.ServiceName = &notFoundServiceName
604613
dbClientMock.On("DeviceServiceNameExists", *notFoundService.Device.ServiceName).Return(false, nil)
605614

606-
notFountProfileName := "notFoundProfile"
615+
notFoundProfileName := "notFoundProfile"
607616
notFoundProfile := testReq
608-
notFoundProfile.Device.ProfileName = &notFountProfileName
617+
notFoundProfile.Device.ProfileName = &notFoundProfileName
609618
notFoundProfileDeviceModel := dsModels
610-
notFoundProfileDeviceModel.ProfileName = notFountProfileName
619+
notFoundProfileDeviceModel.ProfileName = notFoundProfileName
611620
dbClientMock.On("UpdateDevice", notFoundProfileDeviceModel).Return(
612621
edgexErr.NewCommonEdgeX(edgexErr.KindEntityDoesNotExist,
613-
fmt.Sprintf("device profile '%s' does not exists", notFountProfileName), nil))
622+
fmt.Sprintf("device profile '%s' does not exists", notFoundProfileName), nil))
623+
624+
ownParent := testReq
625+
ownParent.Device.Parent = ownParent.Device.Name
614626

615627
dic.Update(di.ServiceConstructorMap{
616628
container.DBClientInterfaceName: func(get di.Get) interface{} {
@@ -642,7 +654,8 @@ func TestPatchDevice(t *testing.T) {
642654
{"Invalid - invalid protocols", []requests.UpdateDeviceRequest{invalidProtocols}, http.StatusMultiStatus, http.StatusInternalServerError, true, false},
643655
{"Invalid - not found device service", []requests.UpdateDeviceRequest{notFoundService}, http.StatusMultiStatus, http.StatusBadRequest, false, false},
644656
{"Invalid - device service unavailable", []requests.UpdateDeviceRequest{valid}, http.StatusMultiStatus, http.StatusServiceUnavailable, true, false},
645-
{"Valid - empty profile", []requests.UpdateDeviceRequest{emptyProfile}, http.StatusMultiStatus, http.StatusOK, true, true}}
657+
{"Valid - empty profile", []requests.UpdateDeviceRequest{emptyProfile}, http.StatusMultiStatus, http.StatusOK, true, true},
658+
{"Invalid - own parent", []requests.UpdateDeviceRequest{ownParent}, http.StatusMultiStatus, http.StatusBadRequest, false, false}}
646659
for _, testCase := range tests {
647660
t.Run(testCase.name, func(t *testing.T) {
648661
e := echo.New()
@@ -749,6 +762,8 @@ func TestAllDevices(t *testing.T) {
749762
dbClientMock.On("AllDevices", 0, 5, testDeviceLabels).Return([]models.Device{devices[0], devices[1]}, nil)
750763
dbClientMock.On("AllDevices", 1, 2, []string(nil)).Return([]models.Device{devices[1], devices[2]}, nil)
751764
dbClientMock.On("AllDevices", 4, 1, testDeviceLabels).Return([]models.Device{}, edgexErr.NewCommonEdgeX(edgexErr.KindRangeNotSatisfiable, "query objects bounds out of range.", nil))
765+
dbClientMock.On("DeviceTree", "foo", 4, 0, 10, []string(nil)).Return(uint32(expectedDeviceTotalCount), devices, nil)
766+
dbClientMock.On("DeviceTree", "foo", math.MaxInt32, 0, 10, testDeviceLabels).Return(uint32(expectedDeviceTotalCount), devices, nil)
752767
dic.Update(di.ServiceConstructorMap{
753768
container.DBClientInterfaceName: func(get di.Get) interface{} {
754769
return dbClientMock
@@ -762,15 +777,20 @@ func TestAllDevices(t *testing.T) {
762777
offset string
763778
limit string
764779
labels string
780+
descendantsOf string
781+
maxLevels string
765782
errorExpected bool
766783
expectedCount int
767784
expectedTotalCount uint32
768785
expectedStatusCode int
769786
}{
770-
{"Valid - get devices without labels", "0", "10", "", false, 3, expectedDeviceTotalCount, http.StatusOK},
771-
{"Valid - get devices with labels", "0", "5", strings.Join(testDeviceLabels, ","), false, 2, expectedDeviceTotalCount, http.StatusOK},
772-
{"Valid - get devices with offset and no labels", "1", "2", "", false, 2, expectedDeviceTotalCount, http.StatusOK},
773-
{"Invalid - offset out of range", "4", "1", strings.Join(testDeviceLabels, ","), true, 0, expectedDeviceTotalCount, http.StatusRequestedRangeNotSatisfiable},
787+
{"Valid - get devices without labels", "0", "10", "", "", "", false, 3, expectedDeviceTotalCount, http.StatusOK},
788+
{"Valid - get devices with labels", "0", "5", strings.Join(testDeviceLabels, ","), "", "", false, 2, expectedDeviceTotalCount, http.StatusOK},
789+
{"Valid - get devices with offset and no labels", "1", "2", "", "", "", false, 2, expectedDeviceTotalCount, http.StatusOK},
790+
{"Invalid - offset out of range", "4", "1", strings.Join(testDeviceLabels, ","), "", "", true, 0, expectedDeviceTotalCount, http.StatusRequestedRangeNotSatisfiable},
791+
{"Valid - get tree without labels", "0", "10", "", "foo", "4", false, 3, expectedDeviceTotalCount, http.StatusOK},
792+
{"Valid - get tree with labels", "0", "10", strings.Join(testDeviceLabels, ","), "foo", "-1", false, 3, expectedDeviceTotalCount, http.StatusOK},
793+
{"Invalid - maxLevels bad integer", "4", "1", strings.Join(testDeviceLabels, ","), "foo", "bar", true, 0, 0, http.StatusBadRequest},
774794
}
775795
for _, testCase := range tests {
776796
t.Run(testCase.name, func(t *testing.T) {
@@ -782,6 +802,12 @@ func TestAllDevices(t *testing.T) {
782802
if len(testCase.labels) > 0 {
783803
query.Add(common.Labels, testCase.labels)
784804
}
805+
if testCase.descendantsOf != "" {
806+
query.Add(common.DescendantsOf, testCase.descendantsOf)
807+
}
808+
if testCase.maxLevels != "" {
809+
query.Add(common.MaxLevels, testCase.maxLevels)
810+
}
785811
req.URL.RawQuery = query.Encode()
786812
require.NoError(t, err)
787813

internal/core/metadata/infrastructure/interfaces/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type DBClient interface {
5353
DeviceCountByLabels(labels []string) (uint32, errors.EdgeX)
5454
DeviceCountByProfileName(profileName string) (uint32, errors.EdgeX)
5555
DeviceCountByServiceName(serviceName string) (uint32, errors.EdgeX)
56-
56+
DeviceTree(parent string, levels int, offset int, limit int, labels []string) (uint32, []model.Device, errors.EdgeX)
5757
AddProvisionWatcher(pw model.ProvisionWatcher) (model.ProvisionWatcher, errors.EdgeX)
5858
ProvisionWatcherById(id string) (model.ProvisionWatcher, errors.EdgeX)
5959
ProvisionWatcherByName(name string) (model.ProvisionWatcher, errors.EdgeX)

internal/core/metadata/infrastructure/interfaces/mocks/DBClient.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/pkg/infrastructure/postgres/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const (
8484
categoriesField = "Categories"
8585
createdField = "Created"
8686
labelsField = "Labels"
87+
parentField = "Parent"
8788
manufacturerField = "Manufacturer"
8889
modelField = "Model"
8990
nameField = "Name"

0 commit comments

Comments
 (0)