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