Skip to content

Commit c5b33fc

Browse files
committed
feat: Add PATCH tags API to core-metadata
close #5317 Signed-off-by: Ginny Guan <[email protected]>
1 parent 5ff0a09 commit c5b33fc

File tree

7 files changed

+262
-6
lines changed

7 files changed

+262
-6
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/blang/semver/v4 v4.0.0
77
github.com/eclipse/paho.mqtt.golang v1.5.1
88
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45
9-
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21
9+
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22
1010
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18
1111
github.com/edgexfoundry/go-mod-secrets/v4 v4.1.0-dev.7
1212
github.com/fxamacker/cbor/v2 v2.9.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45 h1:e8zhpWhjPfDypTmPxgM
7676
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45/go.mod h1:E9iUXkxMdTMXxyAN/MAr/srf8+ZbmtV+mVJvhW6a//k=
7777
github.com/edgexfoundry/go-mod-configuration/v4 v4.1.0-dev.17 h1:TttwsEqLppEQQz4scPbEdzMtsHC8vj1djd2sgZLfny8=
7878
github.com/edgexfoundry/go-mod-configuration/v4 v4.1.0-dev.17/go.mod h1:IlEPPn0CZX1mDBRX8E6nr7BM/MVxMZV5z9zSTo6fUgo=
79-
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21 h1:gh+CoXbkXa2E3favumU513BYnB8U3ubW2zBUJpWNSwU=
80-
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21/go.mod h1:jDm9E4z9svXErYAxr0oigmVV50wmIoHaveOlP7FBkHQ=
79+
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22 h1:rV4aHYBoLvlyy9XHBDSC+cXbhiyRotF0RqZVI46Rd7I=
80+
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22/go.mod h1:jDm9E4z9svXErYAxr0oigmVV50wmIoHaveOlP7FBkHQ=
8181
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18 h1:iLlwJBZewKcoL+Ao4HQtcVrsfTtCEoGZRiNQjPGucPo=
8282
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18/go.mod h1:PcyJ06iPZfWH88+4Mmk8IJI2pDDclwdwI883wKoKocM=
8383
github.com/edgexfoundry/go-mod-registry/v4 v4.1.0-dev.8 h1:swAEoWn8rr/NXcsBaCxoY+lWSiDUfZUmTyBsi9aM/7o=

internal/core/metadata/application/deviceprofile.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,33 @@ func AllDeviceProfileBasicInfos(offset int, limit int, labels []string, dic *di.
313313
return deviceProfileBasicInfos, totalCount, nil
314314
}
315315

