You've already forked agentic-coding-workflow
feat(project): add workspace_id to issue/requirement with tri-state update
This commit is contained in:
@@ -46,7 +46,7 @@ type Repository interface {
|
||||
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)
|
||||
UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *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
|
||||
@@ -59,6 +59,16 @@ func IsUniqueViolation(err error) bool {
|
||||
return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
|
||||
}
|
||||
|
||||
// IsForeignKeyViolation 报告 err 是否为 PG 外键约束冲突。常见场景:
|
||||
// - issues.(project_id, workspace_id) 复合 FK 在跨 project 关联时被 PG 拒绝;
|
||||
// - requirements.workspace_id 关联到不存在的 workspace 时被 PG 拒绝。
|
||||
//
|
||||
// Repository 层据此把错误翻成 invalid_input(HTTP 400),避免泄漏 5xx。
|
||||
func IsForeignKeyViolation(err error) bool {
|
||||
var pg *pgconn.PgError
|
||||
return errors.As(err, &pg) && pg.Code == pgerrcode.ForeignKeyViolation
|
||||
}
|
||||
|
||||
type pgRepo struct{ q *projectsqlc.Queries }
|
||||
|
||||
// NewPostgresRepository 用现有的 pgxpool 构造 Repository。
|
||||
@@ -200,6 +210,7 @@ func rowToRequirement(row projectsqlc.Requirement) *Requirement {
|
||||
return &Requirement{
|
||||
ID: fromPgUUID(row.ID),
|
||||
ProjectID: fromPgUUID(row.ProjectID),
|
||||
WorkspaceID: fromPgUUIDPtr(row.WorkspaceID),
|
||||
Number: int(row.Number),
|
||||
Title: row.Title,
|
||||
Description: row.Description,
|
||||
@@ -219,11 +230,15 @@ func (r *pgRepo) CreateRequirement(ctx context.Context, in *Requirement) (*Requi
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
OwnerID: toPgUUID(in.OwnerID),
|
||||
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
|
||||
})
|
||||
if err != nil {
|
||||
if IsUniqueViolation(err) {
|
||||
return nil, err // service 层据此重试一次
|
||||
}
|
||||
if IsForeignKeyViolation(err) {
|
||||
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "create requirement")
|
||||
}
|
||||
return rowToRequirement(row), nil
|
||||
@@ -285,11 +300,15 @@ func (r *pgRepo) UpdateRequirement(ctx context.Context, in *Requirement) (*Requi
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
OwnerID: toPgUUID(in.OwnerID),
|
||||
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
|
||||
}
|
||||
if IsForeignKeyViolation(err) {
|
||||
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "update requirement")
|
||||
}
|
||||
return rowToRequirement(row), nil
|
||||
@@ -330,6 +349,7 @@ func rowToIssue(row projectsqlc.Issue) *Issue {
|
||||
ID: fromPgUUID(row.ID),
|
||||
ProjectID: fromPgUUID(row.ProjectID),
|
||||
RequirementID: fromPgUUIDPtr(row.RequirementID),
|
||||
WorkspaceID: fromPgUUIDPtr(row.WorkspaceID),
|
||||
Number: int(row.Number),
|
||||
Title: row.Title,
|
||||
Description: row.Description,
|
||||
@@ -351,11 +371,15 @@ func (r *pgRepo) CreateIssue(ctx context.Context, in *Issue) (*Issue, error) {
|
||||
Description: in.Description,
|
||||
AssigneeID: toPgUUIDPtr(in.AssigneeID),
|
||||
CreatedBy: toPgUUID(in.CreatedBy),
|
||||
WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
|
||||
})
|
||||
if err != nil {
|
||||
if IsUniqueViolation(err) {
|
||||
return nil, err
|
||||
}
|
||||
if IsForeignKeyViolation(err) {
|
||||
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "create issue")
|
||||
}
|
||||
return rowToIssue(row), nil
|
||||
@@ -420,17 +444,21 @@ func (r *pgRepo) ListIssuesByRequirement(ctx context.Context, requirementID uuid
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID) (*Issue, error) {
|
||||
func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error) {
|
||||
row, err := r.q.UpdateIssueCore(ctx, projectsqlc.UpdateIssueCoreParams{
|
||||
ID: toPgUUID(id),
|
||||
Title: title,
|
||||
Description: description,
|
||||
RequirementID: toPgUUIDPtr(requirementID),
|
||||
WorkspaceID: toPgUUIDPtr(workspaceID),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
|
||||
}
|
||||
if IsForeignKeyViolation(err) {
|
||||
return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "update issue core")
|
||||
}
|
||||
return rowToIssue(row), nil
|
||||
|
||||
Reference in New Issue
Block a user