Changes
4 changed files (+278/-21)
-
-
@@ -150,3 +150,17 @@ },}, } } func SetDisplayName(displayName string) *eventV1.Event { return &eventV1.Event{ Event: &eventV1.Event_WorkspaceEvent{ WorkspaceEvent: &workspaceEvent.Event{ Event: &workspaceEvent.Event_WorkspaceDisplayNameSet{ WorkspaceDisplayNameSet: &workspaceEvent.WorkspaceDisplayNameSet{ DisplayName: proto.String(displayName), }, }, }, }, } }
-
-
-
@@ -0,0 +1,188 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package workspace import ( "context" "slices" "strings" "connectrpc.com/connect" "google.golang.org/protobuf/proto" eventV1 "pocka.jp/x/yamori/proto/go/backend/events/v1" workspaceEventV1 "pocka.jp/x/yamori/proto/go/backend/events/workspace/v1" "pocka.jp/x/yamori/proto/go/backend/workspace/v1/types" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" "pocka.jp/x/yamori/backend/core/event" "pocka.jp/x/yamori/backend/core/projection" workspaceEvent "pocka.jp/x/yamori/backend/events/workspace" ) func (s *Service) Update( ctx context.Context, req *connect.Request[workspaceV2.UpdateRequest], ) (*connect.Response[workspaceV2.UpdateResponse], error) { logger := s.core.Logger.With( "service", "yamori.workspace.v2.WorkspaceService", "method", "Update", ) header := req.Header() token, err := s.core.LoadTokenFromCookie(&header) if err != nil || token == nil { // TODO: AuthenticationError に切り替える return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{}, }, }), nil } tx, err := s.core.DB.Begin() if err != nil { logger.Error("Failed to begin transaction", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } defer tx.Rollback() secret, err := projection.GetLoginJwtSecret(tx) if err != nil { logger.Error("Failed to read login_jwt_secret projection", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } workspace, err := projection.GetWorkspace(tx) if err != nil { logger.Error("Failed to read workspace projection", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } users, err := projection.GetUsers(tx) if err != nil { logger.Error("Failed to read users projection", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } if err := event.UpdateProjections(tx, workspace, secret, users); err != nil { logger.Error("Failed to update projections", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } user, err := token.FindUser(secret, users) if err != nil { logger.Warn("Malformed token found", "error", err) // TODO: AuthenticationError に切り替える return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{}, }, }), nil } if !slices.Contains(user.Permissions, types.Permission_PERMISSION_EDIT_WORKSPACE_PROFILE) { // TODO: AuthenticationError もしくは認可エラーに切り替える return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not allowed"), }, }, }), nil } events := make([]*eventV1.Event, 0, 2) displayName := strings.Trim(req.Msg.GetDisplayName(), " \r\n\t") if displayName != "" { events = append(events, workspaceEvent.SetDisplayName(displayName)) } if req.Msg.Abbreviations != nil { events = append(events, workspaceEvent.ConfigureAbbreviations(&workspaceEventV1.AbbreviationsConfigured{ DayOff: req.Msg.Abbreviations.Dayoff, Worked: req.Msg.Abbreviations.Worked, SkipWork: req.Msg.Abbreviations.SkipWork, })) } if len(events) == 0 { return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_Ok{ Ok: projectionWorkspaceToMessage(workspace.Projection), }, }), nil } if err := event.AppendEvents(tx, events); err != nil { logger.Error("Failed to append update events", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } if err := event.UpdateProjections(tx, workspace, secret, users); err != nil { logger.Error("Failed to update projections (after appending events)", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } if err := tx.Commit(); err != nil { logger.Error("Failed to commit transaction", "error", err) return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Database error"), }, }, }), nil } return connect.NewResponse(&workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_Ok{ Ok: projectionWorkspaceToMessage(workspace.Projection), }, }), nil }
-
-
-
@@ -4,15 +4,9 @@package workspace import ( "context" "net/http" "connectrpc.com/connect" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/core" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" workspaceV2connect "pocka.jp/x/yamori/proto/go/workspace/v2/v2connect" )
-
@@ -22,21 +16,6 @@ }func New(core *core.Core) *Service { return &Service{core: core} } func (s *Service) Update( ctx context.Context, req *connect.Request[workspaceV2.UpdateRequest], ) (*connect.Response[workspaceV2.UpdateResponse], error) { res := workspaceV2.UpdateResponse{ Result: &workspaceV2.UpdateResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) Register(mux *http.ServeMux) {
-
-
-
@@ -0,0 +1,76 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only //go:build !js && !wasm package v2 import ( "context" "reflect" "testing" "connectrpc.com/connect" "google.golang.org/protobuf/proto" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" "pocka.jp/x/yamori/proto/go/workspace/v2/v2connect" ) func TestUpdateOK(t *testing.T) { server, jar := setupLogin(t) httpClient := server.Client() httpClient.Jar = jar client := v2connect.NewWorkspaceServiceClient(httpClient, server.URL()) { res, err := client.Update( context.Background(), connect.NewRequest(&workspaceV2.UpdateRequest{ DisplayName: proto.String("Foo Bar"), Abbreviations: &workspaceV2.Abbreviations{ Dayoff: proto.String("DAY_OFF"), }, }), ) if err != nil { t.Fatal(err) } _, ok := res.Msg.Result.(*workspaceV2.UpdateResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } } { res, err := client.Get( context.Background(), connect.NewRequest(&workspaceV2.GetRequest{}), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.GetResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } if v.Ok.GetDisplayName() != "Foo Bar" { t.Errorf("Expected Foo Bar, got %s", v.Ok.GetDisplayName()) } if v.Ok.GetAbbreviations().GetDayoff() != "DAY_OFF" { t.Errorf("Expected dayoff abbreviation to be updated, got %s", v.Ok.GetAbbreviations().GetDayoff()) } if v.Ok.GetAbbreviations().GetPaidLeave() != "年休" { t.Errorf("Expected 年休, got %s", v.Ok.GetAbbreviations().GetPaidLeave()) } } }
-