316+
func PatchDeviceProfileTags(profileName string, dto dtos.UpdateDeviceProfileTags, ctx context.Context, dic *di.Container) errors.EdgeX {
317+
dbClient := container.DBClientFrom(dic.Get)
318+
lc := bootstrapContainer.LoggingClientFrom(dic.Get)
319+
320+
deviceProfile, err := dbClient.DeviceProfileByName(profileName)
321+
if err != nil {
322+
return errors.NewCommonEdgeXWrapper(err)
323+
}
324+
325+
requests.ReplaceDeviceProfileModelTagsWithDTO(&deviceProfile, dto)
326+
327+
err = dbClient.UpdateDeviceProfile(deviceProfile)
328+
if err != nil {
329+
return errors.NewCommonEdgeXWrapper(err)
330+
}
331+
332+
lc.Debugf(
333+
"DeviceProfile device resources/commands tags patched on DB successfully. Correlation-ID: %s ",
334+
correlation.FromContext(ctx),
335+
)
336+
337+
profileDTO := dtos.FromDeviceProfileModelToDTO(deviceProfile)
338+
go publishUpdateDeviceProfileSystemEvent(profileDTO, ctx, dic)
339+
340+
return nil
341+
}
342+
316343
func deviceProfileByDTO(dbClient interfaces.DBClient, dto dtos.UpdateDeviceProfileBasicInfo) (deviceProfile models.DeviceProfile, err errors.EdgeX) {
317344
// The ID or Name is required by DTO and the DTO also accepts empty string ID if the Name is provided
318345
if dto.Id != nil && *dto.Id != "" {

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,34 @@ func (dc *DeviceProfileController) AllDeviceProfileBasicInfos(c echo.Context) er
404404
utils.WriteHttpHeader(w, ctx, http.StatusOK)
405405
return pkg.EncodeAndWriteResponse(response, w, lc)
406406
}
407+
408+
func (dc *DeviceProfileController) PatchDeviceProfileTags(c echo.Context) error {
409+
r := c.Request()
410+
w := c.Response()
411+
if r.Body != nil {
412+
defer func() { _ = r.Body.Close() }()
413+
}
414+
415+
lc := container.LoggingClientFrom(dc.dic.Get)
416+
417+
ctx := r.Context()
418+
419+
// URL parameters
420+
name := c.Param(common.Name)
421+
422+
var reqDTO requestDTO.DeviceProfileTagsRequest
423+
err := dc.jsonDtoReader.Read(r.Body, &reqDTO)
424+
if err != nil {
425+
return utils.WriteErrorResponse(w, ctx, lc, err, "")
426+
}
427+
428+
reqId := reqDTO.RequestId
429+
err = application.PatchDeviceProfileTags(name, reqDTO.UpdateDeviceProfileTags, ctx, dc.dic)
430+
if err != nil {
431+
return utils.WriteErrorResponse(w, ctx, lc, err, reqId)
432+
}
433+
434+
response := commonDTO.NewBaseResponse(reqId, "", http.StatusOK)
435+
utils.WriteHttpHeader(w, ctx, http.StatusOK)
436+
return pkg.EncodeAndWriteResponse(response, w, lc)
437+
}

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,3 +1605,108 @@ func TestAllDeviceProfileBasicInfos(t *testing.T) {
16051605
})
16061606
}
16071607
}
1608+
1609+
func TestPatchDeviceProfileTags(t *testing.T) {
1610+
deviceProfile := dtos.ToDeviceProfileModel(buildTestDeviceProfileRequest().Profile)
1611+
notFoundName := "notFoundName"
1612+
expectedRequestId := ExampleUUID
1613+
updateTags := map[string]any{"TestTagKey": "NewTestTagValue", "TestTagKey2": "TestTagValue2"}
1614+
testReq := requests.DeviceProfileTagsRequest{
1615+
BaseRequest: commonDTO.BaseRequest{
1616+
Versionable: commonDTO.NewVersionable(),
1617+
RequestId: ExampleUUID,
1618+
},
1619+
UpdateDeviceProfileTags: dtos.UpdateDeviceProfileTags{
1620+
DeviceResources: []dtos.UpdateTags{{Name: TestDeviceResourceName, Tags: updateTags}},
1621+
DeviceCommands: []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: updateTags}},
1622+
},
1623+
}
1624+
1625+
valid := testReq
1626+
noRequestId := valid
1627+
noRequestId.RequestId = ""
1628+
1629+
noDRName := testReq
1630+
noDRName.DeviceResources = []dtos.UpdateTags{{Tags: updateTags}}
1631+
emptyDRName := testReq
1632+
emptyDRName.DeviceResources = []dtos.UpdateTags{{Name: " ", Tags: updateTags}}
1633+
noDRTags := testReq
1634+
noDRTags.DeviceResources = []dtos.UpdateTags{{Name: TestDeviceResourceName}}
1635+
emptyDRTags := testReq
1636+
emptyDRTags.DeviceResources = []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: map[string]any{}}}
1637+
1638+
noDCName := testReq
1639+
noDCName.DeviceCommands = []dtos.UpdateTags{{Tags: updateTags}}
1640+
emptyDCName := testReq
1641+
emptyDCName.DeviceCommands = []dtos.UpdateTags{{Name: " ", Tags: updateTags}}
1642+
noDCTags := testReq
1643+
noDCTags.DeviceCommands = []dtos.UpdateTags{{Name: TestDeviceCommandName}}
1644+
1645+
emptyDCTags := testReq
1646+
emptyDCTags.DeviceCommands = []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: map[string]any{}}}
1647+
1648+
dic := mockDic()
1649+
dbClientMock := &mocks.DBClient{}
1650+
dbClientMock.On("DeviceProfileByName", deviceProfile.Name).Return(deviceProfile, nil)
1651+
dbClientMock.On("DeviceProfileByName", notFoundName).Return(deviceProfile, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "not found", nil))
1652+
dbClientMock.On("DevicesByProfileName", 0, -1, deviceProfile.Name).Return([]models.Device{{ServiceName: testDeviceServiceName}}, nil)
1653+
dbClientMock.On("DeviceCountByProfileName", deviceProfile.Name).Return(int64(1), nil)
1654+
dbClientMock.On("UpdateDeviceProfile", mock.Anything).Return(nil)
1655+
dic.Update(di.ServiceConstructorMap{
1656+
container.DBClientInterfaceName: func(get di.Get) interface{} {
1657+
return dbClientMock
1658+
},
1659+
})
1660+
1661+
controller := NewDeviceProfileController(dic)
1662+
require.NotNil(t, controller)
1663+
1664+
tests := []struct {
1665+
name string
1666+
deviceProfileName string
1667+
request requests.DeviceProfileTagsRequest
1668+
expectedStatusCode int
1669+
}{
1670+
{"valid", deviceProfile.Name, valid, http.StatusOK},
1671+
{"valid - no request id", deviceProfile.Name, noRequestId, http.StatusOK},
1672+
{"invalid - device profile not found", notFoundName, valid, http.StatusNotFound},
1673+
{"invalid - no device resource name", deviceProfile.Name, noDRName, http.StatusBadRequest},
1674+
{"invalid - empty device resource name", deviceProfile.Name, emptyDRName, http.StatusBadRequest},
1675+
{"invalid - no device resource tags", deviceProfile.Name, noDRTags, http.StatusBadRequest},
1676+
{"invalid - empty device resource tags", deviceProfile.Name, emptyDRTags, http.StatusBadRequest},
1677+
{"invalid - no device command name", deviceProfile.Name, noDCName, http.StatusBadRequest},
1678+
{"invalid - empty device command name", deviceProfile.Name, emptyDCName, http.StatusBadRequest},
1679+
{"invalid - no device command tags", deviceProfile.Name, noDRTags, http.StatusBadRequest},
1680+
{"invalid - empty device command tags", deviceProfile.Name, emptyDCTags, http.StatusBadRequest},
1681+
}
1682+
for _, testCase := range tests {
1683+
t.Run(testCase.name, func(t *testing.T) {
1684+
e := echo.New()
1685+
jsonData, err := json.Marshal(testCase.request)
1686+
require.NoError(t, err)
1687+
1688+
reader := strings.NewReader(string(jsonData))
1689+
req, err := http.NewRequest(http.MethodPatch, common.ApiDeviceProfileTagsByNameRoute, reader)
1690+
require.NoError(t, err)
1691+
1692+
// Act
1693+
recorder := httptest.NewRecorder()
1694+
c := e.NewContext(req, recorder)
1695+
c.SetParamNames(common.Name)
1696+
c.SetParamValues(testCase.deviceProfileName)
1697+
err = controller.PatchDeviceProfileTags(c)
1698+
require.NoError(t, err)
1699+
1700+
var res commonDTO.BaseResponse
1701+
err = json.Unmarshal(recorder.Body.Bytes(), &res)
1702+
require.NoError(t, err)
1703+
1704+
assert.NotEmpty(t, recorder.Body.String(), "Message is empty")
1705+
assert.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected")
1706+
assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected")
1707+
if res.RequestId != "" {
1708+
assert.Equal(t, expectedRequestId, res.RequestId, "RequestID not as expected")
1709+
}
1710+
})
1711+
}
1712+
}

