You've already forked agentic-coding-workflow
feat(project): postgres repository wrapper with type conversions
This commit is contained in:
@@ -36,6 +36,7 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
// Package project 内的 repository.go 是 sqlc 生成层之上的薄包装:把
|
||||||
|
// pgtype.* 与领域类型互转,把 pgx.ErrNoRows 翻译成 errs.NotFound,把
|
||||||
|
// pg unique_violation(编号竞争)暴露为可识别的 error 让 service 决定重试。
|
||||||
|
package project
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||||
|
projectsqlc "github.com/yan1h/agent-coding-workflow/internal/project/sqlc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository 是 project 模块对 PG 的全部依赖。所有方法都接受 context 并返回
|
||||||
|
// 领域类型。Service 层的单元测试通过手写 fakeRepo 满足该接口。
|
||||||
|
type Repository interface {
|
||||||
|
// Project
|
||||||
|
CreateProject(ctx context.Context, p *Project) (*Project, error)
|
||||||
|
GetProjectBySlug(ctx context.Context, slug string) (*Project, error)
|
||||||
|
GetProjectByID(ctx context.Context, id uuid.UUID) (*Project, error)
|
||||||
|
ListProjects(ctx context.Context, includeArchived bool) ([]*Project, error)
|
||||||
|
UpdateProject(ctx context.Context, p *Project) (*Project, error)
|
||||||
|
ArchiveProject(ctx context.Context, id uuid.UUID) error
|
||||||
|
UnarchiveProject(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
// Requirement
|
||||||
|
CreateRequirement(ctx context.Context, r *Requirement) (*Requirement, error)
|
||||||
|
GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Requirement, error)
|
||||||
|
GetRequirementByID(ctx context.Context, id uuid.UUID) (*Requirement, error)
|
||||||
|
ListRequirements(ctx context.Context, projectID uuid.UUID, phase Phase, status Status) ([]*Requirement, error)
|
||||||
|
UpdateRequirement(ctx context.Context, r *Requirement) (*Requirement, error)
|
||||||
|
UpdateRequirementPhase(ctx context.Context, id uuid.UUID, to Phase) (*Requirement, error)
|
||||||
|
CloseRequirement(ctx context.Context, id uuid.UUID) error
|
||||||
|
ReopenRequirement(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
// Issue
|
||||||
|
CreateIssue(ctx context.Context, i *Issue) (*Issue, error)
|
||||||
|
GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Issue, error)
|
||||||
|
ListIssues(ctx context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error)
|
||||||
|
ListIssuesByRequirement(ctx context.Context, requirementID uuid.UUID) ([]*Issue, error)
|
||||||
|
UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID) (*Issue, error)
|
||||||
|
UpdateIssueAssignee(ctx context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error)
|
||||||
|
CloseIssue(ctx context.Context, id uuid.UUID) error
|
||||||
|
ReopenIssue(ctx context.Context, id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUniqueViolation 报告 err 是否为 PG 唯一约束冲突。Service 层在 Create*
|
||||||
|
// 时碰到此 error 会重试 1 次(编号自增竞争)。
|
||||||
|
func IsUniqueViolation(err error) bool {
|
||||||
|
var pg *pgconn.PgError
|
||||||
|
return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
|
||||||
|
}
|
||||||
|
|
||||||
|
type pgRepo struct{ q *projectsqlc.Queries }
|
||||||
|
|
||||||
|
// NewPostgresRepository 用现有的 pgxpool 构造 Repository。
|
||||||
|
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
|
||||||
|
return &pgRepo{q: projectsqlc.New(pool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 类型转换 helper =====
|
||||||
|
|
||||||
|
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 fromPgTimePtr(t pgtype.Timestamptz) *time.Time {
|
||||||
|
if !t.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := t.Time
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Project =====
|
||||||
|
|
||||||
|
func rowToProject(row projectsqlc.Project) *Project {
|
||||||
|
return &Project{
|
||||||
|
ID: fromPgUUID(row.ID),
|
||||||
|
Slug: row.Slug,
|
||||||
|
Name: row.Name,
|
||||||
|
Description: row.Description,
|
||||||
|
Visibility: Visibility(row.Visibility),
|
||||||
|
OwnerID: fromPgUUID(row.OwnerID),
|
||||||
|
ArchivedAt: fromPgTimePtr(row.ArchivedAt),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) CreateProject(ctx context.Context, p *Project) (*Project, error) {
|
||||||
|
row, err := r.q.CreateProject(ctx, projectsqlc.CreateProjectParams{
|
||||||
|
ID: toPgUUID(p.ID),
|
||||||
|
Slug: p.Slug,
|
||||||
|
Name: p.Name,
|
||||||
|
Description: p.Description,
|
||||||
|
Visibility: string(p.Visibility),
|
||||||
|
OwnerID: toPgUUID(p.OwnerID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if IsUniqueViolation(err) {
|
||||||
|
return nil, errs.New(errs.CodeProjectSlugTaken, "slug 已被使用")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "create project")
|
||||||
|
}
|
||||||
|
return rowToProject(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) GetProjectBySlug(ctx context.Context, slug string) (*Project, error) {
|
||||||
|
row, err := r.q.GetProjectBySlug(ctx, slug)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "project 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "get project by slug")
|
||||||
|
}
|
||||||
|
return rowToProject(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) GetProjectByID(ctx context.Context, id uuid.UUID) (*Project, error) {
|
||||||
|
row, err := r.q.GetProjectByID(ctx, toPgUUID(id))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "project 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "get project by id")
|
||||||
|
}
|
||||||
|
return rowToProject(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ListProjects(ctx context.Context, includeArchived bool) ([]*Project, error) {
|
||||||
|
rows, err := r.q.ListProjects(ctx, includeArchived)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "list projects")
|
||||||
|
}
|
||||||
|
out := make([]*Project, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, rowToProject(row))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UpdateProject(ctx context.Context, p *Project) (*Project, error) {
|
||||||
|
row, err := r.q.UpdateProject(ctx, projectsqlc.UpdateProjectParams{
|
||||||
|
ID: toPgUUID(p.ID),
|
||||||
|
Name: p.Name,
|
||||||
|
Description: p.Description,
|
||||||
|
Visibility: string(p.Visibility),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "project 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "update project")
|
||||||
|
}
|
||||||
|
return rowToProject(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ArchiveProject(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.ArchiveProject(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "archive project")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UnarchiveProject(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.UnarchiveProject(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "unarchive project")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Requirement =====
|
||||||
|
|
||||||
|
func rowToRequirement(row projectsqlc.Requirement) *Requirement {
|
||||||
|
return &Requirement{
|
||||||
|
ID: fromPgUUID(row.ID),
|
||||||
|
ProjectID: fromPgUUID(row.ProjectID),
|
||||||
|
Number: int(row.Number),
|
||||||
|
Title: row.Title,
|
||||||
|
Description: row.Description,
|
||||||
|
Phase: Phase(row.Phase),
|
||||||
|
Status: Status(row.Status),
|
||||||
|
OwnerID: fromPgUUID(row.OwnerID),
|
||||||
|
ClosedAt: fromPgTimePtr(row.ClosedAt),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) CreateRequirement(ctx context.Context, in *Requirement) (*Requirement, error) {
|
||||||
|
row, err := r.q.CreateRequirement(ctx, projectsqlc.CreateRequirementParams{
|
||||||
|
ID: toPgUUID(in.ID),
|
||||||
|
ProjectID: toPgUUID(in.ProjectID),
|
||||||
|
Title: in.Title,
|
||||||
|
Description: in.Description,
|
||||||
|
OwnerID: toPgUUID(in.OwnerID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if IsUniqueViolation(err) {
|
||||||
|
return nil, err // service 层据此重试一次
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "create requirement")
|
||||||
|
}
|
||||||
|
return rowToRequirement(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Requirement, error) {
|
||||||
|
row, err := r.q.GetRequirementByNumber(ctx, projectsqlc.GetRequirementByNumberParams{
|
||||||
|
ProjectID: toPgUUID(projectID),
|
||||||
|
Number: int32(number),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "get requirement by number")
|
||||||
|
}
|
||||||
|
return rowToRequirement(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) GetRequirementByID(ctx context.Context, id uuid.UUID) (*Requirement, error) {
|
||||||
|
row, err := r.q.GetRequirementByID(ctx, toPgUUID(id))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "get requirement by id")
|
||||||
|
}
|
||||||
|
return rowToRequirement(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ListRequirements(ctx context.Context, projectID uuid.UUID, phase Phase, status Status) ([]*Requirement, error) {
|
||||||
|
var phasePtr, statusPtr *string
|
||||||
|
if phase != "" {
|
||||||
|
s := string(phase)
|
||||||
|
phasePtr = &s
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
s := string(status)
|
||||||
|
statusPtr = &s
|
||||||
|
}
|
||||||
|
rows, err := r.q.ListRequirements(ctx, projectsqlc.ListRequirementsParams{
|
||||||
|
ProjectID: toPgUUID(projectID),
|
||||||
|
Phase: phasePtr,
|
||||||
|
Status: statusPtr,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "list requirements")
|
||||||
|
}
|
||||||
|
out := make([]*Requirement, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, rowToRequirement(row))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UpdateRequirement(ctx context.Context, in *Requirement) (*Requirement, error) {
|
||||||
|
row, err := r.q.UpdateRequirement(ctx, projectsqlc.UpdateRequirementParams{
|
||||||
|
ID: toPgUUID(in.ID),
|
||||||
|
Title: in.Title,
|
||||||
|
Description: in.Description,
|
||||||
|
OwnerID: toPgUUID(in.OwnerID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "update requirement")
|
||||||
|
}
|
||||||
|
return rowToRequirement(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UpdateRequirementPhase(ctx context.Context, id uuid.UUID, to Phase) (*Requirement, error) {
|
||||||
|
row, err := r.q.UpdateRequirementPhase(ctx, projectsqlc.UpdateRequirementPhaseParams{
|
||||||
|
ID: toPgUUID(id),
|
||||||
|
Phase: string(to),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "update requirement phase")
|
||||||
|
}
|
||||||
|
return rowToRequirement(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) CloseRequirement(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.CloseRequirement(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "close requirement")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ReopenRequirement(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.ReopenRequirement(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "reopen requirement")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Issue =====
|
||||||
|
|
||||||
|
func rowToIssue(row projectsqlc.Issue) *Issue {
|
||||||
|
return &Issue{
|
||||||
|
ID: fromPgUUID(row.ID),
|
||||||
|
ProjectID: fromPgUUID(row.ProjectID),
|
||||||
|
RequirementID: fromPgUUIDPtr(row.RequirementID),
|
||||||
|
Number: int(row.Number),
|
||||||
|
Title: row.Title,
|
||||||
|
Description: row.Description,
|
||||||
|
Status: Status(row.Status),
|
||||||
|
AssigneeID: fromPgUUIDPtr(row.AssigneeID),
|
||||||
|
CreatedBy: fromPgUUID(row.CreatedBy),
|
||||||
|
ClosedAt: fromPgTimePtr(row.ClosedAt),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) CreateIssue(ctx context.Context, in *Issue) (*Issue, error) {
|
||||||
|
row, err := r.q.CreateIssue(ctx, projectsqlc.CreateIssueParams{
|
||||||
|
ID: toPgUUID(in.ID),
|
||||||
|
ProjectID: toPgUUID(in.ProjectID),
|
||||||
|
RequirementID: toPgUUIDPtr(in.RequirementID),
|
||||||
|
Title: in.Title,
|
||||||
|
Description: in.Description,
|
||||||
|
AssigneeID: toPgUUIDPtr(in.AssigneeID),
|
||||||
|
CreatedBy: toPgUUID(in.CreatedBy),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if IsUniqueViolation(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "create issue")
|
||||||
|
}
|
||||||
|
return rowToIssue(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Issue, error) {
|
||||||
|
row, err := r.q.GetIssueByNumber(ctx, projectsqlc.GetIssueByNumberParams{
|
||||||
|
ProjectID: toPgUUID(projectID),
|
||||||
|
Number: int32(number),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "get issue by number")
|
||||||
|
}
|
||||||
|
return rowToIssue(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ListIssues(ctx context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error) {
|
||||||
|
var statusPtr, reqFilter *string
|
||||||
|
if filter.Status != "" {
|
||||||
|
s := string(filter.Status)
|
||||||
|
statusPtr = &s
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case filter.Requirement == nil:
|
||||||
|
reqFilter = nil
|
||||||
|
case *filter.Requirement == 0:
|
||||||
|
s := "none"
|
||||||
|
reqFilter = &s
|
||||||
|
default:
|
||||||
|
s := "id"
|
||||||
|
reqFilter = &s
|
||||||
|
}
|
||||||
|
rows, err := r.q.ListIssues(ctx, projectsqlc.ListIssuesParams{
|
||||||
|
ProjectID: toPgUUID(projectID),
|
||||||
|
Status: statusPtr,
|
||||||
|
RequirementFilter: reqFilter,
|
||||||
|
RequirementID: toPgUUIDPtr(requirementID),
|
||||||
|
AssigneeID: toPgUUIDPtr(filter.AssigneeID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "list issues")
|
||||||
|
}
|
||||||
|
out := make([]*Issue, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, rowToIssue(row))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ListIssuesByRequirement(ctx context.Context, requirementID uuid.UUID) ([]*Issue, error) {
|
||||||
|
rows, err := r.q.ListIssuesByRequirement(ctx, toPgUUID(requirementID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "list issues by requirement")
|
||||||
|
}
|
||||||
|
out := make([]*Issue, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, rowToIssue(row))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID) (*Issue, error) {
|
||||||
|
row, err := r.q.UpdateIssueCore(ctx, projectsqlc.UpdateIssueCoreParams{
|
||||||
|
ID: toPgUUID(id),
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
RequirementID: toPgUUIDPtr(requirementID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "update issue core")
|
||||||
|
}
|
||||||
|
return rowToIssue(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) UpdateIssueAssignee(ctx context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error) {
|
||||||
|
row, err := r.q.UpdateIssueAssignee(ctx, projectsqlc.UpdateIssueAssigneeParams{
|
||||||
|
ID: toPgUUID(id),
|
||||||
|
AssigneeID: toPgUUIDPtr(assignee),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
|
||||||
|
}
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "update issue assignee")
|
||||||
|
}
|
||||||
|
return rowToIssue(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) CloseIssue(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.CloseIssue(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "close issue")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ReopenIssue(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if err := r.q.ReopenIssue(ctx, toPgUUID(id)); err != nil {
|
||||||
|
return errs.Wrap(err, errs.CodeInternal, "reopen issue")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user