bopodev-db 0.1.1
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 +5 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/bootstrap.d.ts +6 -0
- package/dist/client.d.ts +10 -0
- package/dist/index.d.ts +4 -0
- package/dist/repositories.d.ts +424 -0
- package/dist/schema.d.ts +4027 -0
- package/package.json +19 -0
- package/src/bootstrap.ts +213 -0
- package/src/client.ts +16 -0
- package/src/index.ts +4 -0
- package/src/repositories.ts +769 -0
- package/src/schema.ts +226 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
import { and, desc, eq, inArray, notInArray, sql } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import type { BopoDb } from "./client";
|
|
4
|
+
import {
|
|
5
|
+
activityLogs,
|
|
6
|
+
agents,
|
|
7
|
+
approvalInboxStates,
|
|
8
|
+
approvalRequests,
|
|
9
|
+
auditEvents,
|
|
10
|
+
companies,
|
|
11
|
+
costLedger,
|
|
12
|
+
goals,
|
|
13
|
+
heartbeatRuns,
|
|
14
|
+
issueComments,
|
|
15
|
+
issues,
|
|
16
|
+
projects,
|
|
17
|
+
touchUpdatedAtSql
|
|
18
|
+
} from "./schema";
|
|
19
|
+
|
|
20
|
+
export class RepositoryValidationError extends Error {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "RepositoryValidationError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function assertProjectBelongsToCompany(db: BopoDb, companyId: string, projectId: string) {
|
|
28
|
+
const [project] = await db
|
|
29
|
+
.select({ id: projects.id })
|
|
30
|
+
.from(projects)
|
|
31
|
+
.where(and(eq(projects.companyId, companyId), eq(projects.id, projectId)))
|
|
32
|
+
.limit(1);
|
|
33
|
+
if (!project) {
|
|
34
|
+
throw new RepositoryValidationError("Project not found for company.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertIssueBelongsToCompany(db: BopoDb, companyId: string, issueId: string) {
|
|
39
|
+
const [issue] = await db
|
|
40
|
+
.select({ id: issues.id })
|
|
41
|
+
.from(issues)
|
|
42
|
+
.where(and(eq(issues.companyId, companyId), eq(issues.id, issueId)))
|
|
43
|
+
.limit(1);
|
|
44
|
+
if (!issue) {
|
|
45
|
+
throw new RepositoryValidationError("Issue not found for company.");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function assertGoalBelongsToCompany(db: BopoDb, companyId: string, goalId: string) {
|
|
50
|
+
const [goal] = await db
|
|
51
|
+
.select({ id: goals.id })
|
|
52
|
+
.from(goals)
|
|
53
|
+
.where(and(eq(goals.companyId, companyId), eq(goals.id, goalId)))
|
|
54
|
+
.limit(1);
|
|
55
|
+
if (!goal) {
|
|
56
|
+
throw new RepositoryValidationError("Parent goal not found for company.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function assertAgentBelongsToCompany(db: BopoDb, companyId: string, agentId: string) {
|
|
61
|
+
const [agent] = await db
|
|
62
|
+
.select({ id: agents.id })
|
|
63
|
+
.from(agents)
|
|
64
|
+
.where(and(eq(agents.companyId, companyId), eq(agents.id, agentId)))
|
|
65
|
+
.limit(1);
|
|
66
|
+
if (!agent) {
|
|
67
|
+
throw new RepositoryValidationError("Agent not found for company.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createCompany(db: BopoDb, input: { name: string; mission?: string | null }) {
|
|
72
|
+
const id = nanoid(12);
|
|
73
|
+
await db.insert(companies).values({
|
|
74
|
+
id,
|
|
75
|
+
name: input.name,
|
|
76
|
+
mission: input.mission ?? null
|
|
77
|
+
});
|
|
78
|
+
return { id, ...input };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function listCompanies(db: BopoDb) {
|
|
82
|
+
return db.select().from(companies).orderBy(desc(companies.createdAt));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function updateCompany(
|
|
86
|
+
db: BopoDb,
|
|
87
|
+
input: { id: string; name?: string; mission?: string | null }
|
|
88
|
+
) {
|
|
89
|
+
const [company] = await db
|
|
90
|
+
.update(companies)
|
|
91
|
+
.set(compactUpdate({ name: input.name, mission: input.mission }))
|
|
92
|
+
.where(eq(companies.id, input.id))
|
|
93
|
+
.returning();
|
|
94
|
+
return company ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function deleteCompany(db: BopoDb, id: string) {
|
|
98
|
+
const [deletedCompany] = await db.delete(companies).where(eq(companies.id, id)).returning({ id: companies.id });
|
|
99
|
+
return Boolean(deletedCompany);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function listProjects(db: BopoDb, companyId: string) {
|
|
103
|
+
return db.select().from(projects).where(eq(projects.companyId, companyId)).orderBy(desc(projects.createdAt));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function createProject(
|
|
107
|
+
db: BopoDb,
|
|
108
|
+
input: {
|
|
109
|
+
companyId: string;
|
|
110
|
+
name: string;
|
|
111
|
+
description?: string | null;
|
|
112
|
+
status?: "planned" | "active" | "paused" | "blocked" | "completed" | "archived";
|
|
113
|
+
plannedStartAt?: Date | null;
|
|
114
|
+
workspaceLocalPath?: string | null;
|
|
115
|
+
workspaceGithubRepo?: string | null;
|
|
116
|
+
}
|
|
117
|
+
) {
|
|
118
|
+
const id = nanoid(12);
|
|
119
|
+
await db.insert(projects).values({
|
|
120
|
+
id,
|
|
121
|
+
companyId: input.companyId,
|
|
122
|
+
name: input.name,
|
|
123
|
+
description: input.description ?? null,
|
|
124
|
+
status: input.status ?? "planned",
|
|
125
|
+
plannedStartAt: input.plannedStartAt ?? null,
|
|
126
|
+
workspaceLocalPath: input.workspaceLocalPath ?? null,
|
|
127
|
+
workspaceGithubRepo: input.workspaceGithubRepo ?? null
|
|
128
|
+
});
|
|
129
|
+
return { id, ...input };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function updateProject(
|
|
133
|
+
db: BopoDb,
|
|
134
|
+
input: {
|
|
135
|
+
companyId: string;
|
|
136
|
+
id: string;
|
|
137
|
+
name?: string;
|
|
138
|
+
description?: string | null;
|
|
139
|
+
status?: "planned" | "active" | "paused" | "blocked" | "completed" | "archived";
|
|
140
|
+
plannedStartAt?: Date | null;
|
|
141
|
+
workspaceLocalPath?: string | null;
|
|
142
|
+
workspaceGithubRepo?: string | null;
|
|
143
|
+
}
|
|
144
|
+
) {
|
|
145
|
+
const [project] = await db
|
|
146
|
+
.update(projects)
|
|
147
|
+
.set(
|
|
148
|
+
compactUpdate({
|
|
149
|
+
name: input.name,
|
|
150
|
+
description: input.description,
|
|
151
|
+
status: input.status,
|
|
152
|
+
plannedStartAt: input.plannedStartAt,
|
|
153
|
+
workspaceLocalPath: input.workspaceLocalPath,
|
|
154
|
+
workspaceGithubRepo: input.workspaceGithubRepo
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
.where(and(eq(projects.companyId, input.companyId), eq(projects.id, input.id)))
|
|
158
|
+
.returning();
|
|
159
|
+
return project ?? null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function syncProjectGoals(
|
|
163
|
+
db: BopoDb,
|
|
164
|
+
input: { companyId: string; projectId: string; goalIds: string[] }
|
|
165
|
+
) {
|
|
166
|
+
const dedupedGoalIds = Array.from(new Set(input.goalIds));
|
|
167
|
+
if (dedupedGoalIds.length > 0) {
|
|
168
|
+
const matchingGoals = await db
|
|
169
|
+
.select({ id: goals.id })
|
|
170
|
+
.from(goals)
|
|
171
|
+
.where(and(eq(goals.companyId, input.companyId), inArray(goals.id, dedupedGoalIds)));
|
|
172
|
+
if (matchingGoals.length !== dedupedGoalIds.length) {
|
|
173
|
+
throw new RepositoryValidationError("One or more goals do not belong to the company.");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const detachWhere =
|
|
178
|
+
dedupedGoalIds.length > 0
|
|
179
|
+
? and(eq(goals.companyId, input.companyId), eq(goals.projectId, input.projectId), notInArray(goals.id, dedupedGoalIds))
|
|
180
|
+
: and(eq(goals.companyId, input.companyId), eq(goals.projectId, input.projectId));
|
|
181
|
+
|
|
182
|
+
await db
|
|
183
|
+
.update(goals)
|
|
184
|
+
.set({
|
|
185
|
+
projectId: null,
|
|
186
|
+
updatedAt: touchUpdatedAtSql
|
|
187
|
+
})
|
|
188
|
+
.where(detachWhere);
|
|
189
|
+
|
|
190
|
+
if (dedupedGoalIds.length > 0) {
|
|
191
|
+
await db
|
|
192
|
+
.update(goals)
|
|
193
|
+
.set({
|
|
194
|
+
projectId: input.projectId,
|
|
195
|
+
updatedAt: touchUpdatedAtSql
|
|
196
|
+
})
|
|
197
|
+
.where(and(eq(goals.companyId, input.companyId), inArray(goals.id, dedupedGoalIds)));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function deleteProject(db: BopoDb, companyId: string, id: string) {
|
|
202
|
+
const [deletedProject] = await db
|
|
203
|
+
.delete(projects)
|
|
204
|
+
.where(and(eq(projects.companyId, companyId), eq(projects.id, id)))
|
|
205
|
+
.returning({ id: projects.id });
|
|
206
|
+
return Boolean(deletedProject);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function listIssues(db: BopoDb, companyId: string, projectId?: string) {
|
|
210
|
+
const where = projectId
|
|
211
|
+
? and(eq(issues.companyId, companyId), eq(issues.projectId, projectId))
|
|
212
|
+
: eq(issues.companyId, companyId);
|
|
213
|
+
|
|
214
|
+
return db.select().from(issues).where(where).orderBy(desc(issues.updatedAt));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function createIssue(
|
|
218
|
+
db: BopoDb,
|
|
219
|
+
input: {
|
|
220
|
+
companyId: string;
|
|
221
|
+
projectId: string;
|
|
222
|
+
parentIssueId?: string | null;
|
|
223
|
+
title: string;
|
|
224
|
+
body?: string;
|
|
225
|
+
status?: string;
|
|
226
|
+
priority?: string;
|
|
227
|
+
assigneeAgentId?: string | null;
|
|
228
|
+
labels?: string[];
|
|
229
|
+
tags?: string[];
|
|
230
|
+
}
|
|
231
|
+
) {
|
|
232
|
+
await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
|
|
233
|
+
if (input.parentIssueId) {
|
|
234
|
+
await assertIssueBelongsToCompany(db, input.companyId, input.parentIssueId);
|
|
235
|
+
}
|
|
236
|
+
if (input.assigneeAgentId) {
|
|
237
|
+
await assertAgentBelongsToCompany(db, input.companyId, input.assigneeAgentId);
|
|
238
|
+
}
|
|
239
|
+
const id = nanoid(12);
|
|
240
|
+
await db.insert(issues).values({
|
|
241
|
+
id,
|
|
242
|
+
companyId: input.companyId,
|
|
243
|
+
projectId: input.projectId,
|
|
244
|
+
parentIssueId: input.parentIssueId ?? null,
|
|
245
|
+
title: input.title,
|
|
246
|
+
body: input.body,
|
|
247
|
+
status: input.status ?? "todo",
|
|
248
|
+
priority: input.priority ?? "none",
|
|
249
|
+
assigneeAgentId: input.assigneeAgentId ?? null,
|
|
250
|
+
labelsJson: JSON.stringify(input.labels ?? []),
|
|
251
|
+
tagsJson: JSON.stringify(input.tags ?? [])
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return { id, ...input };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function updateIssue(
|
|
258
|
+
db: BopoDb,
|
|
259
|
+
input: {
|
|
260
|
+
companyId: string;
|
|
261
|
+
id: string;
|
|
262
|
+
projectId?: string;
|
|
263
|
+
title?: string;
|
|
264
|
+
body?: string | null;
|
|
265
|
+
status?: string;
|
|
266
|
+
priority?: string;
|
|
267
|
+
assigneeAgentId?: string | null;
|
|
268
|
+
labels?: string[];
|
|
269
|
+
tags?: string[];
|
|
270
|
+
}
|
|
271
|
+
) {
|
|
272
|
+
if (input.projectId) {
|
|
273
|
+
await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
|
|
274
|
+
}
|
|
275
|
+
if (input.assigneeAgentId) {
|
|
276
|
+
await assertAgentBelongsToCompany(db, input.companyId, input.assigneeAgentId);
|
|
277
|
+
}
|
|
278
|
+
const [issue] = await db
|
|
279
|
+
.update(issues)
|
|
280
|
+
.set(
|
|
281
|
+
compactUpdate({
|
|
282
|
+
projectId: input.projectId,
|
|
283
|
+
title: input.title,
|
|
284
|
+
body: input.body,
|
|
285
|
+
status: input.status,
|
|
286
|
+
priority: input.priority,
|
|
287
|
+
assigneeAgentId: input.assigneeAgentId,
|
|
288
|
+
labelsJson: input.labels ? JSON.stringify(input.labels) : undefined,
|
|
289
|
+
tagsJson: input.tags ? JSON.stringify(input.tags) : undefined,
|
|
290
|
+
updatedAt: touchUpdatedAtSql
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
.where(and(eq(issues.companyId, input.companyId), eq(issues.id, input.id)))
|
|
294
|
+
.returning();
|
|
295
|
+
return issue ?? null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function deleteIssue(db: BopoDb, companyId: string, id: string) {
|
|
299
|
+
const [deletedIssue] = await db
|
|
300
|
+
.delete(issues)
|
|
301
|
+
.where(and(eq(issues.companyId, companyId), eq(issues.id, id)))
|
|
302
|
+
.returning({ id: issues.id });
|
|
303
|
+
return Boolean(deletedIssue);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function addIssueComment(
|
|
307
|
+
db: BopoDb,
|
|
308
|
+
input: {
|
|
309
|
+
companyId: string;
|
|
310
|
+
issueId: string;
|
|
311
|
+
authorType: "human" | "agent" | "system";
|
|
312
|
+
authorId?: string | null;
|
|
313
|
+
body: string;
|
|
314
|
+
}
|
|
315
|
+
) {
|
|
316
|
+
await assertIssueBelongsToCompany(db, input.companyId, input.issueId);
|
|
317
|
+
const id = nanoid(12);
|
|
318
|
+
await db.insert(issueComments).values({
|
|
319
|
+
id,
|
|
320
|
+
companyId: input.companyId,
|
|
321
|
+
issueId: input.issueId,
|
|
322
|
+
authorType: input.authorType,
|
|
323
|
+
authorId: input.authorId ?? null,
|
|
324
|
+
body: input.body
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return { id, ...input };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function listIssueComments(db: BopoDb, companyId: string, issueId: string) {
|
|
331
|
+
return db
|
|
332
|
+
.select()
|
|
333
|
+
.from(issueComments)
|
|
334
|
+
.where(and(eq(issueComments.companyId, companyId), eq(issueComments.issueId, issueId)))
|
|
335
|
+
.orderBy(desc(issueComments.createdAt));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function updateIssueComment(
|
|
339
|
+
db: BopoDb,
|
|
340
|
+
input: {
|
|
341
|
+
companyId: string;
|
|
342
|
+
issueId: string;
|
|
343
|
+
id: string;
|
|
344
|
+
body: string;
|
|
345
|
+
}
|
|
346
|
+
) {
|
|
347
|
+
const [comment] = await db
|
|
348
|
+
.update(issueComments)
|
|
349
|
+
.set({ body: input.body })
|
|
350
|
+
.where(
|
|
351
|
+
and(
|
|
352
|
+
eq(issueComments.companyId, input.companyId),
|
|
353
|
+
eq(issueComments.issueId, input.issueId),
|
|
354
|
+
eq(issueComments.id, input.id)
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
.returning();
|
|
358
|
+
return comment ?? null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function deleteIssueComment(db: BopoDb, companyId: string, issueId: string, id: string) {
|
|
362
|
+
const [deletedComment] = await db
|
|
363
|
+
.delete(issueComments)
|
|
364
|
+
.where(and(eq(issueComments.companyId, companyId), eq(issueComments.issueId, issueId), eq(issueComments.id, id)))
|
|
365
|
+
.returning({ id: issueComments.id });
|
|
366
|
+
return Boolean(deletedComment);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function createGoal(
|
|
370
|
+
db: BopoDb,
|
|
371
|
+
input: {
|
|
372
|
+
companyId: string;
|
|
373
|
+
projectId?: string | null;
|
|
374
|
+
parentGoalId?: string | null;
|
|
375
|
+
level: "company" | "project" | "agent";
|
|
376
|
+
title: string;
|
|
377
|
+
description?: string;
|
|
378
|
+
}
|
|
379
|
+
) {
|
|
380
|
+
if (input.projectId) {
|
|
381
|
+
await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
|
|
382
|
+
}
|
|
383
|
+
if (input.parentGoalId) {
|
|
384
|
+
await assertGoalBelongsToCompany(db, input.companyId, input.parentGoalId);
|
|
385
|
+
}
|
|
386
|
+
const id = nanoid(12);
|
|
387
|
+
await db.insert(goals).values({
|
|
388
|
+
id,
|
|
389
|
+
companyId: input.companyId,
|
|
390
|
+
projectId: input.projectId ?? null,
|
|
391
|
+
parentGoalId: input.parentGoalId ?? null,
|
|
392
|
+
level: input.level,
|
|
393
|
+
title: input.title,
|
|
394
|
+
description: input.description ?? null
|
|
395
|
+
});
|
|
396
|
+
return { id, ...input };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function listGoals(db: BopoDb, companyId: string) {
|
|
400
|
+
return db.select().from(goals).where(eq(goals.companyId, companyId)).orderBy(desc(goals.updatedAt));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function updateGoal(
|
|
404
|
+
db: BopoDb,
|
|
405
|
+
input: {
|
|
406
|
+
companyId: string;
|
|
407
|
+
id: string;
|
|
408
|
+
projectId?: string | null;
|
|
409
|
+
parentGoalId?: string | null;
|
|
410
|
+
level?: "company" | "project" | "agent";
|
|
411
|
+
title?: string;
|
|
412
|
+
description?: string | null;
|
|
413
|
+
status?: string;
|
|
414
|
+
}
|
|
415
|
+
) {
|
|
416
|
+
if (input.projectId) {
|
|
417
|
+
await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
|
|
418
|
+
}
|
|
419
|
+
if (input.parentGoalId) {
|
|
420
|
+
await assertGoalBelongsToCompany(db, input.companyId, input.parentGoalId);
|
|
421
|
+
}
|
|
422
|
+
const [goal] = await db
|
|
423
|
+
.update(goals)
|
|
424
|
+
.set(
|
|
425
|
+
compactUpdate({
|
|
426
|
+
projectId: input.projectId,
|
|
427
|
+
parentGoalId: input.parentGoalId,
|
|
428
|
+
level: input.level,
|
|
429
|
+
title: input.title,
|
|
430
|
+
description: input.description,
|
|
431
|
+
status: input.status,
|
|
432
|
+
updatedAt: touchUpdatedAtSql
|
|
433
|
+
})
|
|
434
|
+
)
|
|
435
|
+
.where(and(eq(goals.companyId, input.companyId), eq(goals.id, input.id)))
|
|
436
|
+
.returning();
|
|
437
|
+
return goal ?? null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export async function deleteGoal(db: BopoDb, companyId: string, id: string) {
|
|
441
|
+
const [deletedGoal] = await db
|
|
442
|
+
.delete(goals)
|
|
443
|
+
.where(and(eq(goals.companyId, companyId), eq(goals.id, id)))
|
|
444
|
+
.returning({ id: goals.id });
|
|
445
|
+
return Boolean(deletedGoal);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function createAgent(
|
|
449
|
+
db: BopoDb,
|
|
450
|
+
input: {
|
|
451
|
+
companyId: string;
|
|
452
|
+
managerAgentId?: string | null;
|
|
453
|
+
role: string;
|
|
454
|
+
name: string;
|
|
455
|
+
providerType: "claude_code" | "codex" | "http" | "shell";
|
|
456
|
+
heartbeatCron: string;
|
|
457
|
+
monthlyBudgetUsd: string;
|
|
458
|
+
canHireAgents?: boolean;
|
|
459
|
+
initialState?: Record<string, unknown>;
|
|
460
|
+
}
|
|
461
|
+
) {
|
|
462
|
+
if (input.managerAgentId) {
|
|
463
|
+
await assertAgentBelongsToCompany(db, input.companyId, input.managerAgentId);
|
|
464
|
+
}
|
|
465
|
+
const id = nanoid(12);
|
|
466
|
+
await db.insert(agents).values({
|
|
467
|
+
id,
|
|
468
|
+
companyId: input.companyId,
|
|
469
|
+
managerAgentId: input.managerAgentId ?? null,
|
|
470
|
+
role: input.role,
|
|
471
|
+
name: input.name,
|
|
472
|
+
providerType: input.providerType,
|
|
473
|
+
heartbeatCron: input.heartbeatCron,
|
|
474
|
+
monthlyBudgetUsd: input.monthlyBudgetUsd,
|
|
475
|
+
canHireAgents: input.canHireAgents ?? false,
|
|
476
|
+
stateBlob: JSON.stringify(input.initialState ?? {})
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return { id, ...input };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function listAgents(db: BopoDb, companyId: string) {
|
|
483
|
+
return db.select().from(agents).where(eq(agents.companyId, companyId)).orderBy(desc(agents.createdAt));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function updateAgent(
|
|
487
|
+
db: BopoDb,
|
|
488
|
+
input: {
|
|
489
|
+
companyId: string;
|
|
490
|
+
id: string;
|
|
491
|
+
managerAgentId?: string | null;
|
|
492
|
+
role?: string;
|
|
493
|
+
name?: string;
|
|
494
|
+
providerType?: "claude_code" | "codex" | "http" | "shell";
|
|
495
|
+
status?: string;
|
|
496
|
+
heartbeatCron?: string;
|
|
497
|
+
monthlyBudgetUsd?: string;
|
|
498
|
+
canHireAgents?: boolean;
|
|
499
|
+
stateBlob?: Record<string, unknown>;
|
|
500
|
+
}
|
|
501
|
+
) {
|
|
502
|
+
if (input.managerAgentId) {
|
|
503
|
+
await assertAgentBelongsToCompany(db, input.companyId, input.managerAgentId);
|
|
504
|
+
}
|
|
505
|
+
const [agent] = await db
|
|
506
|
+
.update(agents)
|
|
507
|
+
.set(
|
|
508
|
+
compactUpdate({
|
|
509
|
+
managerAgentId: input.managerAgentId,
|
|
510
|
+
role: input.role,
|
|
511
|
+
name: input.name,
|
|
512
|
+
providerType: input.providerType,
|
|
513
|
+
status: input.status,
|
|
514
|
+
heartbeatCron: input.heartbeatCron,
|
|
515
|
+
monthlyBudgetUsd: input.monthlyBudgetUsd,
|
|
516
|
+
canHireAgents: input.canHireAgents,
|
|
517
|
+
stateBlob: input.stateBlob ? JSON.stringify(input.stateBlob) : undefined,
|
|
518
|
+
updatedAt: touchUpdatedAtSql
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
.where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.id)))
|
|
522
|
+
.returning();
|
|
523
|
+
return agent ?? null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function deleteAgent(db: BopoDb, companyId: string, id: string) {
|
|
527
|
+
const [deletedAgent] = await db
|
|
528
|
+
.delete(agents)
|
|
529
|
+
.where(and(eq(agents.companyId, companyId), eq(agents.id, id)))
|
|
530
|
+
.returning({ id: agents.id });
|
|
531
|
+
return Boolean(deletedAgent);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function appendAuditEvent(
|
|
535
|
+
db: BopoDb,
|
|
536
|
+
input: {
|
|
537
|
+
companyId: string;
|
|
538
|
+
actorType: "human" | "agent" | "system";
|
|
539
|
+
actorId?: string | null;
|
|
540
|
+
eventType: string;
|
|
541
|
+
entityType: string;
|
|
542
|
+
entityId: string;
|
|
543
|
+
correlationId?: string | null;
|
|
544
|
+
payload: Record<string, unknown>;
|
|
545
|
+
}
|
|
546
|
+
) {
|
|
547
|
+
const id = nanoid(14);
|
|
548
|
+
await db.insert(auditEvents).values({
|
|
549
|
+
id,
|
|
550
|
+
companyId: input.companyId,
|
|
551
|
+
actorType: input.actorType,
|
|
552
|
+
actorId: input.actorId ?? null,
|
|
553
|
+
eventType: input.eventType,
|
|
554
|
+
entityType: input.entityType,
|
|
555
|
+
entityId: input.entityId,
|
|
556
|
+
correlationId: input.correlationId ?? null,
|
|
557
|
+
payloadJson: JSON.stringify(input.payload)
|
|
558
|
+
});
|
|
559
|
+
return id;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export async function listAuditEvents(db: BopoDb, companyId: string, limit = 100) {
|
|
563
|
+
return db
|
|
564
|
+
.select()
|
|
565
|
+
.from(auditEvents)
|
|
566
|
+
.where(eq(auditEvents.companyId, companyId))
|
|
567
|
+
.orderBy(desc(auditEvents.createdAt))
|
|
568
|
+
.limit(limit);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export async function createApprovalRequest(
|
|
572
|
+
db: BopoDb,
|
|
573
|
+
input: {
|
|
574
|
+
companyId: string;
|
|
575
|
+
requestedByAgentId?: string | null;
|
|
576
|
+
action: string;
|
|
577
|
+
payload: Record<string, unknown>;
|
|
578
|
+
}
|
|
579
|
+
) {
|
|
580
|
+
const id = nanoid(12);
|
|
581
|
+
await db.insert(approvalRequests).values({
|
|
582
|
+
id,
|
|
583
|
+
companyId: input.companyId,
|
|
584
|
+
requestedByAgentId: input.requestedByAgentId ?? null,
|
|
585
|
+
action: input.action,
|
|
586
|
+
payloadJson: JSON.stringify(input.payload),
|
|
587
|
+
status: "pending"
|
|
588
|
+
});
|
|
589
|
+
return id;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export async function getApprovalRequest(db: BopoDb, companyId: string, approvalId: string) {
|
|
593
|
+
const [approval] = await db
|
|
594
|
+
.select()
|
|
595
|
+
.from(approvalRequests)
|
|
596
|
+
.where(and(eq(approvalRequests.companyId, companyId), eq(approvalRequests.id, approvalId)))
|
|
597
|
+
.limit(1);
|
|
598
|
+
|
|
599
|
+
return approval ?? null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export async function listApprovalRequests(db: BopoDb, companyId: string) {
|
|
603
|
+
return db
|
|
604
|
+
.select()
|
|
605
|
+
.from(approvalRequests)
|
|
606
|
+
.where(eq(approvalRequests.companyId, companyId))
|
|
607
|
+
.orderBy(desc(approvalRequests.createdAt));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function listApprovalInboxStates(db: BopoDb, companyId: string, actorId: string) {
|
|
611
|
+
return db
|
|
612
|
+
.select()
|
|
613
|
+
.from(approvalInboxStates)
|
|
614
|
+
.where(and(eq(approvalInboxStates.companyId, companyId), eq(approvalInboxStates.actorId, actorId)))
|
|
615
|
+
.orderBy(desc(approvalInboxStates.updatedAt));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export async function markApprovalInboxSeen(
|
|
619
|
+
db: BopoDb,
|
|
620
|
+
input: {
|
|
621
|
+
companyId: string;
|
|
622
|
+
actorId: string;
|
|
623
|
+
approvalId: string;
|
|
624
|
+
seenAt?: Date;
|
|
625
|
+
}
|
|
626
|
+
) {
|
|
627
|
+
const seenAt = input.seenAt ?? new Date();
|
|
628
|
+
await db
|
|
629
|
+
.insert(approvalInboxStates)
|
|
630
|
+
.values({
|
|
631
|
+
companyId: input.companyId,
|
|
632
|
+
actorId: input.actorId,
|
|
633
|
+
approvalId: input.approvalId,
|
|
634
|
+
seenAt
|
|
635
|
+
})
|
|
636
|
+
.onConflictDoUpdate({
|
|
637
|
+
target: [approvalInboxStates.companyId, approvalInboxStates.actorId, approvalInboxStates.approvalId],
|
|
638
|
+
set: {
|
|
639
|
+
seenAt,
|
|
640
|
+
updatedAt: sql`CURRENT_TIMESTAMP`
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export async function markApprovalInboxDismissed(
|
|
646
|
+
db: BopoDb,
|
|
647
|
+
input: {
|
|
648
|
+
companyId: string;
|
|
649
|
+
actorId: string;
|
|
650
|
+
approvalId: string;
|
|
651
|
+
dismissedAt?: Date;
|
|
652
|
+
}
|
|
653
|
+
) {
|
|
654
|
+
const dismissedAt = input.dismissedAt ?? new Date();
|
|
655
|
+
await db
|
|
656
|
+
.insert(approvalInboxStates)
|
|
657
|
+
.values({
|
|
658
|
+
companyId: input.companyId,
|
|
659
|
+
actorId: input.actorId,
|
|
660
|
+
approvalId: input.approvalId,
|
|
661
|
+
dismissedAt
|
|
662
|
+
})
|
|
663
|
+
.onConflictDoUpdate({
|
|
664
|
+
target: [approvalInboxStates.companyId, approvalInboxStates.actorId, approvalInboxStates.approvalId],
|
|
665
|
+
set: {
|
|
666
|
+
dismissedAt,
|
|
667
|
+
updatedAt: sql`CURRENT_TIMESTAMP`
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export async function clearApprovalInboxDismissed(
|
|
673
|
+
db: BopoDb,
|
|
674
|
+
input: {
|
|
675
|
+
companyId: string;
|
|
676
|
+
actorId: string;
|
|
677
|
+
approvalId: string;
|
|
678
|
+
}
|
|
679
|
+
) {
|
|
680
|
+
await db
|
|
681
|
+
.insert(approvalInboxStates)
|
|
682
|
+
.values({
|
|
683
|
+
companyId: input.companyId,
|
|
684
|
+
actorId: input.actorId,
|
|
685
|
+
approvalId: input.approvalId,
|
|
686
|
+
dismissedAt: null
|
|
687
|
+
})
|
|
688
|
+
.onConflictDoUpdate({
|
|
689
|
+
target: [approvalInboxStates.companyId, approvalInboxStates.actorId, approvalInboxStates.approvalId],
|
|
690
|
+
set: {
|
|
691
|
+
dismissedAt: null,
|
|
692
|
+
updatedAt: sql`CURRENT_TIMESTAMP`
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function appendCost(
|
|
698
|
+
db: BopoDb,
|
|
699
|
+
input: {
|
|
700
|
+
companyId: string;
|
|
701
|
+
providerType: string;
|
|
702
|
+
tokenInput: number;
|
|
703
|
+
tokenOutput: number;
|
|
704
|
+
usdCost: string;
|
|
705
|
+
projectId?: string | null;
|
|
706
|
+
issueId?: string | null;
|
|
707
|
+
agentId?: string | null;
|
|
708
|
+
}
|
|
709
|
+
) {
|
|
710
|
+
const id = nanoid(14);
|
|
711
|
+
await db.insert(costLedger).values({
|
|
712
|
+
id,
|
|
713
|
+
companyId: input.companyId,
|
|
714
|
+
providerType: input.providerType,
|
|
715
|
+
tokenInput: input.tokenInput,
|
|
716
|
+
tokenOutput: input.tokenOutput,
|
|
717
|
+
usdCost: input.usdCost,
|
|
718
|
+
projectId: input.projectId ?? null,
|
|
719
|
+
issueId: input.issueId ?? null,
|
|
720
|
+
agentId: input.agentId ?? null
|
|
721
|
+
});
|
|
722
|
+
return id;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function listCostEntries(db: BopoDb, companyId: string, limit = 200) {
|
|
726
|
+
return db
|
|
727
|
+
.select()
|
|
728
|
+
.from(costLedger)
|
|
729
|
+
.where(eq(costLedger.companyId, companyId))
|
|
730
|
+
.orderBy(desc(costLedger.createdAt))
|
|
731
|
+
.limit(limit);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export async function listHeartbeatRuns(db: BopoDb, companyId: string, limit = 100) {
|
|
735
|
+
return db
|
|
736
|
+
.select()
|
|
737
|
+
.from(heartbeatRuns)
|
|
738
|
+
.where(eq(heartbeatRuns.companyId, companyId))
|
|
739
|
+
.orderBy(desc(heartbeatRuns.startedAt))
|
|
740
|
+
.limit(limit);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export async function appendActivity(
|
|
744
|
+
db: BopoDb,
|
|
745
|
+
input: {
|
|
746
|
+
companyId: string;
|
|
747
|
+
issueId?: string | null;
|
|
748
|
+
actorType: "human" | "agent" | "system";
|
|
749
|
+
actorId?: string | null;
|
|
750
|
+
eventType: string;
|
|
751
|
+
payload: Record<string, unknown>;
|
|
752
|
+
}
|
|
753
|
+
) {
|
|
754
|
+
const id = nanoid(12);
|
|
755
|
+
await db.insert(activityLogs).values({
|
|
756
|
+
id,
|
|
757
|
+
companyId: input.companyId,
|
|
758
|
+
issueId: input.issueId ?? null,
|
|
759
|
+
actorType: input.actorType,
|
|
760
|
+
actorId: input.actorId ?? null,
|
|
761
|
+
eventType: input.eventType,
|
|
762
|
+
payloadJson: JSON.stringify(input.payload)
|
|
763
|
+
});
|
|
764
|
+
return id;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function compactUpdate<T extends Record<string, unknown>>(input: T) {
|
|
768
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
769
|
+
}
|