internal/core/metadata/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
4343
r.GET(common.ApiDeviceProfileByManufacturerAndModelRoute, dc.DeviceProfilesByManufacturerAndModel, authenticationHook)
4444
r.PATCH(common.ApiDeviceProfileBasicInfoRoute, dc.PatchDeviceProfileBasicInfo, authenticationHook)
4545
r.GET(common.ApiAllDeviceProfileBasicInfoRoute, dc.AllDeviceProfileBasicInfos, authenticationHook)
46+
r.PATCH(common.ApiDeviceProfileTagsByNameRoute, dc.PatchDeviceProfileTags, authenticationHook)
4647

4748
// Device Resource
4849
dr := metadataController.NewDeviceResourceController(dic)

openapi/core-metadata.yaml

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ components:
304304
properties:
305305
type: object
306306
description: A map of properties required to address the given device
307-
308307
DeviceProfileBasicInfo:
309308
description: "A profile basic information"
310309
type: object
@@ -336,6 +335,32 @@ components:
336335
$ref: '#/components/schemas/DeviceProfileBasicInfo'
337336
required:
338337
- profileName
338+
UpdateTags:
339+
type: object
340+
properties:
341+
name:
342+
type: string
343+
description: device resource or device command name
344+
tags:
345+
type: object
346+
description: A map of tags to add or update
347+
required:
348+
- name
349+
- tags
350+
DeviceProfileTagsRequest:
351+
description: "Add/Update tags of device resources/device commands in an existing profile"
352+
type: object
353+
allOf:
354+
- $ref: '#/components/schemas/BaseRequest'
355+
properties:
356+
deviceResources:
357+
type: array
358+
items:
359+
$ref: '#/components/schemas/UpdateTags'
360+
deviceCommands:
361+
type: array
362+
items:
363+
$ref: '#/components/schemas/UpdateTags'
339364
MultiDeviceProfileBasicInfosResponse:
340365
allOf:
341366
- $ref: '#/components/schemas/BaseWithTotalCountResponse'
@@ -446,7 +471,7 @@ components:
446471
type: boolean
447472
description: "Indicate the visibility of the DeviceResource via a CoreCommand."
448473
tags:
449-
type: string
474+
type: object
450475
description: "Tags for adding additional information on reading level"
451476
properties:
452477
$ref: '#/components/schemas/ResourceProperties'
@@ -853,7 +878,7 @@ components:
853878
name:
854879
type: string
855880
tags:
856-
type: string
881+
type: object
857882
description: "Tags for adding additional information on event level"
858883
isHidden:
859884
type: boolean
@@ -2671,6 +2696,73 @@ paths:
26712696
examples:
26722697
500Example:
26732698
$ref: '#/components/examples/500Example'
2699+
'/deviceprofile/name/{name}/tags':
2700+
parameters:
2701+
- $ref: '#/components/parameters/correlatedRequestHeader'
2702+
- name: name
2703+
in: path
2704+
required: true
2705+
schema:
2706+
type: string
2707+
description: "The unique name of a device profile"
2708+
patch:
2709+
summary: "Allows adding/updating device resources/device commands tags field to an existing device profile. Deleting existing tags is not supported."
2710+
requestBody:
2711+
required: true
2712+
content:
2713+
application/json:
2714+
schema:
2715+
type: object
2716+
$ref: '#/components/schemas/DeviceProfileTagsRequest'
2717+
example:
2718+
apiVersion: "v3"
2719+
requestId: "2463bff9-aa53-4bc4-bebf-42fe81146ea8"
2720+
deviceResources:
2721+
- name: "Float32"
2722+
tags:
2723+
tag1: "field1Value"
2724+
deviceCommands:
2725+
- name: "Float32"
2726+
tags:
2727+
tag2:
2728+
field3: "field3Value"
2729+
responses:
2730+
'200':
2731+
description: "Update successful"
2732+
headers:
2733+
X-Correlation-ID:
2734+
$ref: '#/components/headers/correlatedResponseHeader'
2735+
content:
2736+
application/json:
2737+
schema:
2738+
$ref: '#/components/schemas/BaseResponse'
2739+
examples:
2740+
200Example:
2741+
$ref: '#/components/examples/200Example'
2742+
'400':
2743+
description: "Request is in an invalid state"
2744+
headers:
2745+
X-Correlation-ID:
2746+
$ref: '#/components/headers/correlatedResponseHeader'
2747+
content:
2748+
application/json:
2749+
schema:
2750+
$ref: '#/components/schemas/ErrorResponse'
2751+
examples:
2752+
400Example:
2753+
$ref: '#/components/examples/400Example'
2754+
'500':
2755+
description: An unexpected error occurred on the server
2756+
headers:
2757+
X-Correlation-ID:
2758+
$ref: '#/components/headers/correlatedResponseHeader'
2759+
content:
2760+
application/json:
2761+
schema:
2762+
$ref: '#/components/schemas/ErrorResponse'
2763+
examples:
2764+
500Example:
2765+
$ref: '#/components/examples/500Example'
26742766
'/deviceprofile/basicinfo':
26752767
parameters:
26762768
- $ref: '#/components/parameters/correlatedRequestHeader'

0 commit comments

Comments
 (0)