2026-07-15 17:33:55 +08:00
|
|
|
// Package handler provides HTTP handlers for various endpoints.
|
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-30 15:32:22 +08:00
|
|
|
"encoding/json"
|
2026-07-15 17:33:55 +08:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-07-30 15:32:22 +08:00
|
|
|
"strconv"
|
2026-07-15 17:33:55 +08:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"modelRT/common"
|
|
|
|
|
"modelRT/common/errcode"
|
|
|
|
|
"modelRT/constants"
|
|
|
|
|
"modelRT/diagram"
|
|
|
|
|
"modelRT/logger"
|
|
|
|
|
"modelRT/model"
|
|
|
|
|
"modelRT/orm"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-07-30 15:32:22 +08:00
|
|
|
"github.com/redis/go-redis/v9"
|
2026-07-15 17:33:55 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DataObjectAttributeQueryHandler define data object attribute value query process API
|
|
|
|
|
func DataObjectAttributeQueryHandler(c *gin.Context) {
|
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
|
|
2026-07-17 16:40:34 +08:00
|
|
|
token, field, err := parseDataObjectAttributeQuery(c)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error(ctx, "query token from query parameters failed", "error", err, "url", c.Request.RequestURI)
|
2026-07-15 17:33:55 +08:00
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 11:32:40 +08:00
|
|
|
if err := validateDataObjectField(dataObjectType, field); err != nil {
|
2026-07-16 16:04:39 +08:00
|
|
|
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
value, err := queryDataObjectAttributeValue(
|
|
|
|
|
ctx,
|
|
|
|
|
dataObjectType,
|
|
|
|
|
token,
|
|
|
|
|
field,
|
|
|
|
|
loadDataObjectHashField,
|
|
|
|
|
loadMeasurementValueMetadata,
|
|
|
|
|
queryMeasurementRealtimeValue,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if isDataObjectTokenNotFound(err) {
|
|
|
|
|
logger.Warn(ctx, "query data-object token from redis failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
2026-07-16 16:04:39 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
logger.Error(ctx, "query data-object attribute from redis failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeServerError, dataObjectAttributeFailureMessage(dataObjectType), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-15 17:33:55 +08:00
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
result := dataObjectAttributeQueryResult{
|
|
|
|
|
Token: token,
|
|
|
|
|
Field: field,
|
|
|
|
|
Code: errcode.ErrProcessSuccess.Code(),
|
|
|
|
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
|
|
|
|
Value: value,
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
2026-07-30 15:32:22 +08:00
|
|
|
renderRespSuccess(c, constants.RespCodeSuccess, dataObjectAttributeSuccessMessage(dataObjectType), map[string]any{
|
|
|
|
|
"attributes": []dataObjectAttributeQueryResult{result},
|
|
|
|
|
})
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 16:40:34 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
type dataObjectHashFieldLoader func(context.Context, constants.DataObjectType, string, string) (string, error)
|
2026-07-15 17:33:55 +08:00
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
type measurementValueMetadataLoader func(context.Context, string) (orm.JSONMap, int, error)
|
2026-07-16 16:04:39 +08:00
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
type measurementValueLoader func(context.Context, orm.JSONMap, int) (any, error)
|
2026-07-16 16:04:39 +08:00
|
|
|
|
|
|
|
|
var measurementDataObjectFields = map[string]struct{}{
|
|
|
|
|
"value": {},
|
|
|
|
|
"mode": {},
|
|
|
|
|
"meta": {},
|
|
|
|
|
"type": {},
|
|
|
|
|
"name": {},
|
|
|
|
|
"description": {},
|
|
|
|
|
"id": {},
|
|
|
|
|
"size": {},
|
|
|
|
|
"data_source": {},
|
|
|
|
|
"event_plan": {},
|
|
|
|
|
"binding": {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parameterDataObjectFields = map[string]struct{}{
|
|
|
|
|
"value": {},
|
|
|
|
|
"name": {},
|
|
|
|
|
"meta": {},
|
|
|
|
|
"type": {},
|
|
|
|
|
"description": {},
|
|
|
|
|
"id": {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:33:55 +08:00
|
|
|
type dataObjectAttributeQueryResult struct {
|
|
|
|
|
Token string `json:"token"`
|
|
|
|
|
Field string `json:"field"`
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
Value any `json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 11:32:40 +08:00
|
|
|
func validateDataObjectField(dataObjectType constants.DataObjectType, field string) error {
|
2026-07-16 16:04:39 +08:00
|
|
|
field = strings.ToLower(field)
|
|
|
|
|
switch dataObjectType {
|
|
|
|
|
case constants.DataObjectTypeMeasurement:
|
|
|
|
|
if _, ok := measurementDataObjectFields[field]; ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
|
|
|
|
case constants.DataObjectTypeParameter:
|
|
|
|
|
if _, ok := parameterDataObjectFields[field]; ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("invalid data object type %q", dataObjectType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
func queryDataObjectAttributeValue(
|
2026-07-16 16:04:39 +08:00
|
|
|
ctx context.Context,
|
2026-07-30 15:32:22 +08:00
|
|
|
dataObjectType constants.DataObjectType,
|
|
|
|
|
token string,
|
2026-07-16 16:04:39 +08:00
|
|
|
field string,
|
2026-07-30 15:32:22 +08:00
|
|
|
loadHashField dataObjectHashFieldLoader,
|
|
|
|
|
loadMeasurementMetadata measurementValueMetadataLoader,
|
|
|
|
|
loadMeasurementValue measurementValueLoader,
|
2026-07-16 16:04:39 +08:00
|
|
|
) (any, error) {
|
2026-07-30 15:32:22 +08:00
|
|
|
if dataObjectType == constants.DataObjectTypeMeasurement && field == "value" {
|
|
|
|
|
if loadMeasurementMetadata == nil {
|
|
|
|
|
return nil, fmt.Errorf("measurement value metadata loader is nil")
|
|
|
|
|
}
|
|
|
|
|
if loadMeasurementValue == nil {
|
|
|
|
|
return nil, fmt.Errorf("measurement value loader is nil")
|
|
|
|
|
}
|
|
|
|
|
dataSource, size, err := loadMeasurementMetadata(ctx, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return loadMeasurementValue(ctx, dataSource, size)
|
2026-07-16 16:04:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
if loadHashField == nil {
|
|
|
|
|
return nil, fmt.Errorf("data-object hash field loader is nil")
|
|
|
|
|
}
|
|
|
|
|
rawValue, err := loadHashField(ctx, dataObjectType, token, field)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if dataObjectType == constants.DataObjectTypeParameter && field == "value" {
|
|
|
|
|
attributeType, err := loadHashField(ctx, dataObjectType, token, "type")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-07-16 16:04:39 +08:00
|
|
|
}
|
2026-07-30 15:32:22 +08:00
|
|
|
return decodeParameterHashValue(rawValue, attributeType)
|
2026-07-16 16:04:39 +08:00
|
|
|
}
|
2026-07-30 15:32:22 +08:00
|
|
|
return decodeDataObjectHashField(dataObjectType, field, rawValue)
|
2026-07-16 16:04:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
func loadDataObjectHashField(
|
2026-07-15 17:33:55 +08:00
|
|
|
ctx context.Context,
|
2026-07-30 15:32:22 +08:00
|
|
|
dataObjectType constants.DataObjectType,
|
|
|
|
|
token string,
|
2026-07-15 17:33:55 +08:00
|
|
|
field string,
|
2026-07-30 15:32:22 +08:00
|
|
|
) (string, error) {
|
|
|
|
|
rdb := diagram.GetRedisClientInstance()
|
|
|
|
|
if rdb == nil {
|
|
|
|
|
return "", fmt.Errorf("redis client is not initialized")
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
2026-07-31 13:39:18 +08:00
|
|
|
canonicalKey, err := model.ResolveDataObjectRedisKey(ctx, rdb, dataObjectType, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
value, err := rdb.HGet(ctx, canonicalKey, field).Result()
|
2026-07-30 15:32:22 +08:00
|
|
|
if errors.Is(err, redis.Nil) {
|
2026-07-31 13:39:18 +08:00
|
|
|
return "", fmt.Errorf("canonical redis data-object hash %q does not contain field %q", canonicalKey, field)
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
if err != nil {
|
2026-07-31 13:39:18 +08:00
|
|
|
return "", fmt.Errorf("query canonical redis hash %q field %q: %w", canonicalKey, field, err)
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadMeasurementValueMetadata(ctx context.Context, token string) (orm.JSONMap, int, error) {
|
|
|
|
|
rdb := diagram.GetRedisClientInstance()
|
|
|
|
|
if rdb == nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("redis client is not initialized")
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
2026-07-31 13:39:18 +08:00
|
|
|
canonicalKey, err := model.ResolveDataObjectRedisKey(ctx, rdb, constants.DataObjectTypeMeasurement, token)
|
2026-07-30 15:32:22 +08:00
|
|
|
if err != nil {
|
2026-07-31 13:39:18 +08:00
|
|
|
return nil, 0, err
|
|
|
|
|
}
|
|
|
|
|
values, err := rdb.HMGet(ctx, canonicalKey, "data_source", "size").Result()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("query canonical redis hash %q measurement value metadata: %w", canonicalKey, err)
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
if len(values) != 2 {
|
2026-07-31 13:39:18 +08:00
|
|
|
return nil, 0, fmt.Errorf("canonical redis hash %q returned %d measurement metadata fields", canonicalKey, len(values))
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
if values[0] == nil || values[1] == nil {
|
|
|
|
|
missingFields := make([]string, 0, 2)
|
|
|
|
|
if values[0] == nil {
|
|
|
|
|
missingFields = append(missingFields, "data_source")
|
|
|
|
|
}
|
|
|
|
|
if values[1] == nil {
|
|
|
|
|
missingFields = append(missingFields, "size")
|
|
|
|
|
}
|
|
|
|
|
return nil, 0, fmt.Errorf(
|
2026-07-31 13:39:18 +08:00
|
|
|
"canonical redis measurement hash %q does not contain field(s) %s",
|
|
|
|
|
canonicalKey,
|
2026-07-30 15:32:22 +08:00
|
|
|
strings.Join(missingFields, ", "),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawDataSource, ok := values[0].(string)
|
|
|
|
|
if !ok {
|
2026-07-31 13:39:18 +08:00
|
|
|
return nil, 0, fmt.Errorf("canonical redis measurement hash %q data_source has type %T", canonicalKey, values[0])
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
var dataSource orm.JSONMap
|
|
|
|
|
if err := json.Unmarshal([]byte(rawDataSource), &dataSource); err != nil {
|
2026-07-31 13:39:18 +08:00
|
|
|
return nil, 0, fmt.Errorf("decode measurement data_source from canonical redis hash %q: %w", canonicalKey, err)
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawSize, ok := values[1].(string)
|
|
|
|
|
if !ok {
|
2026-07-31 13:39:18 +08:00
|
|
|
return nil, 0, fmt.Errorf("canonical redis measurement hash %q size has type %T", canonicalKey, values[1])
|
2026-07-30 15:32:22 +08:00
|
|
|
}
|
|
|
|
|
size, err := strconv.Atoi(rawSize)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("decode measurement size %q: %w", rawSize, err)
|
|
|
|
|
}
|
|
|
|
|
if size <= 0 {
|
|
|
|
|
return nil, 0, fmt.Errorf("measurement window size must be greater than 0, got %d", size)
|
|
|
|
|
}
|
|
|
|
|
return dataSource, size, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeDataObjectHashField(
|
|
|
|
|
dataObjectType constants.DataObjectType,
|
|
|
|
|
field string,
|
|
|
|
|
rawValue string,
|
|
|
|
|
) (any, error) {
|
|
|
|
|
if dataObjectType != constants.DataObjectTypeMeasurement {
|
|
|
|
|
return rawValue, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch field {
|
2026-07-15 17:33:55 +08:00
|
|
|
case "mode":
|
2026-07-30 15:32:22 +08:00
|
|
|
value, err := strconv.ParseInt(rawValue, 10, 16)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode measurement mode %q: %w", rawValue, err)
|
|
|
|
|
}
|
|
|
|
|
return int16(value), nil
|
2026-07-15 17:33:55 +08:00
|
|
|
case "size":
|
2026-07-30 15:32:22 +08:00
|
|
|
value, err := strconv.Atoi(rawValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode measurement size %q: %w", rawValue, err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
case "data_source", "event_plan", "binding":
|
|
|
|
|
return decodeRedisJSON(rawValue)
|
|
|
|
|
default:
|
|
|
|
|
return rawValue, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeParameterHashValue(rawValue, attributeType string) (any, error) {
|
|
|
|
|
normalizedType := strings.ToUpper(strings.TrimSpace(attributeType))
|
|
|
|
|
if rawValue == "null" {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case normalizedType == "BOOLEAN":
|
|
|
|
|
value, err := strconv.ParseBool(rawValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode parameter boolean value %q: %w", rawValue, err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
case normalizedType == "SMALLINT",
|
|
|
|
|
normalizedType == "INTEGER",
|
|
|
|
|
normalizedType == "BIGINT":
|
|
|
|
|
value, err := strconv.ParseInt(rawValue, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode parameter integer value %q: %w", rawValue, err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
case normalizedType == "REAL",
|
|
|
|
|
normalizedType == "DOUBLE PRECISION",
|
|
|
|
|
strings.HasPrefix(normalizedType, "NUMERIC"),
|
|
|
|
|
strings.HasPrefix(normalizedType, "DECIMAL"):
|
|
|
|
|
if _, err := strconv.ParseFloat(rawValue, 64); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode parameter numeric value %q: %w", rawValue, err)
|
|
|
|
|
}
|
|
|
|
|
return json.Number(rawValue), nil
|
|
|
|
|
case normalizedType == "JSON",
|
|
|
|
|
normalizedType == "JSONB",
|
|
|
|
|
strings.HasSuffix(normalizedType, "[]"):
|
|
|
|
|
return decodeRedisJSON(rawValue)
|
2026-07-15 17:33:55 +08:00
|
|
|
default:
|
2026-07-30 15:32:22 +08:00
|
|
|
return rawValue, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeRedisJSON(rawValue string) (any, error) {
|
|
|
|
|
var value any
|
|
|
|
|
decoder := json.NewDecoder(strings.NewReader(rawValue))
|
|
|
|
|
decoder.UseNumber()
|
|
|
|
|
if err := decoder.Decode(&value); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("decode redis JSON value: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isDataObjectTokenNotFound(err error) bool {
|
|
|
|
|
return errors.Is(err, common.ErrParameterTokenNotFound) ||
|
|
|
|
|
errors.Is(err, common.ErrMeasurementTokenNotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func dataObjectAttributeSuccessMessage(dataObjectType constants.DataObjectType) string {
|
|
|
|
|
if dataObjectType == constants.DataObjectTypeParameter {
|
|
|
|
|
return "query parameter attribute success"
|
|
|
|
|
}
|
|
|
|
|
return "query measurement attribute success"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func dataObjectAttributeFailureMessage(dataObjectType constants.DataObjectType) string {
|
|
|
|
|
if dataObjectType == constants.DataObjectTypeParameter {
|
|
|
|
|
return "query parameter attribute failed"
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
2026-07-30 15:32:22 +08:00
|
|
|
return "query measurement attribute failed"
|
2026-07-15 17:33:55 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
func queryMeasurementRealtimeValue(ctx context.Context, dataSource orm.JSONMap, size int) (any, error) {
|
2026-07-15 17:33:55 +08:00
|
|
|
queryKey, err := model.GenerateMeasureIdentifier(dataSource)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("generate measurement redis key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 15:32:22 +08:00
|
|
|
value, err := diagram.NewRedisClient().QueryLatestMeasurementValues(ctx, queryKey, size)
|
2026-07-15 17:33:55 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("query real-time measurement value by key %q: %w", queryKey, err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|