refactor: use query parameters for data-object attribute APIs

- read token and field from query parameters
- default missing or empty fields to value
- expose attribute query and update through GET and PATCH routes
- return true only for measurement mode 1
- add query parsing and measurement mode test coverage
This commit is contained in:
douxu 2026-07-17 16:40:34 +08:00
parent 38f21f3cda
commit ab029e292b
3 changed files with 114 additions and 56 deletions

View File

@ -24,16 +24,9 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient() pgClient := database.GetPostgresDBClient()
token := c.Param("token") token, field, err := parseDataObjectAttributeQuery(c)
field := c.Param("field") if err != nil {
if token == "" { logger.Error(ctx, "query token from query parameters failed", "error", err, "url", c.Request.RequestURI)
err := fmt.Errorf("tokens is missing from the path")
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
} else if field == "" {
err := fmt.Errorf("field is missing from the path")
logger.Error(ctx, "query field from path failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
} }
@ -45,8 +38,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
return return
} }
normalizedField := strings.ToLower(field) if err := validateDataObjectField(token, dataObjectType, field); err != nil {
if err := validateDataObjectField(token, dataObjectType, normalizedField); err != nil {
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err) logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
@ -94,7 +86,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
case constants.DataObjectTypeParameter: case constants.DataObjectTypeParameter:
value, err := buildParameterAttributeValue( value, err := buildParameterAttributeValue(
ctx, ctx,
normalizedField, field,
parameter, parameter,
func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) { func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) {
return database.QueryParameterDataObjectValue(ctx, pgClient, parameter) return database.QueryParameterDataObjectValue(ctx, pgClient, parameter)
@ -117,7 +109,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
result := dataObjectAttributeQueryResult{ result := dataObjectAttributeQueryResult{
Token: token, Token: token,
Field: normalizedField, Field: field,
Code: errcode.ErrProcessSuccess.Code(), Code: errcode.ErrProcessSuccess.Code(),
Msg: errcode.ErrProcessSuccess.Msg(), Msg: errcode.ErrProcessSuccess.Msg(),
Value: value, Value: value,
@ -128,7 +120,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
case constants.DataObjectTypeMeasurement: case constants.DataObjectTypeMeasurement:
value, err := buildMeasurementAttributeValue( value, err := buildMeasurementAttributeValue(
ctx, ctx,
normalizedField, field,
measurement, measurement,
measurementComponent, measurementComponent,
queryMeasurementRealtimeValue, queryMeasurementRealtimeValue,
@ -147,7 +139,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
result := dataObjectAttributeQueryResult{ result := dataObjectAttributeQueryResult{
Token: token, Token: token,
Field: normalizedField, Field: field,
Code: errcode.ErrProcessSuccess.Code(), Code: errcode.ErrProcessSuccess.Code(),
Msg: errcode.ErrProcessSuccess.Msg(), Msg: errcode.ErrProcessSuccess.Msg(),
Value: value, Value: value,
@ -160,6 +152,19 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
} }
} }
func parseDataObjectAttributeQuery(c *gin.Context) (string, string, error) {
token := c.Query("token")
if token == "" {
return "", "", fmt.Errorf("token is missing from query parameters")
}
field := strings.ToLower(c.Query("field"))
if field == "" {
field = "value"
}
return token, field, nil
}
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error) type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error) type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error)
@ -285,15 +290,7 @@ func buildMeasurementAttributeValue(
} }
return loadValue(ctx, measurement.DataSource) return loadValue(ctx, measurement.DataSource)
case "mode": case "mode":
fmt.Println("measurement.Mode:", measurement.Mode) return measurement.Mode == 1, nil
switch measurement.Mode {
case 0:
return false, nil
case 1:
return true, nil
default:
return nil, fmt.Errorf("invalid measurement mode %d: expected 0 or 1", measurement.Mode)
}
case "meta": case "meta":
return "MEASUREMENT", nil return "MEASUREMENT", nil
case "type": case "type":

View File

@ -2,6 +2,8 @@ package handler
import ( import (
"context" "context"
"net/http"
"net/http/httptest"
"testing" "testing"
"modelRT/common" "modelRT/common"
@ -9,10 +11,75 @@ import (
"modelRT/database" "modelRT/database"
"modelRT/orm" "modelRT/orm"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestParseDataObjectAttributeQuery(t *testing.T) {
const token = "nspath.component.bay.measurement"
tests := []struct {
name string
target string
wantToken string
wantField string
wantErr string
}{
{
name: "reads token and field from query parameters",
target: "/data-object/attribute?token=" + token + "&field=NAME",
wantToken: token,
wantField: "name",
},
{
name: "defaults missing field to value",
target: "/data-object/attribute?token=" + token,
wantToken: token,
wantField: "value",
},
{
name: "defaults empty field to value",
target: "/data-object/attribute?token=" + token + "&field=",
wantToken: token,
wantField: "value",
},
{
name: "rejects missing token",
target: "/data-object/attribute?field=value",
wantErr: "token is missing from query parameters",
},
{
name: "rejects empty token",
target: "/data-object/attribute?token=&field=value",
wantErr: "token is missing from query parameters",
},
{
name: "does not read legacy path parameters",
target: "/data-object/attribute/" + token + "/value",
wantErr: "token is missing from query parameters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = httptest.NewRequest(http.MethodGet, tt.target, nil)
token, field, err := parseDataObjectAttributeQuery(ctx)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
assert.Empty(t, token)
assert.Empty(t, field)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantToken, token)
assert.Equal(t, tt.wantField, field)
})
}
}
func TestValidateDataObjectField(t *testing.T) { func TestValidateDataObjectField(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -182,34 +249,28 @@ func TestBuildMeasurementAttributeValueRejectsUnsupportedField(t *testing.T) {
func TestBuildMeasurementAttributeValueMode(t *testing.T) { func TestBuildMeasurementAttributeValueMode(t *testing.T) {
component := &orm.Component{} component := &orm.Component{}
tests := []struct {
name string
mode int16
expected bool
}{
{name: "automatic mode", mode: 1, expected: true},
{name: "manual mode", mode: 0, expected: false},
{name: "other positive mode", mode: 2, expected: false},
{name: "negative mode", mode: -1, expected: false},
}
manualMode, err := buildMeasurementAttributeValue( for _, tt := range tests {
context.Background(), t.Run(tt.name, func(t *testing.T) {
"mode", actual, err := buildMeasurementAttributeValue(
&orm.Measurement{Mode: 0}, context.Background(),
component, "mode",
nil, &orm.Measurement{Mode: tt.mode},
) component,
require.NoError(t, err) nil,
assert.Equal(t, false, manualMode) )
require.NoError(t, err)
automaticMode, err := buildMeasurementAttributeValue( assert.Equal(t, tt.expected, actual)
context.Background(), })
"mode", }
&orm.Measurement{Mode: 1},
component,
nil,
)
require.NoError(t, err)
assert.Equal(t, true, automaticMode)
_, err = buildMeasurementAttributeValue(
context.Background(),
"mode",
&orm.Measurement{Mode: 2},
component,
nil,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "expected 0 or 1")
} }

View File

@ -11,6 +11,6 @@ import (
func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) { func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) {
g := rg.Group("/data-object/") g := rg.Group("/data-object/")
g.Use(middlewares...) g.Use(middlewares...)
g.GET("attribute/:token/:field", handler.DataObjectAttributeQueryHandler) g.GET("attribute", handler.DataObjectAttributeQueryHandler)
g.POST("update", handler.DataObjectAttributeUpdateHandler) g.PATCH("attribute", handler.DataObjectAttributeUpdateHandler)
} }