bopodev-db 0.1.14 → 0.1.16

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.
@@ -19,7 +19,11 @@ import {
19
19
  pluginConfigs,
20
20
  pluginRuns,
21
21
  plugins,
22
+ projectWorkspaces,
22
23
  projects,
24
+ templateInstalls,
25
+ templateVersions,
26
+ templates,
23
27
  touchUpdatedAtSql
24
28
  } from "./schema";
25
29
 
@@ -74,6 +78,17 @@ async function assertAgentBelongsToCompany(db: BopoDb, companyId: string, agentI
74
78
  }
75
79
  }
76
80
 
81
+ async function assertTemplateBelongsToCompany(db: BopoDb, companyId: string, templateId: string) {
82
+ const [template] = await db
83
+ .select({ id: templates.id })
84
+ .from(templates)
85
+ .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
86
+ .limit(1);
87
+ if (!template) {
88
+ throw new RepositoryValidationError("Template not found for company.");
89
+ }
90
+ }
91
+
77
92
  export async function createCompany(db: BopoDb, input: { name: string; mission?: string | null }) {
78
93
  const id = nanoid(12);
79
94
  await db.insert(companies).values({
@@ -106,7 +121,8 @@ export async function deleteCompany(db: BopoDb, id: string) {
106
121
  }
107
122
 
108
123
  export async function listProjects(db: BopoDb, companyId: string) {
109
- return db.select().from(projects).where(eq(projects.companyId, companyId)).orderBy(desc(projects.createdAt));
124
+ const rows = await db.select().from(projects).where(eq(projects.companyId, companyId)).orderBy(desc(projects.createdAt));
125
+ return hydrateProjectsWithWorkspaces(db, rows);
110
126
  }
111
127
 
112
128
  export async function createProject(
@@ -118,6 +134,7 @@ export async function createProject(
118
134
  description?: string | null;
119
135
  status?: "planned" | "active" | "paused" | "blocked" | "completed" | "archived";
120
136
  plannedStartAt?: Date | null;
137
+ executionWorkspacePolicy?: Record<string, unknown> | null;
121
138
  workspaceLocalPath?: string | null;
122
139
  workspaceGithubRepo?: string | null;
123
140
  }
@@ -130,10 +147,21 @@ export async function createProject(
130
147
  description: input.description ?? null,
131
148
  status: input.status ?? "planned",
132
149
  plannedStartAt: input.plannedStartAt ?? null,
133
- workspaceLocalPath: input.workspaceLocalPath ?? null,
134
- workspaceGithubRepo: input.workspaceGithubRepo ?? null
150
+ executionWorkspacePolicy: input.executionWorkspacePolicy ? JSON.stringify(input.executionWorkspacePolicy) : null
135
151
  });
136
- return { id, ...input };
152
+ const legacyWorkspaceLocalPath = input.workspaceLocalPath?.trim();
153
+ const legacyWorkspaceGithubRepo = input.workspaceGithubRepo?.trim();
154
+ if ((legacyWorkspaceLocalPath && legacyWorkspaceLocalPath.length > 0) || (legacyWorkspaceGithubRepo && legacyWorkspaceGithubRepo.length > 0)) {
155
+ await createProjectWorkspace(db, {
156
+ companyId: input.companyId,
157
+ projectId: id,
158
+ name: input.name,
159
+ cwd: legacyWorkspaceLocalPath && legacyWorkspaceLocalPath.length > 0 ? legacyWorkspaceLocalPath : null,
160
+ repoUrl: legacyWorkspaceGithubRepo && legacyWorkspaceGithubRepo.length > 0 ? legacyWorkspaceGithubRepo : null,
161
+ isPrimary: true
162
+ });
163
+ }
164
+ return getProjectById(db, input.companyId, id);
137
165
  }
138
166
 
139
167
  export async function updateProject(
@@ -145,6 +173,7 @@ export async function updateProject(
145
173
  description?: string | null;
146
174
  status?: "planned" | "active" | "paused" | "blocked" | "completed" | "archived";
147
175
  plannedStartAt?: Date | null;
176
+ executionWorkspacePolicy?: Record<string, unknown> | null;
148
177
  workspaceLocalPath?: string | null;
149
178
  workspaceGithubRepo?: string | null;
150
179
  }
@@ -157,13 +186,222 @@ export async function updateProject(
157
186
  description: input.description,
158
187
  status: input.status,
159
188
  plannedStartAt: input.plannedStartAt,
160
- workspaceLocalPath: input.workspaceLocalPath,
161
- workspaceGithubRepo: input.workspaceGithubRepo
189
+ executionWorkspacePolicy:
190
+ input.executionWorkspacePolicy === undefined
191
+ ? undefined
192
+ : input.executionWorkspacePolicy === null
193
+ ? null
194
+ : JSON.stringify(input.executionWorkspacePolicy),
195
+ updatedAt: touchUpdatedAtSql
162
196
  })
163
197
  )
164
198
  .where(and(eq(projects.companyId, input.companyId), eq(projects.id, input.id)))
165
199
  .returning();
166
- return project ?? null;
200
+ if (!project) {
201
+ return null;
202
+ }
203
+ if (input.workspaceLocalPath !== undefined || input.workspaceGithubRepo !== undefined) {
204
+ const existingWorkspaces = await listProjectWorkspaces(db, input.companyId, input.id);
205
+ const primaryWorkspace = existingWorkspaces.find((workspace) => workspace.isPrimary) ?? existingWorkspaces[0] ?? null;
206
+ const hasAnyWorkspaceField =
207
+ (input.workspaceLocalPath?.trim() ?? "").length > 0 || (input.workspaceGithubRepo?.trim() ?? "").length > 0;
208
+ if (!hasAnyWorkspaceField) {
209
+ if (primaryWorkspace) {
210
+ await updateProjectWorkspace(db, {
211
+ companyId: input.companyId,
212
+ projectId: input.id,
213
+ id: primaryWorkspace.id,
214
+ cwd: null,
215
+ repoUrl: null
216
+ });
217
+ }
218
+ } else if (primaryWorkspace) {
219
+ await updateProjectWorkspace(db, {
220
+ companyId: input.companyId,
221
+ projectId: input.id,
222
+ id: primaryWorkspace.id,
223
+ cwd: input.workspaceLocalPath ?? null,
224
+ repoUrl: input.workspaceGithubRepo ?? null,
225
+ isPrimary: true
226
+ });
227
+ } else {
228
+ await createProjectWorkspace(db, {
229
+ companyId: input.companyId,
230
+ projectId: input.id,
231
+ name: input.name ?? project.name,
232
+ cwd: input.workspaceLocalPath ?? null,
233
+ repoUrl: input.workspaceGithubRepo ?? null,
234
+ isPrimary: true
235
+ });
236
+ }
237
+ }
238
+ return getProjectById(db, input.companyId, project.id);
239
+ }
240
+
241
+ export async function listProjectWorkspaces(db: BopoDb, companyId: string, projectId: string) {
242
+ return db
243
+ .select()
244
+ .from(projectWorkspaces)
245
+ .where(and(eq(projectWorkspaces.companyId, companyId), eq(projectWorkspaces.projectId, projectId)))
246
+ .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
247
+ }
248
+
249
+ export async function createProjectWorkspace(
250
+ db: BopoDb,
251
+ input: {
252
+ companyId: string;
253
+ projectId: string;
254
+ name: string;
255
+ cwd?: string | null;
256
+ repoUrl?: string | null;
257
+ repoRef?: string | null;
258
+ isPrimary?: boolean;
259
+ }
260
+ ) {
261
+ const id = nanoid(12);
262
+ return db.transaction(async (tx) => {
263
+ const existingWorkspaces = await tx
264
+ .select({ id: projectWorkspaces.id })
265
+ .from(projectWorkspaces)
266
+ .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId)))
267
+ .limit(1);
268
+ const shouldBePrimary = input.isPrimary === true || existingWorkspaces.length === 0;
269
+ if (shouldBePrimary) {
270
+ await tx
271
+ .update(projectWorkspaces)
272
+ .set({ isPrimary: false, updatedAt: touchUpdatedAtSql })
273
+ .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId)));
274
+ }
275
+ const [workspace] = await tx
276
+ .insert(projectWorkspaces)
277
+ .values({
278
+ id,
279
+ companyId: input.companyId,
280
+ projectId: input.projectId,
281
+ name: input.name,
282
+ cwd: input.cwd ?? null,
283
+ repoUrl: input.repoUrl ?? null,
284
+ repoRef: input.repoRef ?? null,
285
+ isPrimary: shouldBePrimary
286
+ })
287
+ .returning();
288
+ return workspace;
289
+ });
290
+ }
291
+
292
+ export async function updateProjectWorkspace(
293
+ db: BopoDb,
294
+ input: {
295
+ companyId: string;
296
+ projectId: string;
297
+ id: string;
298
+ name?: string;
299
+ cwd?: string | null;
300
+ repoUrl?: string | null;
301
+ repoRef?: string | null;
302
+ isPrimary?: boolean;
303
+ }
304
+ ) {
305
+ return db.transaction(async (tx) => {
306
+ if (input.isPrimary === true) {
307
+ await tx
308
+ .update(projectWorkspaces)
309
+ .set({ isPrimary: false, updatedAt: touchUpdatedAtSql })
310
+ .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId)));
311
+ }
312
+
313
+ const [workspace] = await tx
314
+ .update(projectWorkspaces)
315
+ .set(
316
+ compactUpdate({
317
+ name: input.name,
318
+ cwd: input.cwd,
319
+ repoUrl: input.repoUrl,
320
+ repoRef: input.repoRef,
321
+ isPrimary: input.isPrimary,
322
+ updatedAt: touchUpdatedAtSql
323
+ })
324
+ )
325
+ .where(
326
+ and(
327
+ eq(projectWorkspaces.companyId, input.companyId),
328
+ eq(projectWorkspaces.projectId, input.projectId),
329
+ eq(projectWorkspaces.id, input.id)
330
+ )
331
+ )
332
+ .returning();
333
+
334
+ if (!workspace) {
335
+ return null;
336
+ }
337
+
338
+ const primary = await tx
339
+ .select({ id: projectWorkspaces.id })
340
+ .from(projectWorkspaces)
341
+ .where(
342
+ and(
343
+ eq(projectWorkspaces.companyId, input.companyId),
344
+ eq(projectWorkspaces.projectId, input.projectId),
345
+ eq(projectWorkspaces.isPrimary, true)
346
+ )
347
+ )
348
+ .limit(1);
349
+ if (primary.length === 0) {
350
+ await tx
351
+ .update(projectWorkspaces)
352
+ .set({ isPrimary: true, updatedAt: touchUpdatedAtSql })
353
+ .where(
354
+ and(
355
+ eq(projectWorkspaces.companyId, input.companyId),
356
+ eq(projectWorkspaces.projectId, input.projectId),
357
+ eq(projectWorkspaces.id, workspace.id)
358
+ )
359
+ );
360
+ const [rehydrated] = await tx
361
+ .select()
362
+ .from(projectWorkspaces)
363
+ .where(eq(projectWorkspaces.id, workspace.id))
364
+ .limit(1);
365
+ return rehydrated ?? workspace;
366
+ }
367
+ return workspace;
368
+ });
369
+ }
370
+
371
+ export async function deleteProjectWorkspace(
372
+ db: BopoDb,
373
+ input: { companyId: string; projectId: string; id: string }
374
+ ) {
375
+ return db.transaction(async (tx) => {
376
+ const [workspace] = await tx
377
+ .delete(projectWorkspaces)
378
+ .where(
379
+ and(
380
+ eq(projectWorkspaces.companyId, input.companyId),
381
+ eq(projectWorkspaces.projectId, input.projectId),
382
+ eq(projectWorkspaces.id, input.id)
383
+ )
384
+ )
385
+ .returning();
386
+ if (!workspace) {
387
+ return null;
388
+ }
389
+ if (workspace.isPrimary) {
390
+ const [fallback] = await tx
391
+ .select({ id: projectWorkspaces.id })
392
+ .from(projectWorkspaces)
393
+ .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId)))
394
+ .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
395
+ .limit(1);
396
+ if (fallback) {
397
+ await tx
398
+ .update(projectWorkspaces)
399
+ .set({ isPrimary: true, updatedAt: touchUpdatedAtSql })
400
+ .where(eq(projectWorkspaces.id, fallback.id));
401
+ }
402
+ }
403
+ return workspace;
404
+ });
167
405
  }
