From 9b30ecf066199815cf17e33658c7ab383af5ddaf Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Sun, 11 Aug 2024 00:06:50 +0000 Subject: [PATCH] Return assignments from the API --- api/comments_test.go | 4 +- .../000002_create_tickets_table.up.sql | 3 +- api/database/queries/assignments.sql | 12 +- api/database/queries/labels.sql | 15 +- api/database/queries/tickets.sql | 20 +- api/testutil/testutil.go | 4 +- api/tickets.go | 185 +++++++++++------- api/tickets_test.go | 46 +++-- 8 files changed, 186 insertions(+), 103 deletions(-) diff --git a/api/comments_test.go b/api/comments_test.go index 66e03ff..3ba2a06 100644 --- a/api/comments_test.go +++ b/api/comments_test.go @@ -96,7 +96,7 @@ func TestDeleteComment_FailWhenUserIsNotAdminOrOwner(t *testing.T) { require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode) - member := testutil.NewMember(t, &sdk) + member, _ := testutil.NewMember(t, &sdk) memberSdk := tEnv.AuthSDK(member.Email, member.Password) httpRes, err = memberSdk.DeleteComment(ticketRes.Data.ID, commentRes.Data.ID) @@ -167,7 +167,7 @@ func TestPatchComment_FailWhenUserIsNotAdminOrOwner(t *testing.T) { require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode) - member := testutil.NewMember(t, &sdk) + member, _ := testutil.NewMember(t, &sdk) memberSdk := tEnv.AuthSDK(member.Email, member.Password) newContent := gofakeit.Sentence(10) diff --git a/api/database/migrations/000002_create_tickets_table.up.sql b/api/database/migrations/000002_create_tickets_table.up.sql index 1471724..3e0c5aa 100644 --- a/api/database/migrations/000002_create_tickets_table.up.sql +++ b/api/database/migrations/000002_create_tickets_table.up.sql @@ -20,5 +20,6 @@ CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS ticket_labels ( ticket_id INTEGER REFERENCES tickets (id) ON DELETE CASCADE, label_id INTEGER REFERENCES labels (id) ON DELETE CASCADE, - PRIMARY KEY (ticket_id, label_id) + PRIMARY KEY (ticket_id, label_id), + UNIQUE (ticket_id, label_id) ); \ No newline at end of file diff --git a/api/database/queries/assignments.sql b/api/database/queries/assignments.sql index 71821af..971f732 100644 --- a/api/database/queries/assignments.sql +++ b/api/database/queries/assignments.sql @@ -5,4 +5,14 @@ RETURNING *; -- name: DeleteAssignment :exec DELETE FROM assignments -WHERE id = $1; \ No newline at end of file +WHERE id = $1; + +-- name: DeleteAssignmentByTicketIDAndUserID :exec +DELETE FROM assignments +WHERE ticket_id = $1 AND user_id = $2; + +-- name: GetAssignmentsByTicketID :many +SELECT assignments.*, sqlc.embed(users) +FROM assignments +JOIN users ON assignments.user_id = users.id +WHERE ticket_id = $1; \ No newline at end of file diff --git a/api/database/queries/labels.sql b/api/database/queries/labels.sql index 4d26b83..0e6072b 100644 --- a/api/database/queries/labels.sql +++ b/api/database/queries/labels.sql @@ -1,13 +1,13 @@ -- name: AssignLabelToTicket :exec -WITH label AS ( +INSERT INTO ticket_labels (ticket_id, label_id) +VALUES ( + @ticket_id, + ( SELECT id FROM labels WHERE name = @label_name -) -INSERT INTO ticket_labels (ticket_id, label_id) -SELECT @ticket_id, id -FROM label -RETURNING *; + ) +); -- name: UnassignLabelFromTicket :exec DELETE FROM ticket_labels @@ -24,4 +24,5 @@ SELECT * FROM labels; -- name: CreateLabel :one INSERT INTO labels (name, created_by) VALUES ($1, $2) -RETURNING *; \ No newline at end of file +RETURNING *; + diff --git a/api/database/queries/tickets.sql b/api/database/queries/tickets.sql index f3dc00b..dff12e2 100644 --- a/api/database/queries/tickets.sql +++ b/api/database/queries/tickets.sql @@ -3,21 +3,17 @@ INSERT INTO tickets (title, created_by) VALUES ($1, $2) RETURNING *; --- name: GetTicketsByIDs :many -SELECT tickets.*, sqlc.embed(users), array_agg(labels.name)::text[] AS labels -FROM tickets -JOIN ticket_labels ON tickets.id = ticket_labels.ticket_id -JOIN labels ON ticket_labels.label_id = labels.id -JOIN users ON tickets.created_by = users.id -WHERE tickets.id = ANY(@ids::int[]) -GROUP BY tickets.id, users.id; - -- name: GetTicketByID :one -SELECT tickets.*, sqlc.embed(users), array_remove(array_agg(labels.name), NULL)::text[] AS labels +SELECT + tickets.*, + sqlc.embed(users), + array_remove(array_agg(DISTINCT labels.name), NULL)::text[] AS labels, + array_remove(array_agg(DISTINCT assignments.user_id), NULL)::integer[] AS assigned_to FROM tickets LEFT JOIN ticket_labels ON tickets.id = ticket_labels.ticket_id LEFT JOIN labels ON ticket_labels.label_id = labels.id LEFT JOIN users ON tickets.created_by = users.id +LEFT JOIN assignments ON tickets.id = assignments.ticket_id WHERE tickets.id = @id GROUP BY tickets.id, users.id LIMIT 1; @@ -42,11 +38,13 @@ RETURNING *; SELECT tickets.*, sqlc.embed(users), - array_remove(array_agg(labels.name), NULL)::text[] AS labels + array_remove(array_agg(DISTINCT labels.name), NULL)::text[] AS labels, + array_remove(array_agg(DISTINCT assignments.user_id), NULL)::integer[] AS assigned_to FROM tickets LEFT JOIN ticket_labels ON tickets.id = ticket_labels.ticket_id LEFT JOIN labels ON ticket_labels.label_id = labels.id LEFT JOIN users ON tickets.created_by = users.id +LEFT JOIN assignments ON tickets.id = assignments.ticket_id WHERE CASE WHEN @title::text != '' THEN diff --git a/api/testutil/testutil.go b/api/testutil/testutil.go index 29b7647..d0291e2 100644 --- a/api/testutil/testutil.go +++ b/api/testutil/testutil.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func NewMember(t *testing.T, sdk *sdk.Client) api.CreateUserRequest { +func NewMember(t *testing.T, sdk *sdk.Client) (api.CreateUserRequest, api.CreateUserResponse) { req := api.CreateUserRequest{ Name: gofakeit.Name(), Username: gofakeit.Username(), @@ -25,7 +25,7 @@ func NewMember(t *testing.T, sdk *sdk.Client) api.CreateUserRequest { require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode) - return req + return req, res } func RequireValidationError(t *testing.T, errors []api.ValidationError, field string, validator string) { diff --git a/api/tickets.go b/api/tickets.go index 7a04286..eb832d7 100644 --- a/api/tickets.go +++ b/api/tickets.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "slices" "strconv" @@ -17,15 +18,17 @@ type CreateTicketRequest struct { Title string `json:"title" validate:"required,min=3,max=70"` Description string `json:"description" validate:"required,min=10"` Labels []string `json:"labels,omitempty"` + AssignedTo []int32 `json:"assigned_to,omitempty"` } type Ticket struct { - ID int32 `json:"id"` - Title string `json:"title"` - Status string `json:"status"` - Labels []string `json:"labels"` - CreatedBy User `json:"created_by"` - CreatedAt string `json:"created_at"` + ID int32 `json:"id"` + Title string `json:"title"` + Status string `json:"status"` + Labels []string `json:"labels"` + AssignedTo []int32 `json:"assigned_to"` + CreatedBy User `json:"created_by"` + CreatedAt string `json:"created_at"` } type CreateTicketResponse = Response[Ticket] @@ -37,11 +40,11 @@ func (server *Server) createTicket(c *gin.Context) { server.jsonReq(c, &req) var ( - ticket sqlc.Ticket - err error + newTicket sqlc.GetTicketByIDRow + err error ) err = server.db.TX(func(ctx context.Context, qtx *sqlc.Queries, _ pgx.Tx) error { - ticket, err = qtx.CreateTicket(ctx, sqlc.CreateTicketParams{ + t, err := qtx.CreateTicket(ctx, sqlc.CreateTicketParams{ Title: req.Title, CreatedBy: user.ID, }) @@ -51,17 +54,17 @@ func (server *Server) createTicket(c *gin.Context) { _, err = qtx.CreateComment(ctx, sqlc.CreateCommentParams{ Content: req.Description, - TicketID: ticket.ID, + TicketID: t.ID, UserID: user.ID, }) if err != nil { return err } - if len(req.Labels) > 0 { + if req.Labels != nil { for _, labelName := range req.Labels { err = qtx.AssignLabelToTicket(ctx, sqlc.AssignLabelToTicketParams{ - TicketID: ticket.ID, + TicketID: t.ID, LabelName: labelName, }) if err != nil { @@ -70,6 +73,25 @@ func (server *Server) createTicket(c *gin.Context) { } } + if req.AssignedTo != nil { + for _, userID := range req.AssignedTo { + a, err := qtx.CreateAssignment(ctx, sqlc.CreateAssignmentParams{ + TicketID: t.ID, + UserID: userID, + AssignedBy: user.ID, + }) + fmt.Print(a) + if err != nil { + return err + } + } + } + + newTicket, err = qtx.GetTicketByID(ctx, t.ID) + if err != nil { + return err + } + return nil }) @@ -80,17 +102,18 @@ func (server *Server) createTicket(c *gin.Context) { c.JSON(http.StatusCreated, CreateTicketResponse{ Data: Ticket{ - ID: ticket.ID, - Title: ticket.Title, - Status: string(ticket.Status), - Labels: req.Labels, - CreatedAt: ticket.CreatedAt.Time.Format(time.RFC3339), + ID: newTicket.ID, + Title: newTicket.Title, + Status: string(newTicket.Status), + Labels: newTicket.Labels, + AssignedTo: newTicket.AssignedTo, + CreatedAt: newTicket.CreatedAt.Time.Format(time.RFC3339), CreatedBy: User{ - ID: user.ID, - Name: user.Name, - Username: user.Username, - Email: user.Email, - Role: string(user.Role), + ID: newTicket.User.ID, + Name: newTicket.User.Name, + Username: newTicket.User.Username, + Email: newTicket.User.Email, + Role: string(newTicket.User.Role), }, }, }) @@ -159,11 +182,12 @@ func (server *Server) tickets(c *gin.Context) { tickets := make([]Ticket, len(ticketRows)) for i, ticket := range ticketRows { tickets[i] = Ticket{ - ID: ticket.ID, - Title: ticket.Title, - Status: string(ticket.Status), - Labels: ticket.Labels, - CreatedAt: ticket.CreatedAt.Time.Format(time.RFC3339), + ID: ticket.ID, + Title: ticket.Title, + Status: string(ticket.Status), + Labels: ticket.Labels, + AssignedTo: ticket.AssignedTo, + CreatedAt: ticket.CreatedAt.Time.Format(time.RFC3339), CreatedBy: User{ ID: ticket.User.ID, Name: ticket.User.Name, @@ -223,6 +247,7 @@ type PatchTicketRequest struct { Title string `json:"title,omitempty" validate:"omitempty,min=3,max=70"` Description string `json:"description,omitempty" validate:"omitempty,min=10"` Labels []string `json:"labels,omitempty"` + AssignedTo []int32 `json:"assignments,omitempty"` } type PatchTicketResponse = Response[Ticket] @@ -257,37 +282,62 @@ func (server *Server) patchTicket(c *gin.Context) { ticket.Title = req.Title } - var labelsToUnassign []string - for _, oldLabel := range ticket.Labels { - if !slices.Contains(req.Labels, oldLabel) { - labelsToUnassign = append(labelsToUnassign, oldLabel) + if req.Labels != nil { + for _, oldLabelName := range ticket.Labels { + if !slices.Contains(req.Labels, oldLabelName) { + err := qtx.UnassignLabelFromTicket(ctx, sqlc.UnassignLabelFromTicketParams{ + TicketID: ticket.ID, + LabelName: oldLabelName, + }) + if err != nil { + return err + } + } } - } - - var labelsToAssign []string - for _, newLabel := range req.Labels { - if !slices.Contains(ticket.Labels, newLabel) { - labelsToAssign = append(labelsToAssign, newLabel) + for _, newLabelName := range req.Labels { + if !slices.Contains(ticket.Labels, newLabelName) { + err := qtx.AssignLabelToTicket(ctx, sqlc.AssignLabelToTicketParams{ + TicketID: ticket.ID, + LabelName: newLabelName, + }) + if err != nil { + return err + } + } } } - for _, labelName := range labelsToUnassign { - err := qtx.UnassignLabelFromTicket(ctx, sqlc.UnassignLabelFromTicketParams{ - TicketID: ticket.ID, - LabelName: labelName, - }) + if req.AssignedTo != nil { + assignments, err := qtx.GetAssignmentsByTicketID(ctx, ticket.ID) if err != nil { return err } - } - - for _, labelName := range labelsToAssign { - err := qtx.AssignLabelToTicket(ctx, sqlc.AssignLabelToTicketParams{ - TicketID: ticket.ID, - LabelName: labelName, - }) - if err != nil { - return err + assignedUserIDs := make([]int32, len(assignments)) + for i, assignment := range assignments { + assignedUserIDs[i] = assignment.UserID + } + for _, oldUserID := range assignedUserIDs { + if !slices.Contains(req.AssignedTo, oldUserID) { + err := qtx.DeleteAssignmentByTicketIDAndUserID(ctx, sqlc.DeleteAssignmentByTicketIDAndUserIDParams{ + TicketID: ticket.ID, + UserID: int32(oldUserID), + }) + if err != nil { + return err + } + } + } + for _, newUserID := range req.AssignedTo { + if !slices.Contains(assignedUserIDs, newUserID) { + _, err := qtx.CreateAssignment(ctx, sqlc.CreateAssignmentParams{ + TicketID: ticket.ID, + UserID: int32(newUserID), + AssignedBy: user.ID, + }) + if err != nil { + return err + } + } } } @@ -312,11 +362,12 @@ func (server *Server) patchTicket(c *gin.Context) { case nil: c.JSON(http.StatusOK, PatchTicketResponse{ Data: Ticket{ - ID: updatedTicket.ID, - Title: updatedTicket.Title, - Status: string(updatedTicket.Status), - Labels: updatedTicket.Labels, - CreatedAt: updatedTicket.CreatedAt.Time.Format(time.RFC3339), + ID: updatedTicket.ID, + Title: updatedTicket.Title, + Status: string(updatedTicket.Status), + Labels: updatedTicket.Labels, + AssignedTo: updatedTicket.AssignedTo, + CreatedAt: updatedTicket.CreatedAt.Time.Format(time.RFC3339), CreatedBy: User{ ID: createdBy.ID, Name: createdBy.Name, @@ -359,11 +410,12 @@ func (server *Server) ticket(c *gin.Context) { case nil: c.JSON(http.StatusOK, PatchTicketResponse{ Data: Ticket{ - ID: ticketRow.ID, - Title: ticketRow.Title, - Status: string(ticketRow.Status), - Labels: ticketRow.Labels, - CreatedAt: ticketRow.CreatedAt.Time.Format(time.RFC3339), + ID: ticketRow.ID, + Title: ticketRow.Title, + Status: string(ticketRow.Status), + Labels: ticketRow.Labels, + AssignedTo: ticketRow.AssignedTo, + CreatedAt: ticketRow.CreatedAt.Time.Format(time.RFC3339), CreatedBy: User{ ID: ticketRow.User.ID, Name: ticketRow.User.Name, @@ -415,11 +467,12 @@ func (server *Server) patchTicketStatus(c *gin.Context) { c.JSON(http.StatusOK, PatchTicketStatusResponse{ Data: Ticket{ - ID: updatedTicket.ID, - Title: updatedTicket.Title, - Labels: updatedTicket.Labels, - Status: string(updatedTicket.Status), - CreatedAt: updatedTicket.CreatedAt.Time.Format(time.RFC3339), + ID: updatedTicket.ID, + Title: updatedTicket.Title, + Labels: updatedTicket.Labels, + Status: string(updatedTicket.Status), + AssignedTo: updatedTicket.AssignedTo, + CreatedAt: updatedTicket.CreatedAt.Time.Format(time.RFC3339), CreatedBy: User{ ID: updatedTicket.User.ID, Name: updatedTicket.User.Name, diff --git a/api/tickets_test.go b/api/tickets_test.go index c135449..f29af01 100644 --- a/api/tickets_test.go +++ b/api/tickets_test.go @@ -21,10 +21,17 @@ func TestCreateTicket_Success(t *testing.T) { setup := tEnv.Setup() sdk := tEnv.AuthSDK(setup.Req().Email, setup.Req().Password) + _, userA := testutil.NewMember(t, &sdk) + _, userB := testutil.NewMember(t, &sdk) + sdk.CreateLabel(api.CreateLabelRequest{Name: "bug"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "customer"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "login"}, nil) + req := api.CreateTicketRequest{ Title: "User cannot login", Description: "User cannot login to the system", Labels: []string{"bug", "customer", "login"}, + AssignedTo: []int32{userA.Data.ID, userB.Data.ID}, } var res api.CreateTicketResponse @@ -35,6 +42,7 @@ func TestCreateTicket_Success(t *testing.T) { require.NotEmpty(t, res.Data.ID) require.Equal(t, req.Title, res.Data.Title) require.Equal(t, req.Labels, res.Data.Labels) + require.Equal(t, req.AssignedTo, res.Data.AssignedTo) require.Equal(t, setup.Res().Data.ID, res.Data.CreatedBy.ID) } @@ -146,6 +154,12 @@ func TestTickets_FilterByLabel(t *testing.T) { i++ } + sdk.CreateLabel(api.CreateLabelRequest{Name: "bug"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "site"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "feature"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "request"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "api"}, nil) + createTicket("login not working", []string{"bug"}) createTicket("register not working", []string{"bug"}) createTicket("wrong validation", []string{"bug"}) @@ -222,7 +236,6 @@ func TestDeleteTicket_Success(t *testing.T) { httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", Description: "User cannot login to the system", - Labels: []string{"bug", "customer", "login"}, }, &res) require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode) @@ -244,12 +257,11 @@ func TestDeleteTicket_FailWhenUserIsNotAdminOrCreator(t *testing.T) { httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", Description: "User cannot login to the system", - Labels: []string{"bug", "customer", "login"}, }, &res) require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode) - member := testutil.NewMember(t, &sdk) + member, _ := testutil.NewMember(t, &sdk) memberSdk := tEnv.AuthSDK(member.Email, member.Password) httpRes, err = memberSdk.DeleteTicket(res.Data.ID) @@ -265,6 +277,11 @@ func TestPatchTicket_Success(t *testing.T) { setup := tEnv.Setup() sdk := tEnv.AuthSDK(setup.Req().Email, setup.Req().Password) + sdk.CreateLabel(api.CreateLabelRequest{Name: "bug"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "customer"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "login"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "500"}, nil) + var res api.CreateTicketResponse httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", @@ -286,10 +303,10 @@ func TestPatchTicket_Success(t *testing.T) { require.Equal(t, http.StatusOK, httpRes.StatusCode) require.Equal(t, req.Title, patchRes.Data.Title) - require.Equal(t, req.Labels, patchRes.Data.Labels) + require.ElementsMatch(t, req.Labels, patchRes.Data.Labels) } -func TestPatchTicket_RemoveLabels(t *testing.T) { +func TestPatchTicket_UpdateAssignedTo(t *testing.T) { t.Parallel() tEnv := testutil.NewEnv(t) @@ -297,21 +314,21 @@ func TestPatchTicket_RemoveLabels(t *testing.T) { setup := tEnv.Setup() sdk := tEnv.AuthSDK(setup.Req().Email, setup.Req().Password) - sdk.CreateLabel(api.CreateLabelRequest{Name: "bug"}, nil) - sdk.CreateLabel(api.CreateLabelRequest{Name: "customer"}, nil) - sdk.CreateLabel(api.CreateLabelRequest{Name: "login"}, nil) + _, userA := testutil.NewMember(t, &sdk) + _, userB := testutil.NewMember(t, &sdk) + _, userC := testutil.NewMember(t, &sdk) var res api.CreateTicketResponse httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", Description: "User cannot login to the system", - Labels: []string{"bug", "customer", "login"}, + AssignedTo: []int32{userA.Data.ID}, }, &res) require.NoError(t, err, "error making request") - require.Equal(t, http.StatusCreated, httpRes.StatusCode) + require.Equal(t, http.StatusCreated, httpRes.StatusCode, "error creating ticket") req := api.PatchTicketRequest{ - Labels: []string{"bug"}, + AssignedTo: []int32{userB.Data.ID, userC.Data.ID}, } var patchRes api.PatchTicketResponse @@ -319,7 +336,7 @@ func TestPatchTicket_RemoveLabels(t *testing.T) { require.NoError(t, err, "error making request") require.Equal(t, http.StatusOK, httpRes.StatusCode) - require.Equal(t, req.Labels, patchRes.Data.Labels) + require.Equal(t, req.AssignedTo, patchRes.Data.AssignedTo) } func TestTicket_Success(t *testing.T) { @@ -330,6 +347,10 @@ func TestTicket_Success(t *testing.T) { setup := tEnv.Setup() sdk := tEnv.AuthSDK(setup.Req().Email, setup.Req().Password) + sdk.CreateLabel(api.CreateLabelRequest{Name: "bug"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "customer"}, nil) + sdk.CreateLabel(api.CreateLabelRequest{Name: "login"}, nil) + var res api.CreateTicketResponse httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", @@ -363,7 +384,6 @@ func TestAPI_PatchTicketStatus(t *testing.T) { httpRes, err := sdk.CreateTicket(api.CreateTicketRequest{ Title: "User cannot login", Description: "User cannot login to the system", - Labels: []string{"bug", "customer", "login"}, }, &res) require.NoError(t, err, "error making request") require.Equal(t, http.StatusCreated, httpRes.StatusCode)