Skip to content

Commit

Permalink
feat: Add support for members in rotas (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinrobayna authored Oct 20, 2023
1 parent 35956f9 commit 50d4a2a
Show file tree
Hide file tree
Showing 22 changed files with 752 additions and 62 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ jobs:
strategy:
fail-fast: true
matrix:
go: ['1.20', '1.21']
go: ['1.21']
# Disabling 1.20 because I'm using slices and maybe slog and don't really want to wait.
# go: ['1.20', '1.21']
steps:
- name: Check out code
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions assets/migrations/000003_setup_tables.down.sql
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DROP TABLE MEMBERS;
DROP TABLE ROTAS;
24 changes: 24 additions & 0 deletions assets/migrations/000003_setup_tables.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,27 @@ CREATE TRIGGER rotas_updated_at_trigger
ON ROTAS
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();

CREATE TABLE MEMBERS
(
ID TEXT PRIMARY KEY DEFAULT ('RM' || generate_uid(14)),
ROTA_ID TEXT NOT NULL,
USER_ID TEXT NOT NULL,
METADATA JSONB NOT NULL,
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),

CONSTRAINT fk_rota_id_on_member
FOREIGN KEY (ROTA_ID)
REFERENCES ROTAS (ID)
ON DELETE CASCADE
);

CREATE UNIQUE INDEX idx_unique_user_within_rota ON MEMBERS (ROTA_ID, USER_ID);
CREATE INDEX idx_user_id_on_members ON MEMBERS (USER_ID);

CREATE TRIGGER members_updated_at_trigger
BEFORE UPDATE
ON MEMBERS
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();
14 changes: 13 additions & 1 deletion assets/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ WHERE ID = $1;

-- name: ListRotasByChannel :many
SELECT ROTAS.*
from ROTAS
FROM ROTAS
WHERE ROTAS.CHANNEL_ID = $1
AND ROTAS.TEAM_ID = $2;

Expand All @@ -18,3 +18,15 @@ UPDATE ROTAS
SET NAME = $1,
METADATA = $2
WHERE ID = $3 RETURNING ID;

-- name: saveMember :one
INSERT INTO MEMBERS (ROTA_ID, USER_ID, METADATA)
VALUES ($1, $2, $3) RETURNING ID;

-- name: deleteMember :exec
DELETE FROM MEMBERS WHERE USER_ID = $1;

-- name: ListUserIDsByRotaID :many
SELECT MEMBERS.USER_ID
FROM MEMBERS
WHERE MEMBERS.ROTA_ID = $1;
61 changes: 61 additions & 0 deletions assets/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ SET default_tablespace = '';

SET default_table_access_method = heap;

--
-- Name: members; Type: TABLE; Schema: public; Owner: rotabot
--

CREATE TABLE public.members (
id text DEFAULT ('RM'::text || public.generate_uid(14)) NOT NULL,
rota_id text NOT NULL,
user_id text NOT NULL,
metadata jsonb NOT NULL,
created_at timestamp without time zone DEFAULT now() NOT NULL,
updated_at timestamp without time zone DEFAULT now() NOT NULL
);


ALTER TABLE public.members OWNER TO rotabot;

