@sixb/pg 0.1.1 → 0.1.3

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 (112) hide show
  1. package/README.md +7 -6
  2. package/dist/agents/agent-storage.d.ts +7 -4
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/checkpoints.d.ts +12 -0
  5. package/dist/agents/checkpoints.d.ts.map +1 -0
  6. package/dist/agents/index.d.ts +1 -0
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/messages.d.ts +2 -1
  9. package/dist/agents/messages.d.ts.map +1 -1
  10. package/dist/agents/rows.d.ts +22 -12
  11. package/dist/agents/rows.d.ts.map +1 -1
  12. package/dist/agents/runs.d.ts +3 -2
  13. package/dist/agents/runs.d.ts.map +1 -1
  14. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  15. package/dist/auth-storage/shared.d.ts +0 -4
  16. package/dist/auth-storage/shared.d.ts.map +1 -1
  17. package/dist/connector-connection-storage.d.ts +9 -0
  18. package/dist/connector-connection-storage.d.ts.map +1 -0
  19. package/dist/index.d.ts +9 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5010 -1837
  22. package/dist/index.js.map +31 -26
  23. package/dist/latest-run-query.d.ts.map +1 -1
  24. package/dist/migrations.d.ts.map +1 -1
  25. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  26. package/dist/ontology-storage/materialization-state.d.ts +0 -2
  27. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  28. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  29. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  30. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  31. package/dist/ontology-storage/shared.d.ts +3 -6
  32. package/dist/ontology-storage/shared.d.ts.map +1 -1
  33. package/dist/pg-action-run-storage.d.ts +3 -2
  34. package/dist/pg-action-run-storage.d.ts.map +1 -1
  35. package/dist/pg-ai-cost-storage.d.ts +12 -0
  36. package/dist/pg-ai-cost-storage.d.ts.map +1 -0
  37. package/dist/pg-ai-usage-storage.d.ts +16 -0
  38. package/dist/pg-ai-usage-storage.d.ts.map +1 -0
  39. package/dist/pg-execution-storage.d.ts +14 -0
  40. package/dist/pg-execution-storage.d.ts.map +1 -0
  41. package/dist/pg-pipeline-run-storage.d.ts +5 -2
  42. package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
  43. package/dist/pg-projection-run-storage.d.ts +5 -2
  44. package/dist/pg-projection-run-storage.d.ts.map +1 -1
  45. package/dist/pg-sync-run-storage.d.ts +5 -2
  46. package/dist/pg-sync-run-storage.d.ts.map +1 -1
  47. package/dist/pg-webhook-run-storage.d.ts +5 -2
  48. package/dist/pg-webhook-run-storage.d.ts.map +1 -1
  49. package/dist/pg-workflow-run-storage.d.ts +5 -3
  50. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  51. package/dist/run-list-query.d.ts +1 -1
  52. package/dist/run-list-query.d.ts.map +1 -1
  53. package/dist/testing.d.ts +6 -0
  54. package/dist/testing.d.ts.map +1 -0
  55. package/package.json +3 -3
  56. package/src/agents/agent-storage.ts +9 -5
  57. package/src/agents/checkpoints.ts +175 -0
  58. package/src/agents/index.ts +1 -0
  59. package/src/agents/messages.ts +71 -13
  60. package/src/agents/rows.ts +57 -43
  61. package/src/agents/runs.ts +29 -15
  62. package/src/auth-storage/service-accounts.ts +14 -15
  63. package/src/auth-storage/shared.ts +0 -8
  64. package/src/connector-connection-storage.ts +760 -0
  65. package/src/index.ts +38 -15
  66. package/src/latest-run-query.ts +7 -1
  67. package/src/migrations/004-executions.sql +140 -0
  68. package/src/migrations/005-workflow-executions.sql +10 -0
  69. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  70. package/src/migrations/007-split-overrides.sql +153 -0
  71. package/src/migrations/008-action-executions.sql +7 -0
  72. package/src/migrations/009-agent-executions.sql +21 -0
  73. package/src/migrations/010-ai-usage-accounting-foundation.sql +76 -0
  74. package/src/migrations/011-sync-failure-record.sql +23 -0
  75. package/src/migrations/012-pipeline-failure-record.sql +47 -0
  76. package/src/migrations/013-workflow-failure-record.sql +45 -0
  77. package/src/migrations/014-agent-failure-record.sql +56 -0
  78. package/src/migrations/015-projection-failure-record.sql +20 -0
  79. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  80. package/src/migrations/017-action-failure-record.sql +65 -0
  81. package/src/migrations/018-ontology-outbox-failure-record.sql +29 -0
  82. package/src/migrations/019-webhook-delivery-failure-record.sql +27 -0
  83. package/src/migrations/020-drop-run-usage-projections.sql +8 -0
  84. package/src/migrations/021-sync-pipeline-executions.sql +50 -0
  85. package/src/migrations/022-projection-executions.sql +115 -0
  86. package/src/migrations/023-webhook-executions.sql +30 -0
  87. package/src/migrations/024-ontology-commit-executions.sql +12 -0
  88. package/src/migrations/025-connector-connections.sql +231 -0
  89. package/src/migrations/026-ai-cost-accounting.sql +53 -0
  90. package/src/migrations/027-agent-context-checkpoints.sql +34 -0
  91. package/src/migrations/028-object-override-edit-times.sql +3 -0
  92. package/src/migrations.ts +82 -0
  93. package/src/ontology-storage/materialization-session.ts +25 -28
  94. package/src/ontology-storage/materialization-state.ts +161 -43
  95. package/src/ontology-storage/materialization-writer.ts +149 -76
  96. package/src/ontology-storage/materializations.ts +3 -1
  97. package/src/ontology-storage/outbox.ts +9 -2
  98. package/src/ontology-storage/shared.ts +10 -20
  99. package/src/pg-action-run-storage.ts +43 -67
  100. package/src/pg-ai-cost-storage.ts +707 -0
  101. package/src/pg-ai-usage-storage.ts +290 -0
  102. package/src/pg-execution-storage.ts +147 -0
  103. package/src/pg-pipeline-run-storage.ts +107 -41
  104. package/src/pg-projection-run-storage.ts +159 -91
  105. package/src/pg-sync-run-storage.ts +100 -28
  106. package/src/pg-webhook-run-storage.ts +89 -57
  107. package/src/pg-workflow-run-storage.ts +97 -130
  108. package/src/run-list-query.ts +9 -4
  109. package/src/testing.ts +18 -0
  110. package/dist/pg-webhook-delivery-storage.d.ts +0 -17
  111. package/dist/pg-webhook-delivery-storage.d.ts.map +0 -1
  112. package/src/pg-webhook-delivery-storage.ts +0 -180
