bopodev-db 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import type { BopoDb } from "../client";
3
+ import { agents, goals, issues, projects, templates } from "../schema";
4
+
5
+ export class RepositoryValidationError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = "RepositoryValidationError";
9
+ }
10
+ }
11
+
12
+ export async function assertProjectBelongsToCompany(db: BopoDb, companyId: string, projectId: string) {
13
+ const [project] = await db
14
+ .select({ id: projects.id })
15
+ .from(projects)
16
+ .where(and(eq(projects.companyId, companyId), eq(projects.id, projectId)))
17
+ .limit(1);
18
+ if (!project) {
19
+ throw new RepositoryValidationError("Project not found for company.");
20
+ }
21
+ }
22
+
23
+ export async function assertIssueBelongsToCompany(db: BopoDb, companyId: string, issueId: string) {
24
+ const [issue] = await db
25
+ .select({ id: issues.id })
26
+ .from(issues)
27
+ .where(and(eq(issues.companyId, companyId), eq(issues.id, issueId)))
28
+ .limit(1);
29
+ if (!issue) {
30
+ throw new RepositoryValidationError("Issue not found for company.");
31
+ }
32
+ }
33
+
34
+ export async function assertGoalBelongsToCompany(db: BopoDb, companyId: string, goalId: string) {
35
+ const [goal] = await db
36
+ .select({ id: goals.id })
37
+ .from(goals)
38
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
39
+ .limit(1);
40
+ if (!goal) {
41
+ throw new RepositoryValidationError("Parent goal not found for company.");
42
+ }
43
+ }
44
+
45
+ /** Ensures a goal can be linked to an issue: same company; project-scoped goals must match the issue's project. */
46
+ export async function assertIssueGoalAssignable(
47
+ db: BopoDb,
48
+ companyId: string,
49
+ issueProjectId: string,
50
+ goalId: string | null | undefined
51
+ ) {
52
+ if (!goalId) {
53
+ return;
54
+ }
55
+ const [goal] = await db
56
+ .select({ id: goals.id, projectId: goals.projectId })
57
+ .from(goals)
58
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
59
+ .limit(1);
60
+ if (!goal) {
61
+ throw new RepositoryValidationError("Goal not found for company.");
62
+ }
63
+ if (goal.projectId && goal.projectId !== issueProjectId) {
64
+ throw new RepositoryValidationError("Goal is scoped to a different project than this issue.");
65
+ }
66
+ }
67
+
68
+ /** Validates each goal can be linked to an issue (same company; project goals must match issue project). */
69
+ export async function assertIssueGoalsAssignable(
70
+ db: BopoDb,
71
+ companyId: string,
72
+ issueProjectId: string,
73
+ goalIds: string[]
74
+ ) {
75
+ for (const goalId of goalIds) {
76
+ await assertIssueGoalAssignable(db, companyId, issueProjectId, goalId);
77
+ }
78
+ }
79
+
80
+ export async function assertAgentBelongsToCompany(db: BopoDb, companyId: string, agentId: string) {
81
+ const [agent] = await db
82
+ .select({ id: agents.id })
83
+ .from(agents)
84
+ .where(and(eq(agents.companyId, companyId), eq(agents.id, agentId)))
85
+ .limit(1);
86
+ if (!agent) {
87
+ throw new RepositoryValidationError("Agent not found for company.");
88
+ }
89
+ }
90
+
91
+ export async function assertTemplateBelongsToCompany(db: BopoDb, companyId: string, templateId: string) {
92
+ const [template] = await db
93
+ .select({ id: templates.id })
94
+ .from(templates)
95
+ .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
96
+ .limit(1);
97
+ if (!template) {
98
+ throw new RepositoryValidationError("Template not found for company.");
99
+ }
100
+ }
101
+
102
+ export function compactUpdate<T extends Record<string, unknown>>(input: T) {
103
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
104
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./helpers";
2
+ export * from "./companies";
3
+ export * from "./legacy";
@@ -1,6 +1,6 @@
1
1
  import { and, asc, desc, eq, gt, inArray, notInArray, sql } from "drizzle-orm";
2
2
  import { nanoid } from "nanoid";
3
- import type { BopoDb } from "./client";
3
+ import type { BopoDb } from "../client";
4
4
  import {
5
5
  activityLogs,
6
6
  agents,
@@ -8,7 +8,6 @@ import {
8
8
  approvalInboxStates,
9
9
  approvalRequests,
10
10
  auditEvents,
11
- companies,
12
11
  costLedger,
13
12
  goals,
14
13
  heartbeatRunQueue,
@@ -16,6 +15,7 @@ import {
16
15
  heartbeatRunMessages,
17
16
  issueAttachments,
18
17
  issueComments,
18
+ issueGoals,
19
19
  issues,
20
20
  pluginConfigs,
21
21
  pluginRuns,
@@ -26,100 +26,17 @@ import {
26
26
  templateVersions,
27
27
  templates,
28
28
  touchUpdatedAtSql
29
- } from "./schema";
30
-
31
- export class RepositoryValidationError extends Error {
32
- constructor(message: string) {
33
- super(message);
34
- this.name = "RepositoryValidationError";
35
- }
36
- }
37
-
38
- async function assertProjectBelongsToCompany(db: BopoDb, companyId: string, projectId: string) {
39
- const [project] = await db
40
- .select({ id: projects.id })
41
- .from(projects)
42
- .where(and(eq(projects.companyId, companyId), eq(projects.id, projectId)))
43
- .limit(1);
44
- if (!project) {
45
- throw new RepositoryValidationError("Project not found for company.");
46
- }
47
- }
48
-
49
- async function assertIssueBelongsToCompany(db: BopoDb, companyId: string, issueId: string) {
50
- const [issue] = await db
51
- .select({ id: issues.id })
52
- .from(issues)
53
- .where(and(eq(issues.companyId, companyId), eq(issues.id, issueId)))
54
- .limit(1);
55
- if (!issue) {
56
- throw new RepositoryValidationError("Issue not found for company.");
57
- }
58
- }
59
-
60
- async function assertGoalBelongsToCompany(db: BopoDb, companyId: string, goalId: string) {
61
- const [goal] = await db
62
- .select({ id: goals.id })
63
- .from(goals)
64
- .where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
65
- .limit(1);
66
- if (!goal) {
67
- throw new RepositoryValidationError("Parent goal not found for company.");
68
- }
69
- }
70
-
71
- async function assertAgentBelongsToCompany(db: BopoDb, companyId: string, agentId: string) {
72
- const [agent] = await db
73
- .select({ id: agents.id })
74
- .from(agents)
75
- .where(and(eq(agents.companyId, companyId), eq(agents.id, agentId)))
76
- .limit(1);
77
- if (!agent) {
78
- throw new RepositoryValidationError("Agent not found for company.");
79
- }
80
- }
81
-
82
- async function assertTemplateBelongsToCompany(db: BopoDb, companyId: string, templateId: string) {
83
- const [template] = await db
84
- .select({ id: templates.id })
85
- .from(templates)
86
- .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
87
- .limit(1);
88
- if (!template) {
89
- throw new RepositoryValidationError("Template not found for company.");
90
- }
91
- }
92
-
93
- export async function createCompany(db: BopoDb, input: { name: string; mission?: string | null }) {
94
- const id = nanoid(12);
95
- await db.insert(companies).values({
96
- id,
97
- name: input.name,
98
- mission: input.mission ?? null
99
- });
100
- return { id, ...input };
101
- }
102
-
103
- export async function listCompanies(db: BopoDb) {
104
- return db.select().from(companies).orderBy(desc(companies.createdAt));
105
- }
106
-
107
- export async function updateCompany(
108
- db: BopoDb,
109
- input: { id: string; name?: string; mission?: string | null }
110
- ) {
111
- const [company] = await db
112
- .update(companies)
113
- .set(compactUpdate({ name: input.name, mission: input.mission }))
114
- .where(eq(companies.id, input.id))
115
- .returning();
116
- return company ?? null;
117
- }
118
-
119
- export async function deleteCompany(db: BopoDb, id: string) {
120
- const [deletedCompany] = await db.delete(companies).where(eq(companies.id, id)).returning({ id: companies.id });
121
- return Boolean(deletedCompany);
122
- }
29
+ } from "../schema";
30
+ import {
31
+ assertAgentBelongsToCompany,
32
+ assertGoalBelongsToCompany,
33
+ assertIssueGoalsAssignable,
34
+ assertIssueBelongsToCompany,
35
+ assertProjectBelongsToCompany,
36
+ assertTemplateBelongsToCompany,
37
+ compactUpdate,
38
+ RepositoryValidationError
39
+ } from "./helpers";
123
40
 
124
41
  export async function listProjects(db: BopoDb, companyId: string) {
125
42
  const rows = await db.select().from(projects).where(eq(projects.companyId, companyId)).orderBy(desc(projects.createdAt));
@@ -540,6 +457,46 @@ export async function listIssues(db: BopoDb, companyId: string, projectId?: stri
540
457
  return db.select().from(issues).where(where).orderBy(desc(issues.updatedAt));
541
458
  }
542
459
 
460
+ export async function listIssueGoalIdsBatch(db: BopoDb, companyId: string, issueIds: string[]) {
461
+ const map = new Map<string, string[]>();
462
+ if (issueIds.length === 0) {
463
+ return map;
464
+ }
465
+ const rows = await db
466
+ .select({ issueId: issueGoals.issueId, goalId: issueGoals.goalId })
467
+ .from(issueGoals)
468
+ .where(and(eq(issueGoals.companyId, companyId), inArray(issueGoals.issueId, issueIds)));
469
+ for (const row of rows) {
470
+ const list = map.get(row.issueId) ?? [];
471
+ list.push(row.goalId);
472
+ map.set(row.issueId, list);
473
+ }
474
+ return map;
475
+ }
476
+
477
+ export async function syncIssueGoals(
478
+ db: BopoDb,
479
+ input: { companyId: string; issueId: string; projectId: string; goalIds: string[] }
480
+ ) {
481
+ const dedupedGoalIds = Array.from(new Set(input.goalIds.map((id) => id.trim()).filter((id) => id.length > 0)));
482
+ await assertIssueBelongsToCompany(db, input.companyId, input.issueId);
483
+ await assertIssueGoalsAssignable(db, input.companyId, input.projectId, dedupedGoalIds);
484
+
485
+ await db
486
+ .delete(issueGoals)
487
+ .where(and(eq(issueGoals.companyId, input.companyId), eq(issueGoals.issueId, input.issueId)));
488
+
489
+ if (dedupedGoalIds.length > 0) {
490
+ await db.insert(issueGoals).values(
491
+ dedupedGoalIds.map((goalId) => ({
492
+ issueId: input.issueId,
493
+ goalId,
494
+ companyId: input.companyId
495
+ }))
496
+ );
497
+ }
498
+ }
499
+
543
500
  export async function getIssue(db: BopoDb, companyId: string, issueId: string) {
544
501
  const [row] = await db
545
502
  .select()
@@ -555,8 +512,10 @@ export async function createIssue(
555
512
  companyId: string;
556
513
  projectId: string;
557
514
  parentIssueId?: string | null;
515
+ goalIds?: string[];
558
516
  title: string;
559
517
  body?: string;
518
+ externalLink?: string | null;
560
519
  status?: string;
561
520
  priority?: string;
562
521
  assigneeAgentId?: string | null;
@@ -572,21 +531,35 @@ export async function createIssue(
572
531
  await assertAgentBelongsToCompany(db, input.companyId, input.assigneeAgentId);
573
532
  }
574
533
  const id = nanoid(12);
575
- await db.insert(issues).values({
576
- id,
577
- companyId: input.companyId,
578
- projectId: input.projectId,
579
- parentIssueId: input.parentIssueId ?? null,
580
- title: input.title,
581
- body: input.body,
582
- status: input.status ?? "todo",
583
- priority: input.priority ?? "none",
584
- assigneeAgentId: input.assigneeAgentId ?? null,
585
- labelsJson: JSON.stringify(input.labels ?? []),
586
- tagsJson: JSON.stringify(input.tags ?? [])
534
+ return await db.transaction(async (tx) => {
535
+ const [row] = await tx
536
+ .insert(issues)
537
+ .values({
538
+ id,
539
+ companyId: input.companyId,
540
+ projectId: input.projectId,
541
+ parentIssueId: input.parentIssueId ?? null,
542
+ title: input.title,
543
+ body: input.body,
544
+ externalLink: input.externalLink?.trim() ? input.externalLink.trim() : null,
545
+ status: input.status ?? "todo",
546
+ priority: input.priority ?? "none",
547
+ assigneeAgentId: input.assigneeAgentId ?? null,
548
+ labelsJson: JSON.stringify(input.labels ?? []),
549
+ tagsJson: JSON.stringify(input.tags ?? [])
550
+ })
551
+ .returning();
552
+ if (!row) {
553
+ throw new RepositoryValidationError("Failed to create issue.");
554
+ }
555
+ await syncIssueGoals(tx as unknown as BopoDb, {
556
+ companyId: input.companyId,
557
+ issueId: row.id,
558
+ projectId: input.projectId,
559
+ goalIds: input.goalIds ?? []
560
+ });
561
+ return row;
587
562
  });
588
-
589
- return { id, ...input };
590
563
  }
591
564
 
592
565
  export async function updateIssue(
@@ -595,8 +568,10 @@ export async function updateIssue(
595
568
  companyId: string;
596
569
  id: string;
597
570
  projectId?: string;
571
+ goalIds?: string[];
598
572
  title?: string;
599
573
  body?: string | null;
574
+ externalLink?: string | null;
600
575
  status?: string;
601
576
  priority?: string;
602
577
  assigneeAgentId?: string | null;
@@ -604,6 +579,14 @@ export async function updateIssue(
604
579
  tags?: string[];
605
580
  }
606
581
  ) {
582
+ const [existing] = await db
583
+ .select()
584
+ .from(issues)
585
+ .where(and(eq(issues.companyId, input.companyId), eq(issues.id, input.id)))
586
+ .limit(1);
587
+ if (!existing) {
588
+ return null;
589
+ }
607
590
  if (input.projectId) {
608
591
  await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
609
592
  }
@@ -617,6 +600,12 @@ export async function updateIssue(
617
600
  projectId: input.projectId,
618
601
  title: input.title,
619
602
  body: input.body,
603
+ externalLink:
604
+ input.externalLink === undefined
605
+ ? undefined
606
+ : input.externalLink?.trim()
607
+ ? input.externalLink.trim()
608
+ : null,
620
609
  status: input.status,
621
610
  priority: input.priority,
622
611
  assigneeAgentId: input.assigneeAgentId,
@@ -627,7 +616,32 @@ export async function updateIssue(
627
616
  )
628
617
  .where(and(eq(issues.companyId, input.companyId), eq(issues.id, input.id)))
629
618
  .returning();
630
- return issue ?? null;
619
+ if (!issue) {
620
+ return null;
621
+ }
622
+ if (input.goalIds !== undefined) {
623
+ await syncIssueGoals(db, {
624
+ companyId: input.companyId,
625
+ issueId: issue.id,
626
+ projectId: issue.projectId,
627
+ goalIds: input.goalIds
628
+ });
629
+ } else if (input.projectId && input.projectId !== existing.projectId) {
630
+ const currentRows = await db
631
+ .select({ goalId: issueGoals.goalId })
632
+ .from(issueGoals)
633
+ .where(and(eq(issueGoals.companyId, input.companyId), eq(issueGoals.issueId, issue.id)));
634
+ const currentGoalIds = currentRows.map((row) => row.goalId);
635
+ if (currentGoalIds.length > 0) {
636
+ await syncIssueGoals(db, {
637
+ companyId: input.companyId,
638
+ issueId: issue.id,
639
+ projectId: issue.projectId,
640
+ goalIds: currentGoalIds
641
+ });
642
+ }
643
+ }
644
+ return issue;
631
645
  }
632
646
 
633
647
  export async function deleteIssue(db: BopoDb, companyId: string, id: string) {
@@ -922,6 +936,7 @@ export async function createGoal(
922
936
  companyId: string;
923
937
  projectId?: string | null;
924
938
  parentGoalId?: string | null;
939
+ ownerAgentId?: string | null;
925
940
  level: "company" | "project" | "agent";
926
941
  title: string;
927
942
  description?: string;
@@ -933,12 +948,16 @@ export async function createGoal(
933
948
  if (input.parentGoalId) {
934
949
  await assertGoalBelongsToCompany(db, input.companyId, input.parentGoalId);
935
950
  }
951
+ if (input.ownerAgentId) {
952
+ await assertAgentBelongsToCompany(db, input.companyId, input.ownerAgentId);
953
+ }
936
954
  const id = nanoid(12);
937
955
  await db.insert(goals).values({
938
956
  id,
939
957
  companyId: input.companyId,
940
958
  projectId: input.projectId ?? null,
941
959
  parentGoalId: input.parentGoalId ?? null,
960
+ ownerAgentId: input.ownerAgentId?.trim() ? input.ownerAgentId.trim() : null,
942
961
  level: input.level,
943
962
  title: input.title,
944
963
  description: input.description ?? null
@@ -957,6 +976,7 @@ export async function updateGoal(
957
976
  id: string;
958
977
  projectId?: string | null;
959
978
  parentGoalId?: string | null;
979
+ ownerAgentId?: string | null;
960
980
  level?: "company" | "project" | "agent";
961
981
  title?: string;
962
982
  description?: string | null;
@@ -969,12 +989,21 @@ export async function updateGoal(
969
989
  if (input.parentGoalId) {
970
990
  await assertGoalBelongsToCompany(db, input.companyId, input.parentGoalId);
971
991
  }
992
+ if (input.ownerAgentId) {
993
+ await assertAgentBelongsToCompany(db, input.companyId, input.ownerAgentId);
994
+ }
972
995
  const [goal] = await db
973
996
  .update(goals)
974
997
  .set(
975
998
  compactUpdate({
976
999
  projectId: input.projectId,
977
1000
  parentGoalId: input.parentGoalId,
1001
+ ownerAgentId:
1002
+ input.ownerAgentId === undefined
1003
+ ? undefined
1004
+ : input.ownerAgentId?.trim()
1005
+ ? input.ownerAgentId.trim()
1006
+ : null,
978
1007
  level: input.level,
979
1008
  title: input.title,
980
1009
  description: input.description,
@@ -1003,6 +1032,7 @@ export async function createAgent(
1003
1032
  role: string;
1004
1033
  roleKey?: string | null;
1005
1034
  title?: string | null;
1035
+ capabilities?: string | null;
1006
1036
  name: string;
1007
1037
  providerType:
1008
1038
  | "claude_code"
@@ -1012,6 +1042,7 @@ export async function createAgent(
1012
1042
  | "gemini_cli"
1013
1043
  | "openai_api"
1014
1044
  | "anthropic_api"
1045
+ | "openclaw_gateway"
1015
1046
  | "http"
1016
1047
  | "shell";
1017
1048
  heartbeatCron: string;
@@ -1043,6 +1074,7 @@ export async function createAgent(
1043
1074
  role: input.role,
1044
1075
  roleKey: input.roleKey ?? null,
1045
1076
  title: input.title ?? null,
1077
+ capabilities: input.capabilities ?? null,
1046
1078
  name: input.name,
1047
1079
  providerType: input.providerType,
1048
1080
  heartbeatCron: input.heartbeatCron,
@@ -1078,6 +1110,7 @@ export async function updateAgent(
1078
1110
  role?: string;
1079
1111
  roleKey?: string | null;
1080
1112
  title?: string | null;
1113
+ capabilities?: string | null;
1081
1114
  name?: string;
1082
1115
  providerType?:
1083
1116
  | "claude_code"
@@ -1087,6 +1120,7 @@ export async function updateAgent(
1087
1120
  | "gemini_cli"
1088
1121
  | "openai_api"
1089
1122
  | "anthropic_api"
1123
+ | "openclaw_gateway"
1090
1124
  | "http"
1091
1125
  | "shell";
1092
1126
  status?: string;
@@ -1117,6 +1151,7 @@ export async function updateAgent(
1117
1151
  role: input.role,
1118
1152
  roleKey: input.roleKey,
1119
1153
  title: input.title,
1154
+ capabilities: input.capabilities,
1120
1155
  name: input.name,
1121
1156
  providerType: input.providerType,
1122
1157
  status: input.status,
@@ -1639,6 +1674,7 @@ export async function enqueueHeartbeatJob(
1639
1674
  }
1640
1675
 
1641
1676
  export async function claimNextHeartbeatJob(db: BopoDb, companyId: string) {
1677
+ // Postgres-specific: CTE + NOT EXISTS + FOR UPDATE SKIP LOCKED + UPDATE FROM — kept as raw SQL for correct job claiming under concurrency.
1642
1678
  const result = await db.execute(sql`
1643
1679
  WITH candidate AS (
1644
1680
  SELECT q.id
@@ -1893,6 +1929,7 @@ export async function listHeartbeatRunMessagesForRuns(
1893
1929
  }
1894
1930
  const perRunLimit = Math.min(Math.max(input.perRunLimit ?? 60, 1), 500);
1895
1931
  const runIdValues = sql.join(runIds.map((runId) => sql`(${runId})`), sql`, `);
1932
+ // Window functions (ROW_NUMBER) and VALUES-driven CTE — impractical to express purely with Drizzle’s query builder.
1896
1933
  const rankedRows = await db.execute(sql`
1897
1934
  WITH requested(run_id) AS (
1898
1935
  VALUES ${runIdValues}
@@ -2378,7 +2415,3 @@ export async function createTemplateInstall(
2378
2415
  .returning();
2379
2416
  return row ?? null;
2380
2417
  }
2381
-
2382
- function compactUpdate<T extends Record<string, unknown>>(input: T) {
2383
- return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
2384
- }
package/src/schema.ts CHANGED
@@ -53,6 +53,8 @@ export const goals = pgTable("goals", {
53
53
  .references(() => companies.id, { onDelete: "cascade" }),
54
54
  projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),
55
55
  parentGoalId: text("parent_goal_id"),
56
+ /** When set, this agent-level goal is included only for that agent's heartbeats; null = all agents. */
57
+ ownerAgentId: text("owner_agent_id"),
56
58
  level: text("level").notNull(),
57
59
  title: text("title").notNull(),
58
60
  description: text("description"),
@@ -70,6 +72,7 @@ export const agents = pgTable("agents", {
70
72
  role: text("role").notNull(),
71
73
  roleKey: text("role_key"),
72
74
  title: text("title"),
75
+ capabilities: text("capabilities"),
73
76
  name: text("name").notNull(),
74
77
  providerType: text("provider_type").notNull(),
75
78
  status: text("status").notNull().default("idle"),
@@ -114,12 +117,31 @@ export const issues = pgTable("issues", {
114
117
  assigneeAgentId: text("assignee_agent_id"),
115
118
  labelsJson: text("labels_json").notNull().default("[]"),
116
119
  tagsJson: text("tags_json").notNull().default("[]"),
120
+ /** Optional link to a PR, branch page, or other external tracker (GitHub/GitLab/etc.). */
121
+ externalLink: text("external_link"),
117
122
  isClaimed: boolean("is_claimed").notNull().default(false),
118
123
  claimedByHeartbeatRunId: text("claimed_by_heartbeat_run_id"),
119
124
  createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
120
125
  updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
121
126
  });
122
127
 
128
+ export const issueGoals = pgTable(
129
+ "issue_goals",
130
+ {
131
+ issueId: text("issue_id")
132
+ .notNull()
133
+ .references(() => issues.id, { onDelete: "cascade" }),
134
+ goalId: text("goal_id")
135
+ .notNull()
136
+ .references(() => goals.id, { onDelete: "cascade" }),
137
+ companyId: text("company_id")
138
+ .notNull()
139
+ .references(() => companies.id, { onDelete: "cascade" }),
140
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
141
+ },
142
+ (table) => [primaryKey({ columns: [table.issueId, table.goalId] })]
143
+ );
144
+
123
145
  export const issueComments = pgTable("issue_comments", {
124
146
  id: text("id").primaryKey(),
125
147
  issueId: text("issue_id")
@@ -423,6 +445,7 @@ export const schema = {
423
445
  goals,
424
446
  agents,
425
447
  issues,
448
+ issueGoals,
426
449
  issueComments,
427
450
  issueAttachments,
428
451
  activityLogs,