// Package handler provides HTTP handlers for various endpoints. package handler import ( "bytes" "context" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "modelRT/common" "modelRT/common/errcode" "modelRT/constants" "modelRT/database" "modelRT/diagram" "modelRT/logger" "modelRT/model" "modelRT/orm" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type dataObjectAttributeUpdateRequest struct { Token string `json:"token"` Field string `json:"field"` Value json.RawMessage `json:"value"` } // DataObjectAttributeUpdateHandler updates the writable field of one data object. func DataObjectAttributeUpdateHandler(c *gin.Context) { ctx := c.Request.Context() var request dataObjectAttributeUpdateRequest if err := c.ShouldBindJSON(&request); err != nil { logger.Error(ctx, "unmarshal data-object update request failed", "error", err) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) return } dataObjectType, field, value, err := validateDataObjectAttributeUpdate(request) if err != nil { logger.Warn(ctx, "validate data-object update request failed", "token", request.Token, "field", request.Field, "error", err) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) return } tx := database.GetPostgresDBClient().WithContext(ctx).Begin() if tx.Error != nil { logger.Error(ctx, "begin data-object update transaction failed", "error", tx.Error) renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil) return } message := "data-object attribute update success" var measurementResult measurementUpdateResult switch dataObjectType { case constants.DataObjectTypeParameter: parameter, queryErr := database.QueryParameterByDataObjectToken(ctx, tx, request.Token) if queryErr == nil { queryErr = database.UpdateParameterDataObjectValue(ctx, tx, parameter, value) } err = queryErr case constants.DataObjectTypeMeasurement: measurementResult, err = updateMeasurementDataObject(ctx, tx, request.Token, field, value, writeMeasurementManualValue, writeMeasurementManualValueToDataRT) message = measurementResult.message default: err = fmt.Errorf("unsupported data object type %q", dataObjectType) } if err != nil { _ = tx.Rollback().Error if measurementResult.recordFailure { if logErr := database.AppendMeasurementValueOperation(ctx, database.GetPostgresDBClient(), measurementResult.measurementID, 1, measurementResult.value, time.Now().UTC()); logErr != nil { logger.Error(ctx, "append failed measurement value operation failed", "measurement_id", measurementResult.measurementID, "error", logErr) } } logger.Warn(ctx, "update data-object attribute failed", "token", request.Token, "field", field, "error", err) if isInvalidDataObjectUpdateError(err) { renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) return } renderRespFailure(c, constants.RespCodeFailed, err.Error(), nil) return } if err := tx.Commit().Error; err != nil { logger.Error(ctx, "commit data-object update transaction failed", "token", request.Token, "field", field, "error", err) renderRespFailure(c, constants.RespCodeServerError, "transaction commit failed", nil) return } renderRespSuccess(c, constants.RespCodeSuccess, message, map[string]any{ "token": request.Token, "field": field, "value": value, }) } func validateDataObjectAttributeUpdate(request dataObjectAttributeUpdateRequest) (constants.DataObjectType, string, any, error) { if request.Token == "" { return "", "", nil, fmt.Errorf("token is required") } if len(bytes.TrimSpace(request.Value)) == 0 || bytes.Equal(bytes.TrimSpace(request.Value), []byte("null")) { return "", "", nil, fmt.Errorf("value is required") } field := strings.ToLower(strings.TrimSpace(request.Field)) if field == "" { return "", "", nil, fmt.Errorf("field is required") } dataObjectType, err := model.ClassifyDataObjectToken(request.Token) if err != nil { return "", "", nil, err } switch dataObjectType { case constants.DataObjectTypeParameter: parts := strings.Split(request.Token, ".") attributeGroup := parts[len(parts)-2] if !isWritableParameterAttributeGroup(attributeGroup) { return "", "", nil, fmt.Errorf("parameter updates do not support token6=%s", attributeGroup) } if field != "value" { return "", "", nil, fmt.Errorf("parameter data objects only support updating field value") } value, err := decodeDataObjectUpdateValue(request.Value) return dataObjectType, field, value, err case constants.DataObjectTypeMeasurement: parts := strings.Split(request.Token, ".") if len(parts) == 2 || parts[len(parts)-2] != "bay" { return "", "", nil, fmt.Errorf("measurement updates require token6=bay") } switch field { case "value": value, err := parseMeasurementUpdateValue(request.Value) return dataObjectType, field, value, err case "mode": value, err := parseMeasurementUpdateMode(request.Value) return dataObjectType, field, value, err default: return "", "", nil, fmt.Errorf("measurement data objects only support updating fields value and mode") } default: return "", "", nil, fmt.Errorf("unsupported data object type %q", dataObjectType) } } func isWritableParameterAttributeGroup(group string) bool { switch group { case "rated", "setup", "model", "stable", "craft", "integrity", "behavior": return true default: return false } } func decodeDataObjectUpdateValue(raw json.RawMessage) (any, error) { decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var value any if err := decoder.Decode(&value); err != nil { return nil, fmt.Errorf("decode update value: %w", err) } if number, ok := value.(json.Number); ok { if integer, err := number.Int64(); err == nil { return integer, nil } decimal, err := number.Float64() if err != nil { return nil, fmt.Errorf("invalid numeric update value %q: %w", number, err) } return decimal, nil } return value, nil } func parseMeasurementUpdateValue(raw json.RawMessage) (float64, error) { var number float64 if err := json.Unmarshal(raw, &number); err == nil { return number, nil } var text string if err := json.Unmarshal(raw, &text); err != nil { return 0, fmt.Errorf("measurement value must be a number or numeric string") } number, err := strconv.ParseFloat(text, 64) if err != nil { return 0, fmt.Errorf("measurement value %q is not numeric: %w", text, err) } return number, nil } func parseMeasurementUpdateMode(raw json.RawMessage) (bool, error) { var mode bool if err := json.Unmarshal(raw, &mode); err == nil { return mode, nil } var text string if err := json.Unmarshal(raw, &text); err != nil { return false, fmt.Errorf("measurement mode must be true or false") } switch strings.ToLower(text) { case "true": return true, nil case "false": return false, nil default: return false, fmt.Errorf("measurement mode must be true or false") } } type measurementManualValueWriter func(context.Context, *orm.Measurement, float64) error type measurementDataRTWriter func(context.Context, orm.JSONMap, float64) error type measurementUpdateResult struct { message string measurementID int64 value float64 recordFailure bool } func updateMeasurementDataObject( ctx context.Context, tx *gorm.DB, token, field string, value any, writeManualValue measurementManualValueWriter, writeDataRT measurementDataRTWriter, ) (measurementUpdateResult, error) { measurement, _, err := database.QueryMeasurementByDataObjectToken(ctx, tx, token) if err != nil { return measurementUpdateResult{}, err } lockedMeasurement, err := database.QueryMeasurementByID(ctx, tx, measurement.ID) if err != nil { return measurementUpdateResult{}, fmt.Errorf("lock measurement %d for update: %w", measurement.ID, err) } switch field { case "mode": mode, ok := value.(bool) if !ok { return measurementUpdateResult{}, fmt.Errorf("measurement mode has invalid type %T", value) } currentMode := lockedMeasurement.Mode != 0 if currentMode == mode { return measurementUpdateResult{message: fmt.Sprintf("measurement is already in %s mode", measurementModeName(mode))}, nil } if err := database.UpdateMeasurementModeWithOperation(ctx, tx, lockedMeasurement.ID, mode, time.Now().UTC()); err != nil { return measurementUpdateResult{}, err } return measurementUpdateResult{message: fmt.Sprintf("measurement mode changed to %s", measurementModeName(mode))}, nil case "value": if lockedMeasurement.Mode != 0 { return measurementUpdateResult{}, fmt.Errorf("measurement value is read-only while mode is automatic") } manualValue, ok := value.(float64) if !ok { return measurementUpdateResult{}, fmt.Errorf("measurement value has invalid type %T", value) } failureResult := measurementUpdateResult{ measurementID: lockedMeasurement.ID, value: manualValue, recordFailure: true, } if writeManualValue == nil { return failureResult, errcode.ErrMeasurementValueUpdateFailed.WithCause(fmt.Errorf("measurement manual value writer is nil")) } if err := writeManualValue(ctx, &lockedMeasurement, manualValue); err != nil { return failureResult, errcode.ErrMeasurementValueUpdateFailed.WithCause(err) } if writeDataRT == nil { return failureResult, errcode.ErrMeasurementValueUpdateFailed.WithCause(fmt.Errorf("measurement dataRT writer is nil")) } if err := writeDataRT(ctx, lockedMeasurement.DataSource, manualValue); err != nil { return failureResult, errcode.ErrMeasurementValueUpdateFailed.WithCause(err) } if err := database.AppendMeasurementValueOperation(ctx, tx, lockedMeasurement.ID, 0, manualValue, time.Now().UTC()); err != nil { return failureResult, errcode.ErrMeasurementValueUpdateFailed.WithCause(err) } return measurementUpdateResult{message: "measurement manual value updated"}, nil default: return measurementUpdateResult{}, fmt.Errorf("unsupported measurement update field %q", field) } } func measurementModeName(automatic bool) string { if automatic { return "automatic" } return "manual" } func isInvalidDataObjectUpdateError(err error) bool { return errors.Is(err, common.ErrInvalidParameterToken) || errors.Is(err, common.ErrParameterTokenNotFound) || errors.Is(err, common.ErrAmbiguousParameterToken) || errors.Is(err, common.ErrInvalidMeasurementToken) || errors.Is(err, common.ErrMeasurementTokenNotFound) || errors.Is(err, common.ErrAmbiguousMeasurementToken) } func writeMeasurementManualValue(ctx context.Context, measurement *orm.Measurement, value float64) error { key, err := model.GenerateMeasureIdentifier(measurement.DataSource) if err != nil { return fmt.Errorf("generate measurement redis key: %w", err) } zset := diagram.NewRedisZSet(ctx, key, 0, false) if err := zset.ZADD(key, value, strconv.FormatInt(time.Now().UnixNano(), 10)); err != nil { return fmt.Errorf("write manual measurement value to redis: %w", err) } return nil } func writeMeasurementManualValueToDataRT(_ context.Context, _ orm.JSONMap, _ float64) error { // TODO: call the dataRT HTTP API with the measurement data_source and manual value. return nil }