@@ -1,16 +1,23 @@
1
1
  import type { JsonValue } from "@sixb/core"
2
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
3
+ import { assertSyncRunExecution } from "@sixb/core/internal/sync-run-storage-provider"
2
4
  import type {
5
+ ExecutionStorage,
3
6
  FinishSyncRunInput,
4
7
  ListLatestSyncRunsInput,
5
8
  ListLatestSyncRunsResult,
6
9
  ListSyncRunsInput,
7
10
  ListSyncRunsResult,
11
+ QueueSyncRunInput,
8
12
  StartSyncRunInput,
9
- SyncRunFailure,
10
13
  SyncRunRecord,
11
14
  SyncRunStorage,
12
15
  } from "@sixb/core/storage"
13
- import { SyncRunError } from "@sixb/core/storage"
16
+ import {
17
+ canRequeueSyncRunAfterEnqueueFailure,
18
+ SYNC_RUN_FAILURE_CODES,
19
+ SyncRunError,
20
+ } from "@sixb/core/storage"
14
21
  import { queryLatestRunsByOwnerId } from "./latest-run-query"
15
22
  import type { SqlParameter } from "./pg-client"
16
23
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
@@ -18,29 +25,43 @@ import { isUniqueViolation } from "./storage-errors"
18
25
  import { type PgStoreClient, runPgTransaction } from "./transactions"
19
26
 
