diff --git a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx
index 84b53d68..86847a30 100644
--- a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx
+++ b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx
@@ -134,6 +134,8 @@ vi.mock("@/shared/api", () => ({
const mockIssue: Issue = {
id: "issue-1",
workspace_id: "ws-1",
+ number: 1,
+ identifier: "TES-1",
title: "Implement authentication",
description: "Add JWT auth to the backend",
status: "in_progress",
diff --git a/apps/web/app/(dashboard)/issues/page.test.tsx b/apps/web/app/(dashboard)/issues/page.test.tsx
index ad28a800..62a24c6c 100644
--- a/apps/web/app/(dashboard)/issues/page.test.tsx
+++ b/apps/web/app/(dashboard)/issues/page.test.tsx
@@ -211,6 +211,8 @@ const mockIssues: Issue[] = [
...issueDefaults,
id: "issue-1",
workspace_id: "ws-1",
+ number: 1,
+ identifier: "TES-1",
title: "Implement auth",
description: "Add JWT authentication",
status: "todo",
@@ -227,6 +229,8 @@ const mockIssues: Issue[] = [
...issueDefaults,
id: "issue-2",
workspace_id: "ws-1",
+ number: 2,
+ identifier: "TES-2",
title: "Design landing page",
description: null,
status: "in_progress",
@@ -243,6 +247,8 @@ const mockIssues: Issue[] = [
...issueDefaults,
id: "issue-3",
workspace_id: "ws-1",
+ number: 3,
+ identifier: "TES-3",
title: "Write tests",
description: null,
status: "backlog",
diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx
index f770639c..0596796d 100644
--- a/apps/web/features/issues/components/issue-detail.tsx
+++ b/apps/web/features/issues/components/issue-detail.tsx
@@ -480,7 +480,7 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
>
)}
- {issue.id.slice(0, 8)}
+ {issue.identifier}
{issue.title}
diff --git a/apps/web/features/issues/components/list-row.tsx b/apps/web/features/issues/components/list-row.tsx
index 7f2cfe19..ce03b9f2 100644
--- a/apps/web/features/issues/components/list-row.tsx
+++ b/apps/web/features/issues/components/list-row.tsx
@@ -20,7 +20,7 @@ export function ListRow({ issue }: { issue: Issue }) {
>
- {issue.id.slice(0, 8)}
+ {issue.identifier}
{issue.title}
{issue.due_date && (
diff --git a/apps/web/shared/types/issue.ts b/apps/web/shared/types/issue.ts
index 98620386..808d569d 100644
--- a/apps/web/shared/types/issue.ts
+++ b/apps/web/shared/types/issue.ts
@@ -14,6 +14,8 @@ export type IssueAssigneeType = "member" | "agent";
export interface Issue {
id: string;
workspace_id: string;
+ number: number;
+ identifier: string;
title: string;
description: string | null;
status: IssueStatus;
diff --git a/apps/web/shared/types/workspace.ts b/apps/web/shared/types/workspace.ts
index 9fdc45a4..a6fb98e6 100644
--- a/apps/web/shared/types/workspace.ts
+++ b/apps/web/shared/types/workspace.ts
@@ -13,6 +13,7 @@ export interface Workspace {
context: string | null;
settings: Record;
repos: WorkspaceRepo[];
+ issue_prefix: string;
created_at: string;
updated_at: string;
}
diff --git a/apps/web/test/helpers.tsx b/apps/web/test/helpers.tsx
index 5bb9fe3f..3eb15b31 100644
--- a/apps/web/test/helpers.tsx
+++ b/apps/web/test/helpers.tsx
@@ -22,6 +22,7 @@ export const mockWorkspace: Workspace = {
context: null,
settings: {},
repos: [],
+ issue_prefix: "TES",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
};
diff --git a/server/internal/handler/handler.go b/server/internal/handler/handler.go
index db482fb6..aaee0cd5 100644
--- a/server/internal/handler/handler.go
+++ b/server/internal/handler/handler.go
@@ -179,6 +179,14 @@ func (h *Handler) loadIssueForUser(w http.ResponseWriter, r *http.Request, issue
return db.Issue{}, false
}
+ // Try identifier format first (e.g., "JIA-42").
+ if issue, ok := h.resolveIssueByIdentifier(r.Context(), issueID, resolveWorkspaceID(r)); ok {
+ if _, ok := h.requireWorkspaceMember(w, r, uuidToString(issue.WorkspaceID), "issue not found"); !ok {
+ return db.Issue{}, false
+ }
+ return issue, true
+ }
+
issue, err := h.Queries.GetIssue(r.Context(), parseUUID(issueID))
if err != nil {
writeError(w, http.StatusNotFound, "issue not found")
@@ -192,6 +200,64 @@ func (h *Handler) loadIssueForUser(w http.ResponseWriter, r *http.Request, issue
return issue, true
}
+// resolveIssueByIdentifier tries to look up an issue by "PREFIX-NUMBER" format.
+func (h *Handler) resolveIssueByIdentifier(ctx context.Context, id, workspaceID string) (db.Issue, bool) {
+ parts := splitIdentifier(id)
+ if parts == nil {
+ return db.Issue{}, false
+ }
+ if workspaceID == "" {
+ return db.Issue{}, false
+ }
+ issue, err := h.Queries.GetIssueByNumber(ctx, db.GetIssueByNumberParams{
+ WorkspaceID: parseUUID(workspaceID),
+ Number: parts.number,
+ })
+ if err != nil {
+ return db.Issue{}, false
+ }
+ return issue, true
+}
+
+type identifierParts struct {
+ prefix string
+ number int32
+}
+
+func splitIdentifier(id string) *identifierParts {
+ idx := -1
+ for i := len(id) - 1; i >= 0; i-- {
+ if id[i] == '-' {
+ idx = i
+ break
+ }
+ }
+ if idx <= 0 || idx >= len(id)-1 {
+ return nil
+ }
+ numStr := id[idx+1:]
+ num := 0
+ for _, c := range numStr {
+ if c < '0' || c > '9' {
+ return nil
+ }
+ num = num*10 + int(c-'0')
+ }
+ if num <= 0 {
+ return nil
+ }
+ return &identifierParts{prefix: id[:idx], number: int32(num)}
+}
+
+// getIssuePrefix fetches the issue_prefix for a workspace.
+func (h *Handler) getIssuePrefix(ctx context.Context, workspaceID pgtype.UUID) string {
+ ws, err := h.Queries.GetWorkspace(ctx, workspaceID)
+ if err != nil {
+ return ""
+ }
+ return ws.IssuePrefix
+}
+
func (h *Handler) loadAgentForUser(w http.ResponseWriter, r *http.Request, agentID string) (db.Agent, bool) {
if _, ok := requireUserID(w, r); !ok {
return db.Agent{}, false
diff --git a/server/internal/handler/handler_test.go b/server/internal/handler/handler_test.go
index 25f1408b..e18630f6 100644
--- a/server/internal/handler/handler_test.go
+++ b/server/internal/handler/handler_test.go
@@ -90,10 +90,10 @@ func setupHandlerTestFixture(ctx context.Context, pool *pgxpool.Pool) (string, s
var workspaceID string
if err := pool.QueryRow(ctx, `
- INSERT INTO workspace (name, slug, description)
- VALUES ($1, $2, $3)
+ INSERT INTO workspace (name, slug, description, issue_prefix)
+ VALUES ($1, $2, $3, $4)
RETURNING id
- `, "Handler Tests", handlerTestWorkspaceSlug, "Temporary workspace for handler tests").Scan(&workspaceID); err != nil {
+ `, "Handler Tests", handlerTestWorkspaceSlug, "Temporary workspace for handler tests", "HAN").Scan(&workspaceID); err != nil {
return "", "", err
}
diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go
index 08369f5d..7ad02c9a 100644
--- a/server/internal/handler/issue.go
+++ b/server/internal/handler/issue.go
@@ -20,6 +20,8 @@ import (
type IssueResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
+ Number int32 `json:"number"`
+ Identifier string `json:"identifier"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
@@ -41,10 +43,13 @@ type agentTriggerSnapshot struct {
Config map[string]any `json:"config"`
}
-func issueToResponse(i db.Issue) IssueResponse {
+func issueToResponse(i db.Issue, issuePrefix string) IssueResponse {
+ identifier := issuePrefix + "-" + strconv.Itoa(int(i.Number))
return IssueResponse{
ID: uuidToString(i.ID),
WorkspaceID: uuidToString(i.WorkspaceID),
+ Number: i.Number,
+ Identifier: identifier,
Title: i.Title,
Description: textToPtr(i.Description),
Status: i.Status,
@@ -109,9 +114,10 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
return
}
+ prefix := h.getIssuePrefix(ctx, parseUUID(workspaceID))
resp := make([]IssueResponse, len(issues))
for i, issue := range issues {
- resp[i] = issueToResponse(issue)
+ resp[i] = issueToResponse(issue, prefix)
}
writeJSON(w, http.StatusOK, map[string]any{
@@ -126,7 +132,8 @@ func (h *Handler) GetIssue(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
- writeJSON(w, http.StatusOK, issueToResponse(issue))
+ prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
+ writeJSON(w, http.StatusOK, issueToResponse(issue, prefix))
}
type CreateIssueRequest struct {
@@ -196,7 +203,24 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
dueDate = pgtype.Timestamptz{Time: t, Valid: true}
}
- issue, err := h.Queries.CreateIssue(r.Context(), db.CreateIssueParams{
+ // Use a transaction to atomically increment the workspace issue counter
+ // and create the issue with the assigned number.
+ tx, err := h.TxStarter.Begin(r.Context())
+ if err != nil {
+ writeError(w, http.StatusInternalServerError, "failed to create issue")
+ return
+ }
+ defer tx.Rollback(r.Context())
+
+ qtx := h.Queries.WithTx(tx)
+ issueNumber, err := qtx.IncrementIssueCounter(r.Context(), parseUUID(workspaceID))
+ if err != nil {
+ slog.Warn("increment issue counter failed", append(logger.RequestAttrs(r), "error", err, "workspace_id", workspaceID)...)
+ writeError(w, http.StatusInternalServerError, "failed to create issue")
+ return
+ }
+
+ issue, err := qtx.CreateIssue(r.Context(), db.CreateIssueParams{
WorkspaceID: parseUUID(workspaceID),
Title: req.Title,
Description: ptrToText(req.Description),
@@ -209,6 +233,7 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
ParentIssueID: parentIssueID,
Position: 0,
DueDate: dueDate,
+ Number: issueNumber,
})
if err != nil {
slog.Warn("create issue failed", append(logger.RequestAttrs(r), "error", err, "workspace_id", workspaceID)...)
@@ -216,7 +241,13 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
return
}
- resp := issueToResponse(issue)
+ if err := tx.Commit(r.Context()); err != nil {
+ writeError(w, http.StatusInternalServerError, "failed to create issue")
+ return
+ }
+
+ prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
+ resp := issueToResponse(issue, prefix)
slog.Info("issue created", append(logger.RequestAttrs(r), "issue_id", uuidToString(issue.ID), "title", issue.Title, "status", issue.Status, "workspace_id", workspaceID)...)
h.publish(protocol.EventIssueCreated, workspaceID, "member", creatorID, map[string]any{"issue": resp})
@@ -326,7 +357,8 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
return
}
- resp := issueToResponse(issue)
+ prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
+ resp := issueToResponse(issue, prefix)
slog.Info("issue updated", append(logger.RequestAttrs(r), "issue_id", id, "workspace_id", workspaceID)...)
assigneeChanged := (req.AssigneeType != nil || req.AssigneeID != nil) &&
diff --git a/server/internal/handler/workspace.go b/server/internal/handler/workspace.go
index b8560555..2823be60 100644
--- a/server/internal/handler/workspace.go
+++ b/server/internal/handler/workspace.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"log/slog"
"net/http"
+ "regexp"
"strings"
"github.com/go-chi/chi/v5"
@@ -13,6 +14,22 @@ import (
"github.com/multica-ai/multica/server/pkg/protocol"
)
+var nonAlpha = regexp.MustCompile(`[^a-zA-Z]`)
+
+// generateIssuePrefix produces a 2-5 char uppercase prefix from a workspace name.
+// Examples: "Jiayuan's Workspace" → "JIA", "My Team" → "MYT", "AB" → "AB".
+func generateIssuePrefix(name string) string {
+ letters := nonAlpha.ReplaceAllString(name, "")
+ if len(letters) == 0 {
+ return "WS"
+ }
+ letters = strings.ToUpper(letters)
+ if len(letters) > 3 {
+ letters = letters[:3]
+ }
+ return letters
+}
+
type WorkspaceResponse struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -21,6 +38,7 @@ type WorkspaceResponse struct {
Context *string `json:"context"`
Settings any `json:"settings"`
Repos any `json:"repos"`
+ IssuePrefix string `json:"issue_prefix"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
@@ -48,6 +66,7 @@ func workspaceToResponse(w db.Workspace) WorkspaceResponse {
Context: textToPtr(w.Context),
Settings: settings,
Repos: repos,
+ IssuePrefix: w.IssuePrefix,
CreatedAt: timestampToString(w.CreatedAt),
UpdatedAt: timestampToString(w.UpdatedAt),
}
@@ -110,6 +129,7 @@ type CreateWorkspaceRequest struct {
Slug string `json:"slug"`
Description *string `json:"description"`
Context *string `json:"context"`
+ IssuePrefix *string `json:"issue_prefix"`
}
func (h *Handler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
@@ -138,12 +158,18 @@ func (h *Handler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
}
defer tx.Rollback(r.Context())
+ issuePrefix := generateIssuePrefix(req.Name)
+ if req.IssuePrefix != nil && strings.TrimSpace(*req.IssuePrefix) != "" {
+ issuePrefix = strings.ToUpper(strings.TrimSpace(*req.IssuePrefix))
+ }
+
qtx := h.Queries.WithTx(tx)
ws, err := qtx.CreateWorkspace(r.Context(), db.CreateWorkspaceParams{
Name: req.Name,
Slug: req.Slug,
Description: ptrToText(req.Description),
Context: ptrToText(req.Context),
+ IssuePrefix: issuePrefix,
})
if err != nil {
if isUniqueViolation(err) {
@@ -179,6 +205,7 @@ type UpdateWorkspaceRequest struct {
Context *string `json:"context"`
Settings any `json:"settings"`
Repos any `json:"repos"`
+ IssuePrefix *string `json:"issue_prefix"`
}
func (h *Handler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) {
@@ -218,6 +245,12 @@ func (h *Handler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) {
reposJSON, _ := json.Marshal(req.Repos)
params.Repos = reposJSON
}
+ if req.IssuePrefix != nil {
+ prefix := strings.ToUpper(strings.TrimSpace(*req.IssuePrefix))
+ if prefix != "" {
+ params.IssuePrefix = pgtype.Text{String: prefix, Valid: true}
+ }
+ }
ws, err := h.Queries.UpdateWorkspace(r.Context(), params)
if err != nil {
diff --git a/server/internal/service/task.go b/server/internal/service/task.go
index 7fe74d6d..621fad09 100644
--- a/server/internal/service/task.go
+++ b/server/internal/service/task.go
@@ -7,6 +7,8 @@ import (
"fmt"
"log/slog"
+ "strconv"
+
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/events"
@@ -361,15 +363,24 @@ func (s *TaskService) broadcastTaskEvent(ctx context.Context, eventType string,
}
func (s *TaskService) broadcastIssueUpdated(issue db.Issue) {
+ prefix := s.getIssuePrefix(issue.WorkspaceID)
s.Bus.Publish(events.Event{
Type: protocol.EventIssueUpdated,
WorkspaceID: util.UUIDToString(issue.WorkspaceID),
ActorType: "system",
ActorID: "",
- Payload: map[string]any{"issue": issueToMap(issue)},
+ Payload: map[string]any{"issue": issueToMap(issue, prefix)},
})
}
+func (s *TaskService) getIssuePrefix(workspaceID pgtype.UUID) string {
+ ws, err := s.Queries.GetWorkspace(context.Background(), workspaceID)
+ if err != nil {
+ return ""
+ }
+ return ws.IssuePrefix
+}
+
func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID pgtype.UUID, content, commentType string) {
if content == "" {
return
@@ -410,23 +421,25 @@ func (s *TaskService) createAgentComment(ctx context.Context, issueID, agentID p
})
}
-func issueToMap(issue db.Issue) map[string]any {
+func issueToMap(issue db.Issue, issuePrefix string) map[string]any {
return map[string]any{
- "id": util.UUIDToString(issue.ID),
- "workspace_id": util.UUIDToString(issue.WorkspaceID),
- "title": issue.Title,
- "description": util.TextToPtr(issue.Description),
- "status": issue.Status,
- "priority": issue.Priority,
- "assignee_type": util.TextToPtr(issue.AssigneeType),
- "assignee_id": util.UUIDToPtr(issue.AssigneeID),
- "creator_type": issue.CreatorType,
- "creator_id": util.UUIDToString(issue.CreatorID),
+ "id": util.UUIDToString(issue.ID),
+ "workspace_id": util.UUIDToString(issue.WorkspaceID),
+ "number": issue.Number,
+ "identifier": issuePrefix + "-" + strconv.Itoa(int(issue.Number)),
+ "title": issue.Title,
+ "description": util.TextToPtr(issue.Description),
+ "status": issue.Status,
+ "priority": issue.Priority,
+ "assignee_type": util.TextToPtr(issue.AssigneeType),
+ "assignee_id": util.UUIDToPtr(issue.AssigneeID),
+ "creator_type": issue.CreatorType,
+ "creator_id": util.UUIDToString(issue.CreatorID),
"parent_issue_id": util.UUIDToPtr(issue.ParentIssueID),
- "position": issue.Position,
- "due_date": util.TimestampToPtr(issue.DueDate),
- "created_at": util.TimestampToString(issue.CreatedAt),
- "updated_at": util.TimestampToString(issue.UpdatedAt),
+ "position": issue.Position,
+ "due_date": util.TimestampToPtr(issue.DueDate),
+ "created_at": util.TimestampToString(issue.CreatedAt),
+ "updated_at": util.TimestampToString(issue.UpdatedAt),
}
}
diff --git a/server/migrations/020_issue_number.down.sql b/server/migrations/020_issue_number.down.sql
new file mode 100644
index 00000000..5c6a3061
--- /dev/null
+++ b/server/migrations/020_issue_number.down.sql
@@ -0,0 +1,5 @@
+DROP INDEX IF EXISTS idx_issue_workspace_number;
+ALTER TABLE issue DROP CONSTRAINT IF EXISTS uq_issue_workspace_number;
+ALTER TABLE issue DROP COLUMN IF EXISTS number;
+ALTER TABLE workspace DROP COLUMN IF EXISTS issue_prefix;
+ALTER TABLE workspace DROP COLUMN IF EXISTS issue_counter;
diff --git a/server/migrations/020_issue_number.up.sql b/server/migrations/020_issue_number.up.sql
new file mode 100644
index 00000000..2800cb0e
--- /dev/null
+++ b/server/migrations/020_issue_number.up.sql
@@ -0,0 +1,36 @@
+-- Add issue_prefix and issue_counter to workspace for human-readable issue IDs.
+ALTER TABLE workspace
+ ADD COLUMN issue_prefix TEXT NOT NULL DEFAULT '',
+ ADD COLUMN issue_counter INT NOT NULL DEFAULT 0;
+
+-- Add per-workspace issue number.
+ALTER TABLE issue
+ ADD COLUMN number INT NOT NULL DEFAULT 0;
+
+-- Backfill: generate issue_prefix from workspace name (first 3 uppercase chars).
+UPDATE workspace SET issue_prefix = UPPER(
+ LEFT(REGEXP_REPLACE(name, '[^a-zA-Z]', '', 'g'), 3)
+);
+
+-- Fallback for workspaces with empty prefix after cleanup.
+UPDATE workspace SET issue_prefix = 'WS' WHERE issue_prefix = '';
+
+-- Backfill: assign sequential numbers to existing issues per workspace.
+WITH numbered AS (
+ SELECT id, workspace_id,
+ ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC) AS rn
+ FROM issue
+)
+UPDATE issue SET number = numbered.rn
+FROM numbered WHERE issue.id = numbered.id;
+
+-- Update workspace counters to match.
+UPDATE workspace SET issue_counter = COALESCE(
+ (SELECT MAX(number) FROM issue WHERE issue.workspace_id = workspace.id), 0
+);
+
+-- Add unique constraint.
+ALTER TABLE issue ADD CONSTRAINT uq_issue_workspace_number UNIQUE (workspace_id, number);
+
+-- Index for fast lookup by workspace + number.
+CREATE INDEX idx_issue_workspace_number ON issue(workspace_id, number);
diff --git a/server/pkg/db/generated/issue.sql.go b/server/pkg/db/generated/issue.sql.go
index 8279be45..aae29518 100644
--- a/server/pkg/db/generated/issue.sql.go
+++ b/server/pkg/db/generated/issue.sql.go
@@ -15,10 +15,10 @@ const createIssue = `-- name: CreateIssue :one
INSERT INTO issue (
workspace_id, title, description, status, priority,
assignee_type, assignee_id, creator_type, creator_id,
- parent_issue_id, position, due_date
+ parent_issue_id, position, due_date, number
) VALUES (
- $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
-) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
+) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number
`
type CreateIssueParams struct {
@@ -34,6 +34,7 @@ type CreateIssueParams struct {
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
Position float64 `json:"position"`
DueDate pgtype.Timestamptz `json:"due_date"`
+ Number int32 `json:"number"`
}
func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue, error) {
@@ -50,6 +51,7 @@ func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue
arg.ParentIssueID,
arg.Position,
arg.DueDate,
+ arg.Number,
)
var i Issue
err := row.Scan(
@@ -70,6 +72,7 @@ func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.Number,
)
return i, err
}
@@ -84,7 +87,7 @@ func (q *Queries) DeleteIssue(ctx context.Context, id pgtype.UUID) error {
}
const getIssue = `-- name: GetIssue :one
-SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at FROM issue
+SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number FROM issue
WHERE id = $1
`
@@ -109,12 +112,49 @@ func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) {
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.Number,
+ )
+ return i, err
+}
+
+const getIssueByNumber = `-- name: GetIssueByNumber :one
+SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number FROM issue
+WHERE workspace_id = $1 AND number = $2
+`
+
+type GetIssueByNumberParams struct {
+ WorkspaceID pgtype.UUID `json:"workspace_id"`
+ Number int32 `json:"number"`
+}
+
+func (q *Queries) GetIssueByNumber(ctx context.Context, arg GetIssueByNumberParams) (Issue, error) {
+ row := q.db.QueryRow(ctx, getIssueByNumber, arg.WorkspaceID, arg.Number)
+ var i Issue
+ err := row.Scan(
+ &i.ID,
+ &i.WorkspaceID,
+ &i.Title,
+ &i.Description,
+ &i.Status,
+ &i.Priority,
+ &i.AssigneeType,
+ &i.AssigneeID,
+ &i.CreatorType,
+ &i.CreatorID,
+ &i.ParentIssueID,
+ &i.AcceptanceCriteria,
+ &i.ContextRefs,
+ &i.Position,
+ &i.DueDate,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.Number,
)
return i, err
}
const listIssues = `-- name: ListIssues :many
-SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at FROM issue
+SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number FROM issue
WHERE workspace_id = $1
AND ($4::text IS NULL OR status = $4)
AND ($5::text IS NULL OR priority = $5)
@@ -166,6 +206,7 @@ func (q *Queries) ListIssues(ctx context.Context, arg ListIssuesParams) ([]Issue
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.Number,
); err != nil {
return nil, err
}
@@ -189,7 +230,7 @@ UPDATE issue SET
due_date = $9,
updated_at = now()
WHERE id = $1
-RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
+RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number
`
type UpdateIssueParams struct {
@@ -235,6 +276,7 @@ func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.Number,
)
return i, err
}
@@ -244,7 +286,7 @@ UPDATE issue SET
status = $2,
updated_at = now()
WHERE id = $1
-RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
+RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number
`
type UpdateIssueStatusParams struct {
@@ -273,6 +315,7 @@ func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusPa
&i.DueDate,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.Number,
)
return i, err
}
diff --git a/server/pkg/db/generated/models.go b/server/pkg/db/generated/models.go
index 546a3bad..91c8aad6 100644
--- a/server/pkg/db/generated/models.go
+++ b/server/pkg/db/generated/models.go
@@ -152,6 +152,7 @@ type Issue struct {
DueDate pgtype.Timestamptz `json:"due_date"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ Number int32 `json:"number"`
}
type IssueDependency struct {
@@ -256,13 +257,15 @@ type VerificationCode struct {
}
type Workspace struct {
- ID pgtype.UUID `json:"id"`
- Name string `json:"name"`
- Slug string `json:"slug"`
- Description pgtype.Text `json:"description"`
- Settings []byte `json:"settings"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
- Context pgtype.Text `json:"context"`
- Repos []byte `json:"repos"`
+ ID pgtype.UUID `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+ Description pgtype.Text `json:"description"`
+ Settings []byte `json:"settings"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ Context pgtype.Text `json:"context"`
+ Repos []byte `json:"repos"`
+ IssuePrefix string `json:"issue_prefix"`
+ IssueCounter int32 `json:"issue_counter"`
}
diff --git a/server/pkg/db/generated/workspace.sql.go b/server/pkg/db/generated/workspace.sql.go
index bd527a91..c751ca39 100644
--- a/server/pkg/db/generated/workspace.sql.go
+++ b/server/pkg/db/generated/workspace.sql.go
@@ -12,9 +12,9 @@ import (
)
const createWorkspace = `-- name: CreateWorkspace :one
-INSERT INTO workspace (name, slug, description, context)
-VALUES ($1, $2, $3, $4)
-RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos
+INSERT INTO workspace (name, slug, description, context, issue_prefix)
+VALUES ($1, $2, $3, $4, $5)
+RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter
`
type CreateWorkspaceParams struct {
@@ -22,6 +22,7 @@ type CreateWorkspaceParams struct {
Slug string `json:"slug"`
Description pgtype.Text `json:"description"`
Context pgtype.Text `json:"context"`
+ IssuePrefix string `json:"issue_prefix"`
}
func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (Workspace, error) {
@@ -30,6 +31,7 @@ func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams
arg.Slug,
arg.Description,
arg.Context,
+ arg.IssuePrefix,
)
var i Workspace
err := row.Scan(
@@ -42,6 +44,8 @@ func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams
&i.UpdatedAt,
&i.Context,
&i.Repos,
+ &i.IssuePrefix,
+ &i.IssueCounter,
)
return i, err
}
@@ -56,7 +60,7 @@ func (q *Queries) DeleteWorkspace(ctx context.Context, id pgtype.UUID) error {
}
const getWorkspace = `-- name: GetWorkspace :one
-SELECT id, name, slug, description, settings, created_at, updated_at, context, repos FROM workspace
+SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter FROM workspace
WHERE id = $1
`
@@ -73,12 +77,14 @@ func (q *Queries) GetWorkspace(ctx context.Context, id pgtype.UUID) (Workspace,
&i.UpdatedAt,
&i.Context,
&i.Repos,
+ &i.IssuePrefix,
+ &i.IssueCounter,
)
return i, err
}
const getWorkspaceBySlug = `-- name: GetWorkspaceBySlug :one
-SELECT id, name, slug, description, settings, created_at, updated_at, context, repos FROM workspace
+SELECT id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter FROM workspace
WHERE slug = $1
`
@@ -95,12 +101,27 @@ func (q *Queries) GetWorkspaceBySlug(ctx context.Context, slug string) (Workspac
&i.UpdatedAt,
&i.Context,
&i.Repos,
+ &i.IssuePrefix,
+ &i.IssueCounter,
)
return i, err
}
+const incrementIssueCounter = `-- name: IncrementIssueCounter :one
+UPDATE workspace SET issue_counter = issue_counter + 1
+WHERE id = $1
+RETURNING issue_counter
+`
+
+func (q *Queries) IncrementIssueCounter(ctx context.Context, id pgtype.UUID) (int32, error) {
+ row := q.db.QueryRow(ctx, incrementIssueCounter, id)
+ var issue_counter int32
+ err := row.Scan(&issue_counter)
+ return issue_counter, err
+}
+
const listWorkspaces = `-- name: ListWorkspaces :many
-SELECT w.id, w.name, w.slug, w.description, w.settings, w.created_at, w.updated_at, w.context, w.repos FROM workspace w
+SELECT w.id, w.name, w.slug, w.description, w.settings, w.created_at, w.updated_at, w.context, w.repos, w.issue_prefix, w.issue_counter FROM workspace w
JOIN member m ON m.workspace_id = w.id
WHERE m.user_id = $1
ORDER BY w.created_at ASC
@@ -125,6 +146,8 @@ func (q *Queries) ListWorkspaces(ctx context.Context, userID pgtype.UUID) ([]Wor
&i.UpdatedAt,
&i.Context,
&i.Repos,
+ &i.IssuePrefix,
+ &i.IssueCounter,
); err != nil {
return nil, err
}
@@ -143,9 +166,10 @@ UPDATE workspace SET
context = COALESCE($4, context),
settings = COALESCE($5, settings),
repos = COALESCE($6, repos),
+ issue_prefix = COALESCE($7, issue_prefix),
updated_at = now()
WHERE id = $1
-RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos
+RETURNING id, name, slug, description, settings, created_at, updated_at, context, repos, issue_prefix, issue_counter
`
type UpdateWorkspaceParams struct {
@@ -155,6 +179,7 @@ type UpdateWorkspaceParams struct {
Context pgtype.Text `json:"context"`
Settings []byte `json:"settings"`
Repos []byte `json:"repos"`
+ IssuePrefix pgtype.Text `json:"issue_prefix"`
}
func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (Workspace, error) {
@@ -165,6 +190,7 @@ func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams
arg.Context,
arg.Settings,
arg.Repos,
+ arg.IssuePrefix,
)
var i Workspace
err := row.Scan(
@@ -177,6 +203,8 @@ func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams
&i.UpdatedAt,
&i.Context,
&i.Repos,
+ &i.IssuePrefix,
+ &i.IssueCounter,
)
return i, err
}
diff --git a/server/pkg/db/queries/issue.sql b/server/pkg/db/queries/issue.sql
index efabdc0d..ed5614ba 100644
--- a/server/pkg/db/queries/issue.sql
+++ b/server/pkg/db/queries/issue.sql
@@ -15,11 +15,15 @@ WHERE id = $1;
INSERT INTO issue (
workspace_id, title, description, status, priority,
assignee_type, assignee_id, creator_type, creator_id,
- parent_issue_id, position, due_date
+ parent_issue_id, position, due_date, number
) VALUES (
- $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING *;
+-- name: GetIssueByNumber :one
+SELECT * FROM issue
+WHERE workspace_id = $1 AND number = $2;
+
-- name: UpdateIssue :one
UPDATE issue SET
title = COALESCE(sqlc.narg('title'), title),
diff --git a/server/pkg/db/queries/workspace.sql b/server/pkg/db/queries/workspace.sql
index 73172ede..b69a262c 100644
--- a/server/pkg/db/queries/workspace.sql
+++ b/server/pkg/db/queries/workspace.sql
@@ -13,8 +13,8 @@ SELECT * FROM workspace
WHERE slug = $1;
-- name: CreateWorkspace :one
-INSERT INTO workspace (name, slug, description, context)
-VALUES ($1, $2, $3, $4)
+INSERT INTO workspace (name, slug, description, context, issue_prefix)
+VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: UpdateWorkspace :one
@@ -24,9 +24,15 @@ UPDATE workspace SET
context = COALESCE(sqlc.narg('context'), context),
settings = COALESCE(sqlc.narg('settings'), settings),
repos = COALESCE(sqlc.narg('repos'), repos),
+ issue_prefix = COALESCE(sqlc.narg('issue_prefix'), issue_prefix),
updated_at = now()
WHERE id = $1
RETURNING *;
+-- name: IncrementIssueCounter :one
+UPDATE workspace SET issue_counter = issue_counter + 1
+WHERE id = $1
+RETURNING issue_counter;
+
-- name: DeleteWorkspace :exec
DELETE FROM workspace WHERE id = $1;