--
-- Name: rotas; Type: TABLE; Schema: public; Owner: rotabot
--
Expand Down Expand Up @@ -105,6 +121,14 @@ CREATE TABLE public.schema_migrations (

ALTER TABLE public.schema_migrations OWNER TO rotabot;

--
-- Data for Name: members; Type: TABLE DATA; Schema: public; Owner: rotabot
--

COPY public.members (id, rota_id, user_id, metadata, created_at, updated_at) FROM stdin;
\.


--
-- Data for Name: rotas; Type: TABLE DATA; Schema: public; Owner: rotabot
--
Expand All @@ -122,6 +146,14 @@ COPY public.schema_migrations (version, dirty) FROM stdin;
\.


--
-- Name: members members_pkey; Type: CONSTRAINT; Schema: public; Owner: rotabot
--

ALTER TABLE ONLY public.members
ADD CONSTRAINT members_pkey PRIMARY KEY (id);


--
-- Name: rotas rotas_pkey; Type: CONSTRAINT; Schema: public; Owner: rotabot
--
Expand All @@ -145,13 +177,42 @@ ALTER TABLE ONLY public.schema_migrations
CREATE UNIQUE INDEX idx_unique_rota_within_team_and_channel ON public.rotas USING btree (name, channel_id, team_id);


--
-- Name: idx_unique_user_within_rota; Type: INDEX; Schema: public; Owner: rotabot
--

CREATE UNIQUE INDEX idx_unique_user_within_rota ON public.members USING btree (rota_id, user_id);


--
-- Name: idx_user_id_on_members; Type: INDEX; Schema: public; Owner: rotabot
--

CREATE INDEX idx_user_id_on_members ON public.members USING btree (user_id);


--
-- Name: members members_updated_at_trigger; Type: TRIGGER; Schema: public; Owner: rotabot
--

CREATE TRIGGER members_updated_at_trigger BEFORE UPDATE ON public.members FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamp();


--
-- Name: rotas rotas_updated_at_trigger; Type: TRIGGER; Schema: public; Owner: rotabot
--

CREATE TRIGGER rotas_updated_at_trigger BEFORE UPDATE ON public.rotas FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamp();


--
-- Name: members fk_rota_id_on_member; Type: FK CONSTRAINT; Schema: public; Owner: rotabot
--

ALTER TABLE ONLY public.members
ADD CONSTRAINT fk_rota_id_on_member FOREIGN KEY (rota_id) REFERENCES public.rotas(id) ON DELETE CASCADE;


--
-- PostgreSQL database dump complete
--
Expand Down
13 changes: 13 additions & 0 deletions internal/internal_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package internal

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestDb(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Internal Suite")
}
16 changes: 16 additions & 0 deletions internal/unique.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package internal

func Unique[T comparable](slice []T) []T {
// create a map with all the values as key
uniqMap := make(map[T]struct{})
for _, v := range slice {
uniqMap[v] = struct{}{}
}

// turn the map keys into a slice
uniqSlice := make([]T, 0, len(uniqMap))
for v := range uniqMap {
uniqSlice = append(uniqSlice, v)
}
return uniqSlice
}
43 changes: 43 additions & 0 deletions internal/unique_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package internal

import (
"sort"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Unique", func() {
It("should return unique strings", func() {
arr := []string{
"mice",
"mice",
"mice",
"mice",
"mice",
"mice",
"mice",
"toad",
"toad",
"mice",
}
result := Unique(arr)
sort.Strings(result)
Expect(result).To(Equal([]string{"mice", "toad"}))
})

It("should return unique numbers", func() {
arr := []int{
1,
1,
2,
3,
1,
2,
3,
}
result := Unique(arr)
sort.Ints(result)
Expect(result).To(Equal([]int{1, 2, 3}))
})
})
29 changes: 29 additions & 0 deletions lib/db/mock_db/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions lib/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 74 additions & 7 deletions lib/db/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package db
import (
"context"
"errors"
"slices"

"github.com/rotabot-io/rotabot/internal"

"github.com/jackc/pgx/v5/pgconn"
"github.com/rotabot-io/rotabot/lib/zapctx"
Expand Down Expand Up @@ -36,15 +39,79 @@ func (q *Queries) CreateOrUpdateRota(ctx context.Context, p CreateOrUpdateRotaPa
})
}
if err != nil {
var pgError *pgconn.PgError
if errors.As(err, &pgError) {
switch pgError.Code {
case "23505":
return "", ErrAlreadyExists
}
}
err = mapError(err)
l.Error("failed to save rota", zap.Error(err))
return "", err
}
return rotaId, nil
}

func (q *Queries) UpdateRotaMembers(ctx context.Context, members []Member) error {
l := zapctx.Logger(ctx)
if len(members) == 0 {
l.Warn("no_members_to_update")
return nil
}
rotas := []string{}
for _, m := range members {
rotas = append(rotas, m.RotaID)
}
if len(internal.Unique(rotas)) > 1 {
return ErrMembersBelongToDifferentRotas
}
rotaId := rotas[0]
e, err := q.ListUserIDsByRotaID(ctx, rotaId)
if err != nil {
l.Error("unable_to_fetch_existing_members", zap.Error(err))
return err
}
for _, userId := range e {
// Check existing member needs to be deleted
if inx := slices.IndexFunc(members, func(member Member) bool { return member.UserID == userId }); inx == -1 {
err = q.deleteMember(ctx, userId)
if err != nil {
l.Error("unable_to_delete_member",
zap.Error(err),
zap.String("user_id", userId),
zap.Strings("existing", e),
)
return err
}
}
}
for _, m := range members {
// Check if desired member already exist before adding
if inx := slices.IndexFunc(e, func(userId string) bool { return m.UserID == userId }); inx == -1 {
_, err = q.saveMember(ctx, saveMemberParams{
RotaID: rotaId,
UserID: m.UserID,
Metadata: m.Metadata,
})
}
if err != nil {
err = mapError(err)
l.Error("unable_to_add_member",
zap.Error(err),
zap.String("user_id", m.UserID),
zap.Strings("existing", e),
)
return err
}
// Avoid trying to add the same user_id twice within the same request
e = append(e, m.UserID)
}
return nil
}

func mapError(err error) error {
var pgError *pgconn.PgError
if errors.As(err, &pgError) {
// The magic list of errors can be found here
// https://www.postgresql.org/docs/current/errcodes-appendix.html
switch pgError.Code {
case "23505":
return ErrAlreadyExists
}
}
return err
}
Loading

0 comments on commit 50d4a2a

Please sign in to comment.