20
27
  export class PgSyncRunStorage implements SyncRunStorage {
21
- constructor(private readonly sql: PgStoreClient) {}
28
+ constructor(
29
+ private readonly sql: PgStoreClient,
30
+ private readonly executions: ExecutionStorage
31
+ ) {}
22
32
 
23
- async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
33
+ async queue(input: QueueSyncRunInput): Promise<SyncRunRecord> {
34
+ await assertSyncRunExecution({
35
+ executions: this.executions,
36
+ projectId: input.projectId,
37
+ executionId: input.executionId,
38
+ runId: input.id,
39
+ syncId: input.syncId,
40
+ })
24
41
  try {
25
42
  const [row] = await this.sql<DatabaseRow[]>`
26
43
  INSERT INTO sync_runs (
27
44
  project_id,
28
45
  id,
46
+ execution_id,
29
47
  sync_id,
30
48
  dataset_id,
31
49
  mode,
32
50
  status,
51
+ queued_at,
33
52
  started_at,
34
53
  expected_latest_version_id,
35
54
  commit_message
36
55
  ) VALUES (
37
56
  ${input.projectId},
38
57
  ${input.id},
58
+ ${input.executionId},
39
59
  ${input.syncId},
40
60
  ${input.datasetId},
41
61
  ${input.mode},
42
- ${"running"},
43
- ${input.startedAt ?? new Date()},
62
+ ${"queued"},
63
+ ${input.queuedAt ?? new Date()},
64
+ ${null},
44
65
  ${input.expectedLatestVersionId ?? null},
45
66
  ${input.commitMessage ?? null}
46
67
  )
@@ -50,13 +71,57 @@ export class PgSyncRunStorage implements SyncRunStorage {
50
71
  return rowToSyncRunRecord(row)
51
72
  } catch (error) {
52
73
  if (isUniqueViolation(error)) {
74
+ return this.requeueAfterEnqueueFailure(input)
75
+ }
76
+
77
+ throw error
78
+ }
79
+ }
80
+
81
+ private async requeueAfterEnqueueFailure(input: QueueSyncRunInput): Promise<SyncRunRecord> {
82
+ return runPgTransaction(this.sql, async (tx) => {
83
+ const [existing] = await tx<DatabaseRow[]>`
84
+ SELECT *, checkpoint IS NOT NULL AS checkpoint_present FROM sync_runs
85
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
86
+ FOR UPDATE
87
+ `
88
+ if (!existing || !canRequeueSyncRunAfterEnqueueFailure(rowToSyncRunRecord(existing), input)) {
53
89
  throw new SyncRunError(
54
90
  `[SixbPg] Sync run '${input.id}' already exists for project '${input.projectId}'.`
55
91
  )
56
92
  }
57
93
 
58
- throw error
94
+ const [updated] = await tx<DatabaseRow[]>`
95
+ UPDATE sync_runs
96
+ SET status = ${"queued"}, queued_at = ${input.queuedAt ?? new Date()},
97
+ started_at = ${null}, finished_at = ${null}, rows_read = ${null},
98
+ output_version_id = ${null}, error = ${null}, checkpoint = ${null}
99
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
100
+ RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
101
+ `
102
+ return rowToSyncRunRecord(updated)
103
+ })
104
+ }
105
+
106
+ async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
107
+ const [updated] = await this.sql<DatabaseRow[]>`
108
+ UPDATE sync_runs
109
+ SET status = ${"running"}, started_at = ${input.startedAt ?? new Date()},
110
+ error = ${null}
111
+ WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"queued"}
112
+ RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
113
+ `
114
+ if (updated) return rowToSyncRunRecord(updated)
115
+
116
+ const existing = await this.getById({ projectId: input.projectId, id: input.id })
117
+ if (!existing) {
118
+ throw new SyncRunError(
119
+ `[SixbPg] Sync run '${input.id}' not found for project '${input.projectId}'.`
120
+ )
59
121
  }
122
+ throw new SyncRunError(
123
+ `[SixbPg] Sync run '${input.id}' cannot start from status '${existing.status}'.`
124
+ )
60
125
  }
61
126
 
62
127
  async finish(input: FinishSyncRunInput): Promise<SyncRunRecord> {
@@ -72,6 +137,17 @@ export class PgSyncRunStorage implements SyncRunStorage {
72
137
  )
73
138
  }
74
139
 
140
+ const enqueueFailure =
141
+ input.status === "failed" && input.error.code === "queue.enqueue_failed"
142
+ if (
143
+ (enqueueFailure && existing.status !== "queued") ||
144
+ (!enqueueFailure && existing.status !== "running")
145
+ ) {
146
+ throw new SyncRunError(
147
+ `[SixbPg] Sync run '${input.id}' cannot finish from status '${existing.status}'.`
148
+ )
149
+ }
150
+
75
151
  if (input.status === "succeeded") {
76
152
  if (input.output && input.output.datasetId !== existing.dataset_id) {
77
153
  throw new SyncRunError(
@@ -94,8 +170,7 @@ export class PgSyncRunStorage implements SyncRunStorage {
94
170
  finished_at = ${input.finishedAt ?? new Date()},
95
171
  rows_read = ${input.rowsRead},
96
172
  output_version_id = ${input.output?.versionId ?? null},
97
- error_name = ${null},
98
- error_message = ${null},
173
+ error = ${null},
99
174
  checkpoint = ${serializeCheckpoint(input.checkpoint)}::text::jsonb
100
175
  WHERE project_id = ${input.projectId} AND id = ${input.id}
101
176
  RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
@@ -107,8 +182,7 @@ export class PgSyncRunStorage implements SyncRunStorage {
107
182
  finished_at = ${input.finishedAt ?? new Date()},
108
183
  rows_read = ${input.rowsRead ?? existing.rows_read ?? null},
109
184
  output_version_id = ${null},
110
- error_name = ${input.error?.name ?? null},
111
- error_message = ${input.error?.message ?? null},
185
+ error = ${input.error === undefined ? null : serializeSixbFailure(input.error, SYNC_RUN_FAILURE_CODES)}::text::jsonb,
112
186
  checkpoint = ${null}
113
187
  WHERE project_id = ${input.projectId} AND id = ${input.id}
114
188
  RETURNING *, checkpoint IS NOT NULL AS checkpoint_present
@@ -156,7 +230,13 @@ export class PgSyncRunStorage implements SyncRunStorage {
156
230
  params.push(input.datasetId)
157
231
  }
158
232
 
159
- index = appendRunListFilters(whereClauses, params, index, input)
233
+ index = appendRunListFilters(
234
+ whereClauses,
235
+ params,
236
+ index,
237
+ input,
238
+ "COALESCE(started_at, queued_at)"
239
+ )
160
240
 
161
241
  const { rows, total, hasMore } = await queryRunList<DatabaseRow>({
162
242
  sql: this.sql,
@@ -198,26 +278,17 @@ function serializeCheckpoint(checkpoint: JsonValue | undefined): string | null {
198
278
  return checkpoint !== undefined ? JSON.stringify(checkpoint) : null
199
279
  }
200
280
 
201
- function toSyncRunFailure(row: DatabaseRow): SyncRunFailure | undefined {
202
- if (!row.error_message) {
203
- return undefined
204
- }
205
-
206
- return {
207
- name: row.error_name ?? undefined,
208
- message: row.error_message,
209
- }
210
- }
211
-
212
281
  function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
213
282
  return {
214
283
  id: row.id,
215
284
  projectId: row.project_id,
285
+ executionId: row.execution_id,
216
286
  syncId: row.sync_id,
217
287
  datasetId: row.dataset_id,
218
288
  mode: row.mode,
219
289
  status: row.status,
220
- startedAt: new Date(row.started_at),
290
+ queuedAt: new Date(row.queued_at),
291
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
221
292
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
222
293
  rowsRead: row.rows_read != null ? Number(row.rows_read) : undefined,
223
294
  output: row.output_version_id
@@ -228,7 +299,7 @@ function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
228
299
  : undefined,
229
300
  expectedLatestVersionId: row.expected_latest_version_id ?? undefined,
230
301
  commitMessage: row.commit_message ?? undefined,
231
- error: toSyncRunFailure(row),
302
+ error: row.error === null ? undefined : parseSixbFailure(row.error, SYNC_RUN_FAILURE_CODES),
232
303
  checkpoint: hasCheckpoint(row) ? row.checkpoint : undefined,
233
304
  }
234
305
  }
@@ -246,18 +317,19 @@ function hasCheckpoint(row: DatabaseRow): boolean {
246
317
  interface DatabaseRow {
247
318
  project_id: string
248
319
  id: string
320
+ execution_id: string
249
321
  sync_id: string
250
322
  dataset_id: string
251
323
  mode: SyncRunRecord["mode"]
252
324
  status: SyncRunRecord["status"]
253
- started_at: Date | string
325
+ queued_at: Date | string
326
+ started_at: Date | string | null
254
327
  finished_at: Date | string | null
255
328
  rows_read: number | string | null
256
329
  output_version_id: string | null
257
330
  expected_latest_version_id: string | null
258
331
  commit_message: string | null
259
- error_name: string | null
260
- error_message: string | null
332
+ error: JsonValue | null
261
333
  checkpoint: JsonValue | null
262
334
  checkpoint_present: boolean | number | string
263
335
  }
@@ -1,91 +1,126 @@
1
+ import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
2
+ import { assertWebhookRunExecution } from "@sixb/core/internal/webhook-run-storage-provider"
1
3
  import type {
4
+ ExecutionStorage,
2
5
  FinishWebhookRunInput,
6
+ GetWebhookRunByDeliveryInput,
3
7
  ListWebhookRunsInput,
4
8
  ListWebhookRunsResult,
9
+ RestartWebhookRunInput,
5
10
  StartWebhookRunInput,
6
11
  WebhookRunRecord,
7
12
  WebhookRunStorage,
8
13
  } from "@sixb/core/storage"
9
- import { WebhookRunError } from "@sixb/core/storage"
14
+ import { canRetryWebhookRun, WEBHOOK_RUN_FAILURE_CODES, WebhookRunError } from "@sixb/core/storage"
10
15
  import type { SqlParameter } from "./pg-client"
11
16
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
12
17
  import { isUniqueViolation } from "./storage-errors"
13
18
  import { type PgStoreClient, runPgTransaction } from "./transactions"
14
19
 
15
20
  export class PgWebhookRunStorage implements WebhookRunStorage {
16
- constructor(private readonly sql: PgStoreClient) {}
21
+ constructor(
22
+ private readonly sql: PgStoreClient,
23
+ private readonly executions: ExecutionStorage
24
+ ) {}
17
25
 
18
26
  async start(input: StartWebhookRunInput): Promise<WebhookRunRecord> {
27
+ await assertWebhookRunExecution({
28
+ executions: this.executions,
29
+ projectId: input.projectId,
30
+ executionId: input.executionId,
31
+ runId: input.id,
32
+ route: input.route,
33
+ })
19
34
  try {
20
35
  const [row] = await this.sql<WebhookRunDatabaseRow[]>`
21
36
  INSERT INTO webhook_runs (
22
- project_id,
23
- id,
24
- connector_id,
25
- webhook_id,
26
- status,
27
- method,
28
- route,
29
- started_at
37
+ project_id, id, execution_id, connector_id, webhook_id, status, method, route,
38
+ started_at, request_body_bytes, request_body_sha256, idempotency_key
30
39
  ) VALUES (
31
- ${input.projectId},
32
- ${input.id},
33
- ${input.connectorId},
34
- ${input.webhookId},
35
- ${"running"},
36
- ${input.method},
37
- ${input.route},
38
- ${input.startedAt ?? new Date()}
40
+ ${input.projectId}, ${input.id}, ${input.executionId}, ${input.connectorId},
41
+ ${input.webhookId}, ${"running"}, ${input.method}, ${input.route},
42
+ ${input.startedAt ?? new Date()}, ${input.requestBodyBytes},
43
+ ${input.requestBodySha256}, ${input.idempotencyKey ?? null}
39
44
  )
40
45
  RETURNING *
41
46
  `
42
-
43
47
  return rowToWebhookRunRecord(row)
44
48
  } catch (error) {
45
49
  if (isUniqueViolation(error)) {
46
50
  throw new WebhookRunError(
47
- `[SixbPg] Webhook run '${input.id}' already exists for project '${input.projectId}'.`
51
+ `[SixbPg] Webhook run '${input.id}' or delivery key already exists for project '${input.projectId}'.`,
52
+ "duplicate_run"
48
53
  )
49
54
  }
50
-
51
55
  throw error
52
56
  }
53
57
  }
54
58
 
55
- async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
59
+ async restart(input: RestartWebhookRunInput): Promise<WebhookRunRecord> {
56
60
  return runPgTransaction(this.sql, async (tx) => {
57
61
  const [existing] = await tx<WebhookRunDatabaseRow[]>`
58
62
  SELECT * FROM webhook_runs
59
63
  WHERE project_id = ${input.projectId} AND id = ${input.id}
60
64
  FOR UPDATE
61
65
  `
62
-
63
66
  if (!existing) {
64
67
  throw new WebhookRunError(
65
- `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`
68
+ `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
69
+ "not_found"
70
+ )
71
+ }
72
+ if (!canRetryWebhookRun(rowToWebhookRunRecord(existing))) {
73
+ throw new WebhookRunError(
74
+ `[SixbPg] Webhook run '${input.id}' cannot restart from status '${existing.status}'.`,
75
+ "invalid_transition"
76
+ )
77
+ }
78
+
79
+ const [updated] = await tx<WebhookRunDatabaseRow[]>`
80
+ UPDATE webhook_runs
81
+ SET status = ${"running"}, started_at = ${input.startedAt ?? new Date()},
82
+ finished_at = ${null}, response_status = ${null}, error = ${null}
83
+ WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"failed"}
84
+ RETURNING *
85
+ `
86
+ if (!updated) {
87
+ throw new WebhookRunError(
88
+ `[SixbPg] Webhook run '${input.id}' changed while restarting.`,
89
+ "invalid_transition"
66
90
  )
67
91
  }