168
406
 
169
407
  export async function syncProjectGoals(
@@ -213,6 +451,67 @@ export async function deleteProject(db: BopoDb, companyId: string, id: string) {
213
451
  return Boolean(deletedProject);
214
452
  }
215
453
 
454
+ async function getProjectById(db: BopoDb, companyId: string, projectId: string) {
455
+ const [row] = await db
456
+ .select()
457
+ .from(projects)
458
+ .where(and(eq(projects.companyId, companyId), eq(projects.id, projectId)))
459
+ .limit(1);
460
+ if (!row) {
461
+ return null;
462
+ }
463
+ const [project] = await hydrateProjectsWithWorkspaces(db, [row]);
464
+ return project ?? null;
465
+ }
466
+
467
+ async function hydrateProjectsWithWorkspaces(
468
+ db: BopoDb,
469
+ projectRows: Array<typeof projects.$inferSelect>
470
+ ) {
471
+ if (projectRows.length === 0) {
472
+ return [] as Array<
473
+ typeof projects.$inferSelect & {
474
+ executionWorkspacePolicy: Record<string, unknown> | null;
475
+ workspaces: Array<typeof projectWorkspaces.$inferSelect>;
476
+ primaryWorkspace: typeof projectWorkspaces.$inferSelect | null;
477
+ }
478
+ >;
479
+ }
480
+ const projectIds = projectRows.map((project) => project.id);
481
+ const workspaces = await db
482
+ .select()
483
+ .from(projectWorkspaces)
484
+ .where(inArray(projectWorkspaces.projectId, projectIds))
485
+ .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
486
+ const workspacesByProject = new Map<string, Array<typeof projectWorkspaces.$inferSelect>>();
487
+ for (const workspace of workspaces) {
488
+ const existing = workspacesByProject.get(workspace.projectId) ?? [];
489
+ existing.push(workspace);
490
+ workspacesByProject.set(workspace.projectId, existing);
491
+ }
492
+
493
+ return projectRows.map((project) => {
494
+ const projectWorkspacesRows = workspacesByProject.get(project.id) ?? [];
495
+ const primaryWorkspace = projectWorkspacesRows.find((workspace) => workspace.isPrimary) ?? projectWorkspacesRows[0] ?? null;
496
+ let executionWorkspacePolicy: Record<string, unknown> | null = null;
497
+ if (project.executionWorkspacePolicy) {
498
+ try {
499
+ executionWorkspacePolicy = JSON.parse(project.executionWorkspacePolicy) as Record<string, unknown>;
500
+ } catch {
501
+ executionWorkspacePolicy = null;
502
+ }
503
+ }
504
+ return {
505
+ ...project,
506
+ workspaceLocalPath: primaryWorkspace?.cwd ?? null,
507
+ workspaceGithubRepo: primaryWorkspace?.repoUrl ?? null,
508
+ executionWorkspacePolicy,
509
+ workspaces: projectWorkspacesRows,
510
+ primaryWorkspace
511
+ };
512
+ });
513
+ }
514
+
216
515
  export async function listIssues(db: BopoDb, companyId: string, projectId?: string) {
217
516
  const where = projectId
218
517
  ? and(eq(issues.companyId, companyId), eq(issues.projectId, projectId))
@@ -754,6 +1053,14 @@ export async function listApprovalRequests(db: BopoDb, companyId: string) {
754
1053
  .orderBy(desc(approvalRequests.createdAt));
755
1054
  }
756
1055
 
1056
+ export async function countPendingApprovalRequests(db: BopoDb, companyId: string) {
1057
+ const [row] = await db
1058
+ .select({ count: sql<number>`count(*)` })
1059
+ .from(approvalRequests)
1060
+ .where(and(eq(approvalRequests.companyId, companyId), eq(approvalRequests.status, "pending")));
1061
+ return Number(row?.count ?? 0);
1062
+ }
1063
+
757
1064
  export async function listApprovalInboxStates(db: BopoDb, companyId: string, actorId: string) {
758
1065
  return db
759
1066
  .select()
@@ -1258,6 +1565,215 @@ export async function listPluginRuns(
1258
1565
  .limit(limit);
1259
1566
  }
1260
1567
 
1568
+ export async function listTemplates(db: BopoDb, companyId: string) {
1569
+ return db
1570
+ .select()
1571
+ .from(templates)
1572
+ .where(eq(templates.companyId, companyId))
1573
+ .orderBy(desc(templates.updatedAt), asc(templates.slug));
1574
+ }
1575
+
1576
+ export async function getTemplate(db: BopoDb, companyId: string, templateId: string) {
1577
+ const [template] = await db
1578
+ .select()
1579
+ .from(templates)
1580
+ .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
1581
+ .limit(1);
1582
+ return template ?? null;
1583
+ }
1584
+
1585
+ export async function getTemplateBySlug(db: BopoDb, companyId: string, slug: string) {
1586
+ const [template] = await db
1587
+ .select()
1588
+ .from(templates)
1589
+ .where(and(eq(templates.companyId, companyId), eq(templates.slug, slug)))
1590
+ .limit(1);
1591
+ return template ?? null;
1592
+ }
1593
+
1594
+ export async function createTemplate(
1595
+ db: BopoDb,
1596
+ input: {
1597
+ companyId: string;
1598
+ slug: string;
1599
+ name: string;
1600
+ description?: string | null;
1601
+ currentVersion?: string;
1602
+ status?: "draft" | "published" | "archived";
1603
+ visibility?: "company" | "private";
1604
+ variablesJson?: string;
1605
+ }
1606
+ ) {
1607
+ const id = nanoid(12);
1608
+ const [template] = await db
1609
+ .insert(templates)
1610
+ .values({
1611
+ id,
1612
+ companyId: input.companyId,
1613
+ slug: input.slug,
1614
+ name: input.name,
1615
+ description: input.description ?? null,
1616
+ currentVersion: input.currentVersion ?? "1.0.0",
1617
+ status: input.status ?? "draft",
1618
+ visibility: input.visibility ?? "company",
1619
+ variablesJson: input.variablesJson ?? "[]"
1620
+ })
1621
+ .returning();
1622
+ return template ?? null;
1623
+ }
1624
+
1625
+ export async function updateTemplate(
1626
+ db: BopoDb,
1627
+ input: {
1628
+ companyId: string;
1629
+ id: string;
1630
+ slug?: string;
1631
+ name?: string;
1632
+ description?: string | null;
1633
+ currentVersion?: string;
1634
+ status?: "draft" | "published" | "archived";
1635
+ visibility?: "company" | "private";
1636
+ variablesJson?: string;
1637
+ }
1638
+ ) {
1639
+ const [template] = await db
1640
+ .update(templates)
1641
+ .set(
1642
+ compactUpdate({
1643
+ slug: input.slug,
1644
+ name: input.name,
1645
+ description: input.description,
1646
+ currentVersion: input.currentVersion,
1647
+ status: input.status,
1648
+ visibility: input.visibility,
1649
+ variablesJson: input.variablesJson,
1650
+ updatedAt: touchUpdatedAtSql
1651
+ })
1652
+ )
1653
+ .where(and(eq(templates.companyId, input.companyId), eq(templates.id, input.id)))
1654
+ .returning();
1655
+ return template ?? null;
1656
+ }
1657
+
1658
+ export async function deleteTemplate(db: BopoDb, companyId: string, templateId: string) {
1659
+ const [deleted] = await db
1660
+ .delete(templates)
1661
+ .where(and(eq(templates.companyId, companyId), eq(templates.id, templateId)))
1662
+ .returning({ id: templates.id });
1663
+ return Boolean(deleted);
1664
+ }
1665
+
1666
+ export async function listTemplateVersions(db: BopoDb, companyId: string, templateId: string) {
1667
+ await assertTemplateBelongsToCompany(db, companyId, templateId);
1668
+ return db
1669
+ .select()
1670
+ .from(templateVersions)
1671
+ .where(and(eq(templateVersions.companyId, companyId), eq(templateVersions.templateId, templateId)))
1672
+ .orderBy(desc(templateVersions.createdAt));
1673
+ }
1674
+
1675
+ export async function getTemplateVersionByVersion(
1676
+ db: BopoDb,
1677
+ input: { companyId: string; templateId: string; version: string }
1678
+ ) {
1679
+ const [row] = await db
1680
+ .select()
1681
+ .from(templateVersions)
1682
+ .where(
1683
+ and(
1684
+ eq(templateVersions.companyId, input.companyId),
1685
+ eq(templateVersions.templateId, input.templateId),
1686
+ eq(templateVersions.version, input.version)
1687
+ )
1688
+ )
1689
+ .limit(1);
1690
+ return row ?? null;
1691
+ }
1692
+
1693
+ export async function getCurrentTemplateVersion(db: BopoDb, companyId: string, templateId: string) {
1694
+ const template = await getTemplate(db, companyId, templateId);
1695
+ if (!template) {
1696
+ return null;
1697
+ }
1698
+ return getTemplateVersionByVersion(db, {
1699
+ companyId,
1700
+ templateId,
1701
+ version: template.currentVersion
1702
+ });
1703
+ }
1704
+
1705
+ export async function createTemplateVersion(
1706
+ db: BopoDb,
1707
+ input: {
1708
+ companyId: string;
1709
+ templateId: string;
1710
+ version: string;
1711
+ manifestJson: string;
1712
+ }
1713
+ ) {
1714
+ await assertTemplateBelongsToCompany(db, input.companyId, input.templateId);
1715
+ const id = nanoid(14);
1716
+ const [row] = await db
1717
+ .insert(templateVersions)
1718
+ .values({
1719
+ id,
1720
+ companyId: input.companyId,
1721
+ templateId: input.templateId,
1722
+ version: input.version,
1723
+ manifestJson: input.manifestJson
1724
+ })
1725
+ .returning();
1726
+ return row ?? null;
1727
+ }
1728
+
1729
+ export async function listTemplateInstalls(
1730
+ db: BopoDb,
1731
+ input: { companyId: string; templateId?: string; limit?: number }
1732
+ ) {
1733
+ const limit = Math.min(Math.max(input.limit ?? 200, 1), 1000);
1734
+ return db
1735
+ .select()
1736
+ .from(templateInstalls)
1737
+ .where(
1738
+ and(
1739
+ eq(templateInstalls.companyId, input.companyId),
1740
+ input.templateId ? eq(templateInstalls.templateId, input.templateId) : undefined
1741
+ )
1742
+ )
1743
+ .orderBy(desc(templateInstalls.createdAt))
1744
+ .limit(limit);
1745
+ }
1746
+
1747
+ export async function createTemplateInstall(
1748
+ db: BopoDb,
1749
+ input: {
1750
+ companyId: string;
1751
+ templateId?: string | null;
1752
+ templateVersionId?: string | null;
1753
+ status?: "applied" | "queued" | "failed";
1754
+ summaryJson?: string;
1755
+ variablesJson?: string;
1756
+ }
1757
+ ) {
1758
+ if (input.templateId) {
1759
+ await assertTemplateBelongsToCompany(db, input.companyId, input.templateId);
1760
+ }
1761
+ const id = nanoid(14);
1762
+ const [row] = await db
1763
+ .insert(templateInstalls)
1764
+ .values({
1765
+ id,
1766
+ companyId: input.companyId,
1767
+ templateId: input.templateId ?? null,
1768
+ templateVersionId: input.templateVersionId ?? null,
1769
+ status: input.status ?? "applied",
1770
+ summaryJson: input.summaryJson ?? "{}",
1771
+ variablesJson: input.variablesJson ?? "{}"
1772
+ })
1773
+ .returning();
1774
+ return row ?? null;
1775
+ }
1776
+
1261
1777
  export async function listModelPricing(db: BopoDb, companyId: string) {
1262
1778
  return db
1263
1779
  .select()
package/src/schema.ts CHANGED
@@ -17,9 +17,26 @@ export const projects = pgTable("projects", {
17
17
  description: text("description"),
18
18
  status: text("status").notNull().default("planned"),
19
19
  plannedStartAt: timestamp("planned_start_at", { mode: "date" }),
20
- workspaceLocalPath: text("workspace_local_path"),
21
- workspaceGithubRepo: text("workspace_github_repo"),
22
- createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
20
+ executionWorkspacePolicy: text("execution_workspace_policy"),
21
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
22
+ updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
23
+ });
24
+
25
+ export const projectWorkspaces = pgTable("project_workspaces", {
26
+ id: text("id").primaryKey(),
27
+ companyId: text("company_id")
28
+ .notNull()
29
+ .references(() => companies.id, { onDelete: "cascade" }),
30
+ projectId: text("project_id")
31
+ .notNull()
32
+ .references(() => projects.id, { onDelete: "cascade" }),
33
+ name: text("name").notNull(),
34
+ cwd: text("cwd"),
35
+ repoUrl: text("repo_url"),
36
+ repoRef: text("repo_ref"),
37
+ isPrimary: boolean("is_primary").notNull().default(false),
38
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
39
+ updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
23
40
  });
24
41
 
25
42
  export const goals = pgTable("goals", {
@@ -253,6 +270,48 @@ export const plugins = pgTable("plugins", {
253
270
  updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
254
271
  });
255
272
 
273
+ export const templates = pgTable("templates", {
274
+ id: text("id").primaryKey(),
275
+ companyId: text("company_id")
276
+ .notNull()
277
+ .references(() => companies.id, { onDelete: "cascade" }),
278
+ slug: text("slug").notNull(),
279
+ name: text("name").notNull(),
280
+ description: text("description"),
281
+ currentVersion: text("current_version").notNull().default("1.0.0"),
282
+ status: text("status").notNull().default("draft"),
283
+ visibility: text("visibility").notNull().default("company"),
284
+ variablesJson: text("variables_json").notNull().default("[]"),
285
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
286
+ updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
287
+ });
288
+
289
+ export const templateVersions = pgTable("template_versions", {
290
+ id: text("id").primaryKey(),
291
+ companyId: text("company_id")
292
+ .notNull()
293
+ .references(() => companies.id, { onDelete: "cascade" }),
294
+ templateId: text("template_id")
295
+ .notNull()
296
+ .references(() => templates.id, { onDelete: "cascade" }),
297
+ version: text("version").notNull(),
298
+ manifestJson: text("manifest_json").notNull().default("{}"),
299
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
300
+ });
301
+
302
+ export const templateInstalls = pgTable("template_installs", {
303
+ id: text("id").primaryKey(),
304
+ companyId: text("company_id")
305
+ .notNull()
306
+ .references(() => companies.id, { onDelete: "cascade" }),
307
+ templateId: text("template_id").references(() => templates.id, { onDelete: "set null" }),
308
+ templateVersionId: text("template_version_id").references(() => templateVersions.id, { onDelete: "set null" }),
309
+ status: text("status").notNull().default("applied"),
310
+ summaryJson: text("summary_json").notNull().default("{}"),
311
+ variablesJson: text("variables_json").notNull().default("{}"),
312
+ createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
313
+ });
314
+
256
315
  export const modelPricing = pgTable(
257
316
  "model_pricing",
258
317
  {
@@ -339,8 +398,12 @@ export const schema = {
339
398
  plugins,
340
399
  pluginConfigs,
341
400
  pluginRuns,
401
+ templates,
402
+ templateVersions,
403
+ templateInstalls,
342
404
  modelPricing,
343
- agentIssueLabels
405
+ agentIssueLabels,
406
+ projectWorkspaces
344
407
  };
345
408
 
346
409
  export const touchUpdatedAtSql = sql`CURRENT_TIMESTAMP`;