@sixb/sqlite 0.1.1 → 0.1.2

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 (108) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +4 -2
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/rows.d.ts +3 -10
  7. package/dist/agents/rows.d.ts.map +1 -1
  8. package/dist/agents/runs.d.ts +3 -2
  9. package/dist/agents/runs.d.ts.map +1 -1
  10. package/dist/ai-usage-storage.d.ts +22 -0
  11. package/dist/ai-usage-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  13. package/dist/auth-storage/shared.d.ts +1 -10
  14. package/dist/auth-storage/shared.d.ts.map +1 -1
  15. package/dist/connector-connection-storage.d.ts +9 -0
  16. package/dist/connector-connection-storage.d.ts.map +1 -0
  17. package/dist/execution-storage.d.ts +14 -0
  18. package/dist/execution-storage.d.ts.map +1 -0
  19. package/dist/index.d.ts +13 -9
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3862 -1042
  22. package/dist/index.js.map +30 -27
  23. package/dist/latest-run-query.d.ts.map +1 -1
  24. package/dist/migrations.d.ts +0 -1
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  27. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  28. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  29. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +0 -2
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/shared.d.ts +3 -3
  35. package/dist/ontology-storage/shared.d.ts.map +1 -1
  36. package/dist/ontology-storage/sources.d.ts.map +1 -1
  37. package/dist/pipeline-run-storage.d.ts +7 -2
  38. package/dist/pipeline-run-storage.d.ts.map +1 -1
  39. package/dist/projection-run-storage.d.ts +9 -2
  40. package/dist/projection-run-storage.d.ts.map +1 -1
  41. package/dist/run-list-query.d.ts +1 -1
  42. package/dist/run-list-query.d.ts.map +1 -1
  43. package/dist/sync-run-storage.d.ts +7 -2
  44. package/dist/sync-run-storage.d.ts.map +1 -1
  45. package/dist/testing.d.ts +6 -0
  46. package/dist/testing.d.ts.map +1 -0
  47. package/dist/transactions.d.ts +3 -0
  48. package/dist/transactions.d.ts.map +1 -1
  49. package/dist/webhook-run-storage.d.ts +9 -2
  50. package/dist/webhook-run-storage.d.ts.map +1 -1
  51. package/dist/workflow-run-storage.d.ts +7 -3
  52. package/dist/workflow-run-storage.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/action-run-storage.ts +51 -74
  55. package/src/agents/agent-storage.ts +5 -3
  56. package/src/agents/rows.ts +8 -40
  57. package/src/agents/runs.ts +27 -21
  58. package/src/ai-usage-storage.ts +324 -0
  59. package/src/auth-storage/service-accounts.ts +14 -21
  60. package/src/auth-storage/shared.ts +0 -22
  61. package/src/connector-connection-storage.ts +784 -0
  62. package/src/execution-storage.ts +158 -0
  63. package/src/index.ts +63 -22
  64. package/src/latest-run-query.ts +7 -1
  65. package/src/migrations/004-executions.sql +142 -0
  66. package/src/migrations/005-workflow-executions.sql +7 -0
  67. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  68. package/src/migrations/007-split-overrides.sql +154 -0
  69. package/src/migrations/008-action-executions.sql +4 -0
  70. package/src/migrations/009-agent-executions.sql +115 -0
  71. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  72. package/src/migrations/011-sync-failure-record.sql +22 -0
  73. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  74. package/src/migrations/013-workflow-failure-record.sql +45 -0
  75. package/src/migrations/014-agent-failure-record.sql +59 -0
  76. package/src/migrations/015-projection-failure-record.sql +20 -0
  77. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  78. package/src/migrations/017-action-failure-record.sql +65 -0
  79. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  80. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  81. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  82. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  83. package/src/migrations/022-projection-executions.sql +244 -0
  84. package/src/migrations/023-webhook-executions.sql +51 -0
  85. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  86. package/src/migrations/025-connector-connections.sql +257 -0
  87. package/src/migrations.ts +119 -4
  88. package/src/ontology-storage/materialization-session.ts +317 -143
  89. package/src/ontology-storage/materialization-state.ts +321 -112
  90. package/src/ontology-storage/materialization-writer.ts +142 -67
  91. package/src/ontology-storage/materializations.ts +67 -141
  92. package/src/ontology-storage/outbox.ts +10 -3
  93. package/src/ontology-storage/shared.ts +10 -4
  94. package/src/ontology-storage/sources.ts +4 -1
  95. package/src/pipeline-run-storage.ts +128 -43
  96. package/src/projection-run-storage.ts +209 -97
  97. package/src/run-list-query.ts +9 -4
  98. package/src/sync-run-storage.ts +118 -32
  99. package/src/testing.ts +20 -0
  100. package/src/transactions.ts +10 -3
  101. package/src/webhook-run-storage.ts +118 -74
  102. package/src/workflow-run-storage.ts +117 -131
  103. package/dist/validation.d.ts +0 -5
  104. package/dist/validation.d.ts.map +0 -1
  105. package/dist/webhook-delivery-storage.d.ts +0 -27
  106. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  107. package/src/validation.ts +0 -30
  108. package/src/webhook-delivery-storage.ts +0 -272
@@ -1,17 +1,24 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import type { JsonValue } from "@sixb/core"
3
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
4
+ import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
3
5
  import type {
6
+ ExecutionStorage,
4
7
  FinishSyncRunInput,
5
8
  ListLatestSyncRunsInput,
6
9
  ListLatestSyncRunsResult,
7
10
  ListSyncRunsInput,
8
11
  ListSyncRunsResult,
12
+ QueueSyncRunInput,
9
13
  StartSyncRunInput,
10
- SyncRunFailure,
11
14
  SyncRunRecord,
12
15
  SyncRunStorage,
13
16
  } from "@sixb/core/storage"
14
- import { SyncRunError } from "@sixb/core/storage"
17
+ import {
18
+ canRequeueSyncRunAfterEnqueueFailure,
19
+ SYNC_RUN_FAILURE_CODES,
20
+ SyncRunError,
21
+ } from "@sixb/core/storage"
15
22
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
16
23
  import { installFreshSqliteSchema } from "./migrations"
17
24
  import {
@@ -27,6 +34,8 @@ import {
27
34
  } from "./transactions"
28
35
 
29
36
  export interface SqliteSyncRunStorageOptions {
37
+ /** Execution lookup sharing the same provider transaction. */
38
+ executions: ExecutionStorage
30
39
  /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
31
40
  path?: string
32
41
  /** Internal shared connection used by bundled SqliteStorage. */
@@ -36,18 +45,27 @@ export interface SqliteSyncRunStorageOptions {
36
45
  export class SqliteSyncRunStorage implements SyncRunStorage {
37
46
  private readonly connection: SqliteStoreConnection
38
47
  private readonly db: Database
48
+ private readonly executions: ExecutionStorage
39
49
 
40
- constructor(options: SqliteSyncRunStorageOptions = {}) {
50
+ constructor(options: SqliteSyncRunStorageOptions) {
41
51
  this.connection = openSqliteStoreConnection(options)
42
52
  this.db = this.connection.db
53
+ this.executions = options.executions
43
54
 
44
55
  if (this.connection.installFreshSchema) {
45
56
  installFreshSqliteSchema(this.db)
46
57
  }
47
58
  }
48
59
 
49
- async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
50
- const startedAt = input.startedAt ?? new Date()
60
+ async queue(input: QueueSyncRunInput): Promise<SyncRunRecord> {
61
+ const queuedAt = input.queuedAt ?? new Date()
62
+ await assertSyncRunExecution({
63
+ executions: this.executions,
64
+ projectId: input.projectId,
65
+ executionId: input.executionId,
66
+ runId: input.id,
67
+ syncId: input.syncId,
68
+ })
51
69
 
52
70
  try {
53
71
  this.db
@@ -56,32 +74,33 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
56
74
  INSERT INTO sync_runs (
57
75
  project_id,
58
76
  id,
77
+ execution_id,
59
78
  sync_id,
60
79
  dataset_id,
61
80
  mode,
62
81
  status,
82
+ queued_at,
63
83
  started_at,
64
84
  expected_latest_version_id,
65
85
  commit_message
66
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
86
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
67
87
  `
68
88
  )
69
89
  .run(
70
90
  input.projectId,
71
91
  input.id,
92
+ input.executionId,
72
93
  input.syncId,
73
94
  input.datasetId,
74
95
  input.mode,
75
- "running",
76
- startedAt.toISOString(),
96
+ "queued",
97
+ queuedAt.toISOString(),
77
98
  input.expectedLatestVersionId ?? null,
78
99
  input.commitMessage ?? null
79
100
  )
80
101
  } catch (error) {
81
102
  if (isUniqueConstraintError(error)) {
82
- throw new SyncRunError(
83
- `[SixbSqlite] Sync run '${input.id}' already exists for project '${input.projectId}'.`
84
- )
103
+ return this.requeueAfterEnqueueFailure(input, queuedAt)
85
104
  }
86
105
 
87
106
  throw error
@@ -97,6 +116,70 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
97
116
  return record
98
117
  }
99
118
 
119
+ private async requeueAfterEnqueueFailure(
120
+ input: QueueSyncRunInput,
121
+ queuedAt: Date
122
+ ): Promise<SyncRunRecord> {
123
+ return this.db.transaction(() => {
124
+ const existing = this.db
125
+ .query("SELECT * FROM sync_runs WHERE project_id = ? AND id = ?")
126
+ .get(input.projectId, input.id) as DatabaseRow | null
127
+ if (!existing || !canRequeueSyncRunAfterEnqueueFailure(rowToSyncRunRecord(existing), input)) {
128
+ throw new SyncRunError(
129
+ `[SixbSqlite] Sync run '${input.id}' already exists for project '${input.projectId}'.`
130
+ )
131
+ }
132
+
133
+ this.db
134
+ .query(
135
+ `
136
+ UPDATE sync_runs
137
+ SET status = ?, queued_at = ?, started_at = NULL, finished_at = NULL,
138
+ rows_read = NULL, output_version_id = NULL, error = NULL, checkpoint = NULL
139
+ WHERE project_id = ? AND id = ?
140
+ `
141
+ )
142
+ .run("queued", queuedAt.toISOString(), input.projectId, input.id)
143
+
144
+ const updated = this.db
145
+ .query("SELECT * FROM sync_runs WHERE project_id = ? AND id = ?")
146
+ .get(input.projectId, input.id) as DatabaseRow
147
+ return rowToSyncRunRecord(updated)
148
+ })()
149
+ }
150
+
151
+ async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
152
+ const result = this.db
153
+ .query(
154
+ `
155
+ UPDATE sync_runs
156
+ SET status = ?, started_at = ?, error = NULL
157
+ WHERE project_id = ? AND id = ? AND status = ?
158
+ `
159
+ )
160
+ .run(
161
+ "running",
162
+ (input.startedAt ?? new Date()).toISOString(),
163
+ input.projectId,
164
+ input.id,
165
+ "queued"
166
+ )
167
+ if (result.changes > 0) {
168
+ const record = await this.getById({ projectId: input.projectId, id: input.id })
169
+ if (record) return record
170
+ }
171
+
172
+ const existing = await this.getById({ projectId: input.projectId, id: input.id })
173
+ if (!existing) {
174
+ throw new SyncRunError(
175
+ `[SixbSqlite] Sync run '${input.id}' not found for project '${input.projectId}'.`
176
+ )
177
+ }
178
+ throw new SyncRunError(
179
+ `[SixbSqlite] Sync run '${input.id}' cannot start from status '${existing.status}'.`
180
+ )
181
+ }
182
+
100
183
  async finish(input: FinishSyncRunInput): Promise<SyncRunRecord> {
101
184
  return this.db.transaction(() => {
102
185
  const existing = this.db
@@ -109,6 +192,17 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
109
192
  )
110
193
  }
111
194
 
195
+ const enqueueFailure =
196
+ input.status === "failed" && input.error.code === "queue.enqueue_failed"
197
+ if (
198
+ (enqueueFailure && existing.status !== "queued") ||
199
+ (!enqueueFailure && existing.status !== "running")
200
+ ) {
201
+ throw new SyncRunError(
202
+ `[SixbSqlite] Sync run '${input.id}' cannot finish from status '${existing.status}'.`
203
+ )
204
+ }
205
+
112
206
  if (input.status === "succeeded") {
113
207
  if (input.output && input.output.datasetId !== existing.dataset_id) {
114
208
  throw new SyncRunError(
@@ -131,8 +225,7 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
131
225
  finished_at = ?,
132
226
  rows_read = ?,
133
227
  output_version_id = ?,
134
- error_name = ?,
135
- error_message = ?,
228
+ error = ?,
136
229
  checkpoint = ?
137
230
  WHERE project_id = ? AND id = ?
138
231
  `
@@ -144,8 +237,9 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
144
237
  ? input.rowsRead
145
238
  : (input.rowsRead ?? existing.rows_read ?? null),
146
239
  input.status === "succeeded" ? (input.output?.versionId ?? null) : null,
147
- input.status === "succeeded" ? null : (input.error?.name ?? null),
148
- input.status === "succeeded" ? null : (input.error?.message ?? null),
240
+ input.status === "succeeded" || input.error === undefined
241
+ ? null
242
+ : serializeSixbFailure(input.error, SYNC_RUN_FAILURE_CODES),
149
243
  input.status === "succeeded" ? serializeCheckpoint(input.checkpoint) : null,
150
244
  input.projectId,
151
245
  input.id
@@ -194,7 +288,7 @@ export class SqliteSyncRunStorage implements SyncRunStorage {
194
288
  args.push(input.datasetId)
195
289
  }
196
290
 
197
- appendRunListFilters(whereClauses, args, input)
291
+ appendRunListFilters(whereClauses, args, input, "COALESCE(started_at, queued_at)")
198
292
 
199
293
  const { rows, total, hasMore } = queryRunList<DatabaseRow>({
200
294
  db: this.db,
@@ -241,26 +335,17 @@ function parseCheckpoint(value: string | null | undefined): JsonValue | undefine
241
335
  return value != null ? (JSON.parse(value) as JsonValue) : undefined
242
336
  }
243
337
 
244
- function toSyncRunFailure(row: DatabaseRow): SyncRunFailure | undefined {
245
- if (!row.error_message) {
246
- return undefined
247
- }
248
-
249
- return {
250
- name: row.error_name ?? undefined,
251
- message: row.error_message,
252
- }
253
- }
254
-
255
338
  function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
256
339
  return {
257
340
  id: row.id,
258
341
  projectId: row.project_id,
342
+ executionId: row.execution_id,
259
343
  syncId: row.sync_id,
260
344
  datasetId: row.dataset_id,
261
345
  mode: row.mode,
262
346
  status: row.status,
263
- startedAt: new Date(row.started_at),
347
+ queuedAt: new Date(row.queued_at),
348
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
264
349
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
265
350
  rowsRead: row.rows_read ?? undefined,
266
351
  output: row.output_version_id
@@ -271,7 +356,7 @@ function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
271
356
  : undefined,
272
357
  expectedLatestVersionId: row.expected_latest_version_id ?? undefined,
273
358
  commitMessage: row.commit_message ?? undefined,
274
- error: toSyncRunFailure(row),
359
+ error: row.error === null ? undefined : parseSixbFailure(row.error, SYNC_RUN_FAILURE_CODES),
275
360
  checkpoint: parseCheckpoint(row.checkpoint),
276
361
  }
277
362
  }
@@ -283,17 +368,18 @@ function isUniqueConstraintError(error: unknown): boolean {
283
368
  interface DatabaseRow {
284
369
  project_id: string
285
370
  id: string
371
+ execution_id: string
286
372
  sync_id: string
287
373
  dataset_id: string
288
374
  mode: SyncRunRecord["mode"]
289
375
  status: SyncRunRecord["status"]
290
- started_at: string
376
+ queued_at: string
377
+ started_at: string | null
291
378
  finished_at: string | null
292
379
  rows_read: number | null
293
380
  output_version_id: string | null
294
381
  expected_latest_version_id: string | null
295
382
  commit_message: string | null
296
- error_name: string | null
297
- error_message: string | null
383
+ error: string | null
298
384
  checkpoint: string | null
299
385
  }
package/src/testing.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface SqliteStorageTestingAdapter {
2
+ advanceConnectorConnectionTime(durationMs: number): void
3
+ }
4
+
5
+ const adapters = new WeakMap<object, SqliteStorageTestingAdapter>()
6
+
7
+ export function registerSqliteStorageTestingAdapter(
8
+ storage: object,
9
+ advanceConnectorConnectionTime: (durationMs: number) => void
10
+ ): void {
11
+ adapters.set(storage, {
12
+ advanceConnectorConnectionTime,
13
+ })
14
+ }
15
+
16
+ export function getSqliteStorageTestingAdapter(storage: object): SqliteStorageTestingAdapter {
17
+ const adapter = adapters.get(storage)
18
+ if (!adapter) throw new Error("[SixbSqlite] Storage testing adapter is unavailable.")
19
+ return adapter
20
+ }
@@ -12,6 +12,7 @@ export interface SqliteStoreConnection {
12
12
  export interface SqliteStoreConnectionOptions {
13
13
  readonly path?: string
14
14
  readonly connection?: SqliteStoreConnection
15
+ readonly readonly?: boolean
15
16
  }
16
17
 
17
18
  export function openSqliteStoreConnection(
@@ -26,13 +27,14 @@ export function openSqliteStoreConnection(
26
27
  }
27
28
 
28
29
  const path = options.path ?? ":memory:"
29
- if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true })
30
- const db = new SqliteDatabase(path)
30
+ if (path !== ":memory:" && !options.readonly) mkdirSync(dirname(path), { recursive: true })
31
+ const db = new SqliteDatabase(path, options.readonly ? { readonly: true } : undefined)
31
32
  db.run("PRAGMA foreign_keys = ON")
33
+ db.run("PRAGMA busy_timeout = 5000")
32
34
  return {
33
35
  db,
34
36
  ownsConnection: true,
35
- installFreshSchema: path === ":memory:",
37
+ installFreshSchema: path === ":memory:" && !options.readonly,
36
38
  }
37
39
  }
38
40
 
@@ -64,6 +66,11 @@ export function runImmediateTransaction<T>(db: Database, run: () => T): T {
64
66
  }
65
67
  }
66
68
 
69
+ /** Lets timers and request handlers run between bounded synchronous SQLite chunks. */
70
+ export function yieldSqliteEventLoop(): Promise<void> {
71
+ return new Promise((resolve) => setImmediate(resolve))
72
+ }
73
+
67
74
  export async function runImmediateTransactionAsync<T>(
68
75
  db: Database,
69
76
  run: () => Promise<T> | T
@@ -1,13 +1,18 @@
1
1
  import type { Database } from "bun:sqlite"
2
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
3
+ import { assertWebhookRunExecution } from "@sixb/core/internal/webhook-run-storage-provider"
2
4
  import type {
5
+ ExecutionStorage,
3
6
  FinishWebhookRunInput,
7
+ GetWebhookRunByDeliveryInput,
4
8
  ListWebhookRunsInput,
5
9
  ListWebhookRunsResult,
10
+ RestartWebhookRunInput,
6
11
  StartWebhookRunInput,
7
12
  WebhookRunRecord,
8
13
  WebhookRunStorage,
9
14
  } from "@sixb/core/storage"
10
- import { WebhookRunError } from "@sixb/core/storage"
15
+ import { canRetryWebhookRun, WEBHOOK_RUN_FAILURE_CODES, WebhookRunError } from "@sixb/core/storage"
11
16
  import { installFreshSqliteSchema } from "./migrations"
12
17
  import {
13
18
  appendRunListFilters,
@@ -23,6 +28,8 @@ import {
23
28
  } from "./transactions"
24
29
 
25
30
  export interface SqliteWebhookRunStorageOptions {
31
+ /** Execution lookup sharing the same provider transaction. */
32
+ executions: ExecutionStorage
26
33
  /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
27
34
  path?: string
28
35
  /** Internal shared connection used by bundled SqliteStorage. */
@@ -32,80 +39,112 @@ export interface SqliteWebhookRunStorageOptions {
32
39
  export class SqliteWebhookRunStorage implements WebhookRunStorage {
33
40
  private readonly connection: SqliteStoreConnection
34
41
  private readonly db: Database
42
+ private readonly executions: ExecutionStorage
35
43
 
36
- constructor(options: SqliteWebhookRunStorageOptions = {}) {
44
+ constructor(options: SqliteWebhookRunStorageOptions) {
37
45
  this.connection = openSqliteStoreConnection(options)
38
46
  this.db = this.connection.db
47
+ this.executions = options.executions
39
48
 
40
- if (this.connection.installFreshSchema) {
41
- installFreshSqliteSchema(this.db)
42
- }
49
+ if (this.connection.installFreshSchema) installFreshSqliteSchema(this.db)
43
50
  }
44
51
 
45
52
  async start(input: StartWebhookRunInput): Promise<WebhookRunRecord> {
46
53
  const startedAt = input.startedAt ?? new Date()
54
+ await assertWebhookRunExecution({
55
+ executions: this.executions,
56
+ projectId: input.projectId,
57
+ executionId: input.executionId,
58
+ runId: input.id,
59
+ route: input.route,
60
+ })
47
61
 
48
62
  try {
49
63
  this.db
50
64
  .query(
51
65
  `
52
66
  INSERT INTO webhook_runs (
53
- project_id,
54
- id,
55
- connector_id,
56
- webhook_id,
57
- status,
58
- method,
59
- route,
60
- started_at
61
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
67
+ project_id, id, execution_id, connector_id, webhook_id, status, method, route,
68
+ started_at, request_body_bytes, request_body_sha256, idempotency_key
69
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
62
70
  `
63
71
  )
64
72
  .run(
65
73
  input.projectId,
66
74
  input.id,
75
+ input.executionId,
67
76
  input.connectorId,
68
77
  input.webhookId,
69
78
  "running",
70
79
  input.method,
71
80
  input.route,
72
- startedAt.toISOString()
81
+ startedAt.toISOString(),
82
+ input.requestBodyBytes,
83
+ input.requestBodySha256,
84
+ input.idempotencyKey ?? null
73
85
  )
74
86
  } catch (error) {
75
87
  if (isUniqueConstraintError(error)) {
76
88
  throw new WebhookRunError(
77
- `[SixbSqlite] Webhook run '${input.id}' already exists for project '${input.projectId}'.`
89
+ `[SixbSqlite] Webhook run '${input.id}' or delivery key already exists for project '${input.projectId}'.`,
90
+ "duplicate_run"
78
91
  )
79
92
  }
80
-
81
93
  throw error
82
94
  }
83
95
 
84
- const record = await this.getById({ projectId: input.projectId, id: input.id })
85
- if (!record) {
86
- throw new WebhookRunError(
87
- `[SixbSqlite] Failed to load webhook run '${input.id}' for project '${input.projectId}'.`
88
- )
89
- }
96
+ return this.requireWebhookRun(input.projectId, input.id)
97
+ }
98
+
99
+ async restart(input: RestartWebhookRunInput): Promise<WebhookRunRecord> {
100
+ return this.db.transaction(() => {
101
+ const existing = this.getWebhookRunRow(input.projectId, input.id)
102
+ if (!existing) {
103
+ throw new WebhookRunError(
104
+ `[SixbSqlite] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
105
+ "not_found"
106
+ )
107
+ }
108
+ if (!canRetryWebhookRun(rowToWebhookRunRecord(existing))) {
109
+ throw new WebhookRunError(
110
+ `[SixbSqlite] Webhook run '${input.id}' cannot restart from status '${existing.status}'.`,
111
+ "invalid_transition"
112
+ )
113
+ }
90
114
 
91
- return record
115
+ const result = this.db
116
+ .query(
117
+ `
118
+ UPDATE webhook_runs
119
+ SET status = 'running', started_at = ?, finished_at = NULL,
120
+ response_status = NULL, error = NULL
121
+ WHERE project_id = ? AND id = ? AND status = 'failed'
122
+ `
123
+ )
124
+ .run((input.startedAt ?? new Date()).toISOString(), input.projectId, input.id)
125
+ if (result.changes === 0) {
126
+ throw new WebhookRunError(
127
+ `[SixbSqlite] Webhook run '${input.id}' changed while restarting.`,
128
+ "invalid_transition"
129
+ )
130
+ }
131
+ return this.requireWebhookRun(input.projectId, input.id)
132
+ })()
92
133
  }
93
134
 
94
135
  async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
95
136
  return this.db.transaction(() => {
96
- const existing = this.db
97
- .query("SELECT * FROM webhook_runs WHERE project_id = ? AND id = ?")
98
- .get(input.projectId, input.id) as WebhookRunDatabaseRow | null
99
-
137
+ const existing = this.getWebhookRunRow(input.projectId, input.id)
100
138
  if (!existing) {
101
139
  throw new WebhookRunError(
102
- `[SixbSqlite] Webhook run '${input.id}' not found for project '${input.projectId}'.`
140
+ `[SixbSqlite] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
141
+ "not_found"
103
142
  )
104
143
  }
105
-
106
144
  if (existing.status !== "running") {
107
145
  throw new WebhookRunError(
108
- `[SixbSqlite] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`
146
+ `[SixbSqlite] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`,
147
+ "invalid_transition"
109
148
  )
110
149
  }
111
150
 
@@ -113,72 +152,63 @@ export class SqliteWebhookRunStorage implements WebhookRunStorage {
113
152
  .query(
114
153
  `
115
154
  UPDATE webhook_runs
116
- SET
117
- status = ?,
118
- finished_at = ?,
119
- request_body_bytes = ?,
120
- response_status = ?,
121
- idempotency_key = ?,
122
- delivery_claim_result = ?,
123
- error = ?
155
+ SET status = ?, finished_at = ?, response_status = ?, error = ?
124
156
  WHERE project_id = ? AND id = ?
125
157
  `
126
158
  )
127
159
  .run(
128
160
  input.status,
129
161
  (input.finishedAt ?? new Date()).toISOString(),
130
- input.requestBodyBytes ?? null,
131
162
  input.responseStatus ?? null,
132
- input.idempotencyKey ?? null,
133
- input.deliveryClaimResult ?? null,
134
- input.status === "succeeded" ? null : (input.error ?? null),
163
+ input.status === "succeeded"
164
+ ? null
165
+ : serializeSixbFailure(input.error, WEBHOOK_RUN_FAILURE_CODES),
135
166
  input.projectId,
136
167
  input.id
137
168
  )
138
-
139
- const updated = this.db
140
- .query("SELECT * FROM webhook_runs WHERE project_id = ? AND id = ?")
141
- .get(input.projectId, input.id) as WebhookRunDatabaseRow
142
-
143
- return rowToWebhookRunRecord(updated)
169
+ return this.requireWebhookRun(input.projectId, input.id)
144
170
  })()
145
171
  }
146
172
 
147
173
  async getById(params: { projectId: string; id: string }): Promise<WebhookRunRecord | null> {
148
- const row = this.db
149
- .query("SELECT * FROM webhook_runs WHERE project_id = ? AND id = ?")
150
- .get(params.projectId, params.id) as WebhookRunDatabaseRow | null
174
+ const row = this.getWebhookRunRow(params.projectId, params.id)
175
+ return row ? rowToWebhookRunRecord(row) : null
176
+ }
151
177
 
178
+ async getByDelivery(input: GetWebhookRunByDeliveryInput): Promise<WebhookRunRecord | null> {
179
+ const row = this.db
180
+ .query(
181
+ `
182
+ SELECT * FROM webhook_runs
183
+ WHERE project_id = ? AND connector_id = ? AND webhook_id = ? AND idempotency_key = ?
184
+ `
185
+ )
186
+ .get(
187
+ input.projectId,
188
+ input.connectorId,
189
+ input.webhookId,
190
+ input.idempotencyKey
191
+ ) as WebhookRunDatabaseRow | null
152
192
  return row ? rowToWebhookRunRecord(row) : null
153
193
  }
154
194
 
155
195
  async list(input: ListWebhookRunsInput): Promise<ListWebhookRunsResult> {
156
- if (hasEmptyStatuses(input)) {
157
- return {
158
- runs: [],
159
- hasMore: false,
160
- total: 0,
161
- }
162
- }
196
+ if (hasEmptyStatuses(input)) return { runs: [], hasMore: false, total: 0 }
163
197
 
164
198
  const whereClauses = ["project_id = ?"]
165
199
  const args: SqliteValue[] = [input.projectId]
166
-
167
200
  if (input.connectorId) {
168
201
  whereClauses.push("connector_id = ?")
169
202
  args.push(input.connectorId)
170
203
  }
171
-
172
204
  if (input.webhookId) {
173
205
  whereClauses.push("webhook_id = ?")
174
206
  args.push(input.webhookId)
175
207
  }
176
-
177
208
  if (input.idempotencyKey) {
178
209
  whereClauses.push("idempotency_key = ?")
179
210
  args.push(input.idempotencyKey)
180
211
  }
181
-
182
212
  appendRunListFilters(whereClauses, args, input)
183
213
 
184
214
  const { rows, total, hasMore } = queryRunList<WebhookRunDatabaseRow>({
@@ -190,23 +220,36 @@ export class SqliteWebhookRunStorage implements WebhookRunStorage {
190
220
  limit: input.limit,
191
221
  offset: input.offset,
192
222
  })
193
-
194
- return {
195
- runs: rows.map(rowToWebhookRunRecord),
196
- hasMore,
197
- total,
198
- }
223
+ return { runs: rows.map(rowToWebhookRunRecord), hasMore, total }
199
224
  }
200
225
 
201
226
  close(): void {
202
227
  closeSqliteStoreConnection(this.connection)
203
228
  }
229
+
230
+ private getWebhookRunRow(projectId: string, id: string): WebhookRunDatabaseRow | null {
231
+ return this.db
232
+ .query("SELECT * FROM webhook_runs WHERE project_id = ? AND id = ?")
233
+ .get(projectId, id) as WebhookRunDatabaseRow | null
234
+ }
235
+
236
+ private requireWebhookRun(projectId: string, id: string): WebhookRunRecord {
237
+ const row = this.getWebhookRunRow(projectId, id)
238
+ if (!row) {
239
+ throw new WebhookRunError(
240
+ `[SixbSqlite] Failed to load webhook run '${id}' for project '${projectId}'.`,
241
+ "not_found"
242
+ )
243
+ }
244
+ return rowToWebhookRunRecord(row)
245
+ }
204
246
  }
205
247
 
206
248
  function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
207
249
  return {
208
250
  id: row.id,
209
251
  projectId: row.project_id,
252
+ executionId: row.execution_id,
210
253
  connectorId: row.connector_id,
211
254
  webhookId: row.webhook_id,
212
255
  status: row.status,
@@ -214,17 +257,18 @@ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
214
257
  route: row.route,
215
258
  startedAt: new Date(row.started_at),
216
259
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
217
- requestBodyBytes: row.request_body_bytes ?? undefined,
260
+ requestBodyBytes: row.request_body_bytes,
261
+ requestBodySha256: row.request_body_sha256,
218
262
  responseStatus: row.response_status ?? undefined,
219
263
  idempotencyKey: row.idempotency_key ?? undefined,
220
- deliveryClaimResult: row.delivery_claim_result ?? undefined,
221
- error: row.error ?? undefined,
264
+ error: row.error === null ? undefined : parseSixbFailure(row.error, WEBHOOK_RUN_FAILURE_CODES),
222
265
  }
223
266
  }
224
267
 
225
268
  interface WebhookRunDatabaseRow {
226
269
  readonly project_id: string
227
270
  readonly id: string
271
+ readonly execution_id: string
228
272
  readonly connector_id: string
229
273
  readonly webhook_id: string
230
274
  readonly status: WebhookRunRecord["status"]
@@ -232,9 +276,9 @@ interface WebhookRunDatabaseRow {
232
276
  readonly route: string
233
277
  readonly started_at: string
234
278
  readonly finished_at: string | null
235
- readonly request_body_bytes: number | null
279
+ readonly request_body_bytes: number
280
+ readonly request_body_sha256: string
236
281
  readonly response_status: number | null
237
282
  readonly idempotency_key: string | null
238
- readonly delivery_claim_result: WebhookRunRecord["deliveryClaimResult"] | null
239
283
  readonly error: string | null
240
284
  }