92
+ return rowToWebhookRunRecord(updated)
93
+ })
94
+ }
68
95
 
96
+ async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
97
+ return runPgTransaction(this.sql, async (tx) => {
98
+ const [existing] = await tx<WebhookRunDatabaseRow[]>`
99
+ SELECT * FROM webhook_runs
100
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
101
+ FOR UPDATE
102
+ `
103
+ if (!existing) {
104
+ throw new WebhookRunError(
105
+ `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`,
106
+ "not_found"
107
+ )
108
+ }
69
109
  if (existing.status !== "running") {
70
110
  throw new WebhookRunError(
71
- `[SixbPg] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`
111
+ `[SixbPg] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`,
112
+ "invalid_transition"
72
113
  )
73
114
  }
74
115
 
75
116
  const [updated] = await tx<WebhookRunDatabaseRow[]>`
76
117
  UPDATE webhook_runs
77
- SET
78
- status = ${input.status},
79
- finished_at = ${input.finishedAt ?? new Date()},
80
- request_body_bytes = ${input.requestBodyBytes ?? null},
81
- response_status = ${input.responseStatus ?? null},
82
- idempotency_key = ${input.idempotencyKey ?? null},
83
- delivery_claim_result = ${input.deliveryClaimResult ?? null},
84
- error = ${input.status === "succeeded" ? null : (input.error ?? null)}
118
+ SET status = ${input.status}, finished_at = ${input.finishedAt ?? new Date()},
119
+ response_status = ${input.responseStatus ?? null},
120
+ error = ${input.status === "succeeded" ? null : serializeSixbFailure(input.error, WEBHOOK_RUN_FAILURE_CODES)}::text::jsonb
85
121
  WHERE project_id = ${input.projectId} AND id = ${input.id}
86
122
  RETURNING *
87
123
  `
88
-
89
124
  return rowToWebhookRunRecord(updated)
90
125
  })
91
126
  }
@@ -95,38 +130,38 @@ export class PgWebhookRunStorage implements WebhookRunStorage {
95
130
  SELECT * FROM webhook_runs
96
131
  WHERE project_id = ${params.projectId} AND id = ${params.id}
97
132
  `
133
+ return row ? rowToWebhookRunRecord(row) : null
134
+ }
98
135
 
136
+ async getByDelivery(input: GetWebhookRunByDeliveryInput): Promise<WebhookRunRecord | null> {
137
+ const [row] = await this.sql<WebhookRunDatabaseRow[]>`
138
+ SELECT * FROM webhook_runs
139
+ WHERE project_id = ${input.projectId}
140
+ AND connector_id = ${input.connectorId}
141
+ AND webhook_id = ${input.webhookId}
142
+ AND idempotency_key = ${input.idempotencyKey}
143
+ `
99
144
  return row ? rowToWebhookRunRecord(row) : null
100
145
  }
101
146
 
102
147
  async list(input: ListWebhookRunsInput): Promise<ListWebhookRunsResult> {
103
- if (hasEmptyStatuses(input)) {
104
- return {
105
- runs: [],
106
- hasMore: false,
107
- total: 0,
108
- }
109
- }
148
+ if (hasEmptyStatuses(input)) return { runs: [], hasMore: false, total: 0 }
110
149
 
111
150
  const whereClauses = ["project_id = $1"]
112
151
  const params: SqlParameter[] = [input.projectId]
113
152
  let index = 2
114
-
115
153
  if (input.connectorId) {
116
154
  whereClauses.push(`connector_id = $${index++}`)
117
155
  params.push(input.connectorId)
118
156
  }
119
-
120
157
  if (input.webhookId) {
121
158
  whereClauses.push(`webhook_id = $${index++}`)
122
159
  params.push(input.webhookId)
123
160
  }
124
-
125
161
  if (input.idempotencyKey) {
126
162
  whereClauses.push(`idempotency_key = $${index++}`)
127
163
  params.push(input.idempotencyKey)
128
164
  }
129
-
130
165
  index = appendRunListFilters(whereClauses, params, index, input)
131
166
 
132
167
  const { rows, total, hasMore } = await queryRunList<WebhookRunDatabaseRow>({
@@ -139,12 +174,7 @@ export class PgWebhookRunStorage implements WebhookRunStorage {
139
174
  limit: input.limit,
140
175
  offset: input.offset,
141
176
  })
142
-
143
- return {
144
- runs: rows.map(rowToWebhookRunRecord),
145
- hasMore,
146
- total,
147
- }
177
+ return { runs: rows.map(rowToWebhookRunRecord), hasMore, total }
148
178
  }
149
179
  }
150
180
 
@@ -152,6 +182,7 @@ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
152
182
  return {
153
183
  id: row.id,
154
184
  projectId: row.project_id,
185
+ executionId: row.execution_id,
155
186
  connectorId: row.connector_id,
156
187
  webhookId: row.webhook_id,
157
188
  status: row.status,
@@ -159,17 +190,18 @@ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
159
190
  route: row.route,
160
191
  startedAt: new Date(row.started_at),
161
192
  finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
162
- requestBodyBytes: row.request_body_bytes != null ? Number(row.request_body_bytes) : undefined,
193
+ requestBodyBytes: Number(row.request_body_bytes),
194
+ requestBodySha256: row.request_body_sha256,
163
195
  responseStatus: row.response_status != null ? Number(row.response_status) : undefined,
164
196
  idempotencyKey: row.idempotency_key ?? undefined,
165
- deliveryClaimResult: row.delivery_claim_result ?? undefined,
166
- error: row.error ?? undefined,
197
+ error: row.error === null ? undefined : parseSixbFailure(row.error, WEBHOOK_RUN_FAILURE_CODES),
167
198
  }
168
199
  }
169
200
 
170
201
  interface WebhookRunDatabaseRow {
171
202
  readonly project_id: string
172
203
  readonly id: string
204
+ readonly execution_id: string
173
205
  readonly connector_id: string
174
206
  readonly webhook_id: string
175
207
  readonly status: WebhookRunRecord["status"]
@@ -177,9 +209,9 @@ interface WebhookRunDatabaseRow {
177
209
  readonly route: string
178
210
  readonly started_at: Date | string
179
211
  readonly finished_at: Date | string | null
180
- readonly request_body_bytes: number | string | null
212
+ readonly request_body_bytes: number | string
213
+ readonly request_body_sha256: string
181
214
  readonly response_status: number | string | null
182
215
  readonly idempotency_key: string | null
183
- readonly delivery_claim_result: WebhookRunRecord["deliveryClaimResult"] | null
184
- readonly error: string | null
216
+ readonly error: unknown | null
185
217
  }