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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/repositories.d.ts +362 -12
- package/dist/schema.d.ts +2286 -1040
- package/package.json +1 -1
- package/src/bootstrap.ts +72 -5
- package/src/repositories.ts +523 -7
- package/src/schema.ts +67 -4
package/src/repositories.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
134
|
-
workspaceGithubRepo: input.workspaceGithubRepo ?? null
|
|
150
|
+
executionWorkspacePolicy: input.executionWorkspacePolicy ? JSON.stringify(input.executionWorkspacePolicy) : null
|
|
135
151
|
});
|
|
136
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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`;
|