@sixb/pg 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,657 @@
1
+ import type { ActionSubject, JsonValue } from "@sixb/core"
2
+ import type {
3
+ ActionRunEffectsRecord,
4
+ ActionRunFailure,
5
+ ActionRunParams,
6
+ ActionRunPhase,
7
+ ActionRunRecord,
8
+ ActionRunStorage,
9
+ ActionRunWritebackRecord,
10
+ EnterActionRunPhaseInput,
11
+ FinishActionRunInput,
12
+ ListActionRunsInput,
13
+ ListActionRunsResult,
14
+ LockActionMaterializationRunInput,
15
+ QueueActionRunInput,
16
+ RecordActionEffectsInput,
17
+ RecordActionWritebackInput,
18
+ StartActionRunInput,
19
+ } from "@sixb/core/storage"
20
+ import {
21
+ ActionRunError,
22
+ actionRunPhaseRecordsEqual,
23
+ canRequeueActionRunAfterEnqueueFailure,
24
+ finishActionRunPhase,
25
+ isTerminalActionRun,
26
+ } from "@sixb/core/storage"
27
+ import type { SQLClient, SqlParameter } from "./pg-client"
28
+ import { isUniqueViolation } from "./storage-errors"
29
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
30
+
31
+ export class PgActionRunStorage implements ActionRunStorage {
32
+ constructor(private readonly sql: PgStoreClient) {}
33
+
34
+ async lockForMaterialization(input: LockActionMaterializationRunInput): Promise<ActionRunRecord> {
35
+ const [row] = await this.sql<DatabaseRow[]>`
36
+ SELECT *
37
+ FROM action_runs
38
+ WHERE project_id = ${input.projectId} AND id = ${input.runId}
39
+ FOR UPDATE
40
+ `
41
+ if (!row) {
42
+ throw new ActionRunError(
43
+ `[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`
44
+ )
45
+ }
46
+ if (row.action_id !== input.actionId) {
47
+ throw new ActionRunError(
48
+ `[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`
49
+ )
50
+ }
51
+ if (row.status !== "running") {
52
+ throw new ActionRunError(
53
+ `[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`
54
+ )
55
+ }
56
+ return rowToActionRunRecord(row)
57
+ }
58
+
59
+ async queue(input: QueueActionRunInput): Promise<ActionRunRecord> {
60
+ try {
61
+ const [row] = await this.sql<DatabaseRow[]>`
62
+ INSERT INTO action_runs (
63
+ project_id,
64
+ id,
65
+ action_id,
66
+ subject_kind,
67
+ object_type_id,
68
+ primary_id,
69
+ status,
70
+ phase,
71
+ queued_at,
72
+ started_at,
73
+ finished_at,
74
+ params,
75
+ idempotency_key,
76
+ writeback_status,
77
+ writeback_completed_at,
78
+ writeback_result,
79
+ writeback_error_name,
80
+ writeback_error_message,
81
+ writeback_error_phase,
82
+ effects_status,
83
+ effects_completed_at,
84
+ effects_error_name,
85
+ effects_error_message,
86
+ effects_error_phase,
87
+ error_name,
88
+ error_message,
89
+ error_phase
90
+ ) VALUES (
91
+ ${input.projectId},
92
+ ${input.id},
93
+ ${input.actionId},
94
+ ${input.subject.kind},
95
+ ${input.subject.kind === "object" ? input.subject.objectTypeId : null},
96
+ ${input.subject.kind === "object" ? input.subject.primaryId : null},
97
+ ${"queued"},
98
+ ${"request"},
99
+ ${input.queuedAt ?? new Date()},
100
+ ${null},
101
+ ${null},
102
+ ${JSON.stringify(input.params)}::text::jsonb,
103
+ ${input.idempotencyKey},
104
+ ${null},
105
+ ${null},
106
+ ${null},
107
+ ${null},
108
+ ${null},
109
+ ${null},
110
+ ${null},
111
+ ${null},
112
+ ${null},
113
+ ${null},
114
+ ${null},
115
+ ${null},
116
+ ${null},
117
+ ${null}
118
+ )
119
+ RETURNING *
120
+ `
121
+
122
+ return rowToActionRunRecord(row)
123
+ } catch (error) {
124
+ if (isUniqueViolation(error)) {
125
+ return this.requeueAfterEnqueueFailure(input)
126
+ }
127
+
128
+ throw error
129
+ }
130
+ }
131
+
132
+ private async requeueAfterEnqueueFailure(input: QueueActionRunInput): Promise<ActionRunRecord> {
133
+ return runPgTransaction(this.sql, async (tx) => {
134
+ const [existing] = await tx<DatabaseRow[]>`
135
+ SELECT * FROM action_runs
136
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
137
+ FOR UPDATE
138
+ `
139
+
140
+ if (
141
+ !existing ||
142
+ !canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)
143
+ ) {
144
+ throw new ActionRunError(
145
+ `[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`
146
+ )
147
+ }
148
+
149
+ const [updated] = await tx<DatabaseRow[]>`
150
+ UPDATE action_runs
151
+ SET
152
+ status = ${"queued"},
153
+ phase = ${"request"},
154
+ queued_at = ${input.queuedAt ?? new Date()},
155
+ started_at = ${null},
156
+ finished_at = ${null},
157
+ writeback_status = ${null},
158
+ writeback_completed_at = ${null},
159
+ writeback_result = ${null},
160
+ writeback_error_name = ${null},
161
+ writeback_error_message = ${null},
162
+ writeback_error_phase = ${null},
163
+ effects_status = ${null},
164
+ effects_completed_at = ${null},
165
+ effects_error_name = ${null},
166
+ effects_error_message = ${null},
167
+ effects_error_phase = ${null},
168
+ error_name = ${null},
169
+ error_message = ${null},
170
+ error_phase = ${null}
171
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
172
+ RETURNING *
173
+ `
174
+
175
+ return rowToActionRunRecord(updated)
176
+ })
177
+ }
178
+
179
+ async start(input: StartActionRunInput): Promise<ActionRunRecord> {
180
+ const [updated] = await this.sql<DatabaseRow[]>`
181
+ UPDATE action_runs
182
+ SET
183
+ status = ${"running"},
184
+ phase = ${input.phase ?? "validation"},
185
+ started_at = ${input.startedAt ?? new Date()},
186
+ error_name = ${null},
187
+ error_message = ${null},
188
+ error_phase = ${null}
189
+ WHERE project_id = ${input.projectId}
190
+ AND id = ${input.id}
191
+ AND status = ${"queued"}
192
+ RETURNING *
193
+ `
194
+
195
+ if (updated) {
196
+ return rowToActionRunRecord(updated)
197
+ }
198
+
199
+ const existing = await this.getById({ projectId: input.projectId, id: input.id })
200
+ if (!existing) {
201
+ throw new ActionRunError(
202
+ `[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`
203
+ )
204
+ }
205
+
206
+ throw new ActionRunError(
207
+ `[SixbPg] Action run '${input.id}' cannot start from status '${existing.status}'.`
208
+ )
209
+ }
210
+
211
+ async enterPhase(input: EnterActionRunPhaseInput): Promise<ActionRunRecord> {
212
+ return runPgTransaction(this.sql, async (tx) => {
213
+ await this.requireRunningRun(tx, input.projectId, input.id, "transition phase")
214
+
215
+ const [updated] = await tx<DatabaseRow[]>`
216
+ UPDATE action_runs
217
+ SET phase = ${input.phase}
218
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
219
+ RETURNING *
220
+ `
221
+
222
+ return rowToActionRunRecord(updated)
223
+ })
224
+ }
225
+
226
+ async recordWriteback(input: RecordActionWritebackInput): Promise<ActionRunRecord> {
227
+ return runPgTransaction(this.sql, async (tx) => {
228
+ const existing = await this.requireRunningRun(
229
+ tx,
230
+ input.projectId,
231
+ input.id,
232
+ "record writeback"
233
+ )
234
+ const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date()))
235
+ const currentWriteback = toActionRunWritebackRecord(existing)
236
+
237
+ if (currentWriteback) {
238
+ if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
239
+ return rowToActionRunRecord(existing)
240
+ }
241
+
242
+ throw new ActionRunError(
243
+ `[SixbPg] Action run '${input.id}' already has a different writeback record.`
244
+ )
245
+ }
246
+
247
+ const [updated] = await tx<DatabaseRow[]>`
248
+ UPDATE action_runs
249
+ SET
250
+ phase = ${"writeback"},
251
+ writeback_status = ${input.status},
252
+ writeback_completed_at = ${nextWriteback.completedAt},
253
+ writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
254
+ writeback_error_name = ${input.status === "failed" ? (input.error.name ?? null) : null},
255
+ writeback_error_message = ${input.status === "failed" ? input.error.message : null},
256
+ writeback_error_phase = ${input.status === "failed" ? (input.error.phase ?? "writeback") : null}
257
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
258
+ RETURNING *
259
+ `
260
+
261
+ return rowToActionRunRecord(updated)
262
+ })
263
+ }
264
+
265
+ async recordEffects(input: RecordActionEffectsInput): Promise<ActionRunRecord> {
266
+ return runPgTransaction(this.sql, async (tx) => {
267
+ const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects")
268
+ const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date()))
269
+ const currentEffects = toActionRunEffectsRecord(existing)
270
+
271
+ if (currentEffects) {
272
+ if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
273
+ return rowToActionRunRecord(existing)
274
+ }
275
+
276
+ throw new ActionRunError(
277
+ `[SixbPg] Action run '${input.id}' already has a different effects record.`
278
+ )
279
+ }
280
+
281
+ const [updated] = await tx<DatabaseRow[]>`
282
+ UPDATE action_runs
283
+ SET
284
+ phase = ${"effects"},
285
+ effects_status = ${input.status},
286
+ effects_completed_at = ${nextEffects.completedAt},
287
+ effects_error_name = ${input.status === "failed" ? (input.error.name ?? null) : null},
288
+ effects_error_message = ${input.status === "failed" ? input.error.message : null},
289
+ effects_error_phase = ${input.status === "failed" ? (input.error.phase ?? "effects") : null}
290
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
291
+ RETURNING *
292
+ `
293
+
294
+ return rowToActionRunRecord(updated)
295
+ })
296
+ }
297
+
298
+ async finish(input: FinishActionRunInput): Promise<ActionRunRecord> {
299
+ return runPgTransaction(this.sql, async (tx) => {
300
+ const [existing] = await tx<DatabaseRow[]>`
301
+ SELECT * FROM action_runs
302
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
303
+ FOR UPDATE
304
+ `
305
+
306
+ if (!existing) {
307
+ throw new ActionRunError(
308
+ `[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`
309
+ )
310
+ }
311
+
312
+ if (isTerminalActionRun({ status: existing.status })) {
313
+ throw new ActionRunError(
314
+ `[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`
315
+ )
316
+ }
317
+
318
+ const phase = finishActionRunPhase(input, existing.phase)
319
+
320
+ const [updated] = await tx<DatabaseRow[]>`
321
+ UPDATE action_runs
322
+ SET
323
+ status = ${input.status},
324
+ phase = ${phase},
325
+ finished_at = ${input.finishedAt ?? new Date()},
326
+ error_name = ${input.status === "succeeded" ? null : (input.error?.name ?? null)},
327
+ error_message = ${input.status === "succeeded" ? null : (input.error?.message ?? null)},
328
+ error_phase = ${input.status === "succeeded" ? null : (input.error?.phase ?? phase)}
329
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
330
+ RETURNING *
331
+ `
332
+
333
+ return rowToActionRunRecord(updated)
334
+ })
335
+ }
336
+
337
+ async getById(params: { projectId: string; id: string }): Promise<ActionRunRecord | null> {
338
+ const [row] = await this.sql<DatabaseRow[]>`
339
+ SELECT * FROM action_runs
340
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
341
+ `
342
+
343
+ return row ? rowToActionRunRecord(row) : null
344
+ }
345
+
346
+ async list(input: ListActionRunsInput): Promise<ListActionRunsResult> {
347
+ if ((input.statuses && input.statuses.length === 0) || input.actionIds?.length === 0) {
348
+ return {
349
+ runs: [],
350
+ hasMore: false,
351
+ total: 0,
352
+ }
353
+ }
354
+
355
+ const whereClauses = ["project_id = $1"]
356
+ const params: SqlParameter[] = [input.projectId]
357
+ let index = 2
358
+
359
+ if (input.actionId) {
360
+ whereClauses.push(`action_id = $${index++}`)
361
+ params.push(input.actionId)
362
+ }
363
+
364
+ if (input.actionIds) {
365
+ const placeholders = input.actionIds.map(() => `$${index++}`)
366
+ whereClauses.push(`action_id IN (${placeholders.join(", ")})`)
367
+ params.push(...input.actionIds)
368
+ }
369
+
370
+ if (input.objectTypeIds) {
371
+ if (input.objectTypeIds.length === 0) {
372
+ whereClauses.push(`subject_kind <> $${index++}`)
373
+ params.push("object")
374
+ } else {
375
+ const subjectKindIndex = index++
376
+ const placeholders = input.objectTypeIds.map(() => `$${index++}`)
377
+ whereClauses.push(
378
+ `(subject_kind <> $${subjectKindIndex} OR object_type_id IN (${placeholders.join(", ")}))`
379
+ )
380
+ params.push("object", ...input.objectTypeIds)
381
+ }
382
+ }
383
+
384
+ if (input.objectTypeId) {
385
+ whereClauses.push(`subject_kind = $${index++}`)
386
+ params.push("object")
387
+ whereClauses.push(`object_type_id = $${index++}`)
388
+ params.push(input.objectTypeId)
389
+ }
390
+
391
+ if (input.primaryId) {
392
+ whereClauses.push(`subject_kind = $${index++}`)
393
+ params.push("object")
394
+ whereClauses.push(`primary_id = $${index++}`)
395
+ params.push(input.primaryId)
396
+ }
397
+
398
+ if (input.subject) {
399
+ whereClauses.push(`subject_kind = $${index++}`)
400
+ params.push(input.subject.kind)
401
+ if (input.subject.kind === "object") {
402
+ whereClauses.push(`object_type_id = $${index++}`)
403
+ params.push(input.subject.objectTypeId)
404
+ whereClauses.push(`primary_id = $${index++}`)
405
+ params.push(input.subject.primaryId)
406
+ }
407
+ }
408
+
409
+ if (input.statuses) {
410
+ const placeholders = input.statuses.map(() => `$${index++}`)
411
+ whereClauses.push(`status IN (${placeholders.join(", ")})`)
412
+ params.push(...input.statuses)
413
+ }
414
+
415
+ if (input.startedAfter) {
416
+ whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`)
417
+ params.push(input.startedAfter)
418
+ }
419
+
420
+ if (input.startedBefore) {
421
+ whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`)
422
+ params.push(input.startedBefore)
423
+ }
424
+
425
+ const where = `WHERE ${whereClauses.join(" AND ")}`
426
+ const order = input.order === "asc" ? "ASC" : "DESC"
427
+ const offset = input.offset ?? 0
428
+
429
+ const [totalRow] = await this.sql.unsafe<{ count: string | number }[]>(
430
+ `SELECT COUNT(*)::bigint AS count FROM action_runs ${where}`,
431
+ params
432
+ )
433
+
434
+ const queryParams = [...params]
435
+ let query = `
436
+ SELECT * FROM action_runs
437
+ ${where}
438
+ ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
439
+ `
440
+
441
+ if (input.limit !== undefined) {
442
+ query += ` LIMIT $${index++} OFFSET $${index++}`
443
+ queryParams.push(input.limit, offset)
444
+ } else if (offset > 0) {
445
+ query += ` OFFSET $${index++}`
446
+ queryParams.push(offset)
447
+ }
448
+
449
+ const rows = await this.sql.unsafe<DatabaseRow[]>(query, queryParams)
450
+ const total = Number(totalRow?.count ?? 0)
451
+ const runs = rows.map((row) => rowToActionRunRecord(row))
452
+
453
+ return {
454
+ runs,
455
+ hasMore: offset + runs.length < total,
456
+ total,
457
+ }
458
+ }
459
+
460
+ private async requireRunningRun(
461
+ runner: SQLClient,
462
+ projectId: string,
463
+ id: string,
464
+ operation: string
465
+ ): Promise<DatabaseRow> {
466
+ const [existing] = await runner<DatabaseRow[]>`
467
+ SELECT * FROM action_runs
468
+ WHERE project_id = ${projectId} AND id = ${id}
469
+ FOR UPDATE
470
+ `
471
+
472
+ if (!existing) {
473
+ throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`)
474
+ }
475
+
476
+ if (existing.status !== "running") {
477
+ throw new ActionRunError(
478
+ `[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`
479
+ )
480
+ }
481
+
482
+ return existing
483
+ }
484
+ }
485
+
486
+ function toActionRunFailure(row: DatabaseRow): ActionRunFailure | undefined {
487
+ return toFailure(row.error_name, row.error_message, row.error_phase)
488
+ }
489
+
490
+ function toFailure(
491
+ name: string | null,
492
+ message: string | null,
493
+ phase: ActionRunPhase | null
494
+ ): ActionRunFailure | undefined {
495
+ if (!message) {
496
+ return undefined
497
+ }
498
+
499
+ return {
500
+ name: name ?? undefined,
501
+ message,
502
+ phase: phase ?? undefined,
503
+ }
504
+ }
505
+
506
+ function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord | undefined {
507
+ if (!row.writeback_status) {
508
+ return undefined
509
+ }
510
+
511
+ const completedAt = row.writeback_completed_at
512
+ ? new Date(row.writeback_completed_at)
513
+ : new Date(row.queued_at)
514
+
515
+ if (row.writeback_status === "succeeded") {
516
+ return {
517
+ status: "succeeded",
518
+ completedAt,
519
+ result: (row.writeback_result ?? null) as JsonValue,
520
+ }
521
+ }
522
+
523
+ return {
524
+ status: "failed",
525
+ completedAt,
526
+ error: toFailure(
527
+ row.writeback_error_name,
528
+ row.writeback_error_message,
529
+ row.writeback_error_phase
530
+ ),
531
+ }
532
+ }
533
+
534
+ function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | undefined {
535
+ if (!row.effects_status) {
536
+ return undefined
537
+ }
538
+
539
+ const completedAt = row.effects_completed_at
540
+ ? new Date(row.effects_completed_at)
541
+ : new Date(row.queued_at)
542
+
543
+ if (row.effects_status === "succeeded") {
544
+ return {
545
+ status: "succeeded",
546
+ completedAt,
547
+ }
548
+ }
549
+
550
+ return {
551
+ status: "failed",
552
+ completedAt,
553
+ error: toFailure(row.effects_error_name, row.effects_error_message, row.effects_error_phase),
554
+ }
555
+ }
556
+
557
+ function toWritebackRecord(
558
+ input: RecordActionWritebackInput,
559
+ completedAt: Date
560
+ ): ActionRunWritebackRecord {
561
+ if (input.status === "succeeded") {
562
+ return {
563
+ status: "succeeded",
564
+ completedAt,
565
+ result: input.result,
566
+ }
567
+ }
568
+
569
+ return {
570
+ status: "failed",
571
+ completedAt,
572
+ error: input.error,
573
+ }
574
+ }
575
+
576
+ function toEffectsRecord(
577
+ input: RecordActionEffectsInput,
578
+ completedAt: Date
579
+ ): ActionRunEffectsRecord {
580
+ if (input.status === "succeeded") {
581
+ return {
582
+ status: "succeeded",
583
+ completedAt,
584
+ }
585
+ }
586
+
587
+ return {
588
+ status: "failed",
589
+ completedAt,
590
+ error: input.error,
591
+ }
592
+ }
593
+
594
+ function rowToActionRunRecord(row: DatabaseRow): ActionRunRecord {
595
+ return {
596
+ id: row.id,
597
+ projectId: row.project_id,
598
+ actionId: row.action_id,
599
+ subject: rowToActionSubject(row),
600
+ status: row.status,
601
+ phase: row.phase ?? undefined,
602
+ queuedAt: new Date(row.queued_at),
603
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
604
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
605
+ params: row.params,
606
+ idempotencyKey: row.idempotency_key,
607
+ writeback: toActionRunWritebackRecord(row),
608
+ effects: toActionRunEffectsRecord(row),
609
+ error: toActionRunFailure(row),
610
+ }
611
+ }
612
+
613
+ function rowToActionSubject(row: DatabaseRow): ActionSubject {
614
+ if (row.subject_kind === "none") {
615
+ return { kind: "none" }
616
+ }
617
+
618
+ if (!row.object_type_id || !row.primary_id) {
619
+ throw new ActionRunError(`[SixbPg] Action run '${row.id}' has an invalid object subject.`)
620
+ }
621
+
622
+ return {
623
+ kind: "object",
624
+ objectTypeId: row.object_type_id,
625
+ primaryId: row.primary_id,
626
+ }
627
+ }
628
+
629
+ interface DatabaseRow {
630
+ project_id: string
631
+ id: string
632
+ action_id: string
633
+ subject_kind: ActionSubject["kind"]
634
+ object_type_id: string | null
635
+ primary_id: string | null
636
+ status: ActionRunRecord["status"]
637
+ phase: ActionRunPhase | null
638
+ queued_at: Date | string
639
+ started_at: Date | string | null
640
+ finished_at: Date | string | null
641
+ params: ActionRunParams
642
+ idempotency_key: string
643
+ writeback_status: ActionRunWritebackRecord["status"] | null
644
+ writeback_completed_at: Date | string | null
645
+ writeback_result: JsonValue | null
646
+ writeback_error_name: string | null
647
+ writeback_error_message: string | null
648
+ writeback_error_phase: ActionRunPhase | null
649
+ effects_status: ActionRunEffectsRecord["status"] | null
650
+ effects_completed_at: Date | string | null
651
+ effects_error_name: string | null
652
+ effects_error_message: string | null
653
+ effects_error_phase: ActionRunPhase | null
654
+ error_name: string | null
655
+ error_message: string | null
656
+ error_phase: ActionRunPhase | null
657
+ }