feat: add issue labels (DB schema, API, and service)
New labels and issue_labels tables with cascade deletes, unique per-company name constraint. CRUD routes for labels, label filtering on issue list, and label sync on issue create/update. All issue responses now include labels array. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
26
packages/db/src/migrations/0018_flat_sleepwalker.sql
Normal file
26
packages/db/src/migrations/0018_flat_sleepwalker.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE "issue_labels" (
|
||||
"issue_id" uuid NOT NULL,
|
||||
"label_id" uuid NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "issue_labels_pk" PRIMARY KEY("issue_id","label_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "labels" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"color" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "issue_labels" ADD CONSTRAINT "issue_labels_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "issue_labels" ADD CONSTRAINT "issue_labels_label_id_labels_id_fk" FOREIGN KEY ("label_id") REFERENCES "public"."labels"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "issue_labels" ADD CONSTRAINT "issue_labels_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "labels" ADD CONSTRAINT "labels_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "issue_labels_issue_idx" ON "issue_labels" USING btree ("issue_id");--> statement-breakpoint
|
||||
CREATE INDEX "issue_labels_label_idx" ON "issue_labels" USING btree ("label_id");--> statement-breakpoint
|
||||
CREATE INDEX "issue_labels_company_idx" ON "issue_labels" USING btree ("company_id");--> statement-breakpoint
|
||||
CREATE INDEX "labels_company_idx" ON "labels" USING btree ("company_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "labels_company_name_idx" ON "labels" USING btree ("company_id","name");
|
||||
5450
packages/db/src/migrations/meta/0018_snapshot.json
Normal file
5450
packages/db/src/migrations/meta/0018_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,12 @@ export { agentRuntimeState } from "./agent_runtime_state.js";
|
||||
export { agentTaskSessions } from "./agent_task_sessions.js";
|
||||
export { agentWakeupRequests } from "./agent_wakeup_requests.js";
|
||||
export { projects } from "./projects.js";
|
||||
export { projectWorkspaces } from "./project_workspaces.js";
|
||||
export { projectGoals } from "./project_goals.js";
|
||||
export { goals } from "./goals.js";
|
||||
export { issues } from "./issues.js";
|
||||
export { labels } from "./labels.js";
|
||||
export { issueLabels } from "./issue_labels.js";
|
||||
export { issueApprovals } from "./issue_approvals.js";
|
||||
export { issueComments } from "./issue_comments.js";
|
||||
export { assets } from "./assets.js";
|
||||
|
||||
20
packages/db/src/schema/issue_labels.ts
Normal file
20
packages/db/src/schema/issue_labels.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { pgTable, uuid, timestamp, index, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { labels } from "./labels.js";
|
||||
|
||||
export const issueLabels = pgTable(
|
||||
"issue_labels",
|
||||
{
|
||||
issueId: uuid("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }),
|
||||
labelId: uuid("label_id").notNull().references(() => labels.id, { onDelete: "cascade" }),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.issueId, table.labelId], name: "issue_labels_pk" }),
|
||||
issueIdx: index("issue_labels_issue_idx").on(table.issueId),
|
||||
labelIdx: index("issue_labels_label_idx").on(table.labelId),
|
||||
companyIdx: index("issue_labels_company_idx").on(table.companyId),
|
||||
}),
|
||||
);
|
||||
18
packages/db/src/schema/labels.ts
Normal file
18
packages/db/src/schema/labels.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { pgTable, uuid, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const labels = pgTable(
|
||||
"labels",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
color: text("color").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("labels_company_idx").on(table.companyId),
|
||||
companyNameIdx: uniqueIndex("labels_company_name_idx").on(table.companyId, table.name),
|
||||
}),
|
||||
);
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
AGENT_STATUSES,
|
||||
AGENT_ADAPTER_TYPES,
|
||||
AGENT_ROLES,
|
||||
AGENT_ICON_NAMES,
|
||||
ISSUE_STATUSES,
|
||||
ISSUE_PRIORITIES,
|
||||
GOAL_LEVELS,
|
||||
@@ -36,6 +37,7 @@ export {
|
||||
type AgentStatus,
|
||||
type AgentAdapterType,
|
||||
type AgentRole,
|
||||
type AgentIconName,
|
||||
type IssueStatus,
|
||||
type IssuePriority,
|
||||
type GoalLevel,
|
||||
@@ -73,9 +75,11 @@ export type {
|
||||
AssetImage,
|
||||
Project,
|
||||
ProjectGoalRef,
|
||||
ProjectWorkspace,
|
||||
Issue,
|
||||
IssueComment,
|
||||
IssueAttachment,
|
||||
IssueLabel,
|
||||
Goal,
|
||||
Approval,
|
||||
ApprovalComment,
|
||||
@@ -126,15 +130,21 @@ export {
|
||||
type UpdateAgentPermissions,
|
||||
createProjectSchema,
|
||||
updateProjectSchema,
|
||||
createProjectWorkspaceSchema,
|
||||
updateProjectWorkspaceSchema,
|
||||
type CreateProject,
|
||||
type UpdateProject,
|
||||
type CreateProjectWorkspace,
|
||||
type UpdateProjectWorkspace,
|
||||
createIssueSchema,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
checkoutIssueSchema,
|
||||
addIssueCommentSchema,
|
||||
linkIssueApprovalSchema,
|
||||
createIssueAttachmentMetadataSchema,
|
||||
type CreateIssue,
|
||||
type CreateIssueLabel,
|
||||
type UpdateIssue,
|
||||
type CheckoutIssue,
|
||||
type AddIssueComment,
|
||||
|
||||
@@ -10,7 +10,7 @@ export type {
|
||||
AdapterEnvironmentTestResult,
|
||||
} from "./agent.js";
|
||||
export type { AssetImage } from "./asset.js";
|
||||
export type { Project, ProjectGoalRef } from "./project.js";
|
||||
export type { Project, ProjectGoalRef, ProjectWorkspace } from "./project.js";
|
||||
export type {
|
||||
Issue,
|
||||
IssueComment,
|
||||
@@ -18,6 +18,7 @@ export type {
|
||||
IssueAncestorProject,
|
||||
IssueAncestorGoal,
|
||||
IssueAttachment,
|
||||
IssueLabel,
|
||||
} from "./issue.js";
|
||||
export type { Goal } from "./goal.js";
|
||||
export type { Approval, ApprovalComment } from "./approval.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IssuePriority, IssueStatus } from "../constants.js";
|
||||
import type { ProjectWorkspace } from "./project.js";
|
||||
|
||||
export interface IssueAncestorProject {
|
||||
id: string;
|
||||
@@ -6,6 +7,8 @@ export interface IssueAncestorProject {
|
||||
description: string | null;
|
||||
status: string;
|
||||
goalId: string | null;
|
||||
workspaces: ProjectWorkspace[];
|
||||
primaryWorkspace: ProjectWorkspace | null;
|
||||
}
|
||||
|
||||
export interface IssueAncestorGoal {
|
||||
@@ -31,6 +34,15 @@ export interface IssueAncestor {
|
||||
goal: IssueAncestorGoal | null;
|
||||
}
|
||||
|
||||
export interface IssueLabel {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface Issue {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -58,6 +70,8 @@ export interface Issue {
|
||||
completedAt: Date | null;
|
||||
cancelledAt: Date | null;
|
||||
hiddenAt: Date | null;
|
||||
labelIds?: string[];
|
||||
labels?: IssueLabel[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@@ -28,18 +28,24 @@ export {
|
||||
export {
|
||||
createProjectSchema,
|
||||
updateProjectSchema,
|
||||
createProjectWorkspaceSchema,
|
||||
updateProjectWorkspaceSchema,
|
||||
type CreateProject,
|
||||
type UpdateProject,
|
||||
type CreateProjectWorkspace,
|
||||
type UpdateProjectWorkspace,
|
||||
} from "./project.js";
|
||||
|
||||
export {
|
||||
createIssueSchema,
|
||||
createIssueLabelSchema,
|
||||
updateIssueSchema,
|
||||
checkoutIssueSchema,
|
||||
addIssueCommentSchema,
|
||||
linkIssueApprovalSchema,
|
||||
createIssueAttachmentMetadataSchema,
|
||||
type CreateIssue,
|
||||
type CreateIssueLabel,
|
||||
type UpdateIssue,
|
||||
type CheckoutIssue,
|
||||
type AddIssueComment,
|
||||
|
||||
@@ -13,10 +13,18 @@ export const createIssueSchema = z.object({
|
||||
assigneeUserId: z.string().optional().nullable(),
|
||||
requestDepth: z.number().int().nonnegative().optional().default(0),
|
||||
billingCode: z.string().optional().nullable(),
|
||||
labelIds: z.array(z.string().uuid()).optional(),
|
||||
});
|
||||
|
||||
export type CreateIssue = z.infer<typeof createIssueSchema>;
|
||||
|
||||
export const createIssueLabelSchema = z.object({
|
||||
name: z.string().trim().min(1).max(48),
|
||||
color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"),
|
||||
});
|
||||
|
||||
export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>;
|
||||
|
||||
export const updateIssueSchema = createIssueSchema.partial().extend({
|
||||
comment: z.string().min(1).optional(),
|
||||
hiddenAt: z.string().datetime().nullable().optional(),
|
||||
|
||||
Reference in New Issue
Block a user