bopodev-db 0.1.27 → 0.1.29
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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/client.d.ts +34 -6
- package/dist/index.d.ts +2 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/ping.d.ts +3 -0
- package/dist/repositories/companies.d.ts +32 -0
- package/dist/repositories/helpers.d.ts +16 -0
- package/dist/repositories/index.d.ts +3 -0
- package/dist/repositories/legacy.d.ts +1415 -0
- package/dist/repositories.d.ts +3 -36
- package/dist/schema.d.ts +218 -0
- package/drizzle.config.ts +10 -0
- package/package.json +12 -3
- package/src/bootstrap.ts +9 -595
- package/src/client.ts +634 -9
- package/src/default-paths.ts +1 -1
- package/src/index.ts +2 -0
- package/src/migrate.ts +20 -0
- package/src/migrations/0000_initial.sql +389 -0
- package/src/migrations/0001_issues_external_link.sql +1 -0
- package/src/migrations/0002_issues_goal_goals_owner_agent.sql +2 -0
- package/src/migrations/0003_issue_goals_junction.sql +12 -0
- package/src/migrations/meta/_journal.json +34 -0
- package/src/ping.ts +7 -0
- package/src/repositories/companies.ts +41 -0
- package/src/repositories/helpers.ts +104 -0
- package/src/repositories/index.ts +3 -0
- package/src/{repositories.ts → repositories/legacy.ts} +150 -123
- package/src/schema.ts +22 -0
|
@@ -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 "
|
|
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 "
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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));
|
|
@@ -215,7 +132,7 @@ export async function updateProject(
|
|
|
215
132
|
}
|
|
216
133
|
if (input.workspaceLocalPath !== undefined || input.workspaceGithubRepo !== undefined) {
|
|
217
134
|
const existingWorkspaces = await listProjectWorkspaces(db, input.companyId, input.id);
|
|
218
|
-
const primaryWorkspace = existingWorkspaces.find((workspace) => workspace.isPrimary) ?? existingWorkspaces[0] ?? null;
|
|
135
|
+
const primaryWorkspace = existingWorkspaces.find((workspace: any) => workspace.isPrimary) ?? existingWorkspaces[0] ?? null;
|
|
219
136
|
const hasAnyWorkspaceField =
|
|
220
137
|
(input.workspaceLocalPath?.trim() ?? "").length > 0 || (input.workspaceGithubRepo?.trim() ?? "").length > 0;
|
|
221
138
|
if (!hasAnyWorkspaceField) {
|
|
@@ -272,7 +189,7 @@ export async function createProjectWorkspace(
|
|
|
272
189
|
}
|
|
273
190
|
) {
|
|
274
191
|
const id = nanoid(12);
|
|
275
|
-
return db.transaction(async (tx) => {
|
|
192
|
+
return db.transaction(async (tx: any) => {
|
|
276
193
|
const existingWorkspaces = await tx
|
|
277
194
|
.select({ id: projectWorkspaces.id })
|
|
278
195
|
.from(projectWorkspaces)
|
|
@@ -315,7 +232,7 @@ export async function updateProjectWorkspace(
|
|
|
315
232
|
isPrimary?: boolean;
|
|
316
233
|
}
|
|
317
234
|
) {
|
|
318
|
-
return db.transaction(async (tx) => {
|
|
235
|
+
return db.transaction(async (tx: any) => {
|
|
319
236
|
if (input.isPrimary === true) {
|
|
320
237
|
await tx
|
|
321
238
|
.update(projectWorkspaces)
|
|
@@ -385,7 +302,7 @@ export async function deleteProjectWorkspace(
|
|
|
385
302
|
db: BopoDb,
|
|
386
303
|
input: { companyId: string; projectId: string; id: string }
|
|
387
304
|
) {
|
|
388
|
-
return db.transaction(async (tx) => {
|
|
305
|
+
return db.transaction(async (tx: any) => {
|
|
389
306
|
const [workspace] = await tx
|
|
390
307
|
.delete(projectWorkspaces)
|
|
391
308
|
.where(
|
|
@@ -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.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -763,7 +777,7 @@ export async function listIssueComments(db: BopoDb, companyId: string, issueId:
|
|
|
763
777
|
.from(issueComments)
|
|
764
778
|
.where(and(eq(issueComments.companyId, companyId), eq(issueComments.issueId, issueId)))
|
|
765
779
|
.orderBy(asc(issueComments.createdAt));
|
|
766
|
-
return comments.map((comment) => normalizeIssueComment(comment));
|
|
780
|
+
return comments.map((comment: any) => normalizeIssueComment(comment));
|
|
767
781
|
}
|
|
768
782
|
|
|
769
783
|
export async function listIssueActivity(db: BopoDb, companyId: string, issueId: string, limit = 100) {
|
|
@@ -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,
|
|
@@ -1639,6 +1668,7 @@ export async function enqueueHeartbeatJob(
|
|
|
1639
1668
|
}
|
|
1640
1669
|
|
|
1641
1670
|
export async function claimNextHeartbeatJob(db: BopoDb, companyId: string) {
|
|
1671
|
+
// Postgres-specific: CTE + NOT EXISTS + FOR UPDATE SKIP LOCKED + UPDATE FROM — kept as raw SQL for correct job claiming under concurrency.
|
|
1642
1672
|
const result = await db.execute(sql`
|
|
1643
1673
|
WITH candidate AS (
|
|
1644
1674
|
SELECT q.id
|
|
@@ -1674,7 +1704,7 @@ export async function claimNextHeartbeatJob(db: BopoDb, companyId: string) {
|
|
|
1674
1704
|
WHERE q.id = c.id
|
|
1675
1705
|
RETURNING q.*;
|
|
1676
1706
|
`);
|
|
1677
|
-
const row =
|
|
1707
|
+
const row = result[0] as Record<string, unknown> | undefined;
|
|
1678
1708
|
return row ? normalizeHeartbeatQueueJob(row) : null;
|
|
1679
1709
|
}
|
|
1680
1710
|
|
|
@@ -1804,7 +1834,7 @@ export async function listHeartbeatQueueJobs(
|
|
|
1804
1834
|
.where(and(...conditions))
|
|
1805
1835
|
.orderBy(asc(heartbeatRunQueue.priority), asc(heartbeatRunQueue.availableAt), asc(heartbeatRunQueue.createdAt))
|
|
1806
1836
|
.limit(limit);
|
|
1807
|
-
return rows.map((row) => normalizeHeartbeatQueueJob(row));
|
|
1837
|
+
return rows.map((row: any) => normalizeHeartbeatQueueJob(row));
|
|
1808
1838
|
}
|
|
1809
1839
|
|
|
1810
1840
|
export async function getHeartbeatRun(db: BopoDb, companyId: string, runId: string) {
|
|
@@ -1893,6 +1923,7 @@ export async function listHeartbeatRunMessagesForRuns(
|
|
|
1893
1923
|
}
|
|
1894
1924
|
const perRunLimit = Math.min(Math.max(input.perRunLimit ?? 60, 1), 500);
|
|
1895
1925
|
const runIdValues = sql.join(runIds.map((runId) => sql`(${runId})`), sql`, `);
|
|
1926
|
+
// Window functions (ROW_NUMBER) and VALUES-driven CTE — impractical to express purely with Drizzle’s query builder.
|
|
1896
1927
|
const rankedRows = await db.execute(sql`
|
|
1897
1928
|
WITH requested(run_id) AS (
|
|
1898
1929
|
VALUES ${runIdValues}
|
|
@@ -1935,7 +1966,7 @@ export async function listHeartbeatRunMessagesForRuns(
|
|
|
1935
1966
|
WHERE rn <= ${perRunLimit}
|
|
1936
1967
|
ORDER BY run_id ASC, sequence ASC
|
|
1937
1968
|
`);
|
|
1938
|
-
const rows =
|
|
1969
|
+
const rows = rankedRows as unknown as Array<{
|
|
1939
1970
|
id: string;
|
|
1940
1971
|
company_id: string;
|
|
1941
1972
|
run_id: string;
|
|
@@ -2378,7 +2409,3 @@ export async function createTemplateInstall(
|
|
|
2378
2409
|
.returning();
|
|
2379
2410
|
return row ?? null;
|
|
2380
2411
|
}
|
|
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"),
|
|
@@ -114,12 +116,31 @@ export const issues = pgTable("issues", {
|
|
|
114
116
|
assigneeAgentId: text("assignee_agent_id"),
|
|
115
117
|
labelsJson: text("labels_json").notNull().default("[]"),
|
|
116
118
|
tagsJson: text("tags_json").notNull().default("[]"),
|
|
119
|
+
/** Optional link to a PR, branch page, or other external tracker (GitHub/GitLab/etc.). */
|
|
120
|
+
externalLink: text("external_link"),
|
|
117
121
|
isClaimed: boolean("is_claimed").notNull().default(false),
|
|
118
122
|
claimedByHeartbeatRunId: text("claimed_by_heartbeat_run_id"),
|
|
119
123
|
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
120
124
|
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
121
125
|
});
|
|
122
126
|
|
|
127
|
+
export const issueGoals = pgTable(
|
|
128
|
+
"issue_goals",
|
|
129
|
+
{
|
|
130
|
+
issueId: text("issue_id")
|
|
131
|
+
.notNull()
|
|
132
|
+
.references(() => issues.id, { onDelete: "cascade" }),
|
|
133
|
+
goalId: text("goal_id")
|
|
134
|
+
.notNull()
|
|
135
|
+
.references(() => goals.id, { onDelete: "cascade" }),
|
|
136
|
+
companyId: text("company_id")
|
|
137
|
+
.notNull()
|
|
138
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
139
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
|
|
140
|
+
},
|
|
141
|
+
(table) => [primaryKey({ columns: [table.issueId, table.goalId] })]
|
|
142
|
+
);
|
|
143
|
+
|
|
123
144
|
export const issueComments = pgTable("issue_comments", {
|
|
124
145
|
id: text("id").primaryKey(),
|
|
125
146
|
issueId: text("issue_id")
|
|
@@ -423,6 +444,7 @@ export const schema = {
|
|
|
423
444
|
goals,
|
|
424
445
|
agents,
|
|
425
446
|
issues,
|
|
447
|
+
issueGoals,
|
|
426
448
|
issueComments,
|
|
427
449
|
issueAttachments,
|
|
428
450
|
activityLogs,
|