Go · 2865 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package review
4
5 import (
6 "context"
7
8 "github.com/jackc/pgx/v5/pgtype"
9
10 issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
11 "github.com/tenseleyFlow/shithub/internal/notif"
12 pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
13 )
14
15 // RequestParams describes a review-request action.
16 type RequestParams struct {
17 PRIssueID int64
18 RequestedUserID int64
19 RequestedByUserID int64
20 }
21
22 // Request creates a pr_review_requests row. Bounded by
23 // MaxReviewersPerPR per the spec pitfalls section.
24 func Request(ctx context.Context, deps Deps, p RequestParams) (pullsdb.PrReviewRequest, error) {
25 q := pullsdb.New()
26 count, err := q.CountActivePRReviewRequests(ctx, deps.Pool, p.PRIssueID)
27 if err != nil {
28 return pullsdb.PrReviewRequest{}, err
29 }
30 if int(count) >= MaxReviewersPerPR {
31 return pullsdb.PrReviewRequest{}, ErrReviewerLimitReached
32 }
33 // Idempotent-ish: if the same reviewer is already pending, don't
34 // add a duplicate row. Walking the active list is cheaper than
35 // adding a partial unique index given v1 PRs typically have
36 // single-digit reviewer counts.
37 existing, err := q.ListPRReviewRequests(ctx, deps.Pool, p.PRIssueID)
38 if err != nil {
39 return pullsdb.PrReviewRequest{}, err
40 }
41 for _, e := range existing {
42 if e.RequestedUserID.Valid && e.RequestedUserID.Int64 == p.RequestedUserID &&
43 !e.DismissedAt.Valid && !e.SatisfiedByReviewID.Valid {
44 return pullsdb.PrReviewRequest{}, ErrReviewerAlreadyPending
45 }
46 }
47 row, err := q.CreatePRReviewRequest(ctx, deps.Pool, pullsdb.CreatePRReviewRequestParams{
48 PrIssueID: p.PRIssueID,
49 RequestedUserID: pgtype.Int8{Int64: p.RequestedUserID, Valid: p.RequestedUserID != 0},
50 RequestedTeamID: pgtype.Int8{Valid: false},
51 RequestedByUserID: pgtype.Int8{Int64: p.RequestedByUserID, Valid: p.RequestedByUserID != 0},
52 })
53 if err != nil {
54 return row, err
55 }
56 // S29: emit a domain event so the fan-out worker can route a
57 // `review_requested` notification to the requested reviewer.
58 // Best-effort — review-request side already succeeded; an emit
59 // failure is logged but doesn't fail the request (the fan-out
60 // worker isn't strict about ordering for this kind).
61 issue, ierr := issuesdb.New().GetIssueByID(ctx, deps.Pool, p.PRIssueID)
62 if ierr == nil {
63 var public bool
64 _ = deps.Pool.QueryRow(
65 ctx,
66 `SELECT visibility = 'public' FROM repos WHERE id = $1`,
67 issue.RepoID,
68 ).Scan(&public)
69 _ = notif.Emit(ctx, deps.Pool, notif.Event{
70 ActorUserID: p.RequestedByUserID,
71 Kind: "review_requested",
72 RepoID: issue.RepoID,
73 SourceKind: "issue",
74 SourceID: issue.ID,
75 Public: public,
76 Extra: map[string]any{
77 "reviewer_user_id": p.RequestedUserID,
78 "issue_number": issue.Number,
79 "issue_title": issue.Title,
80 },
81 })
82 }
83 return row, nil
84 }
85