You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user