You've already forked agentic-coding-workflow
265 lines
8.6 KiB
Go
265 lines
8.6 KiB
Go
// repository.go is a thin wrapper over the sqlc layer: pgtype<->domain
|
|
// conversion, pgx.ErrNoRows -> errs.NotFound translation, and unique-violation
|
|
// detection. Mirrors internal/workspace/repository.go.
|
|
package changerequest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgerrcode"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
changerequestsqlc "github.com/yan1h/agent-coding-workflow/internal/changerequest/sqlc"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
)
|
|
|
|
// Tx is the transaction handle exposed to the service: NextNumber + Create run
|
|
// in one tx so per-project numbering is serialized.
|
|
type Tx interface {
|
|
NextNumber(ctx context.Context, projectID uuid.UUID) (int, error)
|
|
Create(ctx context.Context, cr *ChangeRequest) (*ChangeRequest, error)
|
|
}
|
|
|
|
// Repository is the change-request module's PG dependency.
|
|
type Repository interface {
|
|
GetByID(ctx context.Context, id uuid.UUID) (*ChangeRequest, error)
|
|
GetByNumber(ctx context.Context, projectSlug string, number int) (*ChangeRequest, error)
|
|
ListByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*ChangeRequest, error)
|
|
ListByProject(ctx context.Context, projectID uuid.UUID) ([]*ChangeRequest, error)
|
|
UpdateReview(ctx context.Context, id uuid.UUID, verdict Verdict, reviewedBy uuid.UUID) (*ChangeRequest, error)
|
|
UpdateState(ctx context.Context, id uuid.UUID, st State, mergeSHA string, mergedAt *time.Time) (*ChangeRequest, error)
|
|
UpdateCI(ctx context.Context, id uuid.UUID, ci CIState, st State) (*ChangeRequest, error)
|
|
InTx(ctx context.Context, fn func(Tx) error) error
|
|
}
|
|
|
|
// IsUniqueViolation reports whether err is a PG unique constraint violation.
|
|
func IsUniqueViolation(err error) bool {
|
|
var pg *pgconn.PgError
|
|
return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
|
|
}
|
|
|
|
type pgRepo struct {
|
|
pool *pgxpool.Pool
|
|
q *changerequestsqlc.Queries
|
|
}
|
|
|
|
// NewPostgresRepository builds a Repository from a pgxpool.
|
|
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
|
|
return &pgRepo{pool: pool, q: changerequestsqlc.New(pool)}
|
|
}
|
|
|
|
// ===== conversion helpers =====
|
|
|
|
func toPgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
|
|
|
|
func toPgUUIDPtr(u *uuid.UUID) pgtype.UUID {
|
|
if u == nil {
|
|
return pgtype.UUID{Valid: false}
|
|
}
|
|
return pgtype.UUID{Bytes: *u, Valid: true}
|
|
}
|
|
|
|
func fromPgUUID(p pgtype.UUID) uuid.UUID {
|
|
if !p.Valid {
|
|
return uuid.Nil
|
|
}
|
|
return uuid.UUID(p.Bytes)
|
|
}
|
|
|
|
func fromPgUUIDPtr(p pgtype.UUID) *uuid.UUID {
|
|
if !p.Valid {
|
|
return nil
|
|
}
|
|
u := uuid.UUID(p.Bytes)
|
|
return &u
|
|
}
|
|
|
|
func toPgTimePtr(t *time.Time) pgtype.Timestamptz {
|
|
if t == nil {
|
|
return pgtype.Timestamptz{Valid: false}
|
|
}
|
|
return pgtype.Timestamptz{Time: *t, Valid: true}
|
|
}
|
|
|
|
func fromPgTimePtr(t pgtype.Timestamptz) *time.Time {
|
|
if !t.Valid {
|
|
return nil
|
|
}
|
|
v := t.Time
|
|
return &v
|
|
}
|
|
|
|
func rowToCR(r changerequestsqlc.ChangeRequest) *ChangeRequest {
|
|
return &ChangeRequest{
|
|
ID: fromPgUUID(r.ID),
|
|
ProjectID: fromPgUUID(r.ProjectID),
|
|
WorkspaceID: fromPgUUID(r.WorkspaceID),
|
|
RequirementID: fromPgUUIDPtr(r.RequirementID),
|
|
IssueID: fromPgUUIDPtr(r.IssueID),
|
|
Number: int(r.Number),
|
|
Title: r.Title,
|
|
Description: r.Description,
|
|
SourceBranch: r.SourceBranch,
|
|
TargetBranch: r.TargetBranch,
|
|
State: State(r.State),
|
|
ReviewVerdict: Verdict(r.ReviewVerdict),
|
|
CIState: CIState(r.CiState),
|
|
Provider: r.Provider,
|
|
ExternalID: r.ExternalID,
|
|
ExternalURL: r.ExternalUrl,
|
|
MergeCommitSHA: r.MergeCommitSha,
|
|
ReviewedBy: fromPgUUIDPtr(r.ReviewedBy),
|
|
ReviewedAt: fromPgTimePtr(r.ReviewedAt),
|
|
CreatedBy: fromPgUUID(r.CreatedBy),
|
|
MergedAt: fromPgTimePtr(r.MergedAt),
|
|
CreatedAt: r.CreatedAt.Time,
|
|
UpdatedAt: r.UpdatedAt.Time,
|
|
}
|
|
}
|
|
|
|
// ===== queries =====
|
|
|
|
func (r *pgRepo) GetByID(ctx context.Context, id uuid.UUID) (*ChangeRequest, error) {
|
|
row, err := r.q.GetChangeRequestByID(ctx, toPgUUID(id))
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "change request not found")
|
|
}
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "get change request")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|
|
|
|
func (r *pgRepo) GetByNumber(ctx context.Context, projectSlug string, number int) (*ChangeRequest, error) {
|
|
row, err := r.q.GetChangeRequestByNumber(ctx, changerequestsqlc.GetChangeRequestByNumberParams{
|
|
Slug: projectSlug,
|
|
Number: int32(number),
|
|
})
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "change request not found")
|
|
}
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "get change request by number")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|
|
|
|
func (r *pgRepo) ListByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*ChangeRequest, error) {
|
|
rows, err := r.q.ListChangeRequestsByWorkspace(ctx, toPgUUID(wsID))
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "list change requests by workspace")
|
|
}
|
|
out := make([]*ChangeRequest, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, rowToCR(row))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *pgRepo) ListByProject(ctx context.Context, projectID uuid.UUID) ([]*ChangeRequest, error) {
|
|
rows, err := r.q.ListChangeRequestsByProject(ctx, toPgUUID(projectID))
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "list change requests by project")
|
|
}
|
|
out := make([]*ChangeRequest, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, rowToCR(row))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *pgRepo) UpdateReview(ctx context.Context, id uuid.UUID, verdict Verdict, reviewedBy uuid.UUID) (*ChangeRequest, error) {
|
|
row, err := r.q.UpdateChangeRequestReview(ctx, changerequestsqlc.UpdateChangeRequestReviewParams{
|
|
ID: toPgUUID(id),
|
|
ReviewVerdict: string(verdict),
|
|
ReviewedBy: toPgUUID(reviewedBy),
|
|
})
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "change request not found")
|
|
}
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "update change request review")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|
|
|
|
func (r *pgRepo) UpdateState(ctx context.Context, id uuid.UUID, st State, mergeSHA string, mergedAt *time.Time) (*ChangeRequest, error) {
|
|
row, err := r.q.UpdateChangeRequestState(ctx, changerequestsqlc.UpdateChangeRequestStateParams{
|
|
ID: toPgUUID(id),
|
|
State: string(st),
|
|
MergeCommitSha: mergeSHA,
|
|
MergedAt: toPgTimePtr(mergedAt),
|
|
})
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "change request not found")
|
|
}
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "update change request state")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|
|
|
|
func (r *pgRepo) UpdateCI(ctx context.Context, id uuid.UUID, ci CIState, st State) (*ChangeRequest, error) {
|
|
row, err := r.q.UpdateChangeRequestCI(ctx, changerequestsqlc.UpdateChangeRequestCIParams{
|
|
ID: toPgUUID(id),
|
|
CiState: string(ci),
|
|
State: string(st),
|
|
})
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, errs.New(errs.CodeNotFound, "change request not found")
|
|
}
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "update change request ci")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|
|
|
|
func (r *pgRepo) InTx(ctx context.Context, fn func(Tx) error) error {
|
|
return pgx.BeginFunc(ctx, r.pool, func(tx pgx.Tx) error {
|
|
return fn(&pgTx{q: r.q.WithTx(tx)})
|
|
})
|
|
}
|
|
|
|
type pgTx struct{ q *changerequestsqlc.Queries }
|
|
|
|
func (t *pgTx) NextNumber(ctx context.Context, projectID uuid.UUID) (int, error) {
|
|
n, err := t.q.NextChangeRequestNumber(ctx, toPgUUID(projectID))
|
|
if err != nil {
|
|
return 0, errs.Wrap(err, errs.CodeInternal, "next change request number")
|
|
}
|
|
return int(n), nil
|
|
}
|
|
|
|
func (t *pgTx) Create(ctx context.Context, cr *ChangeRequest) (*ChangeRequest, error) {
|
|
row, err := t.q.CreateChangeRequest(ctx, changerequestsqlc.CreateChangeRequestParams{
|
|
ID: toPgUUID(cr.ID),
|
|
ProjectID: toPgUUID(cr.ProjectID),
|
|
WorkspaceID: toPgUUID(cr.WorkspaceID),
|
|
RequirementID: toPgUUIDPtr(cr.RequirementID),
|
|
IssueID: toPgUUIDPtr(cr.IssueID),
|
|
Number: int32(cr.Number),
|
|
Title: cr.Title,
|
|
Description: cr.Description,
|
|
SourceBranch: cr.SourceBranch,
|
|
TargetBranch: cr.TargetBranch,
|
|
CiState: string(cr.CIState),
|
|
Provider: cr.Provider,
|
|
ExternalID: cr.ExternalID,
|
|
ExternalUrl: cr.ExternalURL,
|
|
CreatedBy: toPgUUID(cr.CreatedBy),
|
|
})
|
|
if err != nil {
|
|
if IsUniqueViolation(err) {
|
|
return nil, errs.Wrap(err, errs.CodeConflict, "change request number already used")
|
|
}
|
|
return nil, errs.Wrap(err, errs.CodeInternal, "create change request")
|
|
}
|
|
return rowToCR(row), nil
|
|
}
|