@cat-factory/node-server 0.95.2 → 0.97.0
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/dist/container.d.ts.map +1 -1
- package/dist/container.js +26 -5
- package/dist/container.js.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +3 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/machineEventRelay.d.ts +21 -0
- package/dist/machineEventRelay.d.ts.map +1 -0
- package/dist/machineEventRelay.js +34 -0
- package/dist/machineEventRelay.js.map +1 -0
- package/dist/propagator.d.ts +1 -1
- package/dist/propagator.d.ts.map +1 -1
- package/dist/propagator.js +6 -1
- package/dist/propagator.js.map +1 -1
- package/dist/repositories/drizzle/_shared.d.ts +2 -0
- package/dist/repositories/drizzle/_shared.d.ts.map +1 -0
- package/dist/repositories/drizzle/_shared.js +11 -0
- package/dist/repositories/drizzle/_shared.js.map +1 -0
- package/dist/repositories/drizzle/accounts.d.ts +62 -0
- package/dist/repositories/drizzle/accounts.d.ts.map +1 -0
- package/dist/repositories/drizzle/accounts.js +438 -0
- package/dist/repositories/drizzle/accounts.js.map +1 -0
- package/dist/repositories/drizzle/board.d.ts +74 -0
- package/dist/repositories/drizzle/board.d.ts.map +1 -0
- package/dist/repositories/drizzle/board.js +489 -0
- package/dist/repositories/drizzle/board.js.map +1 -0
- package/dist/repositories/drizzle/connections.d.ts +84 -0
- package/dist/repositories/drizzle/connections.d.ts.map +1 -0
- package/dist/repositories/drizzle/connections.js +391 -0
- package/dist/repositories/drizzle/connections.js.map +1 -0
- package/dist/repositories/drizzle/execution.d.ts +58 -0
- package/dist/repositories/drizzle/execution.d.ts.map +1 -0
- package/dist/repositories/drizzle/execution.js +585 -0
- package/dist/repositories/drizzle/execution.js.map +1 -0
- package/dist/repositories/drizzle/initiatives.d.ts +54 -0
- package/dist/repositories/drizzle/initiatives.d.ts.map +1 -0
- package/dist/repositories/drizzle/initiatives.js +343 -0
- package/dist/repositories/drizzle/initiatives.js.map +1 -0
- package/dist/repositories/drizzle/kaizen.d.ts +34 -0
- package/dist/repositories/drizzle/kaizen.d.ts.map +1 -0
- package/dist/repositories/drizzle/kaizen.js +192 -0
- package/dist/repositories/drizzle/kaizen.js.map +1 -0
- package/dist/repositories/drizzle/reviews.d.ts +75 -0
- package/dist/repositories/drizzle/reviews.d.ts.map +1 -0
- package/dist/repositories/drizzle/reviews.js +449 -0
- package/dist/repositories/drizzle/reviews.js.map +1 -0
- package/dist/repositories/drizzle/sandbox.d.ts +61 -0
- package/dist/repositories/drizzle/sandbox.d.ts.map +1 -0
- package/dist/repositories/drizzle/sandbox.js +370 -0
- package/dist/repositories/drizzle/sandbox.js.map +1 -0
- package/dist/repositories/drizzle/settings.d.ts +76 -0
- package/dist/repositories/drizzle/settings.d.ts.map +1 -0
- package/dist/repositories/drizzle/settings.js +377 -0
- package/dist/repositories/drizzle/settings.js.map +1 -0
- package/dist/repositories/drizzle/telemetry.d.ts +60 -0
- package/dist/repositories/drizzle/telemetry.d.ts.map +1 -0
- package/dist/repositories/drizzle/telemetry.js +559 -0
- package/dist/repositories/drizzle/telemetry.js.map +1 -0
- package/dist/repositories/drizzle.d.ts +6 -113
- package/dist/repositories/drizzle.d.ts.map +1 -1
- package/dist/repositories/drizzle.js +20 -4118
- package/dist/repositories/drizzle.js.map +1 -1
- package/drizzle/20260716082429_big_polaris/migration.sql +1 -0
- package/drizzle/20260716082429_big_polaris/snapshot.json +15675 -0
- package/package.json +18 -18
|
@@ -1,4121 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async listVisible(scope) {
|
|
16
|
-
if (scope === null) {
|
|
17
|
-
const rows = await this.db.select().from(workspaces).orderBy(desc(workspaces.created_at));
|
|
18
|
-
return rows.map(rowToWorkspace);
|
|
19
|
-
}
|
|
20
|
-
const legacy = and(isNull(workspaces.account_id), eq(workspaces.owner_user_id, scope.ownerUserId));
|
|
21
|
-
const where = scope.accountIds.length > 0
|
|
22
|
-
? or(inArray(workspaces.account_id, scope.accountIds), legacy)
|
|
23
|
-
: legacy;
|
|
24
|
-
const rows = await this.db
|
|
25
|
-
.select()
|
|
26
|
-
.from(workspaces)
|
|
27
|
-
.where(where)
|
|
28
|
-
.orderBy(desc(workspaces.created_at));
|
|
29
|
-
return rows.map(rowToWorkspace);
|
|
30
|
-
}
|
|
31
|
-
async get(id) {
|
|
32
|
-
const [row] = await this.db.select().from(workspaces).where(eq(workspaces.id, id));
|
|
33
|
-
return row ? rowToWorkspace(row) : null;
|
|
34
|
-
}
|
|
35
|
-
async ownerOf(id) {
|
|
36
|
-
const [row] = await this.db
|
|
37
|
-
.select({ owner: workspaces.owner_user_id })
|
|
38
|
-
.from(workspaces)
|
|
39
|
-
.where(eq(workspaces.id, id));
|
|
40
|
-
return row ? row.owner : undefined;
|
|
41
|
-
}
|
|
42
|
-
async accountOf(id) {
|
|
43
|
-
const [row] = await this.db
|
|
44
|
-
.select({ account: workspaces.account_id })
|
|
45
|
-
.from(workspaces)
|
|
46
|
-
.where(eq(workspaces.id, id));
|
|
47
|
-
return row ? row.account : undefined;
|
|
48
|
-
}
|
|
49
|
-
async create(workspace, ownerUserId, accountId) {
|
|
50
|
-
await this.db.insert(workspaces).values({
|
|
51
|
-
id: workspace.id,
|
|
52
|
-
name: workspace.name,
|
|
53
|
-
description: workspace.description,
|
|
54
|
-
created_at: workspace.createdAt,
|
|
55
|
-
owner_user_id: ownerUserId,
|
|
56
|
-
account_id: accountId,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
async rename(id, name) {
|
|
60
|
-
await this.db.update(workspaces).set({ name }).where(eq(workspaces.id, id));
|
|
61
|
-
}
|
|
62
|
-
async setDescription(id, description) {
|
|
63
|
-
await this.db.update(workspaces).set({ description }).where(eq(workspaces.id, id));
|
|
64
|
-
}
|
|
65
|
-
async delete(id, rehome = []) {
|
|
66
|
-
await this.db.transaction(async (tx) => {
|
|
67
|
-
// Re-home shared services FIRST: move each service's blocks + run history to a surviving
|
|
68
|
-
// mounting board by re-stamping their `workspace_id`. Blocks are keyed by `service_id`, so
|
|
69
|
-
// after the move the service's frame no longer lives in THIS workspace — the reclaim below
|
|
70
|
-
// then skips it, leaving the service, its subtree and every OTHER board's mount intact. A
|
|
71
|
-
// shared service therefore outlives its home board's deletion. Mirror any change in the
|
|
72
|
-
// Cloudflare facade's D1WorkspaceRepository.delete.
|
|
73
|
-
for (const { serviceId, toWorkspaceId } of rehome) {
|
|
74
|
-
await tx
|
|
75
|
-
.update(agentRuns)
|
|
76
|
-
.set({ workspace_id: toWorkspaceId })
|
|
77
|
-
.where(inArray(agentRuns.block_id, tx.select({ id: blocks.id }).from(blocks).where(eq(blocks.service_id, serviceId))));
|
|
78
|
-
await tx
|
|
79
|
-
.update(blocks)
|
|
80
|
-
.set({ workspace_id: toWorkspaceId })
|
|
81
|
-
.where(eq(blocks.service_id, serviceId));
|
|
82
|
-
}
|
|
83
|
-
// Reclaim the account-owned services this workspace HOMES (+ every board's mount of them)
|
|
84
|
-
// BEFORE the blocks they reference are dropped. A deleted board that leaves its services
|
|
85
|
-
// behind is not a cosmetic leak: `services` is account-scoped and looked up by
|
|
86
|
-
// (installation_id, repo_github_id), so a dangling service (its frame block gone) keeps the
|
|
87
|
-
// SAME repo from being re-added on any other board in the account. Mirror any change in the
|
|
88
|
-
// Cloudflare facade's D1WorkspaceRepository.delete.
|
|
89
|
-
const homed = await tx
|
|
90
|
-
.select({ id: services.id })
|
|
91
|
-
.from(services)
|
|
92
|
-
.innerJoin(blocks, eq(services.frame_block_id, blocks.id))
|
|
93
|
-
.where(eq(blocks.workspace_id, id));
|
|
94
|
-
const serviceIds = homed.map((r) => r.id);
|
|
95
|
-
if (serviceIds.length) {
|
|
96
|
-
await tx.delete(workspaceServices).where(inArray(workspaceServices.service_id, serviceIds));
|
|
97
|
-
await tx.delete(services).where(inArray(services.id, serviceIds));
|
|
98
|
-
}
|
|
99
|
-
// This workspace's OWN mounts of services homed elsewhere (shared services it mounted).
|
|
100
|
-
await tx.delete(workspaceServices).where(eq(workspaceServices.workspace_id, id));
|
|
101
|
-
// Bulk reclaim of every plain workspace-scoped table (incl. blocks/agent_runs/pipelines/
|
|
102
|
-
// environments) from the shared kernel list — keeps this cascade in lockstep with the
|
|
103
|
-
// Cloudflare facade and stops a new workspace-scoped table silently orphaning. The schema
|
|
104
|
-
// declares no FKs between these tables, so order is free; they only need to run AFTER the
|
|
105
|
-
// `services` reclaim above (which reads `blocks`) and BEFORE the root `workspaces` row.
|
|
106
|
-
for (const table of WORKSPACE_SCOPED_TABLES) {
|
|
107
|
-
await tx.execute(sql `DELETE FROM ${sql.identifier(table)} WHERE workspace_id = ${id}`);
|
|
108
|
-
}
|
|
109
|
-
await tx.delete(workspaces).where(eq(workspaces.id, id));
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
class DrizzleBlockRepository {
|
|
114
|
-
db;
|
|
115
|
-
constructor(db) {
|
|
116
|
-
this.db = db;
|
|
117
|
-
}
|
|
118
|
-
// List reads order by `seq` (insertion order) for parity with the Cloudflare facade's
|
|
119
|
-
// `ORDER BY rowid` — Postgres heap order is otherwise non-deterministic.
|
|
120
|
-
async listByWorkspace(workspaceId) {
|
|
121
|
-
const rows = await this.db
|
|
122
|
-
.select()
|
|
123
|
-
.from(blocks)
|
|
124
|
-
.where(eq(blocks.workspace_id, workspaceId))
|
|
125
|
-
.orderBy(blocks.seq);
|
|
126
|
-
// Snapshot-facing list read: drop a corrupt block rather than failing the whole board load.
|
|
127
|
-
return tryDecodeRows(rows, rowToBlock, (r) => ({ table: 'blocks', id: r.id }));
|
|
128
|
-
}
|
|
129
|
-
async listByService(serviceId) {
|
|
130
|
-
const rows = await this.db
|
|
131
|
-
.select()
|
|
132
|
-
.from(blocks)
|
|
133
|
-
.where(eq(blocks.service_id, serviceId))
|
|
134
|
-
.orderBy(blocks.seq);
|
|
135
|
-
return tryDecodeRows(rows, rowToBlock, (r) => ({ table: 'blocks', id: r.id }));
|
|
136
|
-
}
|
|
137
|
-
async listByServices(serviceIds) {
|
|
138
|
-
if (serviceIds.length === 0)
|
|
139
|
-
return [];
|
|
140
|
-
const out = [];
|
|
141
|
-
// Chunk the IN list to stay well under the bind-parameter limit. Ordering is
|
|
142
|
-
// per-chunk, matching the D1 twin's per-chunk `ORDER BY rowid`.
|
|
143
|
-
for (let i = 0; i < serviceIds.length; i += 500) {
|
|
144
|
-
const rows = await this.db
|
|
145
|
-
.select()
|
|
146
|
-
.from(blocks)
|
|
147
|
-
.where(inArray(blocks.service_id, serviceIds.slice(i, i + 500)))
|
|
148
|
-
.orderBy(blocks.seq);
|
|
149
|
-
out.push(...tryDecodeRows(rows, rowToBlock, (r) => ({ table: 'blocks', id: r.id })));
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
async get(workspaceId, id) {
|
|
154
|
-
const [row] = await this.db
|
|
155
|
-
.select()
|
|
156
|
-
.from(blocks)
|
|
157
|
-
.where(and(eq(blocks.workspace_id, workspaceId), eq(blocks.id, id)));
|
|
158
|
-
return row ? rowToBlock(row) : null;
|
|
159
|
-
}
|
|
160
|
-
async findById(blockId) {
|
|
161
|
-
const [row] = await this.db.select().from(blocks).where(eq(blocks.id, blockId)).limit(1);
|
|
162
|
-
if (!row)
|
|
163
|
-
return null;
|
|
164
|
-
return {
|
|
165
|
-
workspaceId: row.workspace_id,
|
|
166
|
-
serviceId: row.service_id ?? null,
|
|
167
|
-
block: rowToBlock(row),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
async findByIds(blockIds) {
|
|
171
|
-
if (blockIds.length === 0)
|
|
172
|
-
return [];
|
|
173
|
-
const out = [];
|
|
174
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
175
|
-
for (let i = 0; i < blockIds.length; i += 500) {
|
|
176
|
-
const rows = await this.db
|
|
177
|
-
.select()
|
|
178
|
-
.from(blocks)
|
|
179
|
-
.where(inArray(blocks.id, blockIds.slice(i, i + 500)));
|
|
180
|
-
out.push(...rows.map((row) => ({
|
|
181
|
-
workspaceId: row.workspace_id,
|
|
182
|
-
serviceId: row.service_id ?? null,
|
|
183
|
-
block: rowToBlock(row),
|
|
184
|
-
})));
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
async insert(workspaceId, block, serviceId) {
|
|
189
|
-
await this.db.insert(blocks).values({
|
|
190
|
-
workspace_id: workspaceId,
|
|
191
|
-
service_id: serviceId ?? null,
|
|
192
|
-
...blockInsertValues(block),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
async update(workspaceId, id, patch) {
|
|
196
|
-
const set = blockPatchToColumns(patch);
|
|
197
|
-
if (Object.keys(set).length === 0)
|
|
198
|
-
return;
|
|
199
|
-
await this.db
|
|
200
|
-
.update(blocks)
|
|
201
|
-
.set(set)
|
|
202
|
-
.where(and(eq(blocks.workspace_id, workspaceId), eq(blocks.id, id)));
|
|
203
|
-
}
|
|
204
|
-
async setService(workspaceId, ids, serviceId) {
|
|
205
|
-
if (ids.length === 0)
|
|
206
|
-
return;
|
|
207
|
-
await this.db
|
|
208
|
-
.update(blocks)
|
|
209
|
-
.set({ service_id: serviceId })
|
|
210
|
-
.where(and(eq(blocks.workspace_id, workspaceId), inArray(blocks.id, ids)));
|
|
211
|
-
}
|
|
212
|
-
async deleteMany(workspaceId, ids) {
|
|
213
|
-
if (ids.length === 0)
|
|
214
|
-
return;
|
|
215
|
-
await this.db
|
|
216
|
-
.delete(blocks)
|
|
217
|
-
.where(and(eq(blocks.workspace_id, workspaceId), inArray(blocks.id, ids)));
|
|
218
|
-
}
|
|
219
|
-
async countActiveInternal(workspaceId) {
|
|
220
|
-
const [row] = await this.db
|
|
221
|
-
.select({ n: count() })
|
|
222
|
-
.from(blocks)
|
|
223
|
-
.where(and(eq(blocks.workspace_id, workspaceId), eq(blocks.internal, 1), eq(blocks.status, 'in_progress')));
|
|
224
|
-
return row?.n ?? 0;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
class DrizzlePipelineRepository {
|
|
228
|
-
db;
|
|
229
|
-
constructor(db) {
|
|
230
|
-
this.db = db;
|
|
231
|
-
}
|
|
232
|
-
async listByWorkspace(workspaceId) {
|
|
233
|
-
const rows = await this.db
|
|
234
|
-
.select()
|
|
235
|
-
.from(pipelines)
|
|
236
|
-
.where(eq(pipelines.workspace_id, workspaceId))
|
|
237
|
-
// Order by the monotonic insert `seq` so the catalog comes back in the curated
|
|
238
|
-
// `seedPipelines()` order it was inserted in (Postgres gives no row order without
|
|
239
|
-
// ORDER BY) — deterministic snapshots, a stable default `pipelines[0]`, and parity
|
|
240
|
-
// with the Cloudflare facade's `ORDER BY rowid`.
|
|
241
|
-
.orderBy(pipelines.seq);
|
|
242
|
-
return rows.map(rowToPipeline);
|
|
243
|
-
}
|
|
244
|
-
async get(workspaceId, id) {
|
|
245
|
-
const [row] = await this.db
|
|
246
|
-
.select()
|
|
247
|
-
.from(pipelines)
|
|
248
|
-
.where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, id)));
|
|
249
|
-
return row ? rowToPipeline(row) : null;
|
|
250
|
-
}
|
|
251
|
-
async insert(workspaceId, pipeline) {
|
|
252
|
-
await this.db.insert(pipelines).values({
|
|
253
|
-
workspace_id: workspaceId,
|
|
254
|
-
id: pipeline.id,
|
|
255
|
-
name: pipeline.name,
|
|
256
|
-
agent_kinds: JSON.stringify(pipeline.agentKinds),
|
|
257
|
-
gates: pipeline.gates ? JSON.stringify(pipeline.gates) : null,
|
|
258
|
-
thresholds: pipeline.thresholds ? JSON.stringify(pipeline.thresholds) : null,
|
|
259
|
-
enabled: pipeline.enabled ? JSON.stringify(pipeline.enabled) : null,
|
|
260
|
-
consensus: pipeline.consensus ? JSON.stringify(pipeline.consensus) : null,
|
|
261
|
-
gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
|
|
262
|
-
follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
|
|
263
|
-
tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
|
|
264
|
-
step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
|
|
265
|
-
labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
|
|
266
|
-
archived: pipeline.archived ? 1 : null,
|
|
267
|
-
builtin: pipeline.builtin ? 1 : null,
|
|
268
|
-
version: pipeline.version ?? null,
|
|
269
|
-
public: pipeline.public ? 1 : null,
|
|
270
|
-
availability: pipeline.availability ?? null,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
async update(workspaceId, pipeline) {
|
|
274
|
-
// UPDATE in place preserves the row's `seq`, so an edited pipeline keeps its place
|
|
275
|
-
// in the catalog order. `builtin` is immutable, so it is not rewritten. `version` IS
|
|
276
|
-
// rewritten so a reseed bumps the stored copy to the current catalog version.
|
|
277
|
-
await this.db
|
|
278
|
-
.update(pipelines)
|
|
279
|
-
.set({
|
|
280
|
-
name: pipeline.name,
|
|
281
|
-
agent_kinds: JSON.stringify(pipeline.agentKinds),
|
|
282
|
-
gates: pipeline.gates ? JSON.stringify(pipeline.gates) : null,
|
|
283
|
-
thresholds: pipeline.thresholds ? JSON.stringify(pipeline.thresholds) : null,
|
|
284
|
-
enabled: pipeline.enabled ? JSON.stringify(pipeline.enabled) : null,
|
|
285
|
-
consensus: pipeline.consensus ? JSON.stringify(pipeline.consensus) : null,
|
|
286
|
-
gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
|
|
287
|
-
follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
|
|
288
|
-
tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
|
|
289
|
-
step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
|
|
290
|
-
labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
|
|
291
|
-
archived: pipeline.archived ? 1 : null,
|
|
292
|
-
version: pipeline.version ?? null,
|
|
293
|
-
public: pipeline.public ? 1 : null,
|
|
294
|
-
availability: pipeline.availability ?? null,
|
|
295
|
-
})
|
|
296
|
-
.where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, pipeline.id)));
|
|
297
|
-
}
|
|
298
|
-
async delete(workspaceId, id) {
|
|
299
|
-
await this.db
|
|
300
|
-
.delete(pipelines)
|
|
301
|
-
.where(and(eq(pipelines.workspace_id, workspaceId), eq(pipelines.id, id)));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
/** Execution runs live as `kind='execution'` rows of the unified agent_runs table. */
|
|
305
|
-
class DrizzleExecutionRepository {
|
|
306
|
-
db;
|
|
307
|
-
clock;
|
|
308
|
-
constructor(db, clock) {
|
|
309
|
-
this.db = db;
|
|
310
|
-
this.clock = clock;
|
|
311
|
-
}
|
|
312
|
-
isExecution = eq(agentRuns.kind, 'execution');
|
|
313
|
-
async listByWorkspace(workspaceId) {
|
|
314
|
-
const rows = await this.db
|
|
315
|
-
.select()
|
|
316
|
-
.from(agentRuns)
|
|
317
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), this.isExecution))
|
|
318
|
-
.orderBy(agentRuns.created_at);
|
|
319
|
-
// Snapshot-facing list read: drop a corrupt run rather than failing the whole board load.
|
|
320
|
-
return tryDecodeRows(rows, (r) => rowToExecution(r), (r) => ({ table: 'agent_runs', id: r.id }));
|
|
321
|
-
}
|
|
322
|
-
async listLive(workspaceId) {
|
|
323
|
-
// Lean live-run projection: block_id + status + id only, NEVER the heavy `detail` column.
|
|
324
|
-
// Served by idx_agent_runs_ws_kind_status (workspace_id, kind, status). Mirrors the D1 repo.
|
|
325
|
-
// Unordered: both consumers (dispatch guard's block-id Set, resumePaused's id loop) are
|
|
326
|
-
// order-agnostic.
|
|
327
|
-
const rows = await this.db
|
|
328
|
-
.select({ id: agentRuns.id, blockId: agentRuns.block_id, status: agentRuns.status })
|
|
329
|
-
.from(agentRuns)
|
|
330
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), this.isExecution, inArray(agentRuns.status, ['running', 'blocked', 'paused'])));
|
|
331
|
-
return rows.map((r) => ({
|
|
332
|
-
id: r.id,
|
|
333
|
-
blockId: r.blockId ?? '',
|
|
334
|
-
status: r.status,
|
|
335
|
-
}));
|
|
336
|
-
}
|
|
337
|
-
async listByService(serviceId) {
|
|
338
|
-
const rows = await this.db
|
|
339
|
-
.select()
|
|
340
|
-
.from(agentRuns)
|
|
341
|
-
.where(and(eq(agentRuns.service_id, serviceId), this.isExecution))
|
|
342
|
-
.orderBy(agentRuns.created_at);
|
|
343
|
-
return tryDecodeRows(rows, (r) => rowToExecution(r), (r) => ({ table: 'agent_runs', id: r.id }));
|
|
344
|
-
}
|
|
345
|
-
async listByServices(serviceIds) {
|
|
346
|
-
if (serviceIds.length === 0)
|
|
347
|
-
return [];
|
|
348
|
-
const out = [];
|
|
349
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
350
|
-
for (let i = 0; i < serviceIds.length; i += 500) {
|
|
351
|
-
const rows = await this.db
|
|
352
|
-
.select()
|
|
353
|
-
.from(agentRuns)
|
|
354
|
-
.where(and(inArray(agentRuns.service_id, serviceIds.slice(i, i + 500)), this.isExecution))
|
|
355
|
-
.orderBy(agentRuns.created_at);
|
|
356
|
-
out.push(...tryDecodeRows(rows, (r) => rowToExecution(r), (r) => ({ table: 'agent_runs', id: r.id })));
|
|
357
|
-
}
|
|
358
|
-
return out;
|
|
359
|
-
}
|
|
360
|
-
async get(workspaceId, id) {
|
|
361
|
-
const [row] = await this.db
|
|
362
|
-
.select()
|
|
363
|
-
.from(agentRuns)
|
|
364
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution));
|
|
365
|
-
return row ? rowToExecution(row) : null;
|
|
366
|
-
}
|
|
367
|
-
async getByBlock(workspaceId, blockId) {
|
|
368
|
-
const [row] = await this.db
|
|
369
|
-
.select()
|
|
370
|
-
.from(agentRuns)
|
|
371
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.block_id, blockId), this.isExecution));
|
|
372
|
-
return row ? rowToExecution(row) : null;
|
|
373
|
-
}
|
|
374
|
-
async upsert(workspaceId, execution) {
|
|
375
|
-
const now = this.clock.now();
|
|
376
|
-
const detail = executionToDetail(execution);
|
|
377
|
-
// Stamp `service_id` from the run's block (subquery) so a shared service's runs surface on
|
|
378
|
-
// every board that mounts it via `listByService`; refreshed on every write so it follows a
|
|
379
|
-
// reparent that re-homes the block. Mirrors the D1 repo.
|
|
380
|
-
const serviceIdSub = sql `(SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${workspaceId} AND ${blocks.id} = ${execution.blockId})`;
|
|
381
|
-
// `rev` is bumped on every write (and read back onto the instance) so a concurrent
|
|
382
|
-
// compareAndSwap can detect the row moved. A fresh insert starts at 0.
|
|
383
|
-
const rows = await this.db
|
|
384
|
-
.insert(agentRuns)
|
|
385
|
-
.values({
|
|
386
|
-
workspace_id: workspaceId,
|
|
387
|
-
id: execution.id,
|
|
388
|
-
kind: 'execution',
|
|
389
|
-
block_id: execution.blockId,
|
|
390
|
-
status: execution.status,
|
|
391
|
-
detail,
|
|
392
|
-
created_at: now,
|
|
393
|
-
updated_at: now,
|
|
394
|
-
workflow_instance_id: execution.id,
|
|
395
|
-
service_id: serviceIdSub,
|
|
396
|
-
rev: 0,
|
|
397
|
-
})
|
|
398
|
-
// error/failure/workflow_instance_id are left out of the update so they survive
|
|
399
|
-
// normal step writes (see markFailed) — mirrors the D1 repo.
|
|
400
|
-
.onConflictDoUpdate({
|
|
401
|
-
target: [agentRuns.workspace_id, agentRuns.id],
|
|
402
|
-
set: {
|
|
403
|
-
block_id: execution.blockId,
|
|
404
|
-
status: execution.status,
|
|
405
|
-
detail,
|
|
406
|
-
updated_at: now,
|
|
407
|
-
service_id: serviceIdSub,
|
|
408
|
-
rev: sql `${agentRuns.rev} + 1`,
|
|
409
|
-
},
|
|
410
|
-
})
|
|
411
|
-
.returning({ rev: agentRuns.rev });
|
|
412
|
-
if (rows[0])
|
|
413
|
-
execution.rev = rows[0].rev;
|
|
414
|
-
}
|
|
415
|
-
async insertLive(workspaceId, execution, opts) {
|
|
416
|
-
// One live run per block, enforced atomically by the partial unique index
|
|
417
|
-
// `uniq_live_execution_per_block` on (workspace_id, block_id) over live execution rows. The
|
|
418
|
-
// cleanup and the insert run inside ONE transaction so a losing concurrent insert can never
|
|
419
|
-
// wipe the winner: the DELETE only ever removes the block's TERMINAL rows and the caller's
|
|
420
|
-
// own `replaceId` (the run it is knowingly superseding) — NEVER another writer's fresh live
|
|
421
|
-
// row — and the index then rejects a second live insert via DO NOTHING (empty returning).
|
|
422
|
-
// Callers therefore MUST NOT `deleteByBlock` first. The conflict target mirrors the D1 repo
|
|
423
|
-
// and the index predicate exactly; the insert columns mirror upsert (service_id subquery,
|
|
424
|
-
// rev 0).
|
|
425
|
-
const now = this.clock.now();
|
|
426
|
-
const detail = executionToDetail(execution);
|
|
427
|
-
const serviceIdSub = sql `(SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${workspaceId} AND ${blocks.id} = ${execution.blockId})`;
|
|
428
|
-
const terminalOrReplaced = opts?.replaceId
|
|
429
|
-
? or(notInArray(agentRuns.status, ['running', 'blocked', 'paused']), eq(agentRuns.id, opts.replaceId))
|
|
430
|
-
: notInArray(agentRuns.status, ['running', 'blocked', 'paused']);
|
|
431
|
-
const rows = await this.db.transaction(async (tx) => {
|
|
432
|
-
await tx
|
|
433
|
-
.delete(agentRuns)
|
|
434
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.block_id, execution.blockId), eq(agentRuns.kind, 'execution'), terminalOrReplaced));
|
|
435
|
-
return tx
|
|
436
|
-
.insert(agentRuns)
|
|
437
|
-
.values({
|
|
438
|
-
workspace_id: workspaceId,
|
|
439
|
-
id: execution.id,
|
|
440
|
-
kind: 'execution',
|
|
441
|
-
block_id: execution.blockId,
|
|
442
|
-
status: execution.status,
|
|
443
|
-
detail,
|
|
444
|
-
created_at: now,
|
|
445
|
-
updated_at: now,
|
|
446
|
-
workflow_instance_id: execution.id,
|
|
447
|
-
service_id: serviceIdSub,
|
|
448
|
-
rev: 0,
|
|
449
|
-
})
|
|
450
|
-
.onConflictDoNothing({
|
|
451
|
-
target: [agentRuns.workspace_id, agentRuns.block_id],
|
|
452
|
-
// For DO NOTHING, `where` is the conflict target's partial-index predicate (the
|
|
453
|
-
// DO-UPDATE `targetWhere`); it must mirror uniq_live_execution_per_block exactly.
|
|
454
|
-
where: sql `${agentRuns.kind} = 'execution' AND ${agentRuns.status} IN ('running', 'blocked', 'paused')`,
|
|
455
|
-
})
|
|
456
|
-
.returning({ rev: agentRuns.rev });
|
|
457
|
-
});
|
|
458
|
-
if (!rows[0])
|
|
459
|
-
return false;
|
|
460
|
-
execution.rev = rows[0].rev;
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
async compareAndSwap(workspaceId, execution) {
|
|
464
|
-
// Conditional update guarded on the rev last read onto this instance; only writes
|
|
465
|
-
// when the stored row is unchanged. No insert — the run must already exist.
|
|
466
|
-
const expected = execution.rev ?? 0;
|
|
467
|
-
const now = this.clock.now();
|
|
468
|
-
const detail = executionToDetail(execution);
|
|
469
|
-
const serviceIdSub = sql `(SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${workspaceId} AND ${blocks.id} = ${execution.blockId})`;
|
|
470
|
-
const rows = await this.db
|
|
471
|
-
.update(agentRuns)
|
|
472
|
-
.set({
|
|
473
|
-
block_id: execution.blockId,
|
|
474
|
-
status: execution.status,
|
|
475
|
-
detail,
|
|
476
|
-
updated_at: now,
|
|
477
|
-
service_id: serviceIdSub,
|
|
478
|
-
rev: sql `${agentRuns.rev} + 1`,
|
|
479
|
-
})
|
|
480
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, execution.id), this.isExecution, eq(agentRuns.rev, expected)))
|
|
481
|
-
.returning({ rev: agentRuns.rev });
|
|
482
|
-
if (!rows[0])
|
|
483
|
-
return false;
|
|
484
|
-
execution.rev = rows[0].rev;
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
async deleteByBlock(workspaceId, blockId) {
|
|
488
|
-
await this.db
|
|
489
|
-
.delete(agentRuns)
|
|
490
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.block_id, blockId), this.isExecution));
|
|
491
|
-
}
|
|
492
|
-
async listStale(olderThanEpochMs) {
|
|
493
|
-
const rows = await this.db
|
|
494
|
-
.select({ workspaceId: agentRuns.workspace_id, id: agentRuns.id })
|
|
495
|
-
.from(agentRuns)
|
|
496
|
-
.where(and(this.isExecution, eq(agentRuns.status, 'running'), lt(agentRuns.updated_at, olderThanEpochMs)))
|
|
497
|
-
.orderBy(agentRuns.updated_at);
|
|
498
|
-
return rows;
|
|
499
|
-
}
|
|
500
|
-
async markFailed(workspaceId, id, failure) {
|
|
501
|
-
// Guard against clobbering a row that already reached a terminal state: a `stopRun`
|
|
502
|
-
// racing a run that just merged (`done`) or already failed must not overwrite it. This
|
|
503
|
-
// is the authoritative first-write-wins / no-re-fail-a-merged-run check — `failRun`'s
|
|
504
|
-
// in-memory guard reads a snapshot that can be stale by the time this write lands
|
|
505
|
-
// (race-audit 2.3). Mirrors the D1 `AND status NOT IN ('done','failed')`.
|
|
506
|
-
//
|
|
507
|
-
// BUMP `rev` on the terminal write so it participates in the driver's optimistic
|
|
508
|
-
// concurrency: a `casPersist` from an in-flight driver iteration that loaded the run
|
|
509
|
-
// BEFORE this `stopRun`/`failRun` still holds the pre-fail `rev`, so bumping it here makes
|
|
510
|
-
// that stale write miss its `rev = ?` guard → `RunContendedError` → re-drive → the reload
|
|
511
|
-
// sees `failed` and no-ops. Without the bump `markFailed` left `rev` untouched, so a stale
|
|
512
|
-
// `casPersist` writing a non-terminal status (`pollGate` pending, dispatch, …) would MATCH
|
|
513
|
-
// the unchanged `rev` and RESURRECT the stopped run as `running` (race-audit 2.3, the
|
|
514
|
-
// driver-clobbers-terminal direction — the dual of the SQL status guard above). Mirrors the
|
|
515
|
-
// D1 `rev = rev + 1`.
|
|
516
|
-
await this.db
|
|
517
|
-
.update(agentRuns)
|
|
518
|
-
.set({
|
|
519
|
-
status: 'failed',
|
|
520
|
-
error: failure.message,
|
|
521
|
-
failure: JSON.stringify(failure),
|
|
522
|
-
updated_at: this.clock.now(),
|
|
523
|
-
rev: sql `${agentRuns.rev} + 1`,
|
|
524
|
-
})
|
|
525
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution, notInArray(agentRuns.status, ['done', 'failed'])));
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
class DrizzleAgentRunRepository {
|
|
529
|
-
db;
|
|
530
|
-
constructor(db) {
|
|
531
|
-
this.db = db;
|
|
532
|
-
}
|
|
533
|
-
async getRef(workspaceId, id) {
|
|
534
|
-
const [row] = await this.db
|
|
535
|
-
.select({ kind: agentRuns.kind })
|
|
536
|
-
.from(agentRuns)
|
|
537
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id)));
|
|
538
|
-
return row
|
|
539
|
-
? {
|
|
540
|
-
workspaceId,
|
|
541
|
-
id,
|
|
542
|
-
kind: decodeEnum(agentRunKindSchema, row.kind, {
|
|
543
|
-
table: 'agent_runs',
|
|
544
|
-
column: 'kind',
|
|
545
|
-
id,
|
|
546
|
-
}),
|
|
547
|
-
}
|
|
548
|
-
: null;
|
|
549
|
-
}
|
|
550
|
-
async listStale(olderThanEpochMs) {
|
|
551
|
-
const rows = await this.db
|
|
552
|
-
.select({
|
|
553
|
-
workspaceId: agentRuns.workspace_id,
|
|
554
|
-
id: agentRuns.id,
|
|
555
|
-
kind: agentRuns.kind,
|
|
556
|
-
updatedAt: agentRuns.updated_at,
|
|
557
|
-
})
|
|
558
|
-
.from(agentRuns)
|
|
559
|
-
.where(and(eq(agentRuns.status, 'running'), lt(agentRuns.updated_at, olderThanEpochMs)))
|
|
560
|
-
.orderBy(agentRuns.updated_at);
|
|
561
|
-
return rows.map((r) => ({
|
|
562
|
-
workspaceId: r.workspaceId,
|
|
563
|
-
id: r.id,
|
|
564
|
-
updatedAt: r.updatedAt,
|
|
565
|
-
kind: decodeEnum(agentRunKindSchema, r.kind, {
|
|
566
|
-
table: 'agent_runs',
|
|
567
|
-
column: 'kind',
|
|
568
|
-
id: r.id,
|
|
569
|
-
}),
|
|
570
|
-
}));
|
|
571
|
-
}
|
|
572
|
-
async listPausedExecutions() {
|
|
573
|
-
const rows = await this.db
|
|
574
|
-
.select({ workspaceId: agentRuns.workspace_id, id: agentRuns.id })
|
|
575
|
-
.from(agentRuns)
|
|
576
|
-
.where(and(eq(agentRuns.kind, 'execution'), eq(agentRuns.status, 'paused')))
|
|
577
|
-
.orderBy(agentRuns.updated_at);
|
|
578
|
-
return rows.map((r) => ({ workspaceId: r.workspaceId, id: r.id, kind: 'execution' }));
|
|
579
|
-
}
|
|
580
|
-
async liveRunIds(ids) {
|
|
581
|
-
if (ids.length === 0)
|
|
582
|
-
return [];
|
|
583
|
-
const live = [];
|
|
584
|
-
// Chunk the IN list (batch, not a point-read per id) so a large set stays one query each.
|
|
585
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
586
|
-
const rows = await this.db
|
|
587
|
-
.select({ id: agentRuns.id })
|
|
588
|
-
.from(agentRuns)
|
|
589
|
-
.where(and(inArray(agentRuns.status, ['running', 'blocked', 'paused', 'pending']), inArray(agentRuns.id, ids.slice(i, i + 500))));
|
|
590
|
-
for (const r of rows)
|
|
591
|
-
live.push(r.id);
|
|
592
|
-
}
|
|
593
|
-
return live;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
function rowToAccount(row) {
|
|
597
|
-
return {
|
|
598
|
-
id: row.id,
|
|
599
|
-
type: row.type === 'org' ? 'org' : 'personal',
|
|
600
|
-
name: row.name,
|
|
601
|
-
githubAccountLogin: row.github_account_login,
|
|
602
|
-
ownerUserId: row.owner_user_id,
|
|
603
|
-
createdAt: row.created_at,
|
|
604
|
-
...(row.default_cloud_provider
|
|
605
|
-
? { defaultCloudProvider: row.default_cloud_provider }
|
|
606
|
-
: {}),
|
|
607
|
-
...(row.spend_monthly_limit != null ? { spendMonthlyLimit: row.spend_monthly_limit } : {}),
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
class DrizzleAccountRepository {
|
|
611
|
-
db;
|
|
612
|
-
constructor(db) {
|
|
613
|
-
this.db = db;
|
|
614
|
-
}
|
|
615
|
-
async get(id) {
|
|
616
|
-
const [row] = await this.db.select().from(accounts).where(eq(accounts.id, id));
|
|
617
|
-
return row ? rowToAccount(row) : null;
|
|
618
|
-
}
|
|
619
|
-
async listByIds(ids) {
|
|
620
|
-
if (ids.length === 0)
|
|
621
|
-
return [];
|
|
622
|
-
const out = [];
|
|
623
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
624
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
625
|
-
const rows = await this.db
|
|
626
|
-
.select()
|
|
627
|
-
.from(accounts)
|
|
628
|
-
.where(inArray(accounts.id, ids.slice(i, i + 500)));
|
|
629
|
-
for (const row of rows)
|
|
630
|
-
out.push(rowToAccount(row));
|
|
631
|
-
}
|
|
632
|
-
return out;
|
|
633
|
-
}
|
|
634
|
-
async create(account) {
|
|
635
|
-
await this.db.insert(accounts).values({
|
|
636
|
-
id: account.id,
|
|
637
|
-
type: account.type,
|
|
638
|
-
name: account.name,
|
|
639
|
-
github_account_login: account.githubAccountLogin,
|
|
640
|
-
owner_user_id: account.ownerUserId,
|
|
641
|
-
created_at: account.createdAt,
|
|
642
|
-
default_cloud_provider: account.defaultCloudProvider ?? null,
|
|
643
|
-
spend_monthly_limit: account.spendMonthlyLimit ?? null,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
async ensurePersonal(account) {
|
|
647
|
-
// Atomic get-or-create: `ON CONFLICT DO NOTHING` no-ops when a personal account already
|
|
648
|
-
// exists for this owner (the partial unique index `idx_accounts_personal` arbitrates), so
|
|
649
|
-
// concurrent first-sign-in callers converge on the one surviving row instead of racing to
|
|
650
|
-
// a duplicate-key error. Re-select to return whichever row won.
|
|
651
|
-
await this.db
|
|
652
|
-
.insert(accounts)
|
|
653
|
-
.values({
|
|
654
|
-
id: account.id,
|
|
655
|
-
type: account.type,
|
|
656
|
-
name: account.name,
|
|
657
|
-
github_account_login: account.githubAccountLogin,
|
|
658
|
-
owner_user_id: account.ownerUserId,
|
|
659
|
-
created_at: account.createdAt,
|
|
660
|
-
default_cloud_provider: account.defaultCloudProvider ?? null,
|
|
661
|
-
spend_monthly_limit: account.spendMonthlyLimit ?? null,
|
|
662
|
-
})
|
|
663
|
-
.onConflictDoNothing();
|
|
664
|
-
const row = await this.findPersonalByUser(account.ownerUserId ?? '');
|
|
665
|
-
if (!row) {
|
|
666
|
-
throw new Error(`ensurePersonal: personal account missing after insert for ${account.ownerUserId}`);
|
|
667
|
-
}
|
|
668
|
-
return row;
|
|
669
|
-
}
|
|
670
|
-
async rename(id, name) {
|
|
671
|
-
await this.db.update(accounts).set({ name }).where(eq(accounts.id, id));
|
|
672
|
-
}
|
|
673
|
-
async updateSettings(id, patch) {
|
|
674
|
-
const set = {};
|
|
675
|
-
if ('defaultCloudProvider' in patch) {
|
|
676
|
-
set.default_cloud_provider = patch.defaultCloudProvider ?? null;
|
|
677
|
-
}
|
|
678
|
-
if ('spendMonthlyLimit' in patch) {
|
|
679
|
-
set.spend_monthly_limit = patch.spendMonthlyLimit ?? null;
|
|
680
|
-
}
|
|
681
|
-
if (Object.keys(set).length === 0)
|
|
682
|
-
return;
|
|
683
|
-
await this.db.update(accounts).set(set).where(eq(accounts.id, id));
|
|
684
|
-
}
|
|
685
|
-
async findPersonalByUser(userId) {
|
|
686
|
-
const [row] = await this.db
|
|
687
|
-
.select()
|
|
688
|
-
.from(accounts)
|
|
689
|
-
.where(and(eq(accounts.type, 'personal'), eq(accounts.owner_user_id, userId)));
|
|
690
|
-
return row ? rowToAccount(row) : null;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
/** Parse the CSV `roles` column into a non-empty role set (defaults to developer). */
|
|
694
|
-
function parseRoles(csv) {
|
|
695
|
-
const roles = (csv ?? '')
|
|
696
|
-
.split(',')
|
|
697
|
-
.map((r) => r.trim())
|
|
698
|
-
.filter((r) => r === 'admin' || r === 'developer' || r === 'product');
|
|
699
|
-
return roles.length > 0 ? [...new Set(roles)] : ['developer'];
|
|
700
|
-
}
|
|
701
|
-
function rowToMembership(row) {
|
|
702
|
-
return {
|
|
703
|
-
accountId: row.account_id,
|
|
704
|
-
userId: row.user_id,
|
|
705
|
-
roles: parseRoles(row.roles),
|
|
706
|
-
createdAt: row.created_at,
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
class DrizzleMembershipRepository {
|
|
710
|
-
db;
|
|
711
|
-
constructor(db) {
|
|
712
|
-
this.db = db;
|
|
713
|
-
}
|
|
714
|
-
async listByUser(userId) {
|
|
715
|
-
const rows = await this.db
|
|
716
|
-
.select()
|
|
717
|
-
.from(memberships)
|
|
718
|
-
.where(eq(memberships.user_id, userId))
|
|
719
|
-
.orderBy(memberships.created_at);
|
|
720
|
-
return rows.map(rowToMembership);
|
|
721
|
-
}
|
|
722
|
-
async listByAccount(accountId) {
|
|
723
|
-
const rows = await this.db
|
|
724
|
-
.select()
|
|
725
|
-
.from(memberships)
|
|
726
|
-
.where(eq(memberships.account_id, accountId))
|
|
727
|
-
.orderBy(memberships.created_at);
|
|
728
|
-
return rows.map(rowToMembership);
|
|
729
|
-
}
|
|
730
|
-
async get(accountId, userId) {
|
|
731
|
-
const [row] = await this.db
|
|
732
|
-
.select()
|
|
733
|
-
.from(memberships)
|
|
734
|
-
.where(and(eq(memberships.account_id, accountId), eq(memberships.user_id, userId)));
|
|
735
|
-
return row ? rowToMembership(row) : null;
|
|
736
|
-
}
|
|
737
|
-
async upsert(membership) {
|
|
738
|
-
await this.db
|
|
739
|
-
.insert(memberships)
|
|
740
|
-
.values({
|
|
741
|
-
account_id: membership.accountId,
|
|
742
|
-
user_id: membership.userId,
|
|
743
|
-
roles: membership.roles.join(','),
|
|
744
|
-
created_at: membership.createdAt,
|
|
745
|
-
})
|
|
746
|
-
.onConflictDoUpdate({
|
|
747
|
-
target: [memberships.account_id, memberships.user_id],
|
|
748
|
-
set: { roles: membership.roles.join(',') },
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
async remove(accountId, userId) {
|
|
752
|
-
await this.db
|
|
753
|
-
.delete(memberships)
|
|
754
|
-
.where(and(eq(memberships.account_id, accountId), eq(memberships.user_id, userId)));
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
function rowToUser(row) {
|
|
758
|
-
return {
|
|
759
|
-
id: row.id,
|
|
760
|
-
name: row.name,
|
|
761
|
-
email: row.email,
|
|
762
|
-
avatarUrl: row.avatar_url,
|
|
763
|
-
createdAt: row.created_at,
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
function rowToIdentity(row) {
|
|
767
|
-
return {
|
|
768
|
-
userId: row.user_id,
|
|
769
|
-
provider: row.provider,
|
|
770
|
-
subject: row.subject,
|
|
771
|
-
secret: row.secret,
|
|
772
|
-
metadata: row.metadata,
|
|
773
|
-
createdAt: row.created_at,
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
class DrizzleUserRepository {
|
|
777
|
-
db;
|
|
778
|
-
constructor(db) {
|
|
779
|
-
this.db = db;
|
|
780
|
-
}
|
|
781
|
-
async get(id) {
|
|
782
|
-
const [row] = await this.db.select().from(users).where(eq(users.id, id));
|
|
783
|
-
return row ? rowToUser(row) : null;
|
|
784
|
-
}
|
|
785
|
-
async create(user) {
|
|
786
|
-
await this.db.insert(users).values({
|
|
787
|
-
id: user.id,
|
|
788
|
-
name: user.name,
|
|
789
|
-
email: user.email,
|
|
790
|
-
avatar_url: user.avatarUrl,
|
|
791
|
-
created_at: user.createdAt,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
async update(id, patch) {
|
|
795
|
-
const set = {};
|
|
796
|
-
if ('name' in patch)
|
|
797
|
-
set.name = patch.name;
|
|
798
|
-
if ('email' in patch)
|
|
799
|
-
set.email = patch.email;
|
|
800
|
-
if ('avatarUrl' in patch)
|
|
801
|
-
set.avatar_url = patch.avatarUrl;
|
|
802
|
-
if (Object.keys(set).length === 0)
|
|
803
|
-
return;
|
|
804
|
-
await this.db.update(users).set(set).where(eq(users.id, id));
|
|
805
|
-
}
|
|
806
|
-
async findByIdentity(provider, subject) {
|
|
807
|
-
const [row] = await this.db
|
|
808
|
-
.select()
|
|
809
|
-
.from(users)
|
|
810
|
-
.innerJoin(userIdentities, eq(userIdentities.user_id, users.id))
|
|
811
|
-
.where(and(eq(userIdentities.provider, provider), eq(userIdentities.subject, subject)));
|
|
812
|
-
return row ? rowToUser(row.users) : null;
|
|
813
|
-
}
|
|
814
|
-
async findByEmail(email) {
|
|
815
|
-
const [row] = await this.db
|
|
816
|
-
.select()
|
|
817
|
-
.from(users)
|
|
818
|
-
.where(eq(users.email, email.toLowerCase().trim()));
|
|
819
|
-
return row ? rowToUser(row) : null;
|
|
820
|
-
}
|
|
821
|
-
async listByIds(ids) {
|
|
822
|
-
if (ids.length === 0)
|
|
823
|
-
return [];
|
|
824
|
-
const rows = await this.db.select().from(users).where(inArray(users.id, ids));
|
|
825
|
-
return rows.map(rowToUser);
|
|
826
|
-
}
|
|
827
|
-
async getIdentity(provider, subject) {
|
|
828
|
-
const [row] = await this.db
|
|
829
|
-
.select()
|
|
830
|
-
.from(userIdentities)
|
|
831
|
-
.where(and(eq(userIdentities.provider, provider), eq(userIdentities.subject, subject)));
|
|
832
|
-
return row ? rowToIdentity(row) : null;
|
|
833
|
-
}
|
|
834
|
-
async linkIdentity(identity) {
|
|
835
|
-
await this.db
|
|
836
|
-
.insert(userIdentities)
|
|
837
|
-
.values({
|
|
838
|
-
user_id: identity.userId,
|
|
839
|
-
provider: identity.provider,
|
|
840
|
-
subject: identity.subject,
|
|
841
|
-
secret: identity.secret,
|
|
842
|
-
metadata: identity.metadata,
|
|
843
|
-
created_at: identity.createdAt,
|
|
844
|
-
})
|
|
845
|
-
.onConflictDoUpdate({
|
|
846
|
-
target: [userIdentities.provider, userIdentities.subject],
|
|
847
|
-
set: { user_id: identity.userId, secret: identity.secret, metadata: identity.metadata },
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async listIdentities(userId) {
|
|
851
|
-
const rows = await this.db
|
|
852
|
-
.select()
|
|
853
|
-
.from(userIdentities)
|
|
854
|
-
.where(eq(userIdentities.user_id, userId));
|
|
855
|
-
return rows.map(rowToIdentity);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function rowToInvitation(row) {
|
|
859
|
-
return {
|
|
860
|
-
id: row.id,
|
|
861
|
-
accountId: row.account_id,
|
|
862
|
-
email: row.email,
|
|
863
|
-
roles: parseRoles(row.roles),
|
|
864
|
-
tokenHash: row.token_hash,
|
|
865
|
-
invitedBy: row.invited_by,
|
|
866
|
-
status: row.status,
|
|
867
|
-
expiresAt: row.expires_at,
|
|
868
|
-
createdAt: row.created_at,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
class DrizzleAccountInvitationRepository {
|
|
872
|
-
db;
|
|
873
|
-
constructor(db) {
|
|
874
|
-
this.db = db;
|
|
875
|
-
}
|
|
876
|
-
async create(record) {
|
|
877
|
-
await this.db.insert(accountInvitations).values({
|
|
878
|
-
id: record.id,
|
|
879
|
-
account_id: record.accountId,
|
|
880
|
-
email: record.email,
|
|
881
|
-
roles: record.roles.join(','),
|
|
882
|
-
token_hash: record.tokenHash,
|
|
883
|
-
invited_by: record.invitedBy,
|
|
884
|
-
status: record.status,
|
|
885
|
-
expires_at: record.expiresAt,
|
|
886
|
-
created_at: record.createdAt,
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
async get(id) {
|
|
890
|
-
const [row] = await this.db
|
|
891
|
-
.select()
|
|
892
|
-
.from(accountInvitations)
|
|
893
|
-
.where(eq(accountInvitations.id, id));
|
|
894
|
-
return row ? rowToInvitation(row) : null;
|
|
895
|
-
}
|
|
896
|
-
async findByTokenHash(tokenHash) {
|
|
897
|
-
const [row] = await this.db
|
|
898
|
-
.select()
|
|
899
|
-
.from(accountInvitations)
|
|
900
|
-
.where(eq(accountInvitations.token_hash, tokenHash));
|
|
901
|
-
return row ? rowToInvitation(row) : null;
|
|
902
|
-
}
|
|
903
|
-
async listByAccount(accountId) {
|
|
904
|
-
const rows = await this.db
|
|
905
|
-
.select()
|
|
906
|
-
.from(accountInvitations)
|
|
907
|
-
.where(eq(accountInvitations.account_id, accountId))
|
|
908
|
-
.orderBy(desc(accountInvitations.created_at));
|
|
909
|
-
return rows.map(rowToInvitation);
|
|
910
|
-
}
|
|
911
|
-
async setStatus(id, status) {
|
|
912
|
-
await this.db.update(accountInvitations).set({ status }).where(eq(accountInvitations.id, id));
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
function rowToPasswordResetToken(row) {
|
|
916
|
-
return {
|
|
917
|
-
id: row.id,
|
|
918
|
-
userId: row.user_id,
|
|
919
|
-
tokenHash: row.token_hash,
|
|
920
|
-
status: row.status,
|
|
921
|
-
expiresAt: row.expires_at,
|
|
922
|
-
createdAt: row.created_at,
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
class DrizzlePasswordResetTokenRepository {
|
|
926
|
-
db;
|
|
927
|
-
constructor(db) {
|
|
928
|
-
this.db = db;
|
|
929
|
-
}
|
|
930
|
-
async create(record) {
|
|
931
|
-
await this.db.insert(passwordResetTokens).values({
|
|
932
|
-
id: record.id,
|
|
933
|
-
user_id: record.userId,
|
|
934
|
-
token_hash: record.tokenHash,
|
|
935
|
-
status: record.status,
|
|
936
|
-
expires_at: record.expiresAt,
|
|
937
|
-
created_at: record.createdAt,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
async findByTokenHash(tokenHash) {
|
|
941
|
-
const [row] = await this.db
|
|
942
|
-
.select()
|
|
943
|
-
.from(passwordResetTokens)
|
|
944
|
-
.where(eq(passwordResetTokens.token_hash, tokenHash));
|
|
945
|
-
return row ? rowToPasswordResetToken(row) : null;
|
|
946
|
-
}
|
|
947
|
-
async listPendingByUser(userId) {
|
|
948
|
-
const rows = await this.db
|
|
949
|
-
.select()
|
|
950
|
-
.from(passwordResetTokens)
|
|
951
|
-
.where(and(eq(passwordResetTokens.user_id, userId), eq(passwordResetTokens.status, 'pending')))
|
|
952
|
-
.orderBy(desc(passwordResetTokens.created_at));
|
|
953
|
-
return rows.map(rowToPasswordResetToken);
|
|
954
|
-
}
|
|
955
|
-
async setStatus(id, status) {
|
|
956
|
-
await this.db.update(passwordResetTokens).set({ status }).where(eq(passwordResetTokens.id, id));
|
|
957
|
-
}
|
|
958
|
-
async consume(id) {
|
|
959
|
-
// Conditional on `status='pending'` so concurrent redemptions can't both win.
|
|
960
|
-
const result = await this.db
|
|
961
|
-
.update(passwordResetTokens)
|
|
962
|
-
.set({ status: 'used' })
|
|
963
|
-
.where(and(eq(passwordResetTokens.id, id), eq(passwordResetTokens.status, 'pending')));
|
|
964
|
-
return (result.rowCount ?? 0) > 0;
|
|
965
|
-
}
|
|
966
|
-
async deleteExpired(before) {
|
|
967
|
-
const result = await this.db
|
|
968
|
-
.delete(passwordResetTokens)
|
|
969
|
-
.where(lt(passwordResetTokens.expires_at, before));
|
|
970
|
-
return result.rowCount ?? 0;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
function rowToEmailConnection(row) {
|
|
974
|
-
return {
|
|
975
|
-
accountId: row.account_id,
|
|
976
|
-
provider: row.provider,
|
|
977
|
-
fromAddress: row.from_address,
|
|
978
|
-
apiKeyCipher: row.api_key_cipher,
|
|
979
|
-
createdAt: row.created_at,
|
|
980
|
-
updatedAt: row.updated_at,
|
|
981
|
-
deletedAt: row.deleted_at,
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
class DrizzleEmailConnectionRepository {
|
|
985
|
-
db;
|
|
986
|
-
constructor(db) {
|
|
987
|
-
this.db = db;
|
|
988
|
-
}
|
|
989
|
-
async getByAccount(accountId) {
|
|
990
|
-
const [row] = await this.db
|
|
991
|
-
.select()
|
|
992
|
-
.from(emailConnections)
|
|
993
|
-
.where(and(eq(emailConnections.account_id, accountId), isNull(emailConnections.deleted_at)));
|
|
994
|
-
return row ? rowToEmailConnection(row) : null;
|
|
995
|
-
}
|
|
996
|
-
async upsert(record) {
|
|
997
|
-
await this.db
|
|
998
|
-
.insert(emailConnections)
|
|
999
|
-
.values({
|
|
1000
|
-
account_id: record.accountId,
|
|
1001
|
-
provider: record.provider,
|
|
1002
|
-
from_address: record.fromAddress,
|
|
1003
|
-
api_key_cipher: record.apiKeyCipher,
|
|
1004
|
-
created_at: record.createdAt,
|
|
1005
|
-
updated_at: record.updatedAt,
|
|
1006
|
-
deleted_at: record.deletedAt,
|
|
1007
|
-
})
|
|
1008
|
-
.onConflictDoUpdate({
|
|
1009
|
-
target: emailConnections.account_id,
|
|
1010
|
-
set: {
|
|
1011
|
-
provider: record.provider,
|
|
1012
|
-
from_address: record.fromAddress,
|
|
1013
|
-
api_key_cipher: record.apiKeyCipher,
|
|
1014
|
-
updated_at: record.updatedAt,
|
|
1015
|
-
deleted_at: record.deletedAt,
|
|
1016
|
-
},
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
async softDelete(accountId, at) {
|
|
1020
|
-
await this.db
|
|
1021
|
-
.update(emailConnections)
|
|
1022
|
-
.set({ deleted_at: at, updated_at: at })
|
|
1023
|
-
.where(eq(emailConnections.account_id, accountId));
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
class DrizzleTokenUsageRepository {
|
|
1027
|
-
db;
|
|
1028
|
-
constructor(db) {
|
|
1029
|
-
this.db = db;
|
|
1030
|
-
}
|
|
1031
|
-
async record(usage) {
|
|
1032
|
-
await this.db.insert(tokenUsage).values({
|
|
1033
|
-
id: usage.id,
|
|
1034
|
-
workspace_id: usage.workspaceId,
|
|
1035
|
-
account_id: usage.accountId,
|
|
1036
|
-
user_id: usage.userId,
|
|
1037
|
-
execution_id: usage.executionId,
|
|
1038
|
-
agent_kind: usage.agentKind,
|
|
1039
|
-
provider: usage.provider,
|
|
1040
|
-
model: usage.model,
|
|
1041
|
-
input_tokens: usage.inputTokens,
|
|
1042
|
-
output_tokens: usage.outputTokens,
|
|
1043
|
-
cost_estimate: usage.costEstimate,
|
|
1044
|
-
billing: usage.billing,
|
|
1045
|
-
vendor: usage.vendor,
|
|
1046
|
-
created_at: usage.createdAt,
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
async usageBreakdownForWorkspace(workspaceId, epochMs) {
|
|
1050
|
-
// One GROUP BY over the workspace's current period — both billing kinds (the report
|
|
1051
|
-
// shows total usage). Never a per-model loop. sum() of int columns is bigint; cast +
|
|
1052
|
-
// coerce like the totals rollups. Ordered heaviest-first in SQL, mirroring the D1 repo.
|
|
1053
|
-
const rows = await this.db
|
|
1054
|
-
.select({
|
|
1055
|
-
billing: tokenUsage.billing,
|
|
1056
|
-
vendor: tokenUsage.vendor,
|
|
1057
|
-
provider: tokenUsage.provider,
|
|
1058
|
-
model: tokenUsage.model,
|
|
1059
|
-
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1060
|
-
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1061
|
-
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1062
|
-
calls: sql `count(*)::bigint`,
|
|
1063
|
-
})
|
|
1064
|
-
.from(tokenUsage)
|
|
1065
|
-
.where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs)))
|
|
1066
|
-
.groupBy(tokenUsage.billing, tokenUsage.vendor, tokenUsage.provider, tokenUsage.model)
|
|
1067
|
-
.orderBy(sql `(coalesce(sum(${tokenUsage.input_tokens}), 0) + coalesce(sum(${tokenUsage.output_tokens}), 0)) desc`);
|
|
1068
|
-
return rows.map((r) => ({
|
|
1069
|
-
billing: (r.billing === 'subscription' ? 'subscription' : 'metered'),
|
|
1070
|
-
vendor: r.vendor,
|
|
1071
|
-
provider: r.provider,
|
|
1072
|
-
model: r.model,
|
|
1073
|
-
inputTokens: Number(r.input ?? 0),
|
|
1074
|
-
outputTokens: Number(r.output ?? 0),
|
|
1075
|
-
costEstimate: r.cost ?? 0,
|
|
1076
|
-
calls: Number(r.calls ?? 0),
|
|
1077
|
-
}));
|
|
1078
|
-
}
|
|
1079
|
-
async totalsSince(epochMs) {
|
|
1080
|
-
// sum() of int columns is bigint in Postgres — cast to bigint (NOT int4, which
|
|
1081
|
-
// overflows past ~2.1B tokens) and coerce: node-postgres returns bigint as a
|
|
1082
|
-
// string to avoid precision loss, and token totals stay well within Number's
|
|
1083
|
-
// safe-integer range. Matches the 64-bit sum the D1/SQLite store returns.
|
|
1084
|
-
const [row] = await this.db
|
|
1085
|
-
.select({
|
|
1086
|
-
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1087
|
-
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1088
|
-
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1089
|
-
})
|
|
1090
|
-
.from(tokenUsage)
|
|
1091
|
-
.where(and(gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1092
|
-
return {
|
|
1093
|
-
inputTokens: Number(row?.input ?? 0),
|
|
1094
|
-
outputTokens: Number(row?.output ?? 0),
|
|
1095
|
-
costEstimate: row?.cost ?? 0,
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
async totalsSinceForWorkspace(workspaceId, epochMs) {
|
|
1099
|
-
const [row] = await this.db
|
|
1100
|
-
.select({
|
|
1101
|
-
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1102
|
-
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1103
|
-
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1104
|
-
})
|
|
1105
|
-
.from(tokenUsage)
|
|
1106
|
-
.where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1107
|
-
return {
|
|
1108
|
-
inputTokens: Number(row?.input ?? 0),
|
|
1109
|
-
outputTokens: Number(row?.output ?? 0),
|
|
1110
|
-
costEstimate: row?.cost ?? 0,
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
async totalsSinceForAccount(accountId, epochMs) {
|
|
1114
|
-
const [row] = await this.db
|
|
1115
|
-
.select({
|
|
1116
|
-
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1117
|
-
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1118
|
-
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1119
|
-
})
|
|
1120
|
-
.from(tokenUsage)
|
|
1121
|
-
.where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1122
|
-
return {
|
|
1123
|
-
inputTokens: Number(row?.input ?? 0),
|
|
1124
|
-
outputTokens: Number(row?.output ?? 0),
|
|
1125
|
-
costEstimate: row?.cost ?? 0,
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
async totalsSinceForUser(userId, epochMs) {
|
|
1129
|
-
const [row] = await this.db
|
|
1130
|
-
.select({
|
|
1131
|
-
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1132
|
-
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1133
|
-
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1134
|
-
})
|
|
1135
|
-
.from(tokenUsage)
|
|
1136
|
-
.where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1137
|
-
return {
|
|
1138
|
-
inputTokens: Number(row?.input ?? 0),
|
|
1139
|
-
outputTokens: Number(row?.output ?? 0),
|
|
1140
|
-
costEstimate: row?.cost ?? 0,
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
async deleteOlderThan(epochMs) {
|
|
1144
|
-
const deleted = await this.db
|
|
1145
|
-
.delete(tokenUsage)
|
|
1146
|
-
.where(lt(tokenUsage.created_at, epochMs))
|
|
1147
|
-
.returning({ id: tokenUsage.id });
|
|
1148
|
-
return deleted.length;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
class DrizzleUserSettingsRepository {
|
|
1152
|
-
db;
|
|
1153
|
-
constructor(db) {
|
|
1154
|
-
this.db = db;
|
|
1155
|
-
}
|
|
1156
|
-
async get(userId) {
|
|
1157
|
-
const [row] = await this.db.select().from(userSettings).where(eq(userSettings.user_id, userId));
|
|
1158
|
-
return row ? { spendMonthlyLimit: row.spend_monthly_limit } : null;
|
|
1159
|
-
}
|
|
1160
|
-
async upsert(userId, settings) {
|
|
1161
|
-
await this.db
|
|
1162
|
-
.insert(userSettings)
|
|
1163
|
-
.values({
|
|
1164
|
-
user_id: userId,
|
|
1165
|
-
spend_monthly_limit: settings.spendMonthlyLimit ?? null,
|
|
1166
|
-
updated_at: Date.now(),
|
|
1167
|
-
})
|
|
1168
|
-
.onConflictDoUpdate({
|
|
1169
|
-
target: userSettings.user_id,
|
|
1170
|
-
set: { spend_monthly_limit: settings.spendMonthlyLimit ?? null, updated_at: Date.now() },
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
function rowToLlmMetric(row) {
|
|
1175
|
-
return {
|
|
1176
|
-
id: row.id,
|
|
1177
|
-
workspaceId: row.workspace_id,
|
|
1178
|
-
executionId: row.execution_id,
|
|
1179
|
-
agentKind: row.agent_kind,
|
|
1180
|
-
provider: row.provider,
|
|
1181
|
-
model: row.model,
|
|
1182
|
-
createdAt: row.created_at,
|
|
1183
|
-
streaming: row.streaming === 1,
|
|
1184
|
-
messageCount: row.message_count,
|
|
1185
|
-
toolCount: row.tool_count,
|
|
1186
|
-
requestMaxTokens: row.request_max_tokens,
|
|
1187
|
-
promptTokens: row.prompt_tokens,
|
|
1188
|
-
cachedPromptTokens: row.cached_prompt_tokens,
|
|
1189
|
-
completionTokens: row.completion_tokens,
|
|
1190
|
-
totalTokens: row.total_tokens,
|
|
1191
|
-
finishReason: row.finish_reason,
|
|
1192
|
-
upstreamMs: row.upstream_ms,
|
|
1193
|
-
overheadMs: row.overhead_ms,
|
|
1194
|
-
totalMs: row.total_ms,
|
|
1195
|
-
ok: row.ok === 1,
|
|
1196
|
-
httpStatus: row.http_status,
|
|
1197
|
-
errorMessage: row.error_message,
|
|
1198
|
-
promptText: row.prompt_text,
|
|
1199
|
-
promptPrefixCount: row.prompt_prefix_count,
|
|
1200
|
-
promptHash: row.prompt_hash,
|
|
1201
|
-
responseText: row.response_text,
|
|
1202
|
-
reasoningText: row.reasoning_text,
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
class DrizzleLlmCallMetricRepository {
|
|
1206
|
-
db;
|
|
1207
|
-
constructor(db) {
|
|
1208
|
-
this.db = db;
|
|
1209
|
-
}
|
|
1210
|
-
async record(metric) {
|
|
1211
|
-
await this.db.insert(llmCallMetrics).values({
|
|
1212
|
-
id: metric.id,
|
|
1213
|
-
workspace_id: metric.workspaceId,
|
|
1214
|
-
execution_id: metric.executionId,
|
|
1215
|
-
agent_kind: metric.agentKind,
|
|
1216
|
-
provider: metric.provider,
|
|
1217
|
-
model: metric.model,
|
|
1218
|
-
created_at: metric.createdAt,
|
|
1219
|
-
streaming: metric.streaming ? 1 : 0,
|
|
1220
|
-
message_count: metric.messageCount,
|
|
1221
|
-
tool_count: metric.toolCount,
|
|
1222
|
-
request_max_tokens: metric.requestMaxTokens,
|
|
1223
|
-
prompt_tokens: metric.promptTokens,
|
|
1224
|
-
cached_prompt_tokens: metric.cachedPromptTokens,
|
|
1225
|
-
completion_tokens: metric.completionTokens,
|
|
1226
|
-
total_tokens: metric.totalTokens,
|
|
1227
|
-
finish_reason: metric.finishReason,
|
|
1228
|
-
upstream_ms: metric.upstreamMs,
|
|
1229
|
-
overhead_ms: metric.overheadMs,
|
|
1230
|
-
total_ms: metric.totalMs,
|
|
1231
|
-
ok: metric.ok ? 1 : 0,
|
|
1232
|
-
http_status: metric.httpStatus,
|
|
1233
|
-
error_message: metric.errorMessage,
|
|
1234
|
-
prompt_text: metric.promptText,
|
|
1235
|
-
prompt_prefix_count: metric.promptPrefixCount,
|
|
1236
|
-
prompt_hash: metric.promptHash,
|
|
1237
|
-
response_text: metric.responseText,
|
|
1238
|
-
reasoning_text: metric.reasoningText,
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
async latestChainTip(workspaceId, executionId, agentKind) {
|
|
1242
|
-
// The newest call for the conversation; one indexed row, no text columns.
|
|
1243
|
-
const rows = await this.db
|
|
1244
|
-
.select({
|
|
1245
|
-
messageCount: llmCallMetrics.message_count,
|
|
1246
|
-
promptHash: llmCallMetrics.prompt_hash,
|
|
1247
|
-
})
|
|
1248
|
-
.from(llmCallMetrics)
|
|
1249
|
-
.where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId), eq(llmCallMetrics.agent_kind, agentKind)))
|
|
1250
|
-
// message_count breaks a same-millisecond createdAt tie in chain order (it grows
|
|
1251
|
-
// monotonically as the conversation appends); id is the last resort.
|
|
1252
|
-
.orderBy(desc(llmCallMetrics.created_at), desc(llmCallMetrics.message_count), desc(llmCallMetrics.id))
|
|
1253
|
-
.limit(1);
|
|
1254
|
-
const row = rows[0];
|
|
1255
|
-
return row ? { messageCount: row.messageCount, promptHash: row.promptHash } : null;
|
|
1256
|
-
}
|
|
1257
|
-
async listByExecution(workspaceId, executionId, limit, agentKind) {
|
|
1258
|
-
const base = this.db
|
|
1259
|
-
.select()
|
|
1260
|
-
.from(llmCallMetrics)
|
|
1261
|
-
.where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId), ...(agentKind == null ? [] : [eq(llmCallMetrics.agent_kind, agentKind)])))
|
|
1262
|
-
.orderBy(desc(llmCallMetrics.created_at), desc(llmCallMetrics.id));
|
|
1263
|
-
const rows = await (limit == null ? base : base.limit(limit));
|
|
1264
|
-
return rows.map(rowToLlmMetric);
|
|
1265
|
-
}
|
|
1266
|
-
async summarizeByExecution(workspaceId, executionId) {
|
|
1267
|
-
// Aggregate-only: selects no prompt/response text, so it stays cheap on every
|
|
1268
|
-
// execution emit (it backs the live board rollups). int sums fit Number's safe
|
|
1269
|
-
// range here (per-run call counts/tokens are small), so a plain ::bigint cast
|
|
1270
|
-
// matching the SQLite 64-bit sum is unnecessary — totals are coerced below.
|
|
1271
|
-
const reasons = [...LLM_WARNING_FINISH_REASONS];
|
|
1272
|
-
const rows = await this.db
|
|
1273
|
-
.select({
|
|
1274
|
-
agentKind: llmCallMetrics.agent_kind,
|
|
1275
|
-
calls: sql `count(*)::int`,
|
|
1276
|
-
promptTokens: sql `coalesce(sum(${llmCallMetrics.prompt_tokens}), 0)::int`,
|
|
1277
|
-
cachedPromptTokens: sql `coalesce(sum(${llmCallMetrics.cached_prompt_tokens}), 0)::int`,
|
|
1278
|
-
completionTokens: sql `coalesce(sum(${llmCallMetrics.completion_tokens}), 0)::int`,
|
|
1279
|
-
peakCompletionTokens: sql `coalesce(max(${llmCallMetrics.completion_tokens}), 0)::int`,
|
|
1280
|
-
maxOutputTokens: sql `max(${llmCallMetrics.request_max_tokens})`,
|
|
1281
|
-
truncatedCalls: sql `coalesce(sum(case when ${llmCallMetrics.finish_reason} = 'length' then 1 else 0 end), 0)::int`,
|
|
1282
|
-
upstreamMs: sql `coalesce(sum(${llmCallMetrics.upstream_ms}), 0)::int`,
|
|
1283
|
-
overheadMs: sql `coalesce(sum(${llmCallMetrics.overhead_ms}), 0)::int`,
|
|
1284
|
-
errors: sql `coalesce(sum(case when ${llmCallMetrics.ok} = 0 then 1 else 0 end), 0)::int`,
|
|
1285
|
-
// `inArray` builds the IN-list membership: idiomatic, type-checked, and tied to
|
|
1286
|
-
// the shared constant. (A raw `${...finish_reason} in ${reasons}` renders the same
|
|
1287
|
-
// `in ($1, $2)` on this drizzle version; inArray just documents intent and can't
|
|
1288
|
-
// silently mis-bind the array.)
|
|
1289
|
-
warnings: sql `coalesce(sum(case when ${llmCallMetrics.ok} = 1 and ${inArray(llmCallMetrics.finish_reason, reasons)} then 1 else 0 end), 0)::int`,
|
|
1290
|
-
})
|
|
1291
|
-
.from(llmCallMetrics)
|
|
1292
|
-
.where(and(eq(llmCallMetrics.workspace_id, workspaceId), eq(llmCallMetrics.execution_id, executionId)))
|
|
1293
|
-
.groupBy(llmCallMetrics.agent_kind);
|
|
1294
|
-
return rows.map((r) => ({
|
|
1295
|
-
agentKind: r.agentKind,
|
|
1296
|
-
calls: Number(r.calls),
|
|
1297
|
-
promptTokens: Number(r.promptTokens),
|
|
1298
|
-
cachedPromptTokens: Number(r.cachedPromptTokens),
|
|
1299
|
-
completionTokens: Number(r.completionTokens),
|
|
1300
|
-
peakCompletionTokens: Number(r.peakCompletionTokens),
|
|
1301
|
-
maxOutputTokens: r.maxOutputTokens == null ? null : Number(r.maxOutputTokens),
|
|
1302
|
-
truncatedCalls: Number(r.truncatedCalls),
|
|
1303
|
-
upstreamMs: Number(r.upstreamMs),
|
|
1304
|
-
overheadMs: Number(r.overheadMs),
|
|
1305
|
-
errors: Number(r.errors),
|
|
1306
|
-
warnings: Number(r.warnings),
|
|
1307
|
-
}));
|
|
1308
|
-
}
|
|
1309
|
-
async deleteOlderThan(epochMs) {
|
|
1310
|
-
const deleted = await this.db
|
|
1311
|
-
.delete(llmCallMetrics)
|
|
1312
|
-
.where(lt(llmCallMetrics.created_at, epochMs))
|
|
1313
|
-
.returning({ id: llmCallMetrics.id });
|
|
1314
|
-
return deleted.length;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
function rowToAgentContextSnapshot(row) {
|
|
1318
|
-
return {
|
|
1319
|
-
id: row.id,
|
|
1320
|
-
workspaceId: row.workspace_id,
|
|
1321
|
-
executionId: row.execution_id,
|
|
1322
|
-
agentKind: row.agent_kind,
|
|
1323
|
-
stepIndex: row.step_index,
|
|
1324
|
-
createdAt: row.created_at,
|
|
1325
|
-
model: row.model,
|
|
1326
|
-
harness: row.harness,
|
|
1327
|
-
systemPrompt: row.system_prompt,
|
|
1328
|
-
userPrompt: row.user_prompt,
|
|
1329
|
-
fragments: parseJsonArray(row.fragments),
|
|
1330
|
-
contextFiles: parseJsonArray(row.context_files),
|
|
1331
|
-
extras: parseAgentContextExtras(row.extras),
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
/** Parse the extras JSON object column, degrading a malformed value to {}. */
|
|
1335
|
-
function parseAgentContextExtras(text) {
|
|
1336
|
-
try {
|
|
1337
|
-
const parsed = JSON.parse(text);
|
|
1338
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
1339
|
-
? parsed
|
|
1340
|
-
: {};
|
|
1341
|
-
}
|
|
1342
|
-
catch {
|
|
1343
|
-
return {};
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
class DrizzleAgentContextSnapshotRepository {
|
|
1347
|
-
db;
|
|
1348
|
-
constructor(db) {
|
|
1349
|
-
this.db = db;
|
|
1350
|
-
}
|
|
1351
|
-
async record(snapshot) {
|
|
1352
|
-
await this.db.insert(agentContextSnapshots).values({
|
|
1353
|
-
id: snapshot.id,
|
|
1354
|
-
workspace_id: snapshot.workspaceId,
|
|
1355
|
-
execution_id: snapshot.executionId,
|
|
1356
|
-
agent_kind: snapshot.agentKind,
|
|
1357
|
-
step_index: snapshot.stepIndex,
|
|
1358
|
-
created_at: snapshot.createdAt,
|
|
1359
|
-
model: snapshot.model,
|
|
1360
|
-
harness: snapshot.harness,
|
|
1361
|
-
system_prompt: snapshot.systemPrompt,
|
|
1362
|
-
user_prompt: snapshot.userPrompt,
|
|
1363
|
-
fragments: JSON.stringify(snapshot.fragments),
|
|
1364
|
-
context_files: JSON.stringify(snapshot.contextFiles),
|
|
1365
|
-
extras: JSON.stringify(snapshot.extras),
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
async listByExecution(workspaceId, executionId) {
|
|
1369
|
-
const rows = await this.db
|
|
1370
|
-
.select()
|
|
1371
|
-
.from(agentContextSnapshots)
|
|
1372
|
-
.where(and(eq(agentContextSnapshots.workspace_id, workspaceId), eq(agentContextSnapshots.execution_id, executionId)))
|
|
1373
|
-
.orderBy(desc(agentContextSnapshots.created_at), desc(agentContextSnapshots.id));
|
|
1374
|
-
return rows.map(rowToAgentContextSnapshot);
|
|
1375
|
-
}
|
|
1376
|
-
async deleteOlderThan(epochMs) {
|
|
1377
|
-
const deleted = await this.db
|
|
1378
|
-
.delete(agentContextSnapshots)
|
|
1379
|
-
.where(lt(agentContextSnapshots.created_at, epochMs))
|
|
1380
|
-
.returning({ id: agentContextSnapshots.id });
|
|
1381
|
-
return deleted.length;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
function rowToAgentSearchQuery(row) {
|
|
1385
|
-
return {
|
|
1386
|
-
id: row.id,
|
|
1387
|
-
workspaceId: row.workspace_id,
|
|
1388
|
-
executionId: row.execution_id,
|
|
1389
|
-
agentKind: row.agent_kind,
|
|
1390
|
-
// The stored provider column is free-text; narrow it back to the wire union.
|
|
1391
|
-
provider: isWebSearchProvider(row.provider) ? row.provider : null,
|
|
1392
|
-
query: row.query,
|
|
1393
|
-
resultCount: row.result_count,
|
|
1394
|
-
createdAt: row.created_at,
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
class DrizzleAgentSearchQueryRepository {
|
|
1398
|
-
db;
|
|
1399
|
-
constructor(db) {
|
|
1400
|
-
this.db = db;
|
|
1401
|
-
}
|
|
1402
|
-
async record(query) {
|
|
1403
|
-
await this.db.insert(agentSearchQueries).values({
|
|
1404
|
-
id: query.id,
|
|
1405
|
-
workspace_id: query.workspaceId,
|
|
1406
|
-
execution_id: query.executionId,
|
|
1407
|
-
agent_kind: query.agentKind,
|
|
1408
|
-
provider: query.provider,
|
|
1409
|
-
query: query.query,
|
|
1410
|
-
result_count: query.resultCount,
|
|
1411
|
-
created_at: query.createdAt,
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
async listByExecution(workspaceId, executionId) {
|
|
1415
|
-
const rows = await this.db
|
|
1416
|
-
.select()
|
|
1417
|
-
.from(agentSearchQueries)
|
|
1418
|
-
.where(and(eq(agentSearchQueries.workspace_id, workspaceId), eq(agentSearchQueries.execution_id, executionId)))
|
|
1419
|
-
.orderBy(desc(agentSearchQueries.created_at), desc(agentSearchQueries.id));
|
|
1420
|
-
return rows.map(rowToAgentSearchQuery);
|
|
1421
|
-
}
|
|
1422
|
-
async deleteOlderThan(epochMs) {
|
|
1423
|
-
const deleted = await this.db
|
|
1424
|
-
.delete(agentSearchQueries)
|
|
1425
|
-
.where(lt(agentSearchQueries.created_at, epochMs))
|
|
1426
|
-
.returning({ id: agentSearchQueries.id });
|
|
1427
|
-
return deleted.length;
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
function rowToBinaryArtifact(row) {
|
|
1431
|
-
return {
|
|
1432
|
-
id: row.id,
|
|
1433
|
-
workspaceId: row.workspace_id,
|
|
1434
|
-
executionId: row.execution_id,
|
|
1435
|
-
blockId: row.block_id,
|
|
1436
|
-
kind: row.kind,
|
|
1437
|
-
view: row.view,
|
|
1438
|
-
contentType: row.content_type,
|
|
1439
|
-
byteSize: row.byte_size,
|
|
1440
|
-
hash: row.hash,
|
|
1441
|
-
storage: row.storage,
|
|
1442
|
-
storageKey: row.storage_key,
|
|
1443
|
-
createdAt: row.created_at,
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
/** Drizzle/Postgres metadata store for binary artifacts (mirror of D1 migration 0017). */
|
|
1447
|
-
class DrizzleBinaryArtifactMetadataStore {
|
|
1448
|
-
db;
|
|
1449
|
-
constructor(db) {
|
|
1450
|
-
this.db = db;
|
|
1451
|
-
}
|
|
1452
|
-
async insert(record) {
|
|
1453
|
-
await this.db.insert(binaryArtifacts).values({
|
|
1454
|
-
workspace_id: record.workspaceId,
|
|
1455
|
-
id: record.id,
|
|
1456
|
-
execution_id: record.executionId,
|
|
1457
|
-
block_id: record.blockId,
|
|
1458
|
-
kind: record.kind,
|
|
1459
|
-
view: record.view,
|
|
1460
|
-
content_type: record.contentType,
|
|
1461
|
-
byte_size: record.byteSize,
|
|
1462
|
-
hash: record.hash,
|
|
1463
|
-
storage: record.storage,
|
|
1464
|
-
storage_key: record.storageKey,
|
|
1465
|
-
created_at: record.createdAt,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
async get(workspaceId, id) {
|
|
1469
|
-
const rows = await this.db
|
|
1470
|
-
.select()
|
|
1471
|
-
.from(binaryArtifacts)
|
|
1472
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), eq(binaryArtifacts.id, id)))
|
|
1473
|
-
.limit(1);
|
|
1474
|
-
return rows[0] ? rowToBinaryArtifact(rows[0]) : null;
|
|
1475
|
-
}
|
|
1476
|
-
async listByExecution(workspaceId, executionId) {
|
|
1477
|
-
const rows = await this.db
|
|
1478
|
-
.select()
|
|
1479
|
-
.from(binaryArtifacts)
|
|
1480
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), eq(binaryArtifacts.execution_id, executionId)))
|
|
1481
|
-
.orderBy(asc(binaryArtifacts.created_at), asc(binaryArtifacts.id));
|
|
1482
|
-
return rows.map(rowToBinaryArtifact);
|
|
1483
|
-
}
|
|
1484
|
-
async countByExecution(workspaceId, executionId) {
|
|
1485
|
-
const rows = await this.db
|
|
1486
|
-
.select({ n: count() })
|
|
1487
|
-
.from(binaryArtifacts)
|
|
1488
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), eq(binaryArtifacts.execution_id, executionId)));
|
|
1489
|
-
return rows[0]?.n ?? 0;
|
|
1490
|
-
}
|
|
1491
|
-
async listByBlock(workspaceId, blockId) {
|
|
1492
|
-
const rows = await this.db
|
|
1493
|
-
.select()
|
|
1494
|
-
.from(binaryArtifacts)
|
|
1495
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), eq(binaryArtifacts.block_id, blockId)))
|
|
1496
|
-
.orderBy(asc(binaryArtifacts.created_at), asc(binaryArtifacts.id));
|
|
1497
|
-
return rows.map(rowToBinaryArtifact);
|
|
1498
|
-
}
|
|
1499
|
-
async delete(workspaceId, id) {
|
|
1500
|
-
await this.db
|
|
1501
|
-
.delete(binaryArtifacts)
|
|
1502
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), eq(binaryArtifacts.id, id)));
|
|
1503
|
-
}
|
|
1504
|
-
async listOlderThan(workspaceId, olderThan) {
|
|
1505
|
-
const rows = await this.db
|
|
1506
|
-
.select()
|
|
1507
|
-
.from(binaryArtifacts)
|
|
1508
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), lt(binaryArtifacts.created_at, olderThan)));
|
|
1509
|
-
return rows.map(rowToBinaryArtifact);
|
|
1510
|
-
}
|
|
1511
|
-
async deleteOlderThan(workspaceId, olderThan) {
|
|
1512
|
-
const deleted = await this.db
|
|
1513
|
-
.delete(binaryArtifacts)
|
|
1514
|
-
.where(and(eq(binaryArtifacts.workspace_id, workspaceId), lt(binaryArtifacts.created_at, olderThan)))
|
|
1515
|
-
.returning({ id: binaryArtifacts.id });
|
|
1516
|
-
return deleted.length;
|
|
1517
|
-
}
|
|
1518
|
-
async listByWorkspace(workspaceId) {
|
|
1519
|
-
const rows = await this.db
|
|
1520
|
-
.select()
|
|
1521
|
-
.from(binaryArtifacts)
|
|
1522
|
-
.where(eq(binaryArtifacts.workspace_id, workspaceId));
|
|
1523
|
-
return rows.map(rowToBinaryArtifact);
|
|
1524
|
-
}
|
|
1525
|
-
async deleteByWorkspace(workspaceId) {
|
|
1526
|
-
const deleted = await this.db
|
|
1527
|
-
.delete(binaryArtifacts)
|
|
1528
|
-
.where(eq(binaryArtifacts.workspace_id, workspaceId))
|
|
1529
|
-
.returning({ id: binaryArtifacts.id });
|
|
1530
|
-
return deleted.length;
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
function rowToProvisioningLog(row) {
|
|
1534
|
-
return {
|
|
1535
|
-
id: row.id,
|
|
1536
|
-
workspaceId: row.workspace_id,
|
|
1537
|
-
subsystem: row.subsystem,
|
|
1538
|
-
operation: row.operation,
|
|
1539
|
-
targetId: row.target_id,
|
|
1540
|
-
providerId: row.provider_id,
|
|
1541
|
-
blockId: row.block_id,
|
|
1542
|
-
executionId: row.execution_id,
|
|
1543
|
-
outcome: row.outcome,
|
|
1544
|
-
error: row.error,
|
|
1545
|
-
detail: row.detail,
|
|
1546
|
-
createdAt: row.created_at,
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
/** Drizzle/Postgres provisioning-log sink, in its own `provisioning` schema. */
|
|
1550
|
-
class DrizzleProvisioningLogRepository {
|
|
1551
|
-
db;
|
|
1552
|
-
constructor(db) {
|
|
1553
|
-
this.db = db;
|
|
1554
|
-
}
|
|
1555
|
-
async append(record) {
|
|
1556
|
-
await this.db.insert(provisioningLog).values({
|
|
1557
|
-
id: record.id,
|
|
1558
|
-
workspace_id: record.workspaceId,
|
|
1559
|
-
subsystem: record.subsystem,
|
|
1560
|
-
operation: record.operation,
|
|
1561
|
-
target_id: record.targetId,
|
|
1562
|
-
provider_id: record.providerId,
|
|
1563
|
-
block_id: record.blockId,
|
|
1564
|
-
execution_id: record.executionId,
|
|
1565
|
-
outcome: record.outcome,
|
|
1566
|
-
error: record.error,
|
|
1567
|
-
detail: record.detail,
|
|
1568
|
-
created_at: record.createdAt,
|
|
1569
|
-
});
|
|
1570
|
-
}
|
|
1571
|
-
async list(workspaceId, query = {}) {
|
|
1572
|
-
const conditions = [eq(provisioningLog.workspace_id, workspaceId)];
|
|
1573
|
-
if (query.subsystem)
|
|
1574
|
-
conditions.push(eq(provisioningLog.subsystem, query.subsystem));
|
|
1575
|
-
if (query.executionId)
|
|
1576
|
-
conditions.push(eq(provisioningLog.execution_id, query.executionId));
|
|
1577
|
-
if (query.targetId)
|
|
1578
|
-
conditions.push(eq(provisioningLog.target_id, query.targetId));
|
|
1579
|
-
if (query.before != null)
|
|
1580
|
-
conditions.push(lt(provisioningLog.created_at, query.before));
|
|
1581
|
-
const base = this.db
|
|
1582
|
-
.select()
|
|
1583
|
-
.from(provisioningLog)
|
|
1584
|
-
.where(and(...conditions))
|
|
1585
|
-
.orderBy(desc(provisioningLog.created_at), desc(provisioningLog.id));
|
|
1586
|
-
const rows = await (query.limit == null ? base : base.limit(query.limit));
|
|
1587
|
-
return rows.map(rowToProvisioningLog);
|
|
1588
|
-
}
|
|
1589
|
-
async deleteOlderThan(epochMs) {
|
|
1590
|
-
const deleted = await this.db
|
|
1591
|
-
.delete(provisioningLog)
|
|
1592
|
-
.where(lt(provisioningLog.created_at, epochMs))
|
|
1593
|
-
.returning({ id: provisioningLog.id });
|
|
1594
|
-
return deleted.length;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function rowToModelPreset(row) {
|
|
1598
|
-
let overrides = {};
|
|
1599
|
-
try {
|
|
1600
|
-
const parsed = JSON.parse(row.overrides);
|
|
1601
|
-
if (parsed && typeof parsed === 'object')
|
|
1602
|
-
overrides = parsed;
|
|
1603
|
-
}
|
|
1604
|
-
catch {
|
|
1605
|
-
// A malformed JSON column degrades to no overrides (base model applies to all).
|
|
1606
|
-
}
|
|
1607
|
-
return {
|
|
1608
|
-
id: row.id,
|
|
1609
|
-
name: row.name,
|
|
1610
|
-
baseModelId: row.base_model_id,
|
|
1611
|
-
overrides,
|
|
1612
|
-
isDefault: row.is_default === 1,
|
|
1613
|
-
...(row.version != null ? { version: row.version } : {}),
|
|
1614
|
-
createdAt: row.created_at,
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Per-workspace model presets over Postgres (the Drizzle mirror of the Worker's
|
|
1619
|
-
* `D1ModelPresetRepository`, migration 0006). A preset is one `base_model_id` applied
|
|
1620
|
-
* to every agent kind plus per-kind `overrides` (a JSON column). Enforces the
|
|
1621
|
-
* single-default invariant: promoting a preset to default demotes every other in the
|
|
1622
|
-
* workspace before the upsert. The default preset cannot be removed. Behaviourally
|
|
1623
|
-
* identical to the D1 repo so the cross-runtime conformance suite asserts the same
|
|
1624
|
-
* preset resolution.
|
|
1625
|
-
*/
|
|
1626
|
-
class DrizzleModelPresetRepository {
|
|
1627
|
-
db;
|
|
1628
|
-
constructor(db) {
|
|
1629
|
-
this.db = db;
|
|
1630
|
-
}
|
|
1631
|
-
async get(workspaceId, id) {
|
|
1632
|
-
const rows = await this.db
|
|
1633
|
-
.select()
|
|
1634
|
-
.from(modelPresets)
|
|
1635
|
-
.where(and(eq(modelPresets.workspace_id, workspaceId), eq(modelPresets.id, id)))
|
|
1636
|
-
.limit(1);
|
|
1637
|
-
return rows[0] ? rowToModelPreset(rows[0]) : null;
|
|
1638
|
-
}
|
|
1639
|
-
async list(workspaceId) {
|
|
1640
|
-
const rows = await this.db
|
|
1641
|
-
.select()
|
|
1642
|
-
.from(modelPresets)
|
|
1643
|
-
.where(eq(modelPresets.workspace_id, workspaceId))
|
|
1644
|
-
.orderBy(modelPresets.created_at);
|
|
1645
|
-
return rows.map(rowToModelPreset);
|
|
1646
|
-
}
|
|
1647
|
-
async getDefault(workspaceId) {
|
|
1648
|
-
const rows = await this.db
|
|
1649
|
-
.select()
|
|
1650
|
-
.from(modelPresets)
|
|
1651
|
-
.where(and(eq(modelPresets.workspace_id, workspaceId), eq(modelPresets.is_default, 1)))
|
|
1652
|
-
.orderBy(modelPresets.created_at)
|
|
1653
|
-
.limit(1);
|
|
1654
|
-
return rows[0] ? rowToModelPreset(rows[0]) : null;
|
|
1655
|
-
}
|
|
1656
|
-
async upsert(workspaceId, preset) {
|
|
1657
|
-
const values = {
|
|
1658
|
-
workspace_id: workspaceId,
|
|
1659
|
-
id: preset.id,
|
|
1660
|
-
name: preset.name,
|
|
1661
|
-
base_model_id: preset.baseModelId,
|
|
1662
|
-
overrides: JSON.stringify(preset.overrides),
|
|
1663
|
-
is_default: preset.isDefault ? 1 : 0,
|
|
1664
|
-
version: preset.version ?? null,
|
|
1665
|
-
created_at: preset.createdAt,
|
|
1666
|
-
};
|
|
1667
|
-
// Demote + upsert run in one transaction so the single-default invariant can never
|
|
1668
|
-
// be observed broken (zero or two defaults) by a concurrent reader or partial failure.
|
|
1669
|
-
await this.db.transaction(async (tx) => {
|
|
1670
|
-
if (preset.isDefault) {
|
|
1671
|
-
await tx
|
|
1672
|
-
.update(modelPresets)
|
|
1673
|
-
.set({ is_default: 0 })
|
|
1674
|
-
.where(and(eq(modelPresets.workspace_id, workspaceId), sql `${modelPresets.id} <> ${preset.id}`));
|
|
1675
|
-
}
|
|
1676
|
-
await tx
|
|
1677
|
-
.insert(modelPresets)
|
|
1678
|
-
.values(values)
|
|
1679
|
-
.onConflictDoUpdate({
|
|
1680
|
-
target: [modelPresets.workspace_id, modelPresets.id],
|
|
1681
|
-
set: {
|
|
1682
|
-
name: values.name,
|
|
1683
|
-
base_model_id: values.base_model_id,
|
|
1684
|
-
overrides: values.overrides,
|
|
1685
|
-
is_default: values.is_default,
|
|
1686
|
-
version: values.version,
|
|
1687
|
-
},
|
|
1688
|
-
});
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
async remove(workspaceId, id) {
|
|
1692
|
-
await this.db
|
|
1693
|
-
.delete(modelPresets)
|
|
1694
|
-
.where(and(eq(modelPresets.workspace_id, workspaceId), eq(modelPresets.id, id), eq(modelPresets.is_default, 0)));
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* A workspace's default service-fragment selection — one row per workspace in
|
|
1699
|
-
* `workspace_fragment_defaults`, the fragment ids stored as a JSON array (mirror of the
|
|
1700
|
-
* D1 `D1ServiceFragmentDefaultsRepository`). `set` upserts the whole list.
|
|
1701
|
-
*/
|
|
1702
|
-
class DrizzleServiceFragmentDefaultsRepository {
|
|
1703
|
-
db;
|
|
1704
|
-
constructor(db) {
|
|
1705
|
-
this.db = db;
|
|
1706
|
-
}
|
|
1707
|
-
async get(workspaceId) {
|
|
1708
|
-
const [row] = await this.db
|
|
1709
|
-
.select({ fragmentIds: workspaceFragmentDefaults.fragment_ids })
|
|
1710
|
-
.from(workspaceFragmentDefaults)
|
|
1711
|
-
.where(eq(workspaceFragmentDefaults.workspace_id, workspaceId));
|
|
1712
|
-
return row ? JSON.parse(row.fragmentIds) : [];
|
|
1713
|
-
}
|
|
1714
|
-
async set(workspaceId, fragmentIds) {
|
|
1715
|
-
await this.db
|
|
1716
|
-
.insert(workspaceFragmentDefaults)
|
|
1717
|
-
.values({
|
|
1718
|
-
workspace_id: workspaceId,
|
|
1719
|
-
fragment_ids: JSON.stringify(fragmentIds),
|
|
1720
|
-
updated_at: Date.now(),
|
|
1721
|
-
})
|
|
1722
|
-
.onConflictDoUpdate({
|
|
1723
|
-
target: workspaceFragmentDefaults.workspace_id,
|
|
1724
|
-
set: { fragment_ids: JSON.stringify(fragmentIds), updated_at: Date.now() },
|
|
1725
|
-
});
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
function rowToSchedule(row) {
|
|
1729
|
-
const recurrence = {
|
|
1730
|
-
intervalHours: row.interval_hours,
|
|
1731
|
-
weekdays: safeWeekdays(row.weekdays),
|
|
1732
|
-
windowStartHour: row.window_start_hour,
|
|
1733
|
-
windowEndHour: row.window_end_hour,
|
|
1734
|
-
timezone: row.timezone,
|
|
1735
|
-
};
|
|
1736
|
-
const issueIntake = parseIssueIntakeColumn(row.issue_intake);
|
|
1737
|
-
return {
|
|
1738
|
-
id: row.id,
|
|
1739
|
-
serviceId: row.service_id,
|
|
1740
|
-
blockId: row.block_id,
|
|
1741
|
-
frameId: row.frame_id,
|
|
1742
|
-
pipelineId: row.pipeline_id,
|
|
1743
|
-
template: row.template,
|
|
1744
|
-
name: row.name,
|
|
1745
|
-
recurrence,
|
|
1746
|
-
onDemand: row.on_demand === 1,
|
|
1747
|
-
...(issueIntake ? { issueIntake } : {}),
|
|
1748
|
-
enabled: row.enabled === 1,
|
|
1749
|
-
lastRunAt: row.last_run_at,
|
|
1750
|
-
nextRunAt: row.next_run_at,
|
|
1751
|
-
createdAt: row.created_at,
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
function safeWeekdays(value) {
|
|
1755
|
-
try {
|
|
1756
|
-
const parsed = JSON.parse(value);
|
|
1757
|
-
return Array.isArray(parsed) ? parsed.filter((n) => typeof n === 'number') : [];
|
|
1758
|
-
}
|
|
1759
|
-
catch {
|
|
1760
|
-
return [];
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
function rowToRun(row) {
|
|
1764
|
-
return {
|
|
1765
|
-
id: row.id,
|
|
1766
|
-
scheduleId: row.schedule_id,
|
|
1767
|
-
executionId: row.execution_id,
|
|
1768
|
-
status: row.status,
|
|
1769
|
-
startedAt: row.started_at,
|
|
1770
|
-
finishedAt: row.finished_at,
|
|
1771
|
-
outcome: row.outcome,
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
class DrizzlePipelineScheduleRepository {
|
|
1775
|
-
db;
|
|
1776
|
-
constructor(db) {
|
|
1777
|
-
this.db = db;
|
|
1778
|
-
}
|
|
1779
|
-
values(workspaceId, schedule) {
|
|
1780
|
-
const r = schedule.recurrence;
|
|
1781
|
-
return {
|
|
1782
|
-
workspace_id: workspaceId,
|
|
1783
|
-
id: schedule.id,
|
|
1784
|
-
service_id: schedule.serviceId,
|
|
1785
|
-
block_id: schedule.blockId,
|
|
1786
|
-
frame_id: schedule.frameId,
|
|
1787
|
-
pipeline_id: schedule.pipelineId,
|
|
1788
|
-
template: schedule.template,
|
|
1789
|
-
name: schedule.name,
|
|
1790
|
-
interval_hours: r.intervalHours,
|
|
1791
|
-
weekdays: JSON.stringify(r.weekdays),
|
|
1792
|
-
window_start_hour: r.windowStartHour,
|
|
1793
|
-
window_end_hour: r.windowEndHour,
|
|
1794
|
-
timezone: r.timezone,
|
|
1795
|
-
enabled: schedule.enabled ? 1 : 0,
|
|
1796
|
-
on_demand: schedule.onDemand ? 1 : 0,
|
|
1797
|
-
issue_intake: serializeIssueIntakeColumn(schedule.issueIntake),
|
|
1798
|
-
last_run_at: schedule.lastRunAt,
|
|
1799
|
-
next_run_at: schedule.nextRunAt,
|
|
1800
|
-
created_at: schedule.createdAt,
|
|
1801
|
-
};
|
|
1802
|
-
}
|
|
1803
|
-
async get(workspaceId, id) {
|
|
1804
|
-
const [row] = await this.db
|
|
1805
|
-
.select()
|
|
1806
|
-
.from(pipelineSchedules)
|
|
1807
|
-
.where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.id, id)));
|
|
1808
|
-
return row ? rowToSchedule(row) : null;
|
|
1809
|
-
}
|
|
1810
|
-
async getByBlock(workspaceId, blockId) {
|
|
1811
|
-
const [row] = await this.db
|
|
1812
|
-
.select()
|
|
1813
|
-
.from(pipelineSchedules)
|
|
1814
|
-
.where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.block_id, blockId)));
|
|
1815
|
-
return row ? rowToSchedule(row) : null;
|
|
1816
|
-
}
|
|
1817
|
-
async list(workspaceId) {
|
|
1818
|
-
const rows = await this.db
|
|
1819
|
-
.select()
|
|
1820
|
-
.from(pipelineSchedules)
|
|
1821
|
-
.where(eq(pipelineSchedules.workspace_id, workspaceId))
|
|
1822
|
-
.orderBy(pipelineSchedules.created_at);
|
|
1823
|
-
return rows.map(rowToSchedule);
|
|
1824
|
-
}
|
|
1825
|
-
async listByService(serviceId) {
|
|
1826
|
-
const rows = await this.db
|
|
1827
|
-
.select()
|
|
1828
|
-
.from(pipelineSchedules)
|
|
1829
|
-
.where(eq(pipelineSchedules.service_id, serviceId))
|
|
1830
|
-
.orderBy(pipelineSchedules.created_at);
|
|
1831
|
-
return rows.map(rowToSchedule);
|
|
1832
|
-
}
|
|
1833
|
-
async listByServices(serviceIds) {
|
|
1834
|
-
if (serviceIds.length === 0)
|
|
1835
|
-
return [];
|
|
1836
|
-
const out = [];
|
|
1837
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
1838
|
-
for (let i = 0; i < serviceIds.length; i += 500) {
|
|
1839
|
-
const rows = await this.db
|
|
1840
|
-
.select()
|
|
1841
|
-
.from(pipelineSchedules)
|
|
1842
|
-
.where(inArray(pipelineSchedules.service_id, serviceIds.slice(i, i + 500)))
|
|
1843
|
-
.orderBy(pipelineSchedules.created_at);
|
|
1844
|
-
for (const row of rows)
|
|
1845
|
-
out.push(rowToSchedule(row));
|
|
1846
|
-
}
|
|
1847
|
-
return out;
|
|
1848
|
-
}
|
|
1849
|
-
async listDue(asOf) {
|
|
1850
|
-
const rows = await this.db
|
|
1851
|
-
.select()
|
|
1852
|
-
.from(pipelineSchedules)
|
|
1853
|
-
.where(and(eq(pipelineSchedules.enabled, 1), eq(pipelineSchedules.on_demand, 0), lt(pipelineSchedules.next_run_at, asOf + 1)))
|
|
1854
|
-
.orderBy(pipelineSchedules.next_run_at);
|
|
1855
|
-
return rows.map((row) => ({ workspaceId: row.workspace_id, schedule: rowToSchedule(row) }));
|
|
1856
|
-
}
|
|
1857
|
-
async upsert(workspaceId, schedule) {
|
|
1858
|
-
const values = this.values(workspaceId, schedule);
|
|
1859
|
-
await this.db
|
|
1860
|
-
.insert(pipelineSchedules)
|
|
1861
|
-
.values(values)
|
|
1862
|
-
.onConflictDoUpdate({
|
|
1863
|
-
target: [pipelineSchedules.workspace_id, pipelineSchedules.id],
|
|
1864
|
-
set: {
|
|
1865
|
-
service_id: values.service_id,
|
|
1866
|
-
block_id: values.block_id,
|
|
1867
|
-
frame_id: values.frame_id,
|
|
1868
|
-
pipeline_id: values.pipeline_id,
|
|
1869
|
-
template: values.template,
|
|
1870
|
-
name: values.name,
|
|
1871
|
-
interval_hours: values.interval_hours,
|
|
1872
|
-
weekdays: values.weekdays,
|
|
1873
|
-
window_start_hour: values.window_start_hour,
|
|
1874
|
-
window_end_hour: values.window_end_hour,
|
|
1875
|
-
timezone: values.timezone,
|
|
1876
|
-
enabled: values.enabled,
|
|
1877
|
-
on_demand: values.on_demand,
|
|
1878
|
-
issue_intake: values.issue_intake,
|
|
1879
|
-
last_run_at: values.last_run_at,
|
|
1880
|
-
next_run_at: values.next_run_at,
|
|
1881
|
-
},
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
async remove(workspaceId, id) {
|
|
1885
|
-
await this.db
|
|
1886
|
-
.delete(pipelineSchedules)
|
|
1887
|
-
.where(and(eq(pipelineSchedules.workspace_id, workspaceId), eq(pipelineSchedules.id, id)));
|
|
1888
|
-
}
|
|
1889
|
-
async insertRun(workspaceId, run) {
|
|
1890
|
-
await this.db.insert(pipelineScheduleRuns).values({
|
|
1891
|
-
workspace_id: workspaceId,
|
|
1892
|
-
id: run.id,
|
|
1893
|
-
schedule_id: run.scheduleId,
|
|
1894
|
-
execution_id: run.executionId,
|
|
1895
|
-
status: run.status,
|
|
1896
|
-
started_at: run.startedAt,
|
|
1897
|
-
finished_at: run.finishedAt,
|
|
1898
|
-
outcome: run.outcome,
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
async updateRun(workspaceId, runId, patch) {
|
|
1902
|
-
const set = {};
|
|
1903
|
-
if (patch.status !== undefined)
|
|
1904
|
-
set.status = patch.status;
|
|
1905
|
-
if (patch.finishedAt !== undefined)
|
|
1906
|
-
set.finished_at = patch.finishedAt;
|
|
1907
|
-
if (patch.outcome !== undefined)
|
|
1908
|
-
set.outcome = patch.outcome;
|
|
1909
|
-
if (patch.executionId !== undefined)
|
|
1910
|
-
set.execution_id = patch.executionId;
|
|
1911
|
-
if (Object.keys(set).length === 0)
|
|
1912
|
-
return;
|
|
1913
|
-
await this.db
|
|
1914
|
-
.update(pipelineScheduleRuns)
|
|
1915
|
-
.set(set)
|
|
1916
|
-
.where(and(eq(pipelineScheduleRuns.workspace_id, workspaceId), eq(pipelineScheduleRuns.id, runId)));
|
|
1917
|
-
}
|
|
1918
|
-
async listRuns(workspaceId, scheduleId) {
|
|
1919
|
-
const rows = await this.db
|
|
1920
|
-
.select()
|
|
1921
|
-
.from(pipelineScheduleRuns)
|
|
1922
|
-
.where(and(eq(pipelineScheduleRuns.workspace_id, workspaceId), eq(pipelineScheduleRuns.schedule_id, scheduleId)))
|
|
1923
|
-
.orderBy(desc(pipelineScheduleRuns.started_at));
|
|
1924
|
-
return rows.map(rowToRun);
|
|
1925
|
-
}
|
|
1926
|
-
async pruneRunsBefore(before) {
|
|
1927
|
-
const rows = await this.db
|
|
1928
|
-
.delete(pipelineScheduleRuns)
|
|
1929
|
-
.where(lt(pipelineScheduleRuns.started_at, before))
|
|
1930
|
-
.returning({ id: pipelineScheduleRuns.id });
|
|
1931
|
-
return rows.length;
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
class DrizzleTrackerSettingsRepository {
|
|
1935
|
-
db;
|
|
1936
|
-
constructor(db) {
|
|
1937
|
-
this.db = db;
|
|
1938
|
-
}
|
|
1939
|
-
async get(workspaceId) {
|
|
1940
|
-
const [row] = await this.db
|
|
1941
|
-
.select()
|
|
1942
|
-
.from(trackerSettings)
|
|
1943
|
-
.where(eq(trackerSettings.workspace_id, workspaceId));
|
|
1944
|
-
if (!row)
|
|
1945
|
-
return null;
|
|
1946
|
-
return {
|
|
1947
|
-
tracker: row.tracker ?? null,
|
|
1948
|
-
jiraProjectKey: row.jira_project_key,
|
|
1949
|
-
linearTeamId: row.linear_team_id,
|
|
1950
|
-
writebackCommentOnPrOpen: row.writeback_comment_on_pr_open === 1,
|
|
1951
|
-
writebackResolveOnMerge: row.writeback_resolve_on_merge === 1,
|
|
1952
|
-
updatedAt: row.updated_at,
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
async put(workspaceId, settings) {
|
|
1956
|
-
await this.db
|
|
1957
|
-
.insert(trackerSettings)
|
|
1958
|
-
.values({
|
|
1959
|
-
workspace_id: workspaceId,
|
|
1960
|
-
tracker: settings.tracker,
|
|
1961
|
-
jira_project_key: settings.jiraProjectKey,
|
|
1962
|
-
linear_team_id: settings.linearTeamId,
|
|
1963
|
-
writeback_comment_on_pr_open: settings.writebackCommentOnPrOpen ? 1 : 0,
|
|
1964
|
-
writeback_resolve_on_merge: settings.writebackResolveOnMerge ? 1 : 0,
|
|
1965
|
-
updated_at: settings.updatedAt,
|
|
1966
|
-
})
|
|
1967
|
-
.onConflictDoUpdate({
|
|
1968
|
-
target: trackerSettings.workspace_id,
|
|
1969
|
-
set: {
|
|
1970
|
-
tracker: settings.tracker,
|
|
1971
|
-
jira_project_key: settings.jiraProjectKey,
|
|
1972
|
-
linear_team_id: settings.linearTeamId,
|
|
1973
|
-
writeback_comment_on_pr_open: settings.writebackCommentOnPrOpen ? 1 : 0,
|
|
1974
|
-
writeback_resolve_on_merge: settings.writebackResolveOnMerge ? 1 : 0,
|
|
1975
|
-
updated_at: settings.updatedAt,
|
|
1976
|
-
},
|
|
1977
|
-
});
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
function rowToService(row) {
|
|
1981
|
-
return {
|
|
1982
|
-
id: row.id,
|
|
1983
|
-
accountId: row.account_id,
|
|
1984
|
-
frameBlockId: row.frame_block_id,
|
|
1985
|
-
installationId: row.installation_id,
|
|
1986
|
-
repoGithubId: row.repo_github_id,
|
|
1987
|
-
directory: row.directory,
|
|
1988
|
-
createdAt: row.created_at,
|
|
1989
|
-
};
|
|
1990
|
-
}
|
|
1991
|
-
/** Account-owned services (migration 0030). The canonical, shareable board unit. */
|
|
1992
|
-
export class DrizzleServiceRepository {
|
|
1993
|
-
db;
|
|
1994
|
-
constructor(db) {
|
|
1995
|
-
this.db = db;
|
|
1996
|
-
}
|
|
1997
|
-
async get(id) {
|
|
1998
|
-
const [row] = await this.db.select().from(services).where(eq(services.id, id));
|
|
1999
|
-
return row ? rowToService(row) : null;
|
|
2000
|
-
}
|
|
2001
|
-
async getByFrameBlock(frameBlockId) {
|
|
2002
|
-
const [row] = await this.db
|
|
2003
|
-
.select()
|
|
2004
|
-
.from(services)
|
|
2005
|
-
.where(eq(services.frame_block_id, frameBlockId));
|
|
2006
|
-
return row ? rowToService(row) : null;
|
|
2007
|
-
}
|
|
2008
|
-
async listByFrameBlocks(frameBlockIds) {
|
|
2009
|
-
if (frameBlockIds.length === 0)
|
|
2010
|
-
return [];
|
|
2011
|
-
const out = [];
|
|
2012
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
2013
|
-
for (let i = 0; i < frameBlockIds.length; i += 500) {
|
|
2014
|
-
const rows = await this.db
|
|
2015
|
-
.select()
|
|
2016
|
-
.from(services)
|
|
2017
|
-
.where(inArray(services.frame_block_id, frameBlockIds.slice(i, i + 500)));
|
|
2018
|
-
for (const row of rows)
|
|
2019
|
-
out.push(rowToService(row));
|
|
2020
|
-
}
|
|
2021
|
-
return out;
|
|
2022
|
-
}
|
|
2023
|
-
async listByAccount(accountId) {
|
|
2024
|
-
// NULL-safe match so the legacy/unscoped org (accountId null) lists cleanly.
|
|
2025
|
-
const rows = await this.db
|
|
2026
|
-
.select()
|
|
2027
|
-
.from(services)
|
|
2028
|
-
.where(sql `${services.account_id} IS NOT DISTINCT FROM ${accountId}`)
|
|
2029
|
-
.orderBy(services.created_at);
|
|
2030
|
-
return rows.map(rowToService);
|
|
2031
|
-
}
|
|
2032
|
-
async listByIds(ids) {
|
|
2033
|
-
if (ids.length === 0)
|
|
2034
|
-
return [];
|
|
2035
|
-
const out = [];
|
|
2036
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
2037
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
2038
|
-
const rows = await this.db
|
|
2039
|
-
.select()
|
|
2040
|
-
.from(services)
|
|
2041
|
-
.where(inArray(services.id, ids.slice(i, i + 500)));
|
|
2042
|
-
for (const row of rows)
|
|
2043
|
-
out.push(rowToService(row));
|
|
2044
|
-
}
|
|
2045
|
-
return out;
|
|
2046
|
-
}
|
|
2047
|
-
async getByRepo(installationId, repoGithubId) {
|
|
2048
|
-
const [row] = await this.db
|
|
2049
|
-
.select()
|
|
2050
|
-
.from(services)
|
|
2051
|
-
.where(and(eq(services.installation_id, installationId), eq(services.repo_github_id, repoGithubId)));
|
|
2052
|
-
return row ? rowToService(row) : null;
|
|
2053
|
-
}
|
|
2054
|
-
async insert(service) {
|
|
2055
|
-
await this.db.insert(services).values({
|
|
2056
|
-
id: service.id,
|
|
2057
|
-
account_id: service.accountId,
|
|
2058
|
-
frame_block_id: service.frameBlockId,
|
|
2059
|
-
installation_id: service.installationId,
|
|
2060
|
-
repo_github_id: service.repoGithubId,
|
|
2061
|
-
directory: service.directory ?? null,
|
|
2062
|
-
created_at: service.createdAt,
|
|
2063
|
-
});
|
|
2064
|
-
}
|
|
2065
|
-
async update(id, patch) {
|
|
2066
|
-
const set = {};
|
|
2067
|
-
if ('accountId' in patch)
|
|
2068
|
-
set.account_id = patch.accountId ?? null;
|
|
2069
|
-
if ('installationId' in patch)
|
|
2070
|
-
set.installation_id = patch.installationId ?? null;
|
|
2071
|
-
if ('repoGithubId' in patch)
|
|
2072
|
-
set.repo_github_id = patch.repoGithubId ?? null;
|
|
2073
|
-
if ('directory' in patch)
|
|
2074
|
-
set.directory = patch.directory ?? null;
|
|
2075
|
-
if (Object.keys(set).length === 0)
|
|
2076
|
-
return;
|
|
2077
|
-
await this.db.update(services).set(set).where(eq(services.id, id));
|
|
2078
|
-
}
|
|
2079
|
-
async delete(id) {
|
|
2080
|
-
await this.db.delete(services).where(eq(services.id, id));
|
|
2081
|
-
}
|
|
2082
|
-
async deleteMany(ids) {
|
|
2083
|
-
if (ids.length === 0)
|
|
2084
|
-
return;
|
|
2085
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
2086
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
2087
|
-
await this.db.delete(services).where(inArray(services.id, ids.slice(i, i + 500)));
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
function rowToMount(row) {
|
|
2092
|
-
return {
|
|
2093
|
-
workspaceId: row.workspace_id,
|
|
2094
|
-
serviceId: row.service_id,
|
|
2095
|
-
position: { x: row.pos_x, y: row.pos_y },
|
|
2096
|
-
size: row.width !== null && row.height !== null ? { w: row.width, h: row.height } : null,
|
|
2097
|
-
createdAt: row.created_at,
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2100
|
-
/** A service mounted onto a workspace board + its per-workspace layout (migration 0030). */
|
|
2101
|
-
class DrizzleWorkspaceMountRepository {
|
|
2102
|
-
db;
|
|
2103
|
-
constructor(db) {
|
|
2104
|
-
this.db = db;
|
|
2105
|
-
}
|
|
2106
|
-
async listByWorkspace(workspaceId) {
|
|
2107
|
-
const rows = await this.db
|
|
2108
|
-
.select()
|
|
2109
|
-
.from(workspaceServices)
|
|
2110
|
-
.where(eq(workspaceServices.workspace_id, workspaceId))
|
|
2111
|
-
.orderBy(workspaceServices.created_at);
|
|
2112
|
-
return rows.map(rowToMount);
|
|
2113
|
-
}
|
|
2114
|
-
async listByService(serviceId) {
|
|
2115
|
-
const rows = await this.db
|
|
2116
|
-
.select()
|
|
2117
|
-
.from(workspaceServices)
|
|
2118
|
-
.where(eq(workspaceServices.service_id, serviceId))
|
|
2119
|
-
.orderBy(workspaceServices.created_at);
|
|
2120
|
-
return rows.map(rowToMount);
|
|
2121
|
-
}
|
|
2122
|
-
async listByServiceIds(serviceIds) {
|
|
2123
|
-
if (serviceIds.length === 0)
|
|
2124
|
-
return [];
|
|
2125
|
-
const rows = await this.db
|
|
2126
|
-
.select()
|
|
2127
|
-
.from(workspaceServices)
|
|
2128
|
-
.where(inArray(workspaceServices.service_id, serviceIds))
|
|
2129
|
-
.orderBy(workspaceServices.created_at);
|
|
2130
|
-
return rows.map(rowToMount);
|
|
2131
|
-
}
|
|
2132
|
-
async listWorkspaceIdsMountingBlock(originWorkspaceId, blockId) {
|
|
2133
|
-
// One join: the service owning the block → the workspaces that mount it. A block with no
|
|
2134
|
-
// service makes the subquery NULL, which matches no rows (`service_id = NULL`) → empty.
|
|
2135
|
-
const rows = await this.db
|
|
2136
|
-
.select({ workspaceId: workspaceServices.workspace_id })
|
|
2137
|
-
.from(workspaceServices)
|
|
2138
|
-
.where(sql `${workspaceServices.service_id} = (SELECT ${blocks.service_id} FROM ${blocks} WHERE ${blocks.workspace_id} = ${originWorkspaceId} AND ${blocks.id} = ${blockId})`);
|
|
2139
|
-
return rows.map((r) => r.workspaceId);
|
|
2140
|
-
}
|
|
2141
|
-
async countByServiceIds(serviceIds) {
|
|
2142
|
-
if (serviceIds.length === 0)
|
|
2143
|
-
return {};
|
|
2144
|
-
const rows = await this.db
|
|
2145
|
-
.select({ serviceId: workspaceServices.service_id, n: sql `count(*)` })
|
|
2146
|
-
.from(workspaceServices)
|
|
2147
|
-
.where(inArray(workspaceServices.service_id, serviceIds))
|
|
2148
|
-
.groupBy(workspaceServices.service_id);
|
|
2149
|
-
const counts = {};
|
|
2150
|
-
for (const row of rows)
|
|
2151
|
-
counts[row.serviceId] = Number(row.n);
|
|
2152
|
-
return counts;
|
|
2153
|
-
}
|
|
2154
|
-
async get(workspaceId, serviceId) {
|
|
2155
|
-
const [row] = await this.db
|
|
2156
|
-
.select()
|
|
2157
|
-
.from(workspaceServices)
|
|
2158
|
-
.where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
|
|
2159
|
-
return row ? rowToMount(row) : null;
|
|
2160
|
-
}
|
|
2161
|
-
async upsert(mount) {
|
|
2162
|
-
await this.db
|
|
2163
|
-
.insert(workspaceServices)
|
|
2164
|
-
.values({
|
|
2165
|
-
workspace_id: mount.workspaceId,
|
|
2166
|
-
service_id: mount.serviceId,
|
|
2167
|
-
pos_x: mount.position.x,
|
|
2168
|
-
pos_y: mount.position.y,
|
|
2169
|
-
width: mount.size?.w ?? null,
|
|
2170
|
-
height: mount.size?.h ?? null,
|
|
2171
|
-
created_at: mount.createdAt,
|
|
2172
|
-
})
|
|
2173
|
-
.onConflictDoUpdate({
|
|
2174
|
-
target: [workspaceServices.workspace_id, workspaceServices.service_id],
|
|
2175
|
-
set: {
|
|
2176
|
-
pos_x: mount.position.x,
|
|
2177
|
-
pos_y: mount.position.y,
|
|
2178
|
-
width: mount.size?.w ?? null,
|
|
2179
|
-
height: mount.size?.h ?? null,
|
|
2180
|
-
},
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
async update(workspaceId, serviceId, patch) {
|
|
2184
|
-
const set = {};
|
|
2185
|
-
if (patch.position) {
|
|
2186
|
-
set.pos_x = patch.position.x;
|
|
2187
|
-
set.pos_y = patch.position.y;
|
|
2188
|
-
}
|
|
2189
|
-
if ('size' in patch) {
|
|
2190
|
-
set.width = patch.size?.w ?? null;
|
|
2191
|
-
set.height = patch.size?.h ?? null;
|
|
2192
|
-
}
|
|
2193
|
-
if (Object.keys(set).length === 0)
|
|
2194
|
-
return;
|
|
2195
|
-
await this.db
|
|
2196
|
-
.update(workspaceServices)
|
|
2197
|
-
.set(set)
|
|
2198
|
-
.where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
|
|
2199
|
-
}
|
|
2200
|
-
async remove(workspaceId, serviceId) {
|
|
2201
|
-
await this.db
|
|
2202
|
-
.delete(workspaceServices)
|
|
2203
|
-
.where(and(eq(workspaceServices.workspace_id, workspaceId), eq(workspaceServices.service_id, serviceId)));
|
|
2204
|
-
}
|
|
2205
|
-
async removeByServices(serviceIds) {
|
|
2206
|
-
if (serviceIds.length === 0)
|
|
2207
|
-
return;
|
|
2208
|
-
// Chunk the IN list to stay well under the bind-parameter limit.
|
|
2209
|
-
for (let i = 0; i < serviceIds.length; i += 500) {
|
|
2210
|
-
await this.db
|
|
2211
|
-
.delete(workspaceServices)
|
|
2212
|
-
.where(inArray(workspaceServices.service_id, serviceIds.slice(i, i + 500)));
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
function rowToRequirementReview(row) {
|
|
2217
|
-
return {
|
|
2218
|
-
id: row.id,
|
|
2219
|
-
blockId: row.block_id,
|
|
2220
|
-
status: row.status,
|
|
2221
|
-
items: parseJsonArray(row.items),
|
|
2222
|
-
model: row.model,
|
|
2223
|
-
incorporatedRequirements: row.incorporated_requirements,
|
|
2224
|
-
iteration: row.iteration,
|
|
2225
|
-
maxIterations: row.max_iterations,
|
|
2226
|
-
recommendations: parseJsonArray(row.recommendations ?? '[]'),
|
|
2227
|
-
createdAt: row.created_at,
|
|
2228
|
-
updatedAt: row.updated_at,
|
|
2229
|
-
};
|
|
2230
|
-
}
|
|
2231
|
-
/**
|
|
2232
|
-
* Requirements reviews over Postgres (the Drizzle mirror of the Worker's
|
|
2233
|
-
* `D1RequirementReviewRepository`, migration 0021). The reviewed items live as a JSON
|
|
2234
|
-
* array in `items`; the service keeps at most one live review per block (it deletes
|
|
2235
|
-
* the block's prior review before inserting a fresh one), so `getByBlock` returns the
|
|
2236
|
-
* latest. Behaviourally identical to the D1 repo so the cross-runtime conformance
|
|
2237
|
-
* suite asserts the same requirements-rework substitution against both stores.
|
|
2238
|
-
*/
|
|
2239
|
-
export class DrizzleRequirementReviewRepository {
|
|
2240
|
-
db;
|
|
2241
|
-
constructor(db) {
|
|
2242
|
-
this.db = db;
|
|
2243
|
-
}
|
|
2244
|
-
async getByBlock(workspaceId, blockId) {
|
|
2245
|
-
const rows = await this.db
|
|
2246
|
-
.select()
|
|
2247
|
-
.from(requirementReviews)
|
|
2248
|
-
.where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.block_id, blockId)))
|
|
2249
|
-
.orderBy(desc(requirementReviews.created_at))
|
|
2250
|
-
.limit(1);
|
|
2251
|
-
return rows[0] ? rowToRequirementReview(rows[0]) : null;
|
|
2252
|
-
}
|
|
2253
|
-
async get(workspaceId, id) {
|
|
2254
|
-
const rows = await this.db
|
|
2255
|
-
.select()
|
|
2256
|
-
.from(requirementReviews)
|
|
2257
|
-
.where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.id, id)))
|
|
2258
|
-
.limit(1);
|
|
2259
|
-
return rows[0] ? rowToRequirementReview(rows[0]) : null;
|
|
2260
|
-
}
|
|
2261
|
-
async upsert(workspaceId, review) {
|
|
2262
|
-
const values = {
|
|
2263
|
-
workspace_id: workspaceId,
|
|
2264
|
-
id: review.id,
|
|
2265
|
-
block_id: review.blockId,
|
|
2266
|
-
status: review.status,
|
|
2267
|
-
items: JSON.stringify(review.items),
|
|
2268
|
-
model: review.model,
|
|
2269
|
-
incorporated_requirements: review.incorporatedRequirements,
|
|
2270
|
-
iteration: review.iteration ?? 1,
|
|
2271
|
-
max_iterations: review.maxIterations ?? 1,
|
|
2272
|
-
recommendations: JSON.stringify(review.recommendations ?? []),
|
|
2273
|
-
created_at: review.createdAt,
|
|
2274
|
-
updated_at: review.updatedAt,
|
|
2275
|
-
};
|
|
2276
|
-
await this.db
|
|
2277
|
-
.insert(requirementReviews)
|
|
2278
|
-
.values(values)
|
|
2279
|
-
.onConflictDoUpdate({
|
|
2280
|
-
target: [requirementReviews.workspace_id, requirementReviews.id],
|
|
2281
|
-
set: {
|
|
2282
|
-
block_id: values.block_id,
|
|
2283
|
-
status: values.status,
|
|
2284
|
-
items: values.items,
|
|
2285
|
-
model: values.model,
|
|
2286
|
-
incorporated_requirements: values.incorporated_requirements,
|
|
2287
|
-
iteration: values.iteration,
|
|
2288
|
-
max_iterations: values.max_iterations,
|
|
2289
|
-
recommendations: values.recommendations,
|
|
2290
|
-
updated_at: values.updated_at,
|
|
2291
|
-
},
|
|
2292
|
-
});
|
|
2293
|
-
}
|
|
2294
|
-
async deleteByBlock(workspaceId, blockId) {
|
|
2295
|
-
await this.db
|
|
2296
|
-
.delete(requirementReviews)
|
|
2297
|
-
.where(and(eq(requirementReviews.workspace_id, workspaceId), eq(requirementReviews.block_id, blockId)));
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
function rowToDocInterviewSession(row) {
|
|
2301
|
-
return {
|
|
2302
|
-
id: row.id,
|
|
2303
|
-
blockId: row.block_id,
|
|
2304
|
-
status: row.status,
|
|
2305
|
-
round: row.round,
|
|
2306
|
-
maxRounds: row.max_rounds,
|
|
2307
|
-
qa: parseJsonArray(row.qa),
|
|
2308
|
-
brief: row.brief,
|
|
2309
|
-
model: row.model,
|
|
2310
|
-
createdAt: row.created_at,
|
|
2311
|
-
updatedAt: row.updated_at,
|
|
2312
|
-
};
|
|
2313
|
-
}
|
|
2314
|
-
/**
|
|
2315
|
-
* Interactive document-interview sessions over Postgres (the Drizzle mirror of the Worker's
|
|
2316
|
-
* `D1DocInterviewRepository`, migration 0040). The Q&A live as a JSON array in `qa`; the service
|
|
2317
|
-
* keeps at most one live session per block, so `getByBlock` returns the latest. Behaviourally
|
|
2318
|
-
* identical to the D1 repo so the cross-runtime conformance suite asserts the same interview
|
|
2319
|
-
* brief substitution against both stores.
|
|
2320
|
-
*/
|
|
2321
|
-
export class DrizzleDocInterviewRepository {
|
|
2322
|
-
db;
|
|
2323
|
-
constructor(db) {
|
|
2324
|
-
this.db = db;
|
|
2325
|
-
}
|
|
2326
|
-
async getByBlock(workspaceId, blockId) {
|
|
2327
|
-
const rows = await this.db
|
|
2328
|
-
.select()
|
|
2329
|
-
.from(docInterviewSessions)
|
|
2330
|
-
.where(and(eq(docInterviewSessions.workspace_id, workspaceId), eq(docInterviewSessions.block_id, blockId)))
|
|
2331
|
-
.orderBy(desc(docInterviewSessions.created_at))
|
|
2332
|
-
.limit(1);
|
|
2333
|
-
return rows[0] ? rowToDocInterviewSession(rows[0]) : null;
|
|
2334
|
-
}
|
|
2335
|
-
async get(workspaceId, id) {
|
|
2336
|
-
const rows = await this.db
|
|
2337
|
-
.select()
|
|
2338
|
-
.from(docInterviewSessions)
|
|
2339
|
-
.where(and(eq(docInterviewSessions.workspace_id, workspaceId), eq(docInterviewSessions.id, id)))
|
|
2340
|
-
.limit(1);
|
|
2341
|
-
return rows[0] ? rowToDocInterviewSession(rows[0]) : null;
|
|
2342
|
-
}
|
|
2343
|
-
async upsert(workspaceId, session) {
|
|
2344
|
-
const values = {
|
|
2345
|
-
workspace_id: workspaceId,
|
|
2346
|
-
id: session.id,
|
|
2347
|
-
block_id: session.blockId,
|
|
2348
|
-
status: session.status,
|
|
2349
|
-
round: session.round,
|
|
2350
|
-
max_rounds: session.maxRounds,
|
|
2351
|
-
qa: JSON.stringify(session.qa ?? []),
|
|
2352
|
-
brief: session.brief,
|
|
2353
|
-
model: session.model,
|
|
2354
|
-
created_at: session.createdAt,
|
|
2355
|
-
updated_at: session.updatedAt,
|
|
2356
|
-
};
|
|
2357
|
-
await this.db
|
|
2358
|
-
.insert(docInterviewSessions)
|
|
2359
|
-
.values(values)
|
|
2360
|
-
.onConflictDoUpdate({
|
|
2361
|
-
target: [docInterviewSessions.workspace_id, docInterviewSessions.id],
|
|
2362
|
-
set: {
|
|
2363
|
-
block_id: values.block_id,
|
|
2364
|
-
status: values.status,
|
|
2365
|
-
round: values.round,
|
|
2366
|
-
max_rounds: values.max_rounds,
|
|
2367
|
-
qa: values.qa,
|
|
2368
|
-
brief: values.brief,
|
|
2369
|
-
model: values.model,
|
|
2370
|
-
updated_at: values.updated_at,
|
|
2371
|
-
},
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
async deleteByBlock(workspaceId, blockId) {
|
|
2375
|
-
await this.db
|
|
2376
|
-
.delete(docInterviewSessions)
|
|
2377
|
-
.where(and(eq(docInterviewSessions.workspace_id, workspaceId), eq(docInterviewSessions.block_id, blockId)));
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
function rowToKaizenGrading(row) {
|
|
2381
|
-
return {
|
|
2382
|
-
id: row.id,
|
|
2383
|
-
executionId: row.execution_id,
|
|
2384
|
-
blockId: row.block_id,
|
|
2385
|
-
stepIndex: row.step_index,
|
|
2386
|
-
agentKind: row.agent_kind,
|
|
2387
|
-
model: row.model,
|
|
2388
|
-
promptVersion: row.prompt_version,
|
|
2389
|
-
comboKey: row.combo_key,
|
|
2390
|
-
status: row.status,
|
|
2391
|
-
grade: row.grade,
|
|
2392
|
-
summary: row.summary,
|
|
2393
|
-
recommendations: parseJsonArray(row.recommendations ?? '[]'),
|
|
2394
|
-
graderModel: row.grader_model,
|
|
2395
|
-
error: row.error,
|
|
2396
|
-
createdAt: row.created_at,
|
|
2397
|
-
updatedAt: row.updated_at,
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
/**
|
|
2401
|
-
* Kaizen gradings over Postgres (the Drizzle mirror of the Worker's
|
|
2402
|
-
* `D1KaizenGradingRepository`, migration 0015). One row per (run, step); recommendations
|
|
2403
|
-
* are a JSON array. The unique (execution_id, step_index) index keeps scheduling
|
|
2404
|
-
* idempotent across durable re-drives.
|
|
2405
|
-
*/
|
|
2406
|
-
class DrizzleKaizenGradingRepository {
|
|
2407
|
-
db;
|
|
2408
|
-
constructor(db) {
|
|
2409
|
-
this.db = db;
|
|
2410
|
-
}
|
|
2411
|
-
async upsert(workspaceId, grading) {
|
|
2412
|
-
const values = {
|
|
2413
|
-
workspace_id: workspaceId,
|
|
2414
|
-
id: grading.id,
|
|
2415
|
-
execution_id: grading.executionId,
|
|
2416
|
-
block_id: grading.blockId,
|
|
2417
|
-
step_index: grading.stepIndex,
|
|
2418
|
-
agent_kind: grading.agentKind,
|
|
2419
|
-
model: grading.model,
|
|
2420
|
-
prompt_version: grading.promptVersion,
|
|
2421
|
-
combo_key: grading.comboKey,
|
|
2422
|
-
status: grading.status,
|
|
2423
|
-
grade: grading.grade,
|
|
2424
|
-
summary: grading.summary,
|
|
2425
|
-
recommendations: JSON.stringify(grading.recommendations),
|
|
2426
|
-
grader_model: grading.graderModel,
|
|
2427
|
-
error: grading.error,
|
|
2428
|
-
created_at: grading.createdAt,
|
|
2429
|
-
updated_at: grading.updatedAt,
|
|
2430
|
-
};
|
|
2431
|
-
await this.db
|
|
2432
|
-
.insert(kaizenGradings)
|
|
2433
|
-
.values(values)
|
|
2434
|
-
.onConflictDoUpdate({
|
|
2435
|
-
target: [kaizenGradings.workspace_id, kaizenGradings.id],
|
|
2436
|
-
set: {
|
|
2437
|
-
status: values.status,
|
|
2438
|
-
grade: values.grade,
|
|
2439
|
-
summary: values.summary,
|
|
2440
|
-
recommendations: values.recommendations,
|
|
2441
|
-
grader_model: values.grader_model,
|
|
2442
|
-
error: values.error,
|
|
2443
|
-
updated_at: values.updated_at,
|
|
2444
|
-
},
|
|
2445
|
-
});
|
|
2446
|
-
}
|
|
2447
|
-
async get(workspaceId, id) {
|
|
2448
|
-
const rows = await this.db
|
|
2449
|
-
.select()
|
|
2450
|
-
.from(kaizenGradings)
|
|
2451
|
-
.where(and(eq(kaizenGradings.workspace_id, workspaceId), eq(kaizenGradings.id, id)))
|
|
2452
|
-
.limit(1);
|
|
2453
|
-
return rows[0] ? rowToKaizenGrading(rows[0]) : null;
|
|
2454
|
-
}
|
|
2455
|
-
async getByStep(workspaceId, executionId, stepIndex) {
|
|
2456
|
-
const rows = await this.db
|
|
2457
|
-
.select()
|
|
2458
|
-
.from(kaizenGradings)
|
|
2459
|
-
.where(and(eq(kaizenGradings.workspace_id, workspaceId), eq(kaizenGradings.execution_id, executionId), eq(kaizenGradings.step_index, stepIndex)))
|
|
2460
|
-
.limit(1);
|
|
2461
|
-
return rows[0] ? rowToKaizenGrading(rows[0]) : null;
|
|
2462
|
-
}
|
|
2463
|
-
async listByExecution(workspaceId, executionId) {
|
|
2464
|
-
const rows = await this.db
|
|
2465
|
-
.select()
|
|
2466
|
-
.from(kaizenGradings)
|
|
2467
|
-
.where(and(eq(kaizenGradings.workspace_id, workspaceId), eq(kaizenGradings.execution_id, executionId)))
|
|
2468
|
-
.orderBy(kaizenGradings.step_index);
|
|
2469
|
-
return rows.map(rowToKaizenGrading);
|
|
2470
|
-
}
|
|
2471
|
-
async listByWorkspace(workspaceId, limit = 200) {
|
|
2472
|
-
const rows = await this.db
|
|
2473
|
-
.select()
|
|
2474
|
-
.from(kaizenGradings)
|
|
2475
|
-
.where(eq(kaizenGradings.workspace_id, workspaceId))
|
|
2476
|
-
.orderBy(desc(kaizenGradings.created_at))
|
|
2477
|
-
.limit(limit);
|
|
2478
|
-
return rows.map(rowToKaizenGrading);
|
|
2479
|
-
}
|
|
2480
|
-
async listPending(staleBefore, limit) {
|
|
2481
|
-
const rows = await this.db
|
|
2482
|
-
.select()
|
|
2483
|
-
.from(kaizenGradings)
|
|
2484
|
-
.where(or(eq(kaizenGradings.status, 'scheduled'), and(eq(kaizenGradings.status, 'running'), lt(kaizenGradings.updated_at, staleBefore))))
|
|
2485
|
-
.orderBy(kaizenGradings.updated_at)
|
|
2486
|
-
.limit(limit);
|
|
2487
|
-
return rows.map((row) => ({ workspaceId: row.workspace_id, grading: rowToKaizenGrading(row) }));
|
|
2488
|
-
}
|
|
2489
|
-
async claim(workspaceId, id, staleBefore, now) {
|
|
2490
|
-
// Conditional flip to `running`: succeeds only if the row is still claimable (the same
|
|
2491
|
-
// predicate listPending selects on), so concurrent sweep passes can't both win it.
|
|
2492
|
-
const claimed = await this.db
|
|
2493
|
-
.update(kaizenGradings)
|
|
2494
|
-
.set({ status: 'running', updated_at: now })
|
|
2495
|
-
.where(and(eq(kaizenGradings.workspace_id, workspaceId), eq(kaizenGradings.id, id), or(eq(kaizenGradings.status, 'scheduled'), and(eq(kaizenGradings.status, 'running'), lt(kaizenGradings.updated_at, staleBefore)))))
|
|
2496
|
-
.returning({ id: kaizenGradings.id });
|
|
2497
|
-
return claimed.length > 0;
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
function rowToKaizenVerifiedCombo(row) {
|
|
2501
|
-
return {
|
|
2502
|
-
comboKey: row.combo_key,
|
|
2503
|
-
agentKind: row.agent_kind,
|
|
2504
|
-
model: row.model,
|
|
2505
|
-
promptVersion: row.prompt_version,
|
|
2506
|
-
consecutiveHighGrades: row.consecutive_high_grades,
|
|
2507
|
-
verified: row.verified === 1,
|
|
2508
|
-
verifiedAt: row.verified_at,
|
|
2509
|
-
updatedAt: row.updated_at,
|
|
2510
|
-
};
|
|
2511
|
-
}
|
|
2512
|
-
/**
|
|
2513
|
-
* Kaizen verified-combo progress over Postgres (the Drizzle mirror of the Worker's
|
|
2514
|
-
* `D1KaizenVerifiedComboRepository`, migration 0015).
|
|
2515
|
-
*/
|
|
2516
|
-
class DrizzleKaizenVerifiedComboRepository {
|
|
2517
|
-
db;
|
|
2518
|
-
constructor(db) {
|
|
2519
|
-
this.db = db;
|
|
2520
|
-
}
|
|
2521
|
-
async getByKey(workspaceId, comboKey) {
|
|
2522
|
-
const rows = await this.db
|
|
2523
|
-
.select()
|
|
2524
|
-
.from(kaizenVerifiedCombos)
|
|
2525
|
-
.where(and(eq(kaizenVerifiedCombos.workspace_id, workspaceId), eq(kaizenVerifiedCombos.combo_key, comboKey)))
|
|
2526
|
-
.limit(1);
|
|
2527
|
-
return rows[0] ? rowToKaizenVerifiedCombo(rows[0]) : null;
|
|
2528
|
-
}
|
|
2529
|
-
async upsert(workspaceId, combo) {
|
|
2530
|
-
const values = {
|
|
2531
|
-
workspace_id: workspaceId,
|
|
2532
|
-
combo_key: combo.comboKey,
|
|
2533
|
-
agent_kind: combo.agentKind,
|
|
2534
|
-
model: combo.model,
|
|
2535
|
-
prompt_version: combo.promptVersion,
|
|
2536
|
-
consecutive_high_grades: combo.consecutiveHighGrades,
|
|
2537
|
-
verified: combo.verified ? 1 : 0,
|
|
2538
|
-
verified_at: combo.verifiedAt,
|
|
2539
|
-
updated_at: combo.updatedAt,
|
|
2540
|
-
};
|
|
2541
|
-
await this.db
|
|
2542
|
-
.insert(kaizenVerifiedCombos)
|
|
2543
|
-
.values(values)
|
|
2544
|
-
.onConflictDoUpdate({
|
|
2545
|
-
target: [kaizenVerifiedCombos.workspace_id, kaizenVerifiedCombos.combo_key],
|
|
2546
|
-
set: {
|
|
2547
|
-
consecutive_high_grades: values.consecutive_high_grades,
|
|
2548
|
-
verified: values.verified,
|
|
2549
|
-
verified_at: values.verified_at,
|
|
2550
|
-
updated_at: values.updated_at,
|
|
2551
|
-
},
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
async listByWorkspace(workspaceId) {
|
|
2555
|
-
const rows = await this.db
|
|
2556
|
-
.select()
|
|
2557
|
-
.from(kaizenVerifiedCombos)
|
|
2558
|
-
.where(eq(kaizenVerifiedCombos.workspace_id, workspaceId))
|
|
2559
|
-
.orderBy(desc(kaizenVerifiedCombos.updated_at));
|
|
2560
|
-
return rows.map(rowToKaizenVerifiedCombo);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
function parseJsonArray(raw) {
|
|
2564
|
-
try {
|
|
2565
|
-
const parsed = JSON.parse(raw);
|
|
2566
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
2567
|
-
}
|
|
2568
|
-
catch {
|
|
2569
|
-
return [];
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
function rowToConsensusSession(row) {
|
|
2573
|
-
return {
|
|
2574
|
-
id: row.id,
|
|
2575
|
-
blockId: row.block_id,
|
|
2576
|
-
executionId: row.execution_id,
|
|
2577
|
-
stepIndex: row.step_index,
|
|
2578
|
-
agentKind: row.agent_kind,
|
|
2579
|
-
strategy: row.strategy,
|
|
2580
|
-
status: row.status,
|
|
2581
|
-
participants: parseJsonArray(row.participants),
|
|
2582
|
-
rounds: parseJsonArray(row.rounds),
|
|
2583
|
-
synthesis: row.synthesis,
|
|
2584
|
-
confidence: row.confidence,
|
|
2585
|
-
dissent: parseJsonArray(row.dissent),
|
|
2586
|
-
error: row.error,
|
|
2587
|
-
createdAt: row.created_at,
|
|
2588
|
-
updatedAt: row.updated_at,
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
function rowToClarityReview(row) {
|
|
2592
|
-
let items = [];
|
|
2593
|
-
try {
|
|
2594
|
-
const parsed = JSON.parse(row.items);
|
|
2595
|
-
if (Array.isArray(parsed))
|
|
2596
|
-
items = parsed;
|
|
2597
|
-
}
|
|
2598
|
-
catch {
|
|
2599
|
-
items = [];
|
|
2600
|
-
}
|
|
2601
|
-
return {
|
|
2602
|
-
id: row.id,
|
|
2603
|
-
blockId: row.block_id,
|
|
2604
|
-
status: row.status,
|
|
2605
|
-
items,
|
|
2606
|
-
model: row.model,
|
|
2607
|
-
clarifiedReport: row.clarified_report,
|
|
2608
|
-
iteration: row.iteration,
|
|
2609
|
-
maxIterations: row.max_iterations,
|
|
2610
|
-
createdAt: row.created_at,
|
|
2611
|
-
updatedAt: row.updated_at,
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
/**
|
|
2615
|
-
* Consensus session transcripts (`consensus_sessions`), the Drizzle/Postgres mirror of
|
|
2616
|
-
* {@link D1ConsensusSessionRepository}. One row per (execution, step); the
|
|
2617
|
-
* participants/rounds/dissent live as JSON columns, upserted as the process streams.
|
|
2618
|
-
*/
|
|
2619
|
-
class DrizzleConsensusSessionRepository {
|
|
2620
|
-
db;
|
|
2621
|
-
constructor(db) {
|
|
2622
|
-
this.db = db;
|
|
2623
|
-
}
|
|
2624
|
-
async get(workspaceId, id) {
|
|
2625
|
-
const rows = await this.db
|
|
2626
|
-
.select()
|
|
2627
|
-
.from(consensusSessions)
|
|
2628
|
-
.where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.id, id)))
|
|
2629
|
-
.limit(1);
|
|
2630
|
-
return rows[0] ? rowToConsensusSession(rows[0]) : null;
|
|
2631
|
-
}
|
|
2632
|
-
async getByStep(workspaceId, executionId, stepIndex) {
|
|
2633
|
-
const rows = await this.db
|
|
2634
|
-
.select()
|
|
2635
|
-
.from(consensusSessions)
|
|
2636
|
-
.where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.execution_id, executionId), eq(consensusSessions.step_index, stepIndex)))
|
|
2637
|
-
.orderBy(desc(consensusSessions.created_at))
|
|
2638
|
-
.limit(1);
|
|
2639
|
-
return rows[0] ? rowToConsensusSession(rows[0]) : null;
|
|
2640
|
-
}
|
|
2641
|
-
async getByBlock(workspaceId, blockId) {
|
|
2642
|
-
const rows = await this.db
|
|
2643
|
-
.select()
|
|
2644
|
-
.from(consensusSessions)
|
|
2645
|
-
.where(and(eq(consensusSessions.workspace_id, workspaceId), eq(consensusSessions.block_id, blockId)))
|
|
2646
|
-
.orderBy(desc(consensusSessions.created_at))
|
|
2647
|
-
.limit(1);
|
|
2648
|
-
return rows[0] ? rowToConsensusSession(rows[0]) : null;
|
|
2649
|
-
}
|
|
2650
|
-
async upsert(workspaceId, session) {
|
|
2651
|
-
const values = {
|
|
2652
|
-
workspace_id: workspaceId,
|
|
2653
|
-
id: session.id,
|
|
2654
|
-
block_id: session.blockId,
|
|
2655
|
-
execution_id: session.executionId,
|
|
2656
|
-
step_index: session.stepIndex,
|
|
2657
|
-
agent_kind: session.agentKind,
|
|
2658
|
-
strategy: session.strategy,
|
|
2659
|
-
status: session.status,
|
|
2660
|
-
participants: JSON.stringify(session.participants),
|
|
2661
|
-
rounds: JSON.stringify(session.rounds),
|
|
2662
|
-
synthesis: session.synthesis,
|
|
2663
|
-
confidence: session.confidence ?? null,
|
|
2664
|
-
dissent: JSON.stringify(session.dissent ?? []),
|
|
2665
|
-
error: session.error ?? null,
|
|
2666
|
-
created_at: session.createdAt,
|
|
2667
|
-
updated_at: session.updatedAt,
|
|
2668
|
-
};
|
|
2669
|
-
await this.db
|
|
2670
|
-
.insert(consensusSessions)
|
|
2671
|
-
.values(values)
|
|
2672
|
-
.onConflictDoUpdate({
|
|
2673
|
-
target: [consensusSessions.workspace_id, consensusSessions.id],
|
|
2674
|
-
set: {
|
|
2675
|
-
block_id: values.block_id,
|
|
2676
|
-
execution_id: values.execution_id,
|
|
2677
|
-
step_index: values.step_index,
|
|
2678
|
-
agent_kind: values.agent_kind,
|
|
2679
|
-
strategy: values.strategy,
|
|
2680
|
-
status: values.status,
|
|
2681
|
-
participants: values.participants,
|
|
2682
|
-
rounds: values.rounds,
|
|
2683
|
-
synthesis: values.synthesis,
|
|
2684
|
-
confidence: values.confidence,
|
|
2685
|
-
dissent: values.dissent,
|
|
2686
|
-
error: values.error,
|
|
2687
|
-
updated_at: values.updated_at,
|
|
2688
|
-
},
|
|
2689
|
-
});
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
/**
|
|
2693
|
-
* Clarity (bug-report triage) reviews over Postgres — the Drizzle mirror of the Worker's
|
|
2694
|
-
* `D1ClarityReviewRepository`. Behaviourally identical to the D1 repo so the cross-runtime
|
|
2695
|
-
* conformance suite asserts the same clarified-brief substitution against both stores.
|
|
2696
|
-
*/
|
|
2697
|
-
export class DrizzleClarityReviewRepository {
|
|
2698
|
-
db;
|
|
2699
|
-
constructor(db) {
|
|
2700
|
-
this.db = db;
|
|
2701
|
-
}
|
|
2702
|
-
async getByBlock(workspaceId, blockId) {
|
|
2703
|
-
const rows = await this.db
|
|
2704
|
-
.select()
|
|
2705
|
-
.from(clarityReviews)
|
|
2706
|
-
.where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.block_id, blockId)))
|
|
2707
|
-
.orderBy(desc(clarityReviews.created_at))
|
|
2708
|
-
.limit(1);
|
|
2709
|
-
return rows[0] ? rowToClarityReview(rows[0]) : null;
|
|
2710
|
-
}
|
|
2711
|
-
async get(workspaceId, id) {
|
|
2712
|
-
const rows = await this.db
|
|
2713
|
-
.select()
|
|
2714
|
-
.from(clarityReviews)
|
|
2715
|
-
.where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.id, id)))
|
|
2716
|
-
.limit(1);
|
|
2717
|
-
return rows[0] ? rowToClarityReview(rows[0]) : null;
|
|
2718
|
-
}
|
|
2719
|
-
async upsert(workspaceId, review) {
|
|
2720
|
-
const values = {
|
|
2721
|
-
workspace_id: workspaceId,
|
|
2722
|
-
id: review.id,
|
|
2723
|
-
block_id: review.blockId,
|
|
2724
|
-
status: review.status,
|
|
2725
|
-
items: JSON.stringify(review.items),
|
|
2726
|
-
model: review.model,
|
|
2727
|
-
clarified_report: review.clarifiedReport,
|
|
2728
|
-
iteration: review.iteration ?? 1,
|
|
2729
|
-
max_iterations: review.maxIterations ?? 1,
|
|
2730
|
-
created_at: review.createdAt,
|
|
2731
|
-
updated_at: review.updatedAt,
|
|
2732
|
-
};
|
|
2733
|
-
await this.db
|
|
2734
|
-
.insert(clarityReviews)
|
|
2735
|
-
.values(values)
|
|
2736
|
-
.onConflictDoUpdate({
|
|
2737
|
-
target: [clarityReviews.workspace_id, clarityReviews.id],
|
|
2738
|
-
set: {
|
|
2739
|
-
block_id: values.block_id,
|
|
2740
|
-
status: values.status,
|
|
2741
|
-
items: values.items,
|
|
2742
|
-
model: values.model,
|
|
2743
|
-
clarified_report: values.clarified_report,
|
|
2744
|
-
iteration: values.iteration,
|
|
2745
|
-
max_iterations: values.max_iterations,
|
|
2746
|
-
updated_at: values.updated_at,
|
|
2747
|
-
},
|
|
2748
|
-
});
|
|
2749
|
-
}
|
|
2750
|
-
async deleteByBlock(workspaceId, blockId) {
|
|
2751
|
-
await this.db
|
|
2752
|
-
.delete(clarityReviews)
|
|
2753
|
-
.where(and(eq(clarityReviews.workspace_id, workspaceId), eq(clarityReviews.block_id, blockId)));
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
function rowToBrainstormSession(row) {
|
|
2757
|
-
let items = [];
|
|
2758
|
-
try {
|
|
2759
|
-
const parsed = JSON.parse(row.items);
|
|
2760
|
-
if (Array.isArray(parsed))
|
|
2761
|
-
items = parsed;
|
|
2762
|
-
}
|
|
2763
|
-
catch {
|
|
2764
|
-
items = [];
|
|
2765
|
-
}
|
|
2766
|
-
return {
|
|
2767
|
-
id: row.id,
|
|
2768
|
-
blockId: row.block_id,
|
|
2769
|
-
stage: row.stage,
|
|
2770
|
-
status: row.status,
|
|
2771
|
-
items,
|
|
2772
|
-
model: row.model,
|
|
2773
|
-
convergedDirection: row.converged_direction,
|
|
2774
|
-
iteration: row.iteration,
|
|
2775
|
-
maxIterations: row.max_iterations,
|
|
2776
|
-
createdAt: row.created_at,
|
|
2777
|
-
updatedAt: row.updated_at,
|
|
2778
|
-
};
|
|
2779
|
-
}
|
|
2780
|
-
/**
|
|
2781
|
-
* Brainstorm (structured-dialogue) sessions over Postgres — the Drizzle mirror of the Worker's
|
|
2782
|
-
* `D1BrainstormSessionRepository`. Behaviourally identical so the cross-runtime conformance
|
|
2783
|
-
* suite asserts the same per-stage round-trip and brainstorm direction handoff against both
|
|
2784
|
-
* stores. Keyed per (block, stage): a block may hold a live `requirements` AND `architecture`
|
|
2785
|
-
* session at once.
|
|
2786
|
-
*/
|
|
2787
|
-
class DrizzleBrainstormSessionRepository {
|
|
2788
|
-
db;
|
|
2789
|
-
constructor(db) {
|
|
2790
|
-
this.db = db;
|
|
2791
|
-
}
|
|
2792
|
-
async getByBlockStage(workspaceId, blockId, stage) {
|
|
2793
|
-
const rows = await this.db
|
|
2794
|
-
.select()
|
|
2795
|
-
.from(brainstormSessions)
|
|
2796
|
-
.where(and(eq(brainstormSessions.workspace_id, workspaceId), eq(brainstormSessions.block_id, blockId), eq(brainstormSessions.stage, stage)))
|
|
2797
|
-
.orderBy(desc(brainstormSessions.created_at))
|
|
2798
|
-
.limit(1);
|
|
2799
|
-
return rows[0] ? rowToBrainstormSession(rows[0]) : null;
|
|
2800
|
-
}
|
|
2801
|
-
async get(workspaceId, id) {
|
|
2802
|
-
const rows = await this.db
|
|
2803
|
-
.select()
|
|
2804
|
-
.from(brainstormSessions)
|
|
2805
|
-
.where(and(eq(brainstormSessions.workspace_id, workspaceId), eq(brainstormSessions.id, id)))
|
|
2806
|
-
.limit(1);
|
|
2807
|
-
return rows[0] ? rowToBrainstormSession(rows[0]) : null;
|
|
2808
|
-
}
|
|
2809
|
-
async upsert(workspaceId, session) {
|
|
2810
|
-
const values = {
|
|
2811
|
-
workspace_id: workspaceId,
|
|
2812
|
-
id: session.id,
|
|
2813
|
-
block_id: session.blockId,
|
|
2814
|
-
stage: session.stage,
|
|
2815
|
-
status: session.status,
|
|
2816
|
-
items: JSON.stringify(session.items),
|
|
2817
|
-
model: session.model,
|
|
2818
|
-
converged_direction: session.convergedDirection,
|
|
2819
|
-
iteration: session.iteration ?? 1,
|
|
2820
|
-
max_iterations: session.maxIterations ?? 1,
|
|
2821
|
-
created_at: session.createdAt,
|
|
2822
|
-
updated_at: session.updatedAt,
|
|
2823
|
-
};
|
|
2824
|
-
await this.db
|
|
2825
|
-
.insert(brainstormSessions)
|
|
2826
|
-
.values(values)
|
|
2827
|
-
.onConflictDoUpdate({
|
|
2828
|
-
target: [brainstormSessions.workspace_id, brainstormSessions.id],
|
|
2829
|
-
set: {
|
|
2830
|
-
block_id: values.block_id,
|
|
2831
|
-
stage: values.stage,
|
|
2832
|
-
status: values.status,
|
|
2833
|
-
items: values.items,
|
|
2834
|
-
model: values.model,
|
|
2835
|
-
converged_direction: values.converged_direction,
|
|
2836
|
-
iteration: values.iteration,
|
|
2837
|
-
max_iterations: values.max_iterations,
|
|
2838
|
-
updated_at: values.updated_at,
|
|
2839
|
-
},
|
|
2840
|
-
});
|
|
2841
|
-
}
|
|
2842
|
-
async deleteByBlockStage(workspaceId, blockId, stage) {
|
|
2843
|
-
await this.db
|
|
2844
|
-
.delete(brainstormSessions)
|
|
2845
|
-
.where(and(eq(brainstormSessions.workspace_id, workspaceId), eq(brainstormSessions.block_id, blockId), eq(brainstormSessions.stage, stage)));
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
// The row → entity decode (doc blob + column-lifted keys) is the shared
|
|
2849
|
-
// `decodeInitiativeRow` (contracts), so the Drizzle and D1 repos can't drift.
|
|
2850
|
-
const rowToInitiative = decodeInitiativeRow;
|
|
2851
|
-
/**
|
|
2852
|
-
* Initiatives over Postgres — the Drizzle mirror of the Worker's
|
|
2853
|
-
* `D1InitiativeRepository` (migration 0035). Behaviourally identical so the
|
|
2854
|
-
* cross-runtime conformance suite asserts the same CRUD + rev-guarded CAS against
|
|
2855
|
-
* both stores.
|
|
2856
|
-
*/
|
|
2857
|
-
class DrizzleInitiativeRepository {
|
|
2858
|
-
db;
|
|
2859
|
-
constructor(db) {
|
|
2860
|
-
this.db = db;
|
|
2861
|
-
}
|
|
2862
|
-
async get(workspaceId, id) {
|
|
2863
|
-
const rows = await this.db
|
|
2864
|
-
.select()
|
|
2865
|
-
.from(initiatives)
|
|
2866
|
-
.where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.id, id)))
|
|
2867
|
-
.limit(1);
|
|
2868
|
-
return rows[0] ? rowToInitiative(rows[0]) : null;
|
|
2869
|
-
}
|
|
2870
|
-
async getByBlock(workspaceId, blockId) {
|
|
2871
|
-
const rows = await this.db
|
|
2872
|
-
.select()
|
|
2873
|
-
.from(initiatives)
|
|
2874
|
-
.where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.block_id, blockId)))
|
|
2875
|
-
.limit(1);
|
|
2876
|
-
return rows[0] ? rowToInitiative(rows[0]) : null;
|
|
2877
|
-
}
|
|
2878
|
-
async list(workspaceId) {
|
|
2879
|
-
const rows = await this.db
|
|
2880
|
-
.select()
|
|
2881
|
-
.from(initiatives)
|
|
2882
|
-
.where(eq(initiatives.workspace_id, workspaceId))
|
|
2883
|
-
.orderBy(asc(initiatives.created_at));
|
|
2884
|
-
// Snapshot-facing list read: drop a corrupt row rather than failing the board load.
|
|
2885
|
-
return rows.map(rowToInitiative).filter((i) => i !== null);
|
|
2886
|
-
}
|
|
2887
|
-
async listExecuting() {
|
|
2888
|
-
const rows = await this.db
|
|
2889
|
-
.select()
|
|
2890
|
-
.from(initiatives)
|
|
2891
|
-
.where(eq(initiatives.status, 'executing'))
|
|
2892
|
-
.orderBy(asc(initiatives.created_at));
|
|
2893
|
-
return rows
|
|
2894
|
-
.map((row) => {
|
|
2895
|
-
const initiative = rowToInitiative(row);
|
|
2896
|
-
return initiative ? { workspaceId: row.workspace_id, initiative } : null;
|
|
2897
|
-
})
|
|
2898
|
-
.filter((r) => r !== null);
|
|
2899
|
-
}
|
|
2900
|
-
async insert(workspaceId, initiative) {
|
|
2901
|
-
await this.db.insert(initiatives).values({
|
|
2902
|
-
workspace_id: workspaceId,
|
|
2903
|
-
id: initiative.id,
|
|
2904
|
-
block_id: initiative.blockId,
|
|
2905
|
-
slug: initiative.slug,
|
|
2906
|
-
status: initiative.status,
|
|
2907
|
-
rev: initiative.rev,
|
|
2908
|
-
doc: JSON.stringify(initiative),
|
|
2909
|
-
created_at: initiative.createdAt,
|
|
2910
|
-
updated_at: initiative.updatedAt,
|
|
2911
|
-
});
|
|
2912
|
-
}
|
|
2913
|
-
async compareAndSwap(workspaceId, next, expectedRev) {
|
|
2914
|
-
const result = await this.db
|
|
2915
|
-
.update(initiatives)
|
|
2916
|
-
.set({
|
|
2917
|
-
slug: next.slug,
|
|
2918
|
-
status: next.status,
|
|
2919
|
-
rev: next.rev,
|
|
2920
|
-
doc: JSON.stringify(next),
|
|
2921
|
-
updated_at: next.updatedAt,
|
|
2922
|
-
})
|
|
2923
|
-
.where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.id, next.id), eq(initiatives.rev, expectedRev)));
|
|
2924
|
-
return (result.rowCount ?? 0) > 0;
|
|
2925
|
-
}
|
|
2926
|
-
async delete(workspaceId, id) {
|
|
2927
|
-
await this.db
|
|
2928
|
-
.delete(initiatives)
|
|
2929
|
-
.where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.id, id)));
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
function rowToRiskPolicy(row) {
|
|
2933
|
-
return {
|
|
2934
|
-
id: row.id,
|
|
2935
|
-
name: row.name,
|
|
2936
|
-
maxComplexity: row.max_complexity,
|
|
2937
|
-
maxRisk: row.max_risk,
|
|
2938
|
-
maxImpact: row.max_impact,
|
|
2939
|
-
ciMaxAttempts: row.ci_max_attempts,
|
|
2940
|
-
maxRequirementIterations: row.max_requirement_iterations,
|
|
2941
|
-
maxRequirementConcernAllowed: row.max_requirement_concern_allowed,
|
|
2942
|
-
maxTesterQualityIterations: row.max_tester_quality_iterations,
|
|
2943
|
-
releaseWatchWindowMinutes: row.release_watch_window_minutes,
|
|
2944
|
-
releaseMaxAttempts: row.release_max_attempts,
|
|
2945
|
-
humanReviewGraceMinutes: row.human_review_grace_minutes,
|
|
2946
|
-
autoMergeEnabled: row.auto_merge_enabled === 1,
|
|
2947
|
-
forkDecision: row.fork_decision
|
|
2948
|
-
? JSON.parse(row.fork_decision)
|
|
2949
|
-
: null,
|
|
2950
|
-
isDefault: row.is_default === 1,
|
|
2951
|
-
...(row.version != null ? { version: row.version } : {}),
|
|
2952
|
-
createdAt: row.created_at,
|
|
2953
|
-
};
|
|
2954
|
-
}
|
|
2955
|
-
/**
|
|
2956
|
-
* Per-workspace merge threshold presets over Postgres (the Drizzle mirror of the
|
|
2957
|
-
* Worker's `D1RiskPolicyRepository`, migration 0024). Enforces the single-default
|
|
2958
|
-
* invariant: promoting a preset to default demotes every other in the workspace
|
|
2959
|
-
* before the upsert. The default preset cannot be removed (the service keeps that
|
|
2960
|
-
* rule too; the DELETE also guards `is_default = 0`). Behaviourally identical to the
|
|
2961
|
-
* D1 repo so the cross-runtime conformance suite asserts the same preset resolution.
|
|
2962
|
-
*/
|
|
2963
|
-
class DrizzleRiskPolicyRepository {
|
|
2964
|
-
db;
|
|
2965
|
-
constructor(db) {
|
|
2966
|
-
this.db = db;
|
|
2967
|
-
}
|
|
2968
|
-
async get(workspaceId, id) {
|
|
2969
|
-
const rows = await this.db
|
|
2970
|
-
.select()
|
|
2971
|
-
.from(riskPolicies)
|
|
2972
|
-
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id)))
|
|
2973
|
-
.limit(1);
|
|
2974
|
-
return rows[0] ? rowToRiskPolicy(rows[0]) : null;
|
|
2975
|
-
}
|
|
2976
|
-
async list(workspaceId) {
|
|
2977
|
-
const rows = await this.db
|
|
2978
|
-
.select()
|
|
2979
|
-
.from(riskPolicies)
|
|
2980
|
-
.where(eq(riskPolicies.workspace_id, workspaceId))
|
|
2981
|
-
.orderBy(riskPolicies.created_at);
|
|
2982
|
-
return rows.map(rowToRiskPolicy);
|
|
2983
|
-
}
|
|
2984
|
-
async getDefault(workspaceId) {
|
|
2985
|
-
const rows = await this.db
|
|
2986
|
-
.select()
|
|
2987
|
-
.from(riskPolicies)
|
|
2988
|
-
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.is_default, 1)))
|
|
2989
|
-
.orderBy(riskPolicies.created_at)
|
|
2990
|
-
.limit(1);
|
|
2991
|
-
return rows[0] ? rowToRiskPolicy(rows[0]) : null;
|
|
2992
|
-
}
|
|
2993
|
-
async upsert(workspaceId, preset) {
|
|
2994
|
-
const values = {
|
|
2995
|
-
workspace_id: workspaceId,
|
|
2996
|
-
id: preset.id,
|
|
2997
|
-
name: preset.name,
|
|
2998
|
-
max_complexity: preset.maxComplexity,
|
|
2999
|
-
max_risk: preset.maxRisk,
|
|
3000
|
-
max_impact: preset.maxImpact,
|
|
3001
|
-
ci_max_attempts: preset.ciMaxAttempts,
|
|
3002
|
-
max_requirement_iterations: preset.maxRequirementIterations,
|
|
3003
|
-
max_requirement_concern_allowed: preset.maxRequirementConcernAllowed,
|
|
3004
|
-
max_tester_quality_iterations: preset.maxTesterQualityIterations,
|
|
3005
|
-
release_watch_window_minutes: preset.releaseWatchWindowMinutes,
|
|
3006
|
-
release_max_attempts: preset.releaseMaxAttempts,
|
|
3007
|
-
human_review_grace_minutes: preset.humanReviewGraceMinutes,
|
|
3008
|
-
auto_merge_enabled: preset.autoMergeEnabled ? 1 : 0,
|
|
3009
|
-
fork_decision: preset.forkDecision ? JSON.stringify(preset.forkDecision) : null,
|
|
3010
|
-
version: preset.version ?? null,
|
|
3011
|
-
is_default: preset.isDefault ? 1 : 0,
|
|
3012
|
-
created_at: preset.createdAt,
|
|
3013
|
-
};
|
|
3014
|
-
// Demote + upsert run in one transaction so the single-default invariant can never
|
|
3015
|
-
// be observed broken (zero or two defaults) by a concurrent reader or a partial failure.
|
|
3016
|
-
await this.db.transaction(async (tx) => {
|
|
3017
|
-
// Promoting this preset to default demotes any other default first.
|
|
3018
|
-
if (preset.isDefault) {
|
|
3019
|
-
await tx
|
|
3020
|
-
.update(riskPolicies)
|
|
3021
|
-
.set({ is_default: 0 })
|
|
3022
|
-
.where(and(eq(riskPolicies.workspace_id, workspaceId), sql `${riskPolicies.id} <> ${preset.id}`));
|
|
3023
|
-
}
|
|
3024
|
-
await tx
|
|
3025
|
-
.insert(riskPolicies)
|
|
3026
|
-
.values(values)
|
|
3027
|
-
.onConflictDoUpdate({
|
|
3028
|
-
target: [riskPolicies.workspace_id, riskPolicies.id],
|
|
3029
|
-
set: {
|
|
3030
|
-
name: values.name,
|
|
3031
|
-
max_complexity: values.max_complexity,
|
|
3032
|
-
max_risk: values.max_risk,
|
|
3033
|
-
max_impact: values.max_impact,
|
|
3034
|
-
ci_max_attempts: values.ci_max_attempts,
|
|
3035
|
-
max_requirement_iterations: values.max_requirement_iterations,
|
|
3036
|
-
max_requirement_concern_allowed: values.max_requirement_concern_allowed,
|
|
3037
|
-
max_tester_quality_iterations: values.max_tester_quality_iterations,
|
|
3038
|
-
release_watch_window_minutes: values.release_watch_window_minutes,
|
|
3039
|
-
release_max_attempts: values.release_max_attempts,
|
|
3040
|
-
human_review_grace_minutes: values.human_review_grace_minutes,
|
|
3041
|
-
auto_merge_enabled: values.auto_merge_enabled,
|
|
3042
|
-
fork_decision: values.fork_decision,
|
|
3043
|
-
version: values.version,
|
|
3044
|
-
is_default: values.is_default,
|
|
3045
|
-
},
|
|
3046
|
-
});
|
|
3047
|
-
});
|
|
3048
|
-
}
|
|
3049
|
-
async remove(workspaceId, id) {
|
|
3050
|
-
await this.db
|
|
3051
|
-
.delete(riskPolicies)
|
|
3052
|
-
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id), eq(riskPolicies.is_default, 0)));
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
// Shape-guarded parsers matching the D1 mirror (`D1SharedStackRepository`) EXACTLY, so a
|
|
3056
|
-
// malformed/hand-edited JSON column coerces identically on both stores (a non-array ⇒ `[]`, a
|
|
3057
|
-
// non-object health gate ⇒ `null`) rather than the Node facade handing the domain a raw value the
|
|
3058
|
-
// Worker would have dropped — the "keep the runtimes symmetric" guarantee holds for bad data too.
|
|
3059
|
-
function parseSharedStackArray(json) {
|
|
3060
|
-
try {
|
|
3061
|
-
const parsed = JSON.parse(json);
|
|
3062
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
3063
|
-
}
|
|
3064
|
-
catch {
|
|
3065
|
-
return [];
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
function parseSharedStackHealthGate(json) {
|
|
3069
|
-
if (!json)
|
|
3070
|
-
return null;
|
|
3071
|
-
try {
|
|
3072
|
-
const parsed = JSON.parse(json);
|
|
3073
|
-
return parsed && typeof parsed === 'object'
|
|
3074
|
-
? parsed
|
|
3075
|
-
: null;
|
|
3076
|
-
}
|
|
3077
|
-
catch {
|
|
3078
|
-
return null;
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
function rowToSharedStack(row) {
|
|
3082
|
-
return {
|
|
3083
|
-
id: row.id,
|
|
3084
|
-
workspaceId: row.workspace_id,
|
|
3085
|
-
name: row.name,
|
|
3086
|
-
cloneUrl: row.clone_url,
|
|
3087
|
-
gitRef: row.git_ref,
|
|
3088
|
-
composeFiles: parseSharedStackArray(row.compose_files),
|
|
3089
|
-
composeProfiles: parseSharedStackArray(row.compose_profiles),
|
|
3090
|
-
envFiles: parseSharedStackArray(row.env_files),
|
|
3091
|
-
managedNetworks: parseSharedStackArray(row.managed_networks),
|
|
3092
|
-
setupSteps: parseSharedStackArray(row.setup_steps),
|
|
3093
|
-
prerequisites: parseSharedStackArray(row.prerequisites),
|
|
3094
|
-
healthGate: parseSharedStackHealthGate(row.health_gate),
|
|
3095
|
-
allowHostCommands: row.allow_host_commands === 1,
|
|
3096
|
-
status: row.status,
|
|
3097
|
-
lastError: row.last_error,
|
|
3098
|
-
createdAt: row.created_at,
|
|
3099
|
-
updatedAt: row.updated_at,
|
|
3100
|
-
};
|
|
3101
|
-
}
|
|
3102
|
-
/**
|
|
3103
|
-
* A workspace's shared stacks over Postgres (the Drizzle mirror of the Worker's
|
|
3104
|
-
* `D1SharedStackRepository`, migration 0041). JSON-shaped fields are stored as text JSON and
|
|
3105
|
-
* `allow_host_commands` as 0/1; behaviourally identical to the D1 repo so the cross-runtime
|
|
3106
|
-
* conformance suite asserts the same round-trip.
|
|
3107
|
-
*/
|
|
3108
|
-
class DrizzleSharedStackRepository {
|
|
3109
|
-
db;
|
|
3110
|
-
constructor(db) {
|
|
3111
|
-
this.db = db;
|
|
3112
|
-
}
|
|
3113
|
-
async get(workspaceId, id) {
|
|
3114
|
-
const rows = await this.db
|
|
3115
|
-
.select()
|
|
3116
|
-
.from(sharedStacks)
|
|
3117
|
-
.where(and(eq(sharedStacks.workspace_id, workspaceId), eq(sharedStacks.id, id)))
|
|
3118
|
-
.limit(1);
|
|
3119
|
-
return rows[0] ? rowToSharedStack(rows[0]) : null;
|
|
3120
|
-
}
|
|
3121
|
-
async list(workspaceId) {
|
|
3122
|
-
const rows = await this.db
|
|
3123
|
-
.select()
|
|
3124
|
-
.from(sharedStacks)
|
|
3125
|
-
.where(eq(sharedStacks.workspace_id, workspaceId))
|
|
3126
|
-
.orderBy(sharedStacks.created_at);
|
|
3127
|
-
return rows.map(rowToSharedStack);
|
|
3128
|
-
}
|
|
3129
|
-
async upsert(workspaceId, stack) {
|
|
3130
|
-
const values = {
|
|
3131
|
-
workspace_id: workspaceId,
|
|
3132
|
-
id: stack.id,
|
|
3133
|
-
name: stack.name,
|
|
3134
|
-
clone_url: stack.cloneUrl,
|
|
3135
|
-
git_ref: stack.gitRef,
|
|
3136
|
-
compose_files: JSON.stringify(stack.composeFiles),
|
|
3137
|
-
compose_profiles: JSON.stringify(stack.composeProfiles),
|
|
3138
|
-
env_files: JSON.stringify(stack.envFiles),
|
|
3139
|
-
managed_networks: JSON.stringify(stack.managedNetworks),
|
|
3140
|
-
setup_steps: JSON.stringify(stack.setupSteps),
|
|
3141
|
-
prerequisites: JSON.stringify(stack.prerequisites),
|
|
3142
|
-
health_gate: stack.healthGate ? JSON.stringify(stack.healthGate) : null,
|
|
3143
|
-
allow_host_commands: stack.allowHostCommands ? 1 : 0,
|
|
3144
|
-
status: stack.status,
|
|
3145
|
-
last_error: stack.lastError,
|
|
3146
|
-
created_at: stack.createdAt,
|
|
3147
|
-
updated_at: stack.updatedAt,
|
|
3148
|
-
};
|
|
3149
|
-
await this.db
|
|
3150
|
-
.insert(sharedStacks)
|
|
3151
|
-
.values(values)
|
|
3152
|
-
.onConflictDoUpdate({
|
|
3153
|
-
target: [sharedStacks.workspace_id, sharedStacks.id],
|
|
3154
|
-
set: {
|
|
3155
|
-
name: values.name,
|
|
3156
|
-
clone_url: values.clone_url,
|
|
3157
|
-
git_ref: values.git_ref,
|
|
3158
|
-
compose_files: values.compose_files,
|
|
3159
|
-
compose_profiles: values.compose_profiles,
|
|
3160
|
-
env_files: values.env_files,
|
|
3161
|
-
managed_networks: values.managed_networks,
|
|
3162
|
-
setup_steps: values.setup_steps,
|
|
3163
|
-
prerequisites: values.prerequisites,
|
|
3164
|
-
health_gate: values.health_gate,
|
|
3165
|
-
allow_host_commands: values.allow_host_commands,
|
|
3166
|
-
status: values.status,
|
|
3167
|
-
last_error: values.last_error,
|
|
3168
|
-
updated_at: values.updated_at,
|
|
3169
|
-
},
|
|
3170
|
-
});
|
|
3171
|
-
}
|
|
3172
|
-
async remove(workspaceId, id) {
|
|
3173
|
-
await this.db
|
|
3174
|
-
.delete(sharedStacks)
|
|
3175
|
-
.where(and(eq(sharedStacks.workspace_id, workspaceId), eq(sharedStacks.id, id)));
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
// ---- Sandbox (parallel prompt/model testing surface; migration 0012) --------
|
|
3179
|
-
// The Drizzle mirror of the Worker's five `D1Sandbox*Repository` classes. JSON-shaped
|
|
3180
|
-
// fields are stored as text JSON, parsed defensively; behaviourally identical to the D1
|
|
3181
|
-
// repos so the cross-runtime conformance suite asserts the same Sandbox behaviour.
|
|
3182
|
-
class DrizzleSandboxPromptVersionRepository {
|
|
3183
|
-
db;
|
|
3184
|
-
constructor(db) {
|
|
3185
|
-
this.db = db;
|
|
3186
|
-
}
|
|
3187
|
-
async get(workspaceId, id) {
|
|
3188
|
-
const rows = await this.db
|
|
3189
|
-
.select()
|
|
3190
|
-
.from(sandboxPromptVersions)
|
|
3191
|
-
.where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.id, id)))
|
|
3192
|
-
.limit(1);
|
|
3193
|
-
return rows[0] ? rowToSandboxPromptVersion(rows[0]) : null;
|
|
3194
|
-
}
|
|
3195
|
-
async list(workspaceId) {
|
|
3196
|
-
const rows = await this.db
|
|
3197
|
-
.select()
|
|
3198
|
-
.from(sandboxPromptVersions)
|
|
3199
|
-
.where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), isNull(sandboxPromptVersions.archived_at)))
|
|
3200
|
-
.orderBy(desc(sandboxPromptVersions.created_at));
|
|
3201
|
-
return rows.map((r) => rowToSandboxPromptVersion(r));
|
|
3202
|
-
}
|
|
3203
|
-
async listByKind(workspaceId, agentKind) {
|
|
3204
|
-
const rows = await this.db
|
|
3205
|
-
.select()
|
|
3206
|
-
.from(sandboxPromptVersions)
|
|
3207
|
-
.where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.agent_kind, agentKind), isNull(sandboxPromptVersions.archived_at)))
|
|
3208
|
-
.orderBy(desc(sandboxPromptVersions.created_at));
|
|
3209
|
-
return rows.map((r) => rowToSandboxPromptVersion(r));
|
|
3210
|
-
}
|
|
3211
|
-
async upsert(workspaceId, version) {
|
|
3212
|
-
const values = {
|
|
3213
|
-
workspace_id: workspaceId,
|
|
3214
|
-
id: version.id,
|
|
3215
|
-
lineage_id: version.lineageId,
|
|
3216
|
-
agent_kind: version.agentKind,
|
|
3217
|
-
name: version.name,
|
|
3218
|
-
origin: version.origin,
|
|
3219
|
-
system_text: version.systemText,
|
|
3220
|
-
base_prompt_id: version.basePromptId,
|
|
3221
|
-
version: version.version,
|
|
3222
|
-
parent_id: version.parentId,
|
|
3223
|
-
labels: JSON.stringify(version.labels),
|
|
3224
|
-
created_at: version.createdAt,
|
|
3225
|
-
created_by: version.createdBy,
|
|
3226
|
-
archived_at: version.archivedAt,
|
|
3227
|
-
};
|
|
3228
|
-
await this.db
|
|
3229
|
-
.insert(sandboxPromptVersions)
|
|
3230
|
-
.values(values)
|
|
3231
|
-
.onConflictDoUpdate({
|
|
3232
|
-
target: [sandboxPromptVersions.workspace_id, sandboxPromptVersions.id],
|
|
3233
|
-
set: {
|
|
3234
|
-
lineage_id: values.lineage_id,
|
|
3235
|
-
agent_kind: values.agent_kind,
|
|
3236
|
-
name: values.name,
|
|
3237
|
-
origin: values.origin,
|
|
3238
|
-
system_text: values.system_text,
|
|
3239
|
-
base_prompt_id: values.base_prompt_id,
|
|
3240
|
-
version: values.version,
|
|
3241
|
-
parent_id: values.parent_id,
|
|
3242
|
-
labels: values.labels,
|
|
3243
|
-
created_by: values.created_by,
|
|
3244
|
-
archived_at: values.archived_at,
|
|
3245
|
-
},
|
|
3246
|
-
});
|
|
3247
|
-
}
|
|
3248
|
-
async archive(workspaceId, id, at) {
|
|
3249
|
-
await this.db
|
|
3250
|
-
.update(sandboxPromptVersions)
|
|
3251
|
-
.set({ archived_at: at })
|
|
3252
|
-
.where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.id, id)));
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
class DrizzleSandboxFixtureRepository {
|
|
3256
|
-
db;
|
|
3257
|
-
constructor(db) {
|
|
3258
|
-
this.db = db;
|
|
3259
|
-
}
|
|
3260
|
-
async get(workspaceId, id) {
|
|
3261
|
-
const rows = await this.db
|
|
3262
|
-
.select()
|
|
3263
|
-
.from(sandboxFixtures)
|
|
3264
|
-
.where(and(eq(sandboxFixtures.workspace_id, workspaceId), eq(sandboxFixtures.id, id)))
|
|
3265
|
-
.limit(1);
|
|
3266
|
-
return rows[0] ? rowToSandboxFixture(rows[0]) : null;
|
|
3267
|
-
}
|
|
3268
|
-
async list(workspaceId) {
|
|
3269
|
-
const rows = await this.db
|
|
3270
|
-
.select()
|
|
3271
|
-
.from(sandboxFixtures)
|
|
3272
|
-
.where(eq(sandboxFixtures.workspace_id, workspaceId))
|
|
3273
|
-
.orderBy(sandboxFixtures.created_at);
|
|
3274
|
-
return rows.map((r) => rowToSandboxFixture(r));
|
|
3275
|
-
}
|
|
3276
|
-
async upsert(workspaceId, fixture) {
|
|
3277
|
-
const values = {
|
|
3278
|
-
workspace_id: workspaceId,
|
|
3279
|
-
id: fixture.id,
|
|
3280
|
-
kind: fixture.kind,
|
|
3281
|
-
name: fixture.name,
|
|
3282
|
-
payload: fixture.payload ? JSON.stringify(fixture.payload) : null,
|
|
3283
|
-
repo_ref: fixture.repoRef ? JSON.stringify(fixture.repoRef) : null,
|
|
3284
|
-
objective: fixture.objective ? JSON.stringify(fixture.objective) : null,
|
|
3285
|
-
origin: fixture.origin,
|
|
3286
|
-
created_at: fixture.createdAt,
|
|
3287
|
-
};
|
|
3288
|
-
await this.db
|
|
3289
|
-
.insert(sandboxFixtures)
|
|
3290
|
-
.values(values)
|
|
3291
|
-
.onConflictDoUpdate({
|
|
3292
|
-
target: [sandboxFixtures.workspace_id, sandboxFixtures.id],
|
|
3293
|
-
set: {
|
|
3294
|
-
kind: values.kind,
|
|
3295
|
-
name: values.name,
|
|
3296
|
-
payload: values.payload,
|
|
3297
|
-
repo_ref: values.repo_ref,
|
|
3298
|
-
objective: values.objective,
|
|
3299
|
-
origin: values.origin,
|
|
3300
|
-
},
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
async remove(workspaceId, id) {
|
|
3304
|
-
await this.db
|
|
3305
|
-
.delete(sandboxFixtures)
|
|
3306
|
-
.where(and(eq(sandboxFixtures.workspace_id, workspaceId), eq(sandboxFixtures.id, id)));
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
class DrizzleSandboxExperimentRepository {
|
|
3310
|
-
db;
|
|
3311
|
-
constructor(db) {
|
|
3312
|
-
this.db = db;
|
|
3313
|
-
}
|
|
3314
|
-
async get(workspaceId, id) {
|
|
3315
|
-
const rows = await this.db
|
|
3316
|
-
.select()
|
|
3317
|
-
.from(sandboxExperiments)
|
|
3318
|
-
.where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id)))
|
|
3319
|
-
.limit(1);
|
|
3320
|
-
return rows[0] ? rowToSandboxExperiment(rows[0]) : null;
|
|
3321
|
-
}
|
|
3322
|
-
async list(workspaceId) {
|
|
3323
|
-
const rows = await this.db
|
|
3324
|
-
.select()
|
|
3325
|
-
.from(sandboxExperiments)
|
|
3326
|
-
.where(eq(sandboxExperiments.workspace_id, workspaceId))
|
|
3327
|
-
.orderBy(desc(sandboxExperiments.created_at));
|
|
3328
|
-
return rows.map((r) => rowToSandboxExperiment(r));
|
|
3329
|
-
}
|
|
3330
|
-
async upsert(workspaceId, experiment) {
|
|
3331
|
-
const values = {
|
|
3332
|
-
workspace_id: workspaceId,
|
|
3333
|
-
id: experiment.id,
|
|
3334
|
-
name: experiment.name,
|
|
3335
|
-
agent_kind: experiment.agentKind,
|
|
3336
|
-
judge_model: experiment.judgeModel,
|
|
3337
|
-
repeats: experiment.repeats,
|
|
3338
|
-
status: experiment.status,
|
|
3339
|
-
matrix: JSON.stringify(experiment.matrix),
|
|
3340
|
-
budget_tokens: experiment.budgetTokens,
|
|
3341
|
-
created_at: experiment.createdAt,
|
|
3342
|
-
created_by: experiment.createdBy,
|
|
3343
|
-
};
|
|
3344
|
-
await this.db
|
|
3345
|
-
.insert(sandboxExperiments)
|
|
3346
|
-
.values(values)
|
|
3347
|
-
.onConflictDoUpdate({
|
|
3348
|
-
target: [sandboxExperiments.workspace_id, sandboxExperiments.id],
|
|
3349
|
-
set: {
|
|
3350
|
-
name: values.name,
|
|
3351
|
-
agent_kind: values.agent_kind,
|
|
3352
|
-
judge_model: values.judge_model,
|
|
3353
|
-
repeats: values.repeats,
|
|
3354
|
-
status: values.status,
|
|
3355
|
-
matrix: values.matrix,
|
|
3356
|
-
budget_tokens: values.budget_tokens,
|
|
3357
|
-
created_by: values.created_by,
|
|
3358
|
-
},
|
|
3359
|
-
});
|
|
3360
|
-
}
|
|
3361
|
-
async setStatus(workspaceId, id, status) {
|
|
3362
|
-
await this.db
|
|
3363
|
-
.update(sandboxExperiments)
|
|
3364
|
-
.set({ status })
|
|
3365
|
-
.where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id)));
|
|
3366
|
-
}
|
|
3367
|
-
async claimForRun(workspaceId, id) {
|
|
3368
|
-
// Conditional update: only flips a non-running experiment to `running`. `.returning()`
|
|
3369
|
-
// reports whether this caller won the claim (empty ⇒ already running). Atomic, so
|
|
3370
|
-
// concurrent launches can't both clear + re-expand the grid (see the port doc).
|
|
3371
|
-
const rows = await this.db
|
|
3372
|
-
.update(sandboxExperiments)
|
|
3373
|
-
.set({ status: 'running' })
|
|
3374
|
-
.where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id), ne(sandboxExperiments.status, 'running')))
|
|
3375
|
-
.returning({ id: sandboxExperiments.id });
|
|
3376
|
-
return rows.length > 0;
|
|
3377
|
-
}
|
|
3378
|
-
}
|
|
3379
|
-
class DrizzleSandboxRunRepository {
|
|
3380
|
-
db;
|
|
3381
|
-
constructor(db) {
|
|
3382
|
-
this.db = db;
|
|
3383
|
-
}
|
|
3384
|
-
async get(workspaceId, id) {
|
|
3385
|
-
const rows = await this.db
|
|
3386
|
-
.select()
|
|
3387
|
-
.from(sandboxRuns)
|
|
3388
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.id, id)))
|
|
3389
|
-
.limit(1);
|
|
3390
|
-
return rows[0] ? rowToSandboxRun(rows[0]) : null;
|
|
3391
|
-
}
|
|
3392
|
-
async listByExperiment(workspaceId, experimentId) {
|
|
3393
|
-
const rows = await this.db
|
|
3394
|
-
.select()
|
|
3395
|
-
.from(sandboxRuns)
|
|
3396
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)))
|
|
3397
|
-
.orderBy(sandboxRuns.prompt_version_id, sandboxRuns.model, sandboxRuns.fixture_id, sandboxRuns.repeat_index);
|
|
3398
|
-
return rows.map((r) => rowToSandboxRun(r));
|
|
3399
|
-
}
|
|
3400
|
-
async listQueued(workspaceId, experimentId) {
|
|
3401
|
-
const rows = await this.db
|
|
3402
|
-
.select()
|
|
3403
|
-
.from(sandboxRuns)
|
|
3404
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId), eq(sandboxRuns.status, 'queued')))
|
|
3405
|
-
.orderBy(sandboxRuns.started_at, sandboxRuns.id);
|
|
3406
|
-
return rows.map((r) => rowToSandboxRun(r));
|
|
3407
|
-
}
|
|
3408
|
-
async upsert(workspaceId, run) {
|
|
3409
|
-
const values = {
|
|
3410
|
-
workspace_id: workspaceId,
|
|
3411
|
-
id: run.id,
|
|
3412
|
-
experiment_id: run.experimentId,
|
|
3413
|
-
prompt_version_id: run.promptVersionId,
|
|
3414
|
-
model: run.model,
|
|
3415
|
-
fixture_id: run.fixtureId,
|
|
3416
|
-
repeat_index: run.repeatIndex,
|
|
3417
|
-
status: run.status,
|
|
3418
|
-
output_text: run.outputText,
|
|
3419
|
-
usage: run.usage ? JSON.stringify(run.usage) : null,
|
|
3420
|
-
latency_ms: run.latencyMs,
|
|
3421
|
-
branch: run.branch,
|
|
3422
|
-
pr_url: run.prUrl,
|
|
3423
|
-
diff: run.diff,
|
|
3424
|
-
error: run.error,
|
|
3425
|
-
seed_sha: run.seedSha,
|
|
3426
|
-
prompt_label: run.promptLabel,
|
|
3427
|
-
started_at: run.startedAt,
|
|
3428
|
-
finished_at: run.finishedAt,
|
|
3429
|
-
};
|
|
3430
|
-
await this.db
|
|
3431
|
-
.insert(sandboxRuns)
|
|
3432
|
-
.values(values)
|
|
3433
|
-
.onConflictDoUpdate({
|
|
3434
|
-
target: [sandboxRuns.workspace_id, sandboxRuns.id],
|
|
3435
|
-
set: {
|
|
3436
|
-
experiment_id: values.experiment_id,
|
|
3437
|
-
prompt_version_id: values.prompt_version_id,
|
|
3438
|
-
model: values.model,
|
|
3439
|
-
fixture_id: values.fixture_id,
|
|
3440
|
-
repeat_index: values.repeat_index,
|
|
3441
|
-
status: values.status,
|
|
3442
|
-
output_text: values.output_text,
|
|
3443
|
-
usage: values.usage,
|
|
3444
|
-
latency_ms: values.latency_ms,
|
|
3445
|
-
branch: values.branch,
|
|
3446
|
-
pr_url: values.pr_url,
|
|
3447
|
-
diff: values.diff,
|
|
3448
|
-
error: values.error,
|
|
3449
|
-
seed_sha: values.seed_sha,
|
|
3450
|
-
prompt_label: values.prompt_label,
|
|
3451
|
-
started_at: values.started_at,
|
|
3452
|
-
finished_at: values.finished_at,
|
|
3453
|
-
},
|
|
3454
|
-
});
|
|
3455
|
-
}
|
|
3456
|
-
async setStatus(workspaceId, id, status) {
|
|
3457
|
-
await this.db
|
|
3458
|
-
.update(sandboxRuns)
|
|
3459
|
-
.set({ status })
|
|
3460
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.id, id)));
|
|
3461
|
-
}
|
|
3462
|
-
async removeByExperiment(workspaceId, experimentId) {
|
|
3463
|
-
await this.db
|
|
3464
|
-
.delete(sandboxRuns)
|
|
3465
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)));
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
class DrizzleSandboxGradeRepository {
|
|
3469
|
-
db;
|
|
3470
|
-
constructor(db) {
|
|
3471
|
-
this.db = db;
|
|
3472
|
-
}
|
|
3473
|
-
async getByRun(workspaceId, runId) {
|
|
3474
|
-
const rows = await this.db
|
|
3475
|
-
.select()
|
|
3476
|
-
.from(sandboxGrades)
|
|
3477
|
-
.where(and(eq(sandboxGrades.workspace_id, workspaceId), eq(sandboxGrades.run_id, runId)))
|
|
3478
|
-
.orderBy(desc(sandboxGrades.created_at))
|
|
3479
|
-
.limit(1);
|
|
3480
|
-
return rows[0] ? rowToSandboxGrade(rows[0]) : null;
|
|
3481
|
-
}
|
|
3482
|
-
async listByExperiment(workspaceId, experimentId) {
|
|
3483
|
-
const rows = await this.db
|
|
3484
|
-
.select({ grade: sandboxGrades })
|
|
3485
|
-
.from(sandboxGrades)
|
|
3486
|
-
.innerJoin(sandboxRuns, and(eq(sandboxRuns.workspace_id, sandboxGrades.workspace_id), eq(sandboxRuns.id, sandboxGrades.run_id)))
|
|
3487
|
-
.where(and(eq(sandboxGrades.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)))
|
|
3488
|
-
.orderBy(sandboxGrades.created_at);
|
|
3489
|
-
return rows.map((r) => rowToSandboxGrade(r.grade));
|
|
3490
|
-
}
|
|
3491
|
-
async upsert(workspaceId, grade) {
|
|
3492
|
-
const values = {
|
|
3493
|
-
workspace_id: workspaceId,
|
|
3494
|
-
id: grade.id,
|
|
3495
|
-
run_id: grade.runId,
|
|
3496
|
-
judge_model: grade.judgeModel,
|
|
3497
|
-
scores: JSON.stringify(grade.scores),
|
|
3498
|
-
weighted_total: grade.weightedTotal,
|
|
3499
|
-
objective: grade.objective ? JSON.stringify(grade.objective) : null,
|
|
3500
|
-
created_at: grade.createdAt,
|
|
3501
|
-
};
|
|
3502
|
-
await this.db
|
|
3503
|
-
.insert(sandboxGrades)
|
|
3504
|
-
.values(values)
|
|
3505
|
-
.onConflictDoUpdate({
|
|
3506
|
-
target: [sandboxGrades.workspace_id, sandboxGrades.id],
|
|
3507
|
-
set: {
|
|
3508
|
-
run_id: values.run_id,
|
|
3509
|
-
judge_model: values.judge_model,
|
|
3510
|
-
scores: values.scores,
|
|
3511
|
-
weighted_total: values.weighted_total,
|
|
3512
|
-
objective: values.objective,
|
|
3513
|
-
},
|
|
3514
|
-
});
|
|
3515
|
-
}
|
|
3516
|
-
async removeByExperiment(workspaceId, experimentId) {
|
|
3517
|
-
// Grades carry no experiment_id; scope them through their run's experiment.
|
|
3518
|
-
const runIds = this.db
|
|
3519
|
-
.select({ id: sandboxRuns.id })
|
|
3520
|
-
.from(sandboxRuns)
|
|
3521
|
-
.where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)));
|
|
3522
|
-
await this.db
|
|
3523
|
-
.delete(sandboxGrades)
|
|
3524
|
-
.where(and(eq(sandboxGrades.workspace_id, workspaceId), inArray(sandboxGrades.run_id, runIds)));
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3527
|
-
/**
|
|
3528
|
-
* The Sandbox's persistence as one spreadable mixin (the Drizzle analogue of the
|
|
3529
|
-
* Worker's `selectSandboxDeps`). The Node container spreads `...createDrizzleSandboxDeps(db)`
|
|
3530
|
-
* into its dependencies so the container body never enumerates the Sandbox repos — the
|
|
3531
|
-
* knowledge of which repos exist lives here, next to their implementations. Typed by the
|
|
3532
|
-
* kernel ports (not `CoreDependencies`) so this module stays free of the orchestration import.
|
|
3533
|
-
*/
|
|
3534
|
-
export function createDrizzleSandboxDeps(db) {
|
|
3535
|
-
return {
|
|
3536
|
-
sandboxPromptVersionRepository: new DrizzleSandboxPromptVersionRepository(db),
|
|
3537
|
-
sandboxFixtureRepository: new DrizzleSandboxFixtureRepository(db),
|
|
3538
|
-
sandboxExperimentRepository: new DrizzleSandboxExperimentRepository(db),
|
|
3539
|
-
sandboxRunRepository: new DrizzleSandboxRunRepository(db),
|
|
3540
|
-
sandboxGradeRepository: new DrizzleSandboxGradeRepository(db),
|
|
3541
|
-
};
|
|
3542
|
-
}
|
|
3543
|
-
function rowToWorkspaceSettings(row) {
|
|
3544
|
-
let perType = null;
|
|
3545
|
-
if (row.task_limit_per_type) {
|
|
3546
|
-
try {
|
|
3547
|
-
perType = JSON.parse(row.task_limit_per_type);
|
|
3548
|
-
}
|
|
3549
|
-
catch {
|
|
3550
|
-
perType = null;
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
return {
|
|
3554
|
-
waitingEscalationMinutes: row.waiting_escalation_minutes,
|
|
3555
|
-
taskLimitMode: row.task_limit_mode,
|
|
3556
|
-
taskLimitShared: row.task_limit_shared,
|
|
3557
|
-
taskLimitPerType: perType,
|
|
3558
|
-
storeAgentContext: row.store_agent_context === 1,
|
|
3559
|
-
artifactRetentionDays: row.artifact_retention_days,
|
|
3560
|
-
kaizenEnabled: row.kaizen_enabled === 1,
|
|
3561
|
-
delegateAgentsToRunnerPool: row.delegate_agents_to_runner_pool === 1,
|
|
3562
|
-
spendCurrency: row.spend_currency,
|
|
3563
|
-
spendMonthlyLimit: row.spend_monthly_limit,
|
|
3564
|
-
};
|
|
3565
|
-
}
|
|
3566
|
-
/**
|
|
3567
|
-
* Per-workspace runtime settings over Postgres (the Drizzle mirror of the Worker's
|
|
3568
|
-
* `D1WorkspaceSettingsRepository`, migration 0004). One row per workspace; the service
|
|
3569
|
-
* lazily seeds the default, so an absent row reads as null. Per-type task limits are a
|
|
3570
|
-
* JSON column.
|
|
3571
|
-
*/
|
|
3572
|
-
export class DrizzleWorkspaceSettingsRepository {
|
|
3573
|
-
db;
|
|
3574
|
-
constructor(db) {
|
|
3575
|
-
this.db = db;
|
|
3576
|
-
}
|
|
3577
|
-
async get(workspaceId) {
|
|
3578
|
-
const rows = await this.db
|
|
3579
|
-
.select()
|
|
3580
|
-
.from(workspaceSettings)
|
|
3581
|
-
.where(eq(workspaceSettings.workspace_id, workspaceId))
|
|
3582
|
-
.limit(1);
|
|
3583
|
-
const row = rows[0];
|
|
3584
|
-
return row ? rowToWorkspaceSettings(row) : null;
|
|
3585
|
-
}
|
|
3586
|
-
async listByWorkspaceIds(workspaceIds) {
|
|
3587
|
-
const out = new Map();
|
|
3588
|
-
if (workspaceIds.length === 0)
|
|
3589
|
-
return out;
|
|
3590
|
-
for (let i = 0; i < workspaceIds.length; i += 500) {
|
|
3591
|
-
const rows = await this.db
|
|
3592
|
-
.select()
|
|
3593
|
-
.from(workspaceSettings)
|
|
3594
|
-
.where(inArray(workspaceSettings.workspace_id, workspaceIds.slice(i, i + 500)));
|
|
3595
|
-
for (const row of rows)
|
|
3596
|
-
out.set(row.workspace_id, rowToWorkspaceSettings(row));
|
|
3597
|
-
}
|
|
3598
|
-
return out;
|
|
3599
|
-
}
|
|
3600
|
-
async upsert(workspaceId, settings) {
|
|
3601
|
-
const values = {
|
|
3602
|
-
workspace_id: workspaceId,
|
|
3603
|
-
waiting_escalation_minutes: settings.waitingEscalationMinutes,
|
|
3604
|
-
task_limit_mode: settings.taskLimitMode,
|
|
3605
|
-
task_limit_shared: settings.taskLimitShared,
|
|
3606
|
-
task_limit_per_type: settings.taskLimitPerType
|
|
3607
|
-
? JSON.stringify(settings.taskLimitPerType)
|
|
3608
|
-
: null,
|
|
3609
|
-
store_agent_context: settings.storeAgentContext ? 1 : 0,
|
|
3610
|
-
artifact_retention_days: settings.artifactRetentionDays,
|
|
3611
|
-
kaizen_enabled: settings.kaizenEnabled ? 1 : 0,
|
|
3612
|
-
delegate_agents_to_runner_pool: settings.delegateAgentsToRunnerPool ? 1 : 0,
|
|
3613
|
-
spend_currency: settings.spendCurrency,
|
|
3614
|
-
spend_monthly_limit: settings.spendMonthlyLimit,
|
|
3615
|
-
};
|
|
3616
|
-
await this.db
|
|
3617
|
-
.insert(workspaceSettings)
|
|
3618
|
-
.values(values)
|
|
3619
|
-
.onConflictDoUpdate({
|
|
3620
|
-
target: [workspaceSettings.workspace_id],
|
|
3621
|
-
set: {
|
|
3622
|
-
waiting_escalation_minutes: values.waiting_escalation_minutes,
|
|
3623
|
-
task_limit_mode: values.task_limit_mode,
|
|
3624
|
-
task_limit_shared: values.task_limit_shared,
|
|
3625
|
-
task_limit_per_type: values.task_limit_per_type,
|
|
3626
|
-
store_agent_context: values.store_agent_context,
|
|
3627
|
-
artifact_retention_days: values.artifact_retention_days,
|
|
3628
|
-
kaizen_enabled: values.kaizen_enabled,
|
|
3629
|
-
delegate_agents_to_runner_pool: values.delegate_agents_to_runner_pool,
|
|
3630
|
-
spend_currency: values.spend_currency,
|
|
3631
|
-
spend_monthly_limit: values.spend_monthly_limit,
|
|
3632
|
-
},
|
|
3633
|
-
});
|
|
3634
|
-
}
|
|
3635
|
-
}
|
|
3636
|
-
/**
|
|
3637
|
-
* A workspace's observability connection over Postgres (the Drizzle mirror of the Worker's
|
|
3638
|
-
* `D1ObservabilityConnectionRepository`, migration 0007). One row per workspace; the
|
|
3639
|
-
* provider-specific credentials are stored as a sealed JSON blob (encrypted by the caller),
|
|
3640
|
-
* with a non-secret `summary` blob for display.
|
|
3641
|
-
*/
|
|
3642
|
-
class DrizzleObservabilityConnectionRepository {
|
|
3643
|
-
db;
|
|
3644
|
-
constructor(db) {
|
|
3645
|
-
this.db = db;
|
|
3646
|
-
}
|
|
3647
|
-
async get(workspaceId) {
|
|
3648
|
-
const rows = await this.db
|
|
3649
|
-
.select()
|
|
3650
|
-
.from(observabilityConnections)
|
|
3651
|
-
.where(eq(observabilityConnections.workspace_id, workspaceId))
|
|
3652
|
-
.limit(1);
|
|
3653
|
-
const row = rows[0];
|
|
3654
|
-
if (!row)
|
|
3655
|
-
return null;
|
|
3656
|
-
return {
|
|
3657
|
-
workspaceId: row.workspace_id,
|
|
3658
|
-
provider: row.provider,
|
|
3659
|
-
credentials: row.credentials,
|
|
3660
|
-
summary: row.summary,
|
|
3661
|
-
createdAt: row.created_at,
|
|
3662
|
-
updatedAt: row.updated_at,
|
|
3663
|
-
};
|
|
3664
|
-
}
|
|
3665
|
-
async upsert(record) {
|
|
3666
|
-
const values = {
|
|
3667
|
-
workspace_id: record.workspaceId,
|
|
3668
|
-
provider: record.provider,
|
|
3669
|
-
credentials: record.credentials,
|
|
3670
|
-
summary: record.summary,
|
|
3671
|
-
created_at: record.createdAt,
|
|
3672
|
-
updated_at: record.updatedAt,
|
|
3673
|
-
};
|
|
3674
|
-
await this.db
|
|
3675
|
-
.insert(observabilityConnections)
|
|
3676
|
-
.values(values)
|
|
3677
|
-
.onConflictDoUpdate({
|
|
3678
|
-
target: observabilityConnections.workspace_id,
|
|
3679
|
-
set: {
|
|
3680
|
-
provider: values.provider,
|
|
3681
|
-
credentials: values.credentials,
|
|
3682
|
-
summary: values.summary,
|
|
3683
|
-
updated_at: values.updated_at,
|
|
3684
|
-
},
|
|
3685
|
-
});
|
|
3686
|
-
}
|
|
3687
|
-
async delete(workspaceId) {
|
|
3688
|
-
await this.db
|
|
3689
|
-
.delete(observabilityConnections)
|
|
3690
|
-
.where(eq(observabilityConnections.workspace_id, workspaceId));
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
/**
|
|
3694
|
-
* Postgres-backed modeled subscription quota-cycle counters (the Drizzle mirror of the
|
|
3695
|
-
* Worker's `D1SubscriptionQuotaCycleRepository`, migration 0047), column-for-column so
|
|
3696
|
-
* behaviour matches across stores.
|
|
3697
|
-
*/
|
|
3698
|
-
class DrizzleSubscriptionQuotaCycleRepository {
|
|
3699
|
-
db;
|
|
3700
|
-
constructor(db) {
|
|
3701
|
-
this.db = db;
|
|
3702
|
-
}
|
|
3703
|
-
async recordUsage(key, usage, at, windowMs) {
|
|
3704
|
-
// Windowed UPSERT (mirrors the D1 repo): INSERT anchors a fresh window at `at`; on
|
|
3705
|
-
// conflict, an active window (younger than `windowMs`) accumulates and a stale one
|
|
3706
|
-
// resets to `at`. Every SET RHS reads the row's pre-update values, so referencing
|
|
3707
|
-
// `window_started_at` in the counter branches is safe though it's also reassigned.
|
|
3708
|
-
const cols = subscriptionQuotaCycles;
|
|
3709
|
-
const active = sql `(${at} - ${cols.window_started_at} < ${windowMs})`;
|
|
3710
|
-
await this.db
|
|
3711
|
-
.insert(cols)
|
|
3712
|
-
.values({
|
|
3713
|
-
id: key.id,
|
|
3714
|
-
scope: key.scope,
|
|
3715
|
-
scope_id: key.scopeId,
|
|
3716
|
-
vendor: key.vendor,
|
|
3717
|
-
window_kind: key.windowKind,
|
|
3718
|
-
window_started_at: at,
|
|
3719
|
-
input_tokens: usage.inputTokens,
|
|
3720
|
-
output_tokens: usage.outputTokens,
|
|
3721
|
-
request_count: 1,
|
|
3722
|
-
updated_at: at,
|
|
3723
|
-
})
|
|
3724
|
-
.onConflictDoUpdate({
|
|
3725
|
-
target: [cols.scope, cols.scope_id, cols.vendor, cols.window_kind],
|
|
3726
|
-
set: {
|
|
3727
|
-
window_started_at: sql `CASE WHEN ${active} THEN ${cols.window_started_at} ELSE ${at} END`,
|
|
3728
|
-
input_tokens: sql `CASE WHEN ${active} THEN ${cols.input_tokens} ELSE 0 END + ${usage.inputTokens}`,
|
|
3729
|
-
output_tokens: sql `CASE WHEN ${active} THEN ${cols.output_tokens} ELSE 0 END + ${usage.outputTokens}`,
|
|
3730
|
-
request_count: sql `CASE WHEN ${active} THEN ${cols.request_count} ELSE 0 END + 1`,
|
|
3731
|
-
updated_at: at,
|
|
3732
|
-
},
|
|
3733
|
-
});
|
|
3734
|
-
}
|
|
3735
|
-
async listByScopeVendor(scope, scopeId, vendor) {
|
|
3736
|
-
const rows = await this.db
|
|
3737
|
-
.select()
|
|
3738
|
-
.from(subscriptionQuotaCycles)
|
|
3739
|
-
.where(and(eq(subscriptionQuotaCycles.scope, scope), eq(subscriptionQuotaCycles.scope_id, scopeId), eq(subscriptionQuotaCycles.vendor, vendor)));
|
|
3740
|
-
return rows.map((row) => ({
|
|
3741
|
-
id: row.id,
|
|
3742
|
-
scope: row.scope,
|
|
3743
|
-
scopeId: row.scope_id,
|
|
3744
|
-
vendor: decodeEnum(subscriptionVendorSchema, row.vendor, {
|
|
3745
|
-
table: 'subscription_quota_cycles',
|
|
3746
|
-
column: 'vendor',
|
|
3747
|
-
id: row.id,
|
|
3748
|
-
}),
|
|
3749
|
-
windowKind: row.window_kind,
|
|
3750
|
-
windowStartedAt: row.window_started_at,
|
|
3751
|
-
inputTokens: row.input_tokens,
|
|
3752
|
-
outputTokens: row.output_tokens,
|
|
3753
|
-
requestCount: row.request_count,
|
|
3754
|
-
updatedAt: row.updated_at,
|
|
3755
|
-
}));
|
|
3756
|
-
}
|
|
3757
|
-
async deleteOlderThan(epochMs) {
|
|
3758
|
-
const deleted = await this.db
|
|
3759
|
-
.delete(subscriptionQuotaCycles)
|
|
3760
|
-
.where(lt(subscriptionQuotaCycles.window_started_at, epochMs))
|
|
3761
|
-
.returning({ id: subscriptionQuotaCycles.id });
|
|
3762
|
-
return deleted.length;
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
/**
|
|
3766
|
-
* A workspace's private package-registry connection over Postgres (the Drizzle mirror
|
|
3767
|
-
* of the Worker's `D1PackageRegistryConnectionRepository`, migration 0034). One row per
|
|
3768
|
-
* workspace; the registry entries are stored as ONE sealed JSON array (encrypted by the
|
|
3769
|
-
* caller), with a non-secret `summary` blob for display.
|
|
3770
|
-
*/
|
|
3771
|
-
class DrizzlePackageRegistryConnectionRepository {
|
|
3772
|
-
db;
|
|
3773
|
-
constructor(db) {
|
|
3774
|
-
this.db = db;
|
|
3775
|
-
}
|
|
3776
|
-
async get(workspaceId) {
|
|
3777
|
-
const rows = await this.db
|
|
3778
|
-
.select()
|
|
3779
|
-
.from(packageRegistryConnections)
|
|
3780
|
-
.where(eq(packageRegistryConnections.workspace_id, workspaceId))
|
|
3781
|
-
.limit(1);
|
|
3782
|
-
const row = rows[0];
|
|
3783
|
-
if (!row)
|
|
3784
|
-
return null;
|
|
3785
|
-
return {
|
|
3786
|
-
workspaceId: row.workspace_id,
|
|
3787
|
-
entries: row.entries,
|
|
3788
|
-
summary: row.summary,
|
|
3789
|
-
createdAt: row.created_at,
|
|
3790
|
-
updatedAt: row.updated_at,
|
|
3791
|
-
};
|
|
3792
|
-
}
|
|
3793
|
-
async upsert(record) {
|
|
3794
|
-
const values = {
|
|
3795
|
-
workspace_id: record.workspaceId,
|
|
3796
|
-
entries: record.entries,
|
|
3797
|
-
summary: record.summary,
|
|
3798
|
-
created_at: record.createdAt,
|
|
3799
|
-
updated_at: record.updatedAt,
|
|
3800
|
-
};
|
|
3801
|
-
await this.db
|
|
3802
|
-
.insert(packageRegistryConnections)
|
|
3803
|
-
.values(values)
|
|
3804
|
-
.onConflictDoUpdate({
|
|
3805
|
-
target: packageRegistryConnections.workspace_id,
|
|
3806
|
-
set: {
|
|
3807
|
-
entries: values.entries,
|
|
3808
|
-
summary: values.summary,
|
|
3809
|
-
updated_at: values.updated_at,
|
|
3810
|
-
},
|
|
3811
|
-
});
|
|
3812
|
-
}
|
|
3813
|
-
async delete(workspaceId) {
|
|
3814
|
-
await this.db
|
|
3815
|
-
.delete(packageRegistryConnections)
|
|
3816
|
-
.where(eq(packageRegistryConnections.workspace_id, workspaceId));
|
|
3817
|
-
}
|
|
3818
|
-
}
|
|
3819
|
-
/**
|
|
3820
|
-
* A workspace's incident-enrichment connection over Postgres (the Drizzle mirror of the
|
|
3821
|
-
* Worker's `D1IncidentEnrichmentConnectionRepository`, migration 0013). One row per
|
|
3822
|
-
* workspace; both PagerDuty + incident.io credentials live in ONE sealed JSON blob
|
|
3823
|
-
* (encrypted by the caller), with a non-secret `summary` presence blob.
|
|
3824
|
-
*/
|
|
3825
|
-
class DrizzleIncidentEnrichmentConnectionRepository {
|
|
3826
|
-
db;
|
|
3827
|
-
constructor(db) {
|
|
3828
|
-
this.db = db;
|
|
3829
|
-
}
|
|
3830
|
-
async get(workspaceId) {
|
|
3831
|
-
const rows = await this.db
|
|
3832
|
-
.select()
|
|
3833
|
-
.from(incidentEnrichmentConnections)
|
|
3834
|
-
.where(eq(incidentEnrichmentConnections.workspace_id, workspaceId))
|
|
3835
|
-
.limit(1);
|
|
3836
|
-
const row = rows[0];
|
|
3837
|
-
if (!row)
|
|
3838
|
-
return null;
|
|
3839
|
-
return {
|
|
3840
|
-
workspaceId: row.workspace_id,
|
|
3841
|
-
credentials: row.credentials,
|
|
3842
|
-
summary: row.summary,
|
|
3843
|
-
createdAt: row.created_at,
|
|
3844
|
-
updatedAt: row.updated_at,
|
|
3845
|
-
};
|
|
3846
|
-
}
|
|
3847
|
-
async upsert(record) {
|
|
3848
|
-
const values = {
|
|
3849
|
-
workspace_id: record.workspaceId,
|
|
3850
|
-
credentials: record.credentials,
|
|
3851
|
-
summary: record.summary,
|
|
3852
|
-
created_at: record.createdAt,
|
|
3853
|
-
updated_at: record.updatedAt,
|
|
3854
|
-
};
|
|
3855
|
-
await this.db
|
|
3856
|
-
.insert(incidentEnrichmentConnections)
|
|
3857
|
-
.values(values)
|
|
3858
|
-
.onConflictDoUpdate({
|
|
3859
|
-
target: incidentEnrichmentConnections.workspace_id,
|
|
3860
|
-
set: {
|
|
3861
|
-
credentials: values.credentials,
|
|
3862
|
-
summary: values.summary,
|
|
3863
|
-
updated_at: values.updated_at,
|
|
3864
|
-
},
|
|
3865
|
-
});
|
|
3866
|
-
}
|
|
3867
|
-
async delete(workspaceId) {
|
|
3868
|
-
await this.db
|
|
3869
|
-
.delete(incidentEnrichmentConnections)
|
|
3870
|
-
.where(eq(incidentEnrichmentConnections.workspace_id, workspaceId));
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
/**
|
|
3874
|
-
* Per-account (deployment-wide) settings over Postgres (the Drizzle mirror of the Worker's
|
|
3875
|
-
* `D1AccountSettingsRepository`, migration 0014). One row per account; `config` + `summary`
|
|
3876
|
-
* are non-secret JSON, the ONE sealed `secrets_cipher` blob is encrypted by the caller.
|
|
3877
|
-
*/
|
|
3878
|
-
class DrizzleAccountSettingsRepository {
|
|
3879
|
-
db;
|
|
3880
|
-
constructor(db) {
|
|
3881
|
-
this.db = db;
|
|
3882
|
-
}
|
|
3883
|
-
async getByAccount(accountId) {
|
|
3884
|
-
const rows = await this.db
|
|
3885
|
-
.select()
|
|
3886
|
-
.from(accountSettings)
|
|
3887
|
-
.where(eq(accountSettings.account_id, accountId))
|
|
3888
|
-
.limit(1);
|
|
3889
|
-
const row = rows[0];
|
|
3890
|
-
if (!row)
|
|
3891
|
-
return null;
|
|
3892
|
-
return {
|
|
3893
|
-
accountId: row.account_id,
|
|
3894
|
-
config: row.config,
|
|
3895
|
-
secretsCipher: row.secrets_cipher,
|
|
3896
|
-
summary: row.summary,
|
|
3897
|
-
createdAt: row.created_at,
|
|
3898
|
-
updatedAt: row.updated_at,
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
async upsert(record) {
|
|
3902
|
-
const values = {
|
|
3903
|
-
account_id: record.accountId,
|
|
3904
|
-
config: record.config,
|
|
3905
|
-
secrets_cipher: record.secretsCipher,
|
|
3906
|
-
summary: record.summary,
|
|
3907
|
-
created_at: record.createdAt,
|
|
3908
|
-
updated_at: record.updatedAt,
|
|
3909
|
-
};
|
|
3910
|
-
await this.db
|
|
3911
|
-
.insert(accountSettings)
|
|
3912
|
-
.values(values)
|
|
3913
|
-
.onConflictDoUpdate({
|
|
3914
|
-
target: accountSettings.account_id,
|
|
3915
|
-
set: {
|
|
3916
|
-
config: values.config,
|
|
3917
|
-
secrets_cipher: values.secrets_cipher,
|
|
3918
|
-
summary: values.summary,
|
|
3919
|
-
updated_at: values.updated_at,
|
|
3920
|
-
},
|
|
3921
|
-
});
|
|
3922
|
-
}
|
|
3923
|
-
async listAll() {
|
|
3924
|
-
const rows = await this.db.select().from(accountSettings);
|
|
3925
|
-
return rows.map((row) => ({
|
|
3926
|
-
accountId: row.account_id,
|
|
3927
|
-
config: row.config,
|
|
3928
|
-
secretsCipher: row.secrets_cipher,
|
|
3929
|
-
summary: row.summary,
|
|
3930
|
-
createdAt: row.created_at,
|
|
3931
|
-
updatedAt: row.updated_at,
|
|
3932
|
-
}));
|
|
3933
|
-
}
|
|
3934
|
-
}
|
|
3935
|
-
/** The fixed key for the local-mode settings singleton row (one developer's machine). */
|
|
3936
|
-
const LOCAL_SETTINGS_ID = 'local';
|
|
3937
|
-
/**
|
|
3938
|
-
* The local-mode operational settings singleton (warm-pool sizing + per-repo checkout
|
|
3939
|
-
* reuse), replacing the old `LOCAL_POOL_*` / `HARNESS_*` env vars. One row, addressed by a
|
|
3940
|
-
* fixed id. Local-mode-only — there is no D1 mirror (the warm pool is the local Docker
|
|
3941
|
-
* runner's differentiator).
|
|
3942
|
-
*/
|
|
3943
|
-
export class DrizzleLocalSettingsRepository {
|
|
3944
|
-
db;
|
|
3945
|
-
constructor(db) {
|
|
3946
|
-
this.db = db;
|
|
3947
|
-
}
|
|
3948
|
-
async get() {
|
|
3949
|
-
const rows = await this.db
|
|
3950
|
-
.select()
|
|
3951
|
-
.from(localSettings)
|
|
3952
|
-
.where(eq(localSettings.id, LOCAL_SETTINGS_ID))
|
|
3953
|
-
.limit(1);
|
|
3954
|
-
const row = rows[0];
|
|
3955
|
-
if (!row)
|
|
3956
|
-
return null;
|
|
3957
|
-
return { config: row.config, createdAt: row.created_at, updatedAt: row.updated_at };
|
|
3958
|
-
}
|
|
3959
|
-
async upsert(record) {
|
|
3960
|
-
await this.db
|
|
3961
|
-
.insert(localSettings)
|
|
3962
|
-
.values({
|
|
3963
|
-
id: LOCAL_SETTINGS_ID,
|
|
3964
|
-
config: record.config,
|
|
3965
|
-
created_at: record.createdAt,
|
|
3966
|
-
updated_at: record.updatedAt,
|
|
3967
|
-
})
|
|
3968
|
-
.onConflictDoUpdate({
|
|
3969
|
-
target: localSettings.id,
|
|
3970
|
-
set: { config: record.config, updated_at: record.updatedAt },
|
|
3971
|
-
});
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
function parseReleaseIds(json) {
|
|
3975
|
-
try {
|
|
3976
|
-
const parsed = JSON.parse(json);
|
|
3977
|
-
return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
|
|
3978
|
-
}
|
|
3979
|
-
catch {
|
|
3980
|
-
return [];
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
|
-
function rowToReleaseHealthConfig(row) {
|
|
3984
|
-
return {
|
|
3985
|
-
workspaceId: row.workspace_id,
|
|
3986
|
-
blockId: row.block_id,
|
|
3987
|
-
monitorIds: parseReleaseIds(row.monitor_ids),
|
|
3988
|
-
sloIds: parseReleaseIds(row.slo_ids),
|
|
3989
|
-
envTag: row.env_tag,
|
|
3990
|
-
createdAt: row.created_at,
|
|
3991
|
-
updatedAt: row.updated_at,
|
|
3992
|
-
};
|
|
3993
|
-
}
|
|
3994
|
-
/**
|
|
3995
|
-
* Per-block monitor/SLO mapping for the post-release-health gate over Postgres (the
|
|
3996
|
-
* Drizzle mirror of the Worker's `D1ReleaseHealthConfigRepository`, migration 0003).
|
|
3997
|
-
*/
|
|
3998
|
-
class DrizzleReleaseHealthConfigRepository {
|
|
3999
|
-
db;
|
|
4000
|
-
constructor(db) {
|
|
4001
|
-
this.db = db;
|
|
4002
|
-
}
|
|
4003
|
-
async getByBlock(workspaceId, blockId) {
|
|
4004
|
-
const rows = await this.db
|
|
4005
|
-
.select()
|
|
4006
|
-
.from(releaseHealthConfigs)
|
|
4007
|
-
.where(and(eq(releaseHealthConfigs.workspace_id, workspaceId), eq(releaseHealthConfigs.block_id, blockId)))
|
|
4008
|
-
.limit(1);
|
|
4009
|
-
return rows[0] ? rowToReleaseHealthConfig(rows[0]) : null;
|
|
4010
|
-
}
|
|
4011
|
-
async listByWorkspace(workspaceId) {
|
|
4012
|
-
const rows = await this.db
|
|
4013
|
-
.select()
|
|
4014
|
-
.from(releaseHealthConfigs)
|
|
4015
|
-
.where(eq(releaseHealthConfigs.workspace_id, workspaceId))
|
|
4016
|
-
.orderBy(releaseHealthConfigs.block_id);
|
|
4017
|
-
return rows.map(rowToReleaseHealthConfig);
|
|
4018
|
-
}
|
|
4019
|
-
async upsert(record) {
|
|
4020
|
-
const values = {
|
|
4021
|
-
workspace_id: record.workspaceId,
|
|
4022
|
-
block_id: record.blockId,
|
|
4023
|
-
monitor_ids: JSON.stringify(record.monitorIds),
|
|
4024
|
-
slo_ids: JSON.stringify(record.sloIds),
|
|
4025
|
-
env_tag: record.envTag,
|
|
4026
|
-
created_at: record.createdAt,
|
|
4027
|
-
updated_at: record.updatedAt,
|
|
4028
|
-
};
|
|
4029
|
-
await this.db
|
|
4030
|
-
.insert(releaseHealthConfigs)
|
|
4031
|
-
.values(values)
|
|
4032
|
-
.onConflictDoUpdate({
|
|
4033
|
-
target: [releaseHealthConfigs.workspace_id, releaseHealthConfigs.block_id],
|
|
4034
|
-
set: {
|
|
4035
|
-
monitor_ids: values.monitor_ids,
|
|
4036
|
-
slo_ids: values.slo_ids,
|
|
4037
|
-
env_tag: values.env_tag,
|
|
4038
|
-
updated_at: values.updated_at,
|
|
4039
|
-
},
|
|
4040
|
-
});
|
|
4041
|
-
}
|
|
4042
|
-
async delete(workspaceId, blockId) {
|
|
4043
|
-
await this.db
|
|
4044
|
-
.delete(releaseHealthConfigs)
|
|
4045
|
-
.where(and(eq(releaseHealthConfigs.workspace_id, workspaceId), eq(releaseHealthConfigs.block_id, blockId)));
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4048
|
-
/**
|
|
4049
|
-
* A service frame's sensitive test credentials over Postgres (the Drizzle mirror of the
|
|
4050
|
-
* Worker's `D1TestSecretsRepository`, migration 0044). At most one row per (workspace, block);
|
|
4051
|
-
* `credentials` is a sealed envelope of the `TestSecretEntry[]` JSON, `summary` a non-secret
|
|
4052
|
-
* `TestSecretRef[]` display blob.
|
|
4053
|
-
*/
|
|
4054
|
-
export class DrizzleTestSecretsRepository {
|
|
4055
|
-
db;
|
|
4056
|
-
constructor(db) {
|
|
4057
|
-
this.db = db;
|
|
4058
|
-
}
|
|
4059
|
-
async getByBlock(workspaceId, blockId) {
|
|
4060
|
-
const rows = await this.db
|
|
4061
|
-
.select()
|
|
4062
|
-
.from(testSecrets)
|
|
4063
|
-
.where(and(eq(testSecrets.workspace_id, workspaceId), eq(testSecrets.block_id, blockId)))
|
|
4064
|
-
.limit(1);
|
|
4065
|
-
const row = rows[0];
|
|
4066
|
-
if (!row)
|
|
4067
|
-
return null;
|
|
4068
|
-
return {
|
|
4069
|
-
workspaceId: row.workspace_id,
|
|
4070
|
-
blockId: row.block_id,
|
|
4071
|
-
credentials: row.credentials,
|
|
4072
|
-
summary: row.summary,
|
|
4073
|
-
createdAt: row.created_at,
|
|
4074
|
-
updatedAt: row.updated_at,
|
|
4075
|
-
};
|
|
4076
|
-
}
|
|
4077
|
-
async listByWorkspace(workspaceId) {
|
|
4078
|
-
const rows = await this.db
|
|
4079
|
-
.select()
|
|
4080
|
-
.from(testSecrets)
|
|
4081
|
-
.where(eq(testSecrets.workspace_id, workspaceId))
|
|
4082
|
-
.orderBy(testSecrets.block_id);
|
|
4083
|
-
return rows.map((row) => ({
|
|
4084
|
-
workspaceId: row.workspace_id,
|
|
4085
|
-
blockId: row.block_id,
|
|
4086
|
-
credentials: row.credentials,
|
|
4087
|
-
summary: row.summary,
|
|
4088
|
-
createdAt: row.created_at,
|
|
4089
|
-
updatedAt: row.updated_at,
|
|
4090
|
-
}));
|
|
4091
|
-
}
|
|
4092
|
-
async upsert(record) {
|
|
4093
|
-
const values = {
|
|
4094
|
-
workspace_id: record.workspaceId,
|
|
4095
|
-
block_id: record.blockId,
|
|
4096
|
-
credentials: record.credentials,
|
|
4097
|
-
summary: record.summary,
|
|
4098
|
-
created_at: record.createdAt,
|
|
4099
|
-
updated_at: record.updatedAt,
|
|
4100
|
-
};
|
|
4101
|
-
await this.db
|
|
4102
|
-
.insert(testSecrets)
|
|
4103
|
-
.values(values)
|
|
4104
|
-
.onConflictDoUpdate({
|
|
4105
|
-
target: [testSecrets.workspace_id, testSecrets.block_id],
|
|
4106
|
-
set: {
|
|
4107
|
-
credentials: values.credentials,
|
|
4108
|
-
summary: values.summary,
|
|
4109
|
-
updated_at: values.updated_at,
|
|
4110
|
-
},
|
|
4111
|
-
});
|
|
4112
|
-
}
|
|
4113
|
-
async deleteByBlock(workspaceId, blockId) {
|
|
4114
|
-
await this.db
|
|
4115
|
-
.delete(testSecrets)
|
|
4116
|
-
.where(and(eq(testSecrets.workspace_id, workspaceId), eq(testSecrets.block_id, blockId)));
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
1
|
+
// Barrel for the Drizzle/Postgres core repositories. The concrete port
|
|
2
|
+
// implementations are split by domain under ./drizzle/*; this module assembles them
|
|
3
|
+
// into the CoreRepositories set and re-exports the handful consumed directly by
|
|
4
|
+
// callers (index.ts, container.ts, the test harness). Split out of a single
|
|
5
|
+
// ~5,000-line module — see docs/refactoring-candidates.md #1.
|
|
6
|
+
import { DrizzleBlockRepository, DrizzleServiceFragmentDefaultsRepository, DrizzleServiceRepository, DrizzleWorkspaceMountRepository, DrizzleWorkspaceRepository, } from './drizzle/board.js';
|
|
7
|
+
import { DrizzleAgentRunRepository, DrizzleExecutionRepository, DrizzlePipelineRepository, DrizzlePipelineScheduleRepository, } from './drizzle/execution.js';
|
|
8
|
+
import { DrizzleAccountInvitationRepository, DrizzleAccountRepository, DrizzleEmailConnectionRepository, DrizzleMembershipRepository, DrizzlePasswordResetTokenRepository, DrizzleUserRepository, } from './drizzle/accounts.js';
|
|
9
|
+
import { DrizzleAgentContextSnapshotRepository, DrizzleAgentSearchQueryRepository, DrizzleBinaryArtifactMetadataStore, DrizzleLlmCallMetricRepository, DrizzleProvisioningLogRepository, DrizzleTokenUsageRepository, } from './drizzle/telemetry.js';
|
|
10
|
+
import { DrizzleAccountSettingsRepository, DrizzleModelPresetRepository, DrizzleTrackerSettingsRepository, DrizzleUserSettingsRepository, DrizzleWorkspaceSettingsRepository, } from './drizzle/settings.js';
|
|
11
|
+
import { DrizzleBrainstormSessionRepository, DrizzleClarityReviewRepository, DrizzleConsensusSessionRepository, DrizzleDocInterviewRepository, DrizzleRequirementReviewRepository, } from './drizzle/reviews.js';
|
|
12
|
+
import { DrizzleKaizenGradingRepository, DrizzleKaizenVerifiedComboRepository, } from './drizzle/kaizen.js';
|
|
13
|
+
import { DrizzleInitiativeRepository, DrizzleRiskPolicyRepository, DrizzleSharedStackRepository, } from './drizzle/initiatives.js';
|
|
14
|
+
import { DrizzleIncidentEnrichmentConnectionRepository, DrizzleObservabilityConnectionRepository, DrizzlePackageRegistryConnectionRepository, DrizzleReleaseHealthConfigRepository, DrizzleSubscriptionQuotaCycleRepository, DrizzleTestSecretsRepository, } from './drizzle/connections.js';
|
|
4119
15
|
/** Build the Drizzle/Postgres-backed core repositories. */
|
|
4120
16
|
export function createDrizzleRepositories(db, clock) {
|
|
4121
17
|
return {
|
|
@@ -4163,4 +59,10 @@ export function createDrizzleRepositories(db, clock) {
|
|
|
4163
59
|
provisioningLogRepository: new DrizzleProvisioningLogRepository(db),
|
|
4164
60
|
};
|
|
4165
61
|
}
|
|
62
|
+
// Re-exported for direct consumers (see index.ts / test harness).
|
|
63
|
+
export { DrizzleServiceRepository } from './drizzle/board.js';
|
|
64
|
+
export { DrizzleLocalSettingsRepository, DrizzleWorkspaceSettingsRepository, } from './drizzle/settings.js';
|
|
65
|
+
export { DrizzleClarityReviewRepository, DrizzleDocInterviewRepository, DrizzleRequirementReviewRepository, } from './drizzle/reviews.js';
|
|
66
|
+
export { createDrizzleSandboxDeps } from './drizzle/sandbox.js';
|
|
67
|
+
export { DrizzleTestSecretsRepository } from './drizzle/connections.js';
|
|
4166
68
|
//# sourceMappingURL=drizzle.js.map
|