This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+264
View File
@@ -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
}