Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update goal REST no command #1464

Merged
merged 14 commits into from
Jan 21, 2025
Merged
74 changes: 74 additions & 0 deletions api-description/web-api.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3149,6 +3149,54 @@ paths:
$ref: '#/definitions/experimentCreateGoalRequest'
tags:
- goal
patch:
summary: Update
description: Update a goal.
operationId: web.v1.goal.update
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/experimentUpdateGoalResponse'
"400":
description: Returned for bad requests that may have failed validation.
schema:
$ref: '#/definitions/googlerpcStatus'
examples:
application/json:
code: 3
details: []
message: invalid arguments error
"401":
description: Request could not be authenticated (authentication required).
schema:
$ref: '#/definitions/googlerpcStatus'
examples:
application/json:
code: 16
details: []
message: not authenticated
"503":
description: Returned for internal errors.
schema:
$ref: '#/definitions/googlerpcStatus'
examples:
application/json:
code: 13
details: []
message: internal
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: body
in: body
required: true
schema:
$ref: '#/definitions/experimentUpdateGoalRequest'
tags:
- goal
/v1/goals:
get:
summary: List
Expand Down Expand Up @@ -6185,8 +6233,34 @@ definitions:
type: object
experimentUpdateExperimentResponse:
type: object
experimentUpdateGoalRequest:
type: object
properties:
id:
type: string
renameCommand:
$ref: '#/definitions/experimentRenameGoalCommand'
description: deprecated
changeDescriptionCommand:
$ref: '#/definitions/experimentChangeDescriptionGoalCommand'
description: deprecated
environmentId:
type: string
name:
type: string
description:
type: string
archived:
type: boolean
description: if true, the goal will be archived
required:
- id
- environmentId
experimentUpdateGoalResponse:
type: object
properties:
goal:
$ref: '#/definitions/experimentGoal'
featureAddSegmentUserCommand:
type: object
properties:
Expand Down
4 changes: 2 additions & 2 deletions manifests/bucketeer/charts/web/values.yaml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions pkg/domainevent/domain/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ func LocalizedMessage(eventType proto.Event_Type, localizer locale.Localizer) *p
localizer.MustLocalizeWithTemplate(locale.Goal),
),
}
case proto.Event_GOAL_UPDATED:
return &proto.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalizeWithTemplate(
locale.UpdatedTemplate,
localizer.MustLocalizeWithTemplate(locale.Goal),
),
}
case proto.Event_EXPERIMENT_CREATED:
return &proto.LocalizedMessage{
Locale: localizer.GetLocale(),
Expand Down
131 changes: 130 additions & 1 deletion pkg/experiment/api/goal.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"regexp"
"strconv"

pb "github.com/golang/protobuf/proto"
"go.uber.org/zap"
"google.golang.org/genproto/googleapis/rpc/errdetails"

Expand Down Expand Up @@ -554,6 +555,9 @@ func (s *experimentService) UpdateGoal(
if err != nil {
return nil, err
}
if req.ChangeDescriptionCommand == nil && req.RenameCommand == nil {
return s.updateGoalNoCommand(ctx, req, editor, localizer)
}
if req.Id == "" {
dt, err := statusGoalIDRequired.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Expand Down Expand Up @@ -602,6 +606,131 @@ func (s *experimentService) UpdateGoal(
return &proto.UpdateGoalResponse{}, nil
}

func (s *experimentService) updateGoalNoCommand(
ctx context.Context,
req *proto.UpdateGoalRequest,
editor *eventproto.Editor,
localizer locale.Localizer,
) (*proto.UpdateGoalResponse, error) {
err := s.validateUpdateGoalNoCommandRequest(req, localizer)
if err != nil {
return nil, err
}

tx, err := s.mysqlClient.BeginTx(ctx)
if err != nil {
s.logger.Error(
"Failed to begin transaction",
log.FieldsFromImcomingContext(ctx).AddFields(
zap.Error(err),
)...,
)
dt, err := statusInternal.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.InternalServerError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
var updatedGoal *proto.Goal
err = s.mysqlClient.RunInTransaction(ctx, tx, func() error {
goalStorage := v2es.NewGoalStorage(tx)
goal, err := goalStorage.GetGoal(ctx, req.Id, req.EnvironmentId)
if err != nil {
return err
}
updated, err := goal.Update(
req.Name,
req.Description,
req.Archived,
)
if err != nil {
return err
}
updatedGoal = updated.Goal

var event pb.Message
if req.Archived != nil && req.Archived.Value {
event = &eventproto.GoalArchivedEvent{Id: goal.Id}
} else {
event = &eventproto.GoalUpdatedEvent{
Id: goal.Id,
Name: req.Name,
Description: req.Description,
}
}
e, err := domainevent.NewEvent(
editor,
eventproto.Event_GOAL,
goal.Id,
eventproto.Event_GOAL_UPDATED,
event,
req.EnvironmentId,
updated.Goal,
goal.Goal,
)
if err != nil {
return err
}
if err = s.publisher.Publish(ctx, e); err != nil {
return err
}
return goalStorage.UpdateGoal(ctx, updated, req.EnvironmentId)
})
if err != nil {
if errors.Is(err, v2es.ErrGoalNotFound) || errors.Is(err, v2es.ErrGoalUnexpectedAffectedRows) {
dt, err := statusNotFound.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.NotFoundError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
dt, err := statusInternal.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.InternalServerError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
return &proto.UpdateGoalResponse{
Goal: updatedGoal,
}, nil
}

func (s *experimentService) validateUpdateGoalNoCommandRequest(
req *proto.UpdateGoalRequest,
localizer locale.Localizer,
) error {
if req.Id == "" {
dt, err := statusGoalIDRequired.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalizeWithTemplate(locale.RequiredFieldTemplate, "goal_id"),
})
if err != nil {
return statusInternal.Err()
}
return dt.Err()
}
if req.Name != nil && req.Name.Value == "" {
dt, err := statusGoalNameRequired.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalizeWithTemplate(locale.RequiredFieldTemplate, "name"),
})
if err != nil {
return statusInternal.Err()
}
return dt.Err()
}
return nil
}

func (s *experimentService) ArchiveGoal(
ctx context.Context,
req *proto.ArchiveGoalRequest,
Expand Down Expand Up @@ -748,7 +877,7 @@ func (s *experimentService) updateGoal(
return goalStorage.UpdateGoal(ctx, goal, environmentId)
})
if err != nil {
if err == v2es.ErrGoalNotFound || err == v2es.ErrGoalUnexpectedAffectedRows {
if errors.Is(err, v2es.ErrGoalNotFound) || errors.Is(err, v2es.ErrGoalUnexpectedAffectedRows) {
dt, err := statusNotFound.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.NotFoundError),
Expand Down
Loading
Loading