diff --git a/.gitattributes b/.gitattributes index 83ae9d8..d156857 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ slack/slackclient/mock_slackclient linguist-generated +lib/db/mock_db linguist-generated gen linguist-generated diff --git a/assets/queries/rotas.sql b/assets/queries.sql similarity index 71% rename from assets/queries/rotas.sql rename to assets/queries.sql index 8cd3d9f..ab230e9 100644 --- a/assets/queries/rotas.sql +++ b/assets/queries.sql @@ -9,13 +9,12 @@ from ROTAS WHERE ROTAS.CHANNEL_ID = $1 AND ROTAS.TEAM_ID = $2; --- name: SaveRota :one +-- name: saveRota :one INSERT INTO ROTAS (TEAM_ID, CHANNEL_ID, NAME, METADATA) VALUES ($1, $2, $3, $4) RETURNING ID; --- name: UpdateRota :one +-- name: updateRota :one UPDATE ROTAS -SET NAME = $1, - METADATA = $2 -WHERE ID = $3 -RETURNING ID; +SET NAME = $1, + METADATA = $2 +WHERE ID = $3 RETURNING ID; diff --git a/cmd/app/server.go b/cmd/app/server.go index 2f0d79b..284a3a0 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -19,7 +19,6 @@ import ( "go.uber.org/zap/zapio" genSlack "github.com/rotabot-io/rotabot/gen/slack" - "github.com/rotabot-io/rotabot/lib/db" "github.com/rotabot-io/rotabot/lib/middleware" "github.com/rotabot-io/rotabot/lib/zapctx" ) @@ -30,8 +29,6 @@ type ServerParams struct { AppComponent string MetricsComponent string - Queries *db.Queries - SlackSigningSecret string SlackService genSlack.Service diff --git a/lib/db/metadata.go b/lib/db/metadata.go deleted file mode 100644 index 51dce6a..0000000 --- a/lib/db/metadata.go +++ /dev/null @@ -1,21 +0,0 @@ -package db - -// RotaSchedule is the type that defines how the members of a rota are scheduled -type RotaSchedule string - -// RotaFrequency is the type that defines how long a rota lasts -type RotaFrequency string - -const ( - RFDaily = RotaFrequency("Daily") - RFWeekly = RotaFrequency("Weekly") - RFMonthly = RotaFrequency("Monthly") - - RSCreated = RotaSchedule("Created At") - RSRandom = RotaSchedule("Randomly") -) - -type RotaMetadata struct { - Frequency RotaFrequency `json:"frequency"` - SchedulingType RotaSchedule `json:"scheduling_type"` -} diff --git a/lib/db/mock_db/db.go b/lib/db/mock_db/db.go new file mode 100644 index 0000000..d59fc58 --- /dev/null +++ b/lib/db/mock_db/db.go @@ -0,0 +1,85 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/rotabot-io/rotabot/lib/db (interfaces: Repository) +// +// Generated by this command: +// +// mockgen -package mock_db -destination=mock_db/db.go . Repository +// +// Package mock_db is a generated GoMock package. +package mock_db + +import ( + context "context" + reflect "reflect" + + db "github.com/rotabot-io/rotabot/lib/db" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// CreateOrUpdateRota mocks base method. +func (m *MockRepository) CreateOrUpdateRota(arg0 context.Context, arg1 db.CreateOrUpdateRotaParams) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateRota", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateRota indicates an expected call of CreateOrUpdateRota. +func (mr *MockRepositoryMockRecorder) CreateOrUpdateRota(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateRota", reflect.TypeOf((*MockRepository)(nil).CreateOrUpdateRota), arg0, arg1) +} + +// FindRotaByID mocks base method. +func (m *MockRepository) FindRotaByID(arg0 context.Context, arg1 string) (db.Rota, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindRotaByID", arg0, arg1) + ret0, _ := ret[0].(db.Rota) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindRotaByID indicates an expected call of FindRotaByID. +func (mr *MockRepositoryMockRecorder) FindRotaByID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRotaByID", reflect.TypeOf((*MockRepository)(nil).FindRotaByID), arg0, arg1) +} + +// ListRotasByChannel mocks base method. +func (m *MockRepository) ListRotasByChannel(arg0 context.Context, arg1 db.ListRotasByChannelParams) ([]db.Rota, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRotasByChannel", arg0, arg1) + ret0, _ := ret[0].([]db.Rota) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRotasByChannel indicates an expected call of ListRotasByChannel. +func (mr *MockRepositoryMockRecorder) ListRotasByChannel(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRotasByChannel", reflect.TypeOf((*MockRepository)(nil).ListRotasByChannel), arg0, arg1) +} diff --git a/lib/db/queries.go b/lib/db/queries.go new file mode 100644 index 0000000..3967fcb --- /dev/null +++ b/lib/db/queries.go @@ -0,0 +1,50 @@ +package db + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5/pgconn" + "github.com/rotabot-io/rotabot/lib/zapctx" + "go.uber.org/zap" +) + +type CreateOrUpdateRotaParams struct { + RotaID string + TeamID string + ChannelID string + Name string + Metadata RotaMetadata +} + +func (q *Queries) CreateOrUpdateRota(ctx context.Context, p CreateOrUpdateRotaParams) (string, error) { + l := zapctx.Logger(ctx) + var rotaId string + var err error + if p.RotaID != "" { + rotaId, err = q.updateRota(ctx, updateRotaParams{ + ID: p.RotaID, + Name: p.Name, + Metadata: p.Metadata, + }) + } else { + rotaId, err = q.saveRota(ctx, saveRotaParams{ + Name: p.Name, + TeamID: p.TeamID, + ChannelID: p.ChannelID, + Metadata: p.Metadata, + }) + } + if err != nil { + var pgError *pgconn.PgError + if errors.As(err, &pgError) { + switch pgError.Code { + case "23505": + return "", ErrAlreadyExists + } + } + l.Error("failed to save rota", zap.Error(err)) + return "", err + } + return rotaId, nil +} diff --git a/lib/db/rotas.sql.go b/lib/db/queries.sql.go similarity index 85% rename from lib/db/rotas.sql.go rename to lib/db/queries.sql.go index e6b8ea2..1e1755c 100644 --- a/lib/db/rotas.sql.go +++ b/lib/db/queries.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.22.0 -// source: rotas.sql +// source: queries.sql package db @@ -48,7 +48,7 @@ func (q *Queries) ListRotasByChannel(ctx context.Context, arg ListRotasByChannel return nil, err } defer rows.Close() - var items []Rota + items := []Rota{} for rows.Next() { var i Rota if err := rows.Scan( @@ -70,19 +70,19 @@ func (q *Queries) ListRotasByChannel(ctx context.Context, arg ListRotasByChannel return items, nil } -const saveRota = `-- name: SaveRota :one +const saveRota = `-- name: saveRota :one INSERT INTO ROTAS (TEAM_ID, CHANNEL_ID, NAME, METADATA) VALUES ($1, $2, $3, $4) RETURNING ID ` -type SaveRotaParams struct { +type saveRotaParams struct { TeamID string `json:"team_id"` ChannelID string `json:"channel_id"` Name string `json:"name"` Metadata RotaMetadata `json:"metadata"` } -func (q *Queries) SaveRota(ctx context.Context, arg SaveRotaParams) (string, error) { +func (q *Queries) saveRota(ctx context.Context, arg saveRotaParams) (string, error) { row := q.db.QueryRow(ctx, saveRota, arg.TeamID, arg.ChannelID, @@ -94,21 +94,20 @@ func (q *Queries) SaveRota(ctx context.Context, arg SaveRotaParams) (string, err return id, err } -const updateRota = `-- name: UpdateRota :one +const updateRota = `-- name: updateRota :one UPDATE ROTAS -SET NAME = $1, - METADATA = $2 -WHERE ID = $3 -RETURNING ID +SET NAME = $1, + METADATA = $2 +WHERE ID = $3 RETURNING ID ` -type UpdateRotaParams struct { +type updateRotaParams struct { Name string `json:"name"` Metadata RotaMetadata `json:"metadata"` ID string `json:"id"` } -func (q *Queries) UpdateRota(ctx context.Context, arg UpdateRotaParams) (string, error) { +func (q *Queries) updateRota(ctx context.Context, arg updateRotaParams) (string, error) { row := q.db.QueryRow(ctx, updateRota, arg.Name, arg.Metadata, arg.ID) var id string err := row.Scan(&id) diff --git a/lib/db/rotas_test.go b/lib/db/queries_test.go similarity index 73% rename from lib/db/rotas_test.go rename to lib/db/queries_test.go index 5967bd8..5b1b55b 100644 --- a/lib/db/rotas_test.go +++ b/lib/db/queries_test.go @@ -41,6 +41,62 @@ var _ = Describe("Rotas", func() { }) }) + Describe("CreateOrUpdateRota", func() { + It("Should create rota if id is null", func() { + id, err := q.CreateOrUpdateRota(ctx, CreateOrUpdateRotaParams{ + ChannelID: "foo", + TeamID: "bar", + Name: "baz", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(id).ToNot(BeEmpty()) + }) + + It("Should fail to create two identical rotas", func() { + req := CreateOrUpdateRotaParams{ + ChannelID: "foo", + TeamID: "bar", + Name: "baz", + } + _, err := q.CreateOrUpdateRota(ctx, req) + Expect(err).ToNot(HaveOccurred()) + + _, err = q.CreateOrUpdateRota(ctx, req) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(ErrAlreadyExists)) + }) + + It("Should update rota if id is null", func() { + id, err := q.CreateOrUpdateRota(ctx, CreateOrUpdateRotaParams{ + ChannelID: "foo", + TeamID: "bar", + Name: "baz", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(id).ToNot(BeEmpty()) + + updated, err := q.CreateOrUpdateRota(ctx, CreateOrUpdateRotaParams{ + RotaID: id, + Name: "bazbaz", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(Equal(updated)) + + rota, err := q.FindRotaByID(ctx, id) + Expect(err).ToNot(HaveOccurred()) + Expect(rota.Name).To(Equal("bazbaz")) + }) + + It("Should fail to update something it does not exist", func() { + _, err := q.CreateOrUpdateRota(ctx, CreateOrUpdateRotaParams{ + RotaID: "not_found", + Name: "bazbaz", + }) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(ErrNotFound)) + }) + }) + Describe("FindRotaByID", func() { When("rota does not exist", func() { It("should return ErrNotFound", func() { @@ -50,7 +106,7 @@ var _ = Describe("Rotas", func() { }) When("rota exists", func() { It("should return rota", func() { - id, err := q.SaveRota(ctx, SaveRotaParams{ + id, err := q.saveRota(ctx, saveRotaParams{ ChannelID: "C123", TeamID: "T123", Name: "test", @@ -88,7 +144,7 @@ var _ = Describe("Rotas", func() { }) It("should return rotas when one exist", func() { - _, err := q.SaveRota(ctx, SaveRotaParams{ + _, err := q.saveRota(ctx, saveRotaParams{ ChannelID: channelID, TeamID: teamID, Name: "test", @@ -105,7 +161,7 @@ var _ = Describe("Rotas", func() { }) It("should return return nothing rota is on another team", func() { - _, err := q.SaveRota(ctx, SaveRotaParams{ + _, err := q.saveRota(ctx, saveRotaParams{ ChannelID: channelID, TeamID: "another_team", Name: "test", @@ -135,7 +191,7 @@ var _ = Describe("Rotas", func() { }) It("should create rota", func() { - id, err := q.SaveRota(ctx, SaveRotaParams{ + id, err := q.saveRota(ctx, saveRotaParams{ ChannelID: channelID, TeamID: teamID, Name: name, @@ -149,7 +205,7 @@ var _ = Describe("Rotas", func() { }) It("should fail when rota already exist", func() { - p := SaveRotaParams{ + p := saveRotaParams{ ChannelID: channelID, TeamID: teamID, Name: name, @@ -158,11 +214,11 @@ var _ = Describe("Rotas", func() { SchedulingType: RSRandom, }, } - id, err := q.SaveRota(ctx, p) + id, err := q.saveRota(ctx, p) Expect(err).ToNot(HaveOccurred()) Expect(id).ToNot(BeEmpty()) - _, err = q.SaveRota(ctx, p) + _, err = q.saveRota(ctx, p) Expect(err).To(HaveOccurred()) var pgError *pgconn.PgError @@ -178,9 +234,9 @@ var _ = Describe("Rotas", func() { Describe("UpdateRota", func() { When("rota does not exist", func() { It("should return ErrNotFound", func() { - id, err := q.UpdateRota( + id, err := q.updateRota( ctx, - UpdateRotaParams{ + updateRotaParams{ ID: "not_found", Name: "test", Metadata: RotaMetadata{ @@ -195,7 +251,7 @@ var _ = Describe("Rotas", func() { }) When("rota exists", func() { It("should return rota", func() { - id, err := q.SaveRota(ctx, SaveRotaParams{ + id, err := q.saveRota(ctx, saveRotaParams{ ChannelID: "C123", TeamID: "T123", Name: "test", @@ -206,9 +262,9 @@ var _ = Describe("Rotas", func() { }) Expect(err).ToNot(HaveOccurred()) - id, err = q.UpdateRota( + id, err = q.updateRota( ctx, - UpdateRotaParams{ + updateRotaParams{ ID: id, Name: "test test", Metadata: RotaMetadata{ diff --git a/lib/db/repository.go b/lib/db/repository.go index 77c194a..366bdfb 100644 --- a/lib/db/repository.go +++ b/lib/db/repository.go @@ -1,13 +1,9 @@ +//go:generate mockgen -package mock_db -destination=mock_db/db.go . Repository package db import ( "context" "errors" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/rotabot-io/rotabot/lib/zapctx" - "go.uber.org/zap" ) var ( @@ -15,43 +11,28 @@ var ( ErrNotFound = errors.New("no rows in result set") ) -type CreateOrUpdateRotaParams struct { - RotaID string - TeamID string - ChannelID string - Name string - Metadata RotaMetadata +// RotaSchedule is the type that defines how the members of a rota are scheduled +type RotaSchedule string + +// RotaFrequency is the type that defines how long a rota lasts +type RotaFrequency string + +const ( + RFDaily = RotaFrequency("Daily") + RFWeekly = RotaFrequency("Weekly") + RFMonthly = RotaFrequency("Monthly") + + RSCreated = RotaSchedule("Created At") + RSRandom = RotaSchedule("Randomly") +) + +type RotaMetadata struct { + Frequency RotaFrequency `json:"frequency"` + SchedulingType RotaSchedule `json:"scheduling_type"` } -func CreateOrUpdateRota(ctx context.Context, tx pgx.Tx, p CreateOrUpdateRotaParams) (string, error) { - l := zapctx.Logger(ctx) - client := New(tx) - var rotaId string - var err error - if p.RotaID != "" { - rotaId, err = client.UpdateRota(ctx, UpdateRotaParams{ - ID: p.RotaID, - Name: p.Name, - Metadata: p.Metadata, - }) - } else { - rotaId, err = client.SaveRota(ctx, SaveRotaParams{ - Name: p.Name, - TeamID: p.TeamID, - ChannelID: p.ChannelID, - Metadata: p.Metadata, - }) - } - if err != nil { - var pgError *pgconn.PgError - if errors.As(err, &pgError) { - switch pgError.Code { - case "23505": - return "", ErrAlreadyExists - } - } - l.Error("failed to save rota", zap.Error(err)) - return "", err - } - return rotaId, nil +type Repository interface { + CreateOrUpdateRota(ctx context.Context, p CreateOrUpdateRotaParams) (string, error) + FindRotaByID(ctx context.Context, id string) (Rota, error) + ListRotasByChannel(ctx context.Context, args ListRotasByChannelParams) ([]Rota, error) } diff --git a/lib/db/repository_test.go b/lib/db/repository_test.go deleted file mode 100644 index 734519c..0000000 --- a/lib/db/repository_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package db - -import ( - "context" - "path/filepath" - - "github.com/testcontainers/testcontainers-go" - - "github.com/jackc/pgx/v5" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/rotabot-io/rotabot/internal" - "github.com/testcontainers/testcontainers-go/modules/postgres" -) - -var _ = Describe("Repository", func() { - var ctx context.Context - var connString string - var conn *pgx.Conn - - BeforeEach(func() { - var err error - ctx = context.Background() - - container, err := internal.RunContainer(ctx, - postgres.WithInitScripts(filepath.Join("..", "..", "assets", "structure.sql")), - testcontainers.WithWaitStrategy(internal.DefaultWaitStrategy()), - ) - Expect(err).ToNot(HaveOccurred()) - - connString, err = container.ConnectionString(ctx, "sslmode=disable") - Expect(err).ToNot(HaveOccurred()) - - conn, err = pgx.Connect(ctx, connString) - Expect(err).ToNot(HaveOccurred()) - - DeferCleanup(func() { - _ = container.Terminate(ctx) - _ = conn.Close(ctx) - }) - }) - - It("Should create rota if id is null", func() { - tx, err := conn.Begin(ctx) - Expect(err).ToNot(HaveOccurred()) - - id, err := CreateOrUpdateRota(ctx, tx, CreateOrUpdateRotaParams{ - ChannelID: "foo", - TeamID: "bar", - Name: "baz", - }) - Expect(err).ToNot(HaveOccurred()) - Expect(id).ToNot(BeEmpty()) - }) - - It("Should fail to create two identical rotas", func() { - tx, err := conn.Begin(ctx) - Expect(err).ToNot(HaveOccurred()) - - req := CreateOrUpdateRotaParams{ - ChannelID: "foo", - TeamID: "bar", - Name: "baz", - } - _, err = CreateOrUpdateRota(ctx, tx, req) - Expect(err).ToNot(HaveOccurred()) - - _, err = CreateOrUpdateRota(ctx, tx, req) - Expect(err).To(HaveOccurred()) - Expect(err).To(Equal(ErrAlreadyExists)) - }) - - It("Should update rota if id is null", func() { - tx, err := conn.Begin(ctx) - Expect(err).ToNot(HaveOccurred()) - - id, err := CreateOrUpdateRota(ctx, tx, CreateOrUpdateRotaParams{ - ChannelID: "foo", - TeamID: "bar", - Name: "baz", - }) - Expect(err).ToNot(HaveOccurred()) - Expect(id).ToNot(BeEmpty()) - - updated, err := CreateOrUpdateRota(ctx, tx, CreateOrUpdateRotaParams{ - RotaID: id, - Name: "bazbaz", - }) - Expect(err).ToNot(HaveOccurred()) - Expect(id).To(Equal(updated)) - - rota, err := New(tx).FindRotaByID(ctx, id) - Expect(err).ToNot(HaveOccurred()) - Expect(rota.Name).To(Equal("bazbaz")) - }) - - It("Should fail to update something it does not exist", func() { - tx, err := conn.Begin(ctx) - Expect(err).ToNot(HaveOccurred()) - - _, err = CreateOrUpdateRota(ctx, tx, CreateOrUpdateRotaParams{ - RotaID: "not_found", - Name: "bazbaz", - }) - Expect(err).To(HaveOccurred()) - Expect(err).To(Equal(ErrNotFound)) - }) -}) diff --git a/slack/service.go b/slack/service.go index e3b0118..ca21ed5 100644 --- a/slack/service.go +++ b/slack/service.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/jackc/pgx/v5" + "github.com/rotabot-io/rotabot/lib/db" "github.com/getsentry/sentry-go" "github.com/jackc/pgx/v5/pgxpool" @@ -49,13 +50,6 @@ func (s svc) Commands(ctx context.Context, c *gen.Command) error { } ctx = slackclient.WithClient(ctx, client) - view := views.Home{ - State: &views.HomeState{ - TriggerID: c.TriggerID, - ChannelID: c.ChannelID, - TeamID: c.TeamID, - }, - } tx, err := s.conn.Begin(ctx) if err != nil { l.Error("failed to begin transaction", zap.Error(err)) @@ -68,7 +62,16 @@ func (s svc) Commands(ctx context.Context, c *gen.Command) error { } }(tx, ctx) - p, err := view.BuildProps(ctx, tx) + view := views.Home{ + Repository: db.New(tx), + State: &views.HomeState{ + TriggerID: c.TriggerID, + ChannelID: c.ChannelID, + TeamID: c.TeamID, + }, + } + + p, err := view.BuildProps(ctx) if err != nil { l.Error("failed to build props", zap.Error(err)) return goaerrors.NewInternalError() @@ -108,12 +111,6 @@ func (s svc) MessageActions(ctx context.Context, event *gen.Action) (*gen.Action } ctx = slackclient.WithClient(ctx, client) - view, err := views.Resolve(ctx, views.ResolverParams{Action: action}) - if err != nil { - l.Error("failed to resolve view", zap.Error(err)) - return nil, goaerrors.NewInternalError() - } - tx, err := s.conn.Begin(ctx) if err != nil { l.Error("failed to begin transaction", zap.Error(err)) @@ -125,15 +122,25 @@ func (s svc) MessageActions(ctx context.Context, event *gen.Action) (*gen.Action l.Error("failed to rollback transaction", zap.Error(err)) } }(tx, ctx) + + view, err := views.Resolve(ctx, views.ResolverParams{ + Repository: db.New(tx), + Action: action, + }) + if err != nil { + l.Error("failed to resolve view", zap.Error(err)) + return nil, goaerrors.NewInternalError() + } + // We only use views for now so these are the only events that make sense for us to handle. var res *gen.ActionResponse switch action.Type { // nolint:exhaustive case slack.InteractionTypeBlockActions: - res, err = view.OnAction(ctx, tx) + res, err = view.OnAction(ctx) case slack.InteractionTypeViewSubmission: - res, err = view.OnSubmit(ctx, tx) + res, err = view.OnSubmit(ctx) case slack.InteractionTypeViewClosed: - res, err = view.OnClose(ctx, tx) + res, err = view.OnClose(ctx) default: sentry.CaptureMessage(fmt.Sprintf("unknown_action_type: %s", action.Type)) l.Warn("unknown_action_type", zap.String("type", string(action.Type))) diff --git a/slack/views/home.go b/slack/views/home.go index 675d7c8..8f0ea89 100644 --- a/slack/views/home.go +++ b/slack/views/home.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" - "github.com/jackc/pgx/v5" "github.com/rotabot-io/rotabot/slack/slackclient" "github.com/getsentry/sentry-go" @@ -34,7 +33,8 @@ const ( ) type Home struct { - State *HomeState + Repository db.Repository + State *HomeState } type HomeState struct { @@ -58,9 +58,10 @@ func (v Home) DefaultState() interface{} { return &HomeState{} } -func (v Home) BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error) { +func (v Home) BuildProps(ctx context.Context) (interface{}, error) { l := zapctx.Logger(ctx) - rotas, err := db.New(tx).ListRotasByChannel(ctx, db.ListRotasByChannelParams{ChannelID: v.State.ChannelID, TeamID: v.State.TeamID}) + args := db.ListRotasByChannelParams{ChannelID: v.State.ChannelID, TeamID: v.State.TeamID} + rotas, err := v.Repository.ListRotasByChannel(ctx, args) if err != nil { l.Error("failed to list rotas", zap.Error(err)) return nil, errors.New("failed to list rotas") @@ -93,10 +94,10 @@ func (v Home) BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error) { }, nil } -func (v Home) OnAction(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v Home) OnAction(ctx context.Context) (*gen.ActionResponse, error) { switch v.State.action { case HASaveRota: - return v.handleAddRotaAction(ctx, tx) + return v.handleAddRotaAction(ctx) default: zapctx.Logger(ctx).Warn("unknown_action", zap.String("action", string(v.State.action))) sentry.CaptureMessage("unknown_action") @@ -104,12 +105,12 @@ func (v Home) OnAction(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, err } } -func (v Home) OnClose(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v Home) OnClose(ctx context.Context) (*gen.ActionResponse, error) { zapctx.Logger(ctx).Debug("closing_home_view") return &gen.ActionResponse{}, nil } -func (v Home) OnSubmit(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v Home) OnSubmit(ctx context.Context) (*gen.ActionResponse, error) { zapctx.Logger(ctx).Error("submitting_home_view") return nil, goaerrors.NewInternalError() } @@ -151,7 +152,7 @@ func (v Home) Render(ctx context.Context, p interface{}) error { return err } -func (v Home) handleAddRotaAction(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v Home) handleAddRotaAction(ctx context.Context) (*gen.ActionResponse, error) { l := zapctx.Logger(ctx) view := SaveRota{} view.State = view.DefaultState().(*SaveRotaState) @@ -159,7 +160,7 @@ func (v Home) handleAddRotaAction(ctx context.Context, tx pgx.Tx) (*gen.ActionRe view.State.TeamID = v.State.TeamID view.State.rotaID = v.State.rotaID - p, err := view.BuildProps(ctx, tx) + p, err := view.BuildProps(ctx) if err != nil { l.Error("failed to build props", zap.Error(err)) return nil, errors.New("failed to build add rota props") diff --git a/slack/views/home_test.go b/slack/views/home_test.go index 1959fe0..76dca1a 100644 --- a/slack/views/home_test.go +++ b/slack/views/home_test.go @@ -23,7 +23,7 @@ var _ = Describe("Home", func() { ctx context.Context sc *mock_slackclient.MockSlackClient home *Home - tx pgx.Tx + repo db.Repository conn *pgx.Conn ) @@ -42,14 +42,18 @@ var _ = Describe("Home", func() { conn, err = pgx.Connect(ctx, connString) Expect(err).ToNot(HaveOccurred()) - home = &Home{} - - tx, err = conn.Begin(ctx) + tx, err := conn.Begin(ctx) Expect(err).ToNot(HaveOccurred()) + repo = db.New(tx) + home = &Home{ + Repository: repo, + } + DeferCleanup(func() { _ = container.Terminate(ctx) _ = conn.Close(ctx) + _ = tx.Rollback(ctx) }) }) @@ -79,17 +83,8 @@ var _ = Describe("Home", func() { } }) - It("returns an error when unable to list rotas", func() { - err := tx.Rollback(ctx) - Expect(err).ToNot(HaveOccurred()) - - _, err = home.BuildProps(ctx, tx) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("failed to list rotas")) - }) - It("returns props when no rota exists", func() { - p, err := home.BuildProps(ctx, tx) + p, err := home.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&HomeProps{})) @@ -113,14 +108,18 @@ var _ = Describe("Home", func() { }) It("returns home props when rotas exist", func() { - id, err := db.New(tx).SaveRota(ctx, db.SaveRotaParams{ + id, err := repo.CreateOrUpdateRota(ctx, db.CreateOrUpdateRotaParams{ + Name: "Test Rota", ChannelID: home.State.ChannelID, TeamID: home.State.TeamID, - Name: "Test Rota", + Metadata: db.RotaMetadata{ + Frequency: db.RFMonthly, + SchedulingType: db.RSRandom, + }, }) Expect(err).ToNot(HaveOccurred()) - p, err := home.BuildProps(ctx, tx) + p, err := home.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&HomeProps{})) @@ -170,7 +169,7 @@ var _ = Describe("Home", func() { It("returns an error when actions is unknown", func() { home.State.action = "unknown" - _, err := home.OnAction(ctx, tx) + _, err := home.OnAction(ctx) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError("unknown_action")) }) @@ -187,7 +186,7 @@ var _ = Describe("Home", func() { schedulingType: db.RSCreated, }, } - p, err := addRota.BuildProps(ctx, tx) + p, err := addRota.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) props := p.(*SaveRotaProps) @@ -204,26 +203,14 @@ var _ = Describe("Home", func() { } sc.EXPECT().PushViewContext(ctx, triggerID, expectedModal).Return(nil, nil).Times(1) - _, err = home.OnAction(ctx, tx) - Expect(err).ToNot(HaveOccurred()) - }) - - It("returns error when it's unable to build props", func() { - home.State.action = HASaveRota - home.State.rotaID = "123" - - err := tx.Rollback(ctx) + _, err = home.OnAction(ctx) Expect(err).ToNot(HaveOccurred()) - - _, err = home.OnAction(ctx, tx) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("failed to build add rota props")) }) }) Describe("OnClose", func() { It("returns without doing anything", func() { - res, err := home.OnClose(ctx, tx) + res, err := home.OnClose(ctx) Expect(err).ToNot(HaveOccurred()) expectedRes := &gen.ActionResponse{} @@ -233,7 +220,7 @@ var _ = Describe("Home", func() { Describe("OnSubmit", func() { It("fails", func() { - _, err := home.OnSubmit(ctx, tx) + _, err := home.OnSubmit(ctx) Expect(err).To(HaveOccurred()) }) }) @@ -248,7 +235,7 @@ var _ = Describe("Home", func() { }) It("calls slack to open modal", func() { - p, err := home.BuildProps(ctx, tx) + p, err := home.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&HomeProps{})) props := p.(*HomeProps) diff --git a/slack/views/resolver.go b/slack/views/resolver.go index 1ca5d81..28bd625 100644 --- a/slack/views/resolver.go +++ b/slack/views/resolver.go @@ -16,7 +16,8 @@ import ( ) type ResolverParams struct { - Action slack.InteractionCallback + Repository db.Repository + Action slack.InteractionCallback } var ( @@ -45,6 +46,7 @@ func resolveHomeView(ctx context.Context, p ResolverParams) (View, error) { } view := &Home{} + view.Repository = p.Repository view.State = view.DefaultState().(*HomeState) view.State.TriggerID = p.Action.TriggerID view.State.TeamID = p.Action.Team.ID @@ -71,6 +73,7 @@ func resolveSaveRota(ctx context.Context, p ResolverParams) (View, error) { } view := &SaveRota{} + view.Repository = p.Repository view.State = view.DefaultState().(*SaveRotaState) view.State.TriggerID = p.Action.TriggerID view.State.rotaID = m.RotaID diff --git a/slack/views/save_rota.go b/slack/views/save_rota.go index e16a9c2..15a2398 100644 --- a/slack/views/save_rota.go +++ b/slack/views/save_rota.go @@ -6,7 +6,6 @@ import ( "errors" "github.com/getsentry/sentry-go" - "github.com/jackc/pgx/v5" "github.com/rotabot-io/rotabot/slack/slackclient" gen "github.com/rotabot-io/rotabot/gen/slack" @@ -20,7 +19,8 @@ import ( ) type SaveRota struct { - State *SaveRotaState + Repository db.Repository + State *SaveRotaState } type SaveRotaState struct { @@ -53,7 +53,7 @@ func (v SaveRota) DefaultState() interface{} { } } -func (v SaveRota) BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error) { +func (v SaveRota) BuildProps(ctx context.Context) (interface{}, error) { var title *slack.TextBlockObject var submit *slack.TextBlockObject var rotaName string @@ -62,7 +62,7 @@ func (v SaveRota) BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error l := zapctx.Logger(ctx) if v.State.rotaID != "" { - rota, err := db.New(tx).FindRotaByID(ctx, v.State.rotaID) + rota, err := v.Repository.FindRotaByID(ctx, v.State.rotaID) if err != nil { l.Error("failed_to_find", zap.Error(err)) return nil, err @@ -115,19 +115,19 @@ func (v SaveRota) BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error }, nil } -func (v SaveRota) OnAction(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v SaveRota) OnAction(ctx context.Context) (*gen.ActionResponse, error) { zapctx.Logger(ctx).Debug("action_view") return &gen.ActionResponse{}, nil } -func (v SaveRota) OnClose(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v SaveRota) OnClose(ctx context.Context) (*gen.ActionResponse, error) { zapctx.Logger(ctx).Debug("closing_view") return &gen.ActionResponse{}, nil } -func (v SaveRota) OnSubmit(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) { +func (v SaveRota) OnSubmit(ctx context.Context) (*gen.ActionResponse, error) { l := zapctx.Logger(ctx) - id, err := db.CreateOrUpdateRota(ctx, tx, db.CreateOrUpdateRotaParams{ + id, err := v.Repository.CreateOrUpdateRota(ctx, db.CreateOrUpdateRotaParams{ RotaID: v.State.rotaID, TeamID: v.State.TeamID, ChannelID: v.State.ChannelID, @@ -150,13 +150,14 @@ func (v SaveRota) OnSubmit(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, l.Info("saved_rota", zap.String("id", id)) h := Home{ + Repository: v.Repository, State: &HomeState{ TriggerID: v.State.TriggerID, ChannelID: v.State.ChannelID, TeamID: v.State.TeamID, }, } - p, err := h.BuildProps(ctx, tx) + p, err := h.BuildProps(ctx) if err != nil { l.Error("failed_to_build_home_props", zap.Error(err)) return nil, err diff --git a/slack/views/save_rota_test.go b/slack/views/save_rota_test.go index e907103..4fed2e0 100644 --- a/slack/views/save_rota_test.go +++ b/slack/views/save_rota_test.go @@ -25,8 +25,8 @@ var _ = Describe("SaveRota", func() { var ( ctx context.Context sc *mock_slackclient.MockSlackClient + repo db.Repository addRota *SaveRota - tx pgx.Tx conn *pgx.Conn channelID string teamID string @@ -48,17 +48,21 @@ var _ = Describe("SaveRota", func() { conn, err = pgx.Connect(ctx, connString) Expect(err).ToNot(HaveOccurred()) - addRota = &SaveRota{} + tx, err := conn.Begin(ctx) + Expect(err).ToNot(HaveOccurred()) + + repo = db.New(tx) + addRota = &SaveRota{ + Repository: repo, + } channelID = "CH123" teamID = "TM123" triggerID = "TR123" - tx, err = conn.Begin(ctx) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(func() { _ = container.Terminate(ctx) _ = conn.Close(ctx) + _ = tx.Rollback(ctx) }) }) @@ -100,7 +104,7 @@ var _ = Describe("SaveRota", func() { schedulingType: db.RSRandom, } - p, err := addRota.BuildProps(ctx, tx) + p, err := addRota.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&SaveRotaProps{})) @@ -127,7 +131,7 @@ var _ = Describe("SaveRota", func() { When("Rota does exist", func() { BeforeEach(func() { - id, err := db.New(tx).SaveRota(ctx, db.SaveRotaParams{ + id, err := repo.CreateOrUpdateRota(ctx, db.CreateOrUpdateRotaParams{ Name: "Test Rota", TeamID: teamID, ChannelID: channelID, @@ -146,7 +150,7 @@ var _ = Describe("SaveRota", func() { } }) It("builds the props with the values from the rota", func() { - p, err := addRota.BuildProps(ctx, tx) + p, err := addRota.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&SaveRotaProps{})) @@ -175,7 +179,7 @@ var _ = Describe("SaveRota", func() { Describe("OnAction", func() { It("returns without doing anything", func() { - res, err := addRota.OnAction(ctx, tx) + res, err := addRota.OnAction(ctx) Expect(err).ToNot(HaveOccurred()) expectedRes := &gen.ActionResponse{} @@ -185,7 +189,7 @@ var _ = Describe("SaveRota", func() { Describe("OnClose", func() { It("returns without doing anything", func() { - res, err := addRota.OnClose(ctx, tx) + res, err := addRota.OnClose(ctx) Expect(err).ToNot(HaveOccurred()) expectedRes := &gen.ActionResponse{} @@ -196,10 +200,14 @@ var _ = Describe("SaveRota", func() { Describe("OnSubmit", func() { When("the user creats a rota that already exists", func() { It("returns an error", func() { - _, err := db.New(tx).SaveRota(ctx, db.SaveRotaParams{ + _, err := repo.CreateOrUpdateRota(ctx, db.CreateOrUpdateRotaParams{ Name: "test", TeamID: teamID, ChannelID: channelID, + Metadata: db.RotaMetadata{ + Frequency: db.RFMonthly, + SchedulingType: db.RSRandom, + }, }) Expect(err).ToNot(HaveOccurred()) @@ -212,7 +220,7 @@ var _ = Describe("SaveRota", func() { schedulingType: db.RSCreated, } - res, err := addRota.OnSubmit(ctx, tx) + res, err := addRota.OnSubmit(ctx) Expect(err).ToNot(HaveOccurred()) Expect(res).ToNot(BeNil()) @@ -242,7 +250,7 @@ var _ = Describe("SaveRota", func() { UpdateViewContext(ctx, gomock.Any(), "E123", "", "PV123"). Return(nil, nil).Times(1) - res, err := addRota.OnSubmit(ctx, tx) + res, err := addRota.OnSubmit(ctx) Expect(err).ToNot(HaveOccurred()) Expect(res).ToNot(BeNil()) @@ -262,7 +270,7 @@ var _ = Describe("SaveRota", func() { }) It("calls slack to open modal", func() { - p, err := addRota.BuildProps(ctx, tx) + p, err := addRota.BuildProps(ctx) Expect(err).ToNot(HaveOccurred()) Expect(p).To(BeAssignableToTypeOf(&SaveRotaProps{})) props := p.(*SaveRotaProps) diff --git a/slack/views/view.go b/slack/views/view.go index 63e5172..8534900 100644 --- a/slack/views/view.go +++ b/slack/views/view.go @@ -3,8 +3,6 @@ package views import ( "context" - "github.com/jackc/pgx/v5" - gen "github.com/rotabot-io/rotabot/gen/slack" ) @@ -23,9 +21,9 @@ type Metadata struct { type View interface { CallbackID() ViewType DefaultState() interface{} - BuildProps(ctx context.Context, tx pgx.Tx) (interface{}, error) - OnAction(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) - OnClose(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) - OnSubmit(ctx context.Context, tx pgx.Tx) (*gen.ActionResponse, error) + BuildProps(ctx context.Context) (interface{}, error) + OnAction(ctx context.Context) (*gen.ActionResponse, error) + OnClose(ctx context.Context) (*gen.ActionResponse, error) + OnSubmit(ctx context.Context) (*gen.ActionResponse, error) Render(ctx context.Context, props interface{}) error } diff --git a/sqlc.yaml b/sqlc.yaml index 21d0d72..fd2ea95 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,7 +1,7 @@ version: "2" sql: - engine: "postgresql" - queries: "assets/queries/" + queries: "assets/queries.sql" schema: "assets/migrations/" gen: go: @@ -9,6 +9,7 @@ sql: package: "db" out: "lib/db" emit_json_tags: true + emit_empty_slices: true overrides: - column: "rotas.metadata" go_type: