Go · 5616 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package issues
4
5 import (
6 "context"
7 "errors"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/jackc/pgx/v5/pgtype"
13
14 issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
15 )
16
17 // MilestoneCreateParams is input for CreateMilestone.
18 type MilestoneCreateParams struct {
19 RepoID int64
20 Title string
21 Description string
22 DueOn *time.Time
23 }
24
25 func CreateMilestone(ctx context.Context, deps Deps, p MilestoneCreateParams) (issuesdb.Milestone, error) {
26 title := strings.TrimSpace(p.Title)
27 if title == "" || len(title) > 200 {
28 return issuesdb.Milestone{}, errors.New("issues: milestone title length 1–200")
29 }
30 due := pgtype.Timestamptz{}
31 if p.DueOn != nil {
32 due = pgtype.Timestamptz{Time: *p.DueOn, Valid: true}
33 }
34 q := issuesdb.New()
35 row, err := q.CreateMilestone(ctx, deps.Pool, issuesdb.CreateMilestoneParams{
36 RepoID: p.RepoID, Title: title, Description: p.Description, DueOn: due,
37 })
38 if err != nil {
39 if isUniqueViolation(err) {
40 return issuesdb.Milestone{}, ErrMilestoneExists
41 }
42 return issuesdb.Milestone{}, err
43 }
44 return row, nil
45 }
46
47 type MilestoneUpdateParams struct {
48 ID int64
49 Title string
50 Description string
51 DueOn *time.Time
52 }
53
54 func UpdateMilestone(ctx context.Context, deps Deps, p MilestoneUpdateParams) error {
55 title := strings.TrimSpace(p.Title)
56 if title == "" || len(title) > 200 {
57 return errors.New("issues: milestone title length 1–200")
58 }
59 due := pgtype.Timestamptz{}
60 if p.DueOn != nil {
61 due = pgtype.Timestamptz{Time: *p.DueOn, Valid: true}
62 }
63 q := issuesdb.New()
64 if err := q.UpdateMilestone(ctx, deps.Pool, issuesdb.UpdateMilestoneParams{
65 ID: p.ID, Title: title, Description: p.Description, DueOn: due,
66 }); err != nil {
67 if isUniqueViolation(err) {
68 return ErrMilestoneExists
69 }
70 return err
71 }
72 return nil
73 }
74
75 func SetMilestoneState(ctx context.Context, deps Deps, id int64, state string) error {
76 if state != "open" && state != "closed" {
77 return errors.New("issues: milestone state must be open or closed")
78 }
79 q := issuesdb.New()
80 return q.SetMilestoneState(ctx, deps.Pool, issuesdb.SetMilestoneStateParams{
81 ID: id, State: issuesdb.MilestoneState(state),
82 })
83 }
84
85 func DeleteMilestone(ctx context.Context, deps Deps, id int64) error {
86 q := issuesdb.New()
87 return q.DeleteMilestone(ctx, deps.Pool, id)
88 }
89
90 // AssignMilestone sets an issue's milestone (or clears with milestoneID==0)
91 // and emits a `milestoned`/`demilestoned` event.
92 func AssignMilestone(ctx context.Context, deps Deps, actorUserID, issueID, milestoneID int64) error {
93 q := issuesdb.New()
94 tx, err := deps.Pool.Begin(ctx)
95 if err != nil {
96 return err
97 }
98 committed := false
99 defer func() {
100 if !committed {
101 _ = tx.Rollback(ctx)
102 }
103 }()
104 mid := pgtype.Int8{Int64: milestoneID, Valid: milestoneID != 0}
105 if err := q.SetIssueMilestone(ctx, tx, issuesdb.SetIssueMilestoneParams{
106 ID: issueID, MilestoneID: mid,
107 }); err != nil {
108 return err
109 }
110 kind := "milestoned"
111 mval := strconv.FormatInt(milestoneID, 10)
112 if milestoneID == 0 {
113 kind = "demilestoned"
114 mval = "null"
115 }
116 if _, err := q.InsertIssueEvent(ctx, tx, issuesdb.InsertIssueEventParams{
117 IssueID: issueID,
118 ActorUserID: pgtype.Int8{Int64: actorUserID, Valid: actorUserID != 0},
119 Kind: kind,
120 Meta: []byte(`{"milestone_id":` + mval + `}`),
121 }); err != nil {
122 return err
123 }
124 if err := tx.Commit(ctx); err != nil {
125 return err
126 }
127 committed = true
128 return nil
129 }
130
131 // AssignUser adds an assignee + emits an `assigned` event.
132 func AssignUser(ctx context.Context, deps Deps, actorUserID, issueID, userID int64) error {
133 q := issuesdb.New()
134 tx, err := deps.Pool.Begin(ctx)
135 if err != nil {
136 return err
137 }
138 committed := false
139 defer func() {
140 if !committed {
141 _ = tx.Rollback(ctx)
142 }
143 }()
144 if err := q.AssignUserToIssue(ctx, tx, issuesdb.AssignUserToIssueParams{
145 IssueID: issueID, UserID: userID,
146 AssignedByUserID: pgtype.Int8{Int64: actorUserID, Valid: actorUserID != 0},
147 }); err != nil {
148 return err
149 }
150 if _, err := q.InsertIssueEvent(ctx, tx, issuesdb.InsertIssueEventParams{
151 IssueID: issueID,
152 ActorUserID: pgtype.Int8{Int64: actorUserID, Valid: actorUserID != 0},
153 Kind: "assigned",
154 Meta: []byte(`{"user_id":` + strconv.FormatInt(userID, 10) + `}`),
155 }); err != nil {
156 return err
157 }
158 // S29: domain event so the fan-out worker can route an
159 // `assignment` notification to the assignee.
160 issue, _ := q.GetIssueByID(ctx, tx, issueID)
161 repoVis, _ := repoVisibilityPublic(ctx, tx, issue.RepoID)
162 if err := emitAssignmentEventTx(ctx, tx, issue, actorUserID, userID, repoVis); err != nil {
163 return err
164 }
165 if err := tx.Commit(ctx); err != nil {
166 return err
167 }
168 committed = true
169 return nil
170 }
171
172 // UnassignUser removes an assignee + emits an `unassigned` event.
173 func UnassignUser(ctx context.Context, deps Deps, actorUserID, issueID, userID int64) error {
174 q := issuesdb.New()
175 tx, err := deps.Pool.Begin(ctx)
176 if err != nil {
177 return err
178 }
179 committed := false
180 defer func() {
181 if !committed {
182 _ = tx.Rollback(ctx)
183 }
184 }()
185 if err := q.UnassignUserFromIssue(ctx, tx, issuesdb.UnassignUserFromIssueParams{
186 IssueID: issueID, UserID: userID,
187 }); err != nil {
188 return err
189 }
190 if _, err := q.InsertIssueEvent(ctx, tx, issuesdb.InsertIssueEventParams{
191 IssueID: issueID,
192 ActorUserID: pgtype.Int8{Int64: actorUserID, Valid: actorUserID != 0},
193 Kind: "unassigned",
194 Meta: []byte(`{"user_id":` + strconv.FormatInt(userID, 10) + `}`),
195 }); err != nil {
196 return err
197 }
198 if err := tx.Commit(ctx); err != nil {
199 return err
200 }
201 committed = true
202 return nil
203 }
204