@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,180 @@
1
+ import type {
2
+ TimeseriesHistoryBatchInput,
3
+ TimeseriesHistoryBatchResult,
4
+ TimeseriesPoint,
5
+ TimeseriesStorage,
6
+ } from "@sixb/core/storage"
7
+ import type { SqlParameter } from "./pg-client"
8
+ import type { PgStoreClient } from "./transactions"
9
+
10
+ /**
11
+ * PostgreSQL-based TimeseriesStorage implementation.
12
+ *
13
+ * Stores time-series data with JSONB values and TIMESTAMPTZ timestamps
14
+ * for efficient querying of history and latest values.
15
+ *
16
+ * Requires `search_path` to be set to the Sixb schema on the connection.
17
+ */
18
+ export class PgTimeseriesStorage implements TimeseriesStorage {
19
+ constructor(private readonly sql: PgStoreClient) {}
20
+
21
+ async getHistory(params: {
22
+ projectId: string
23
+ objectTypeId: string
24
+ objectId: string
25
+ propertyId: string
26
+ from?: Date
27
+ to?: Date
28
+ limit?: number
29
+ order?: "asc" | "desc"
30
+ }): Promise<readonly TimeseriesPoint[]> {
31
+ const order = params.order ?? "asc"
32
+
33
+ // Build WHERE conditions using conditional fragments
34
+ const fromFilter = params.from ? this.sql`AND at >= ${params.from}` : this.sql``
35
+ const toFilter = params.to ? this.sql`AND at <= ${params.to}` : this.sql``
36
+ const limitFilter = params.limit !== undefined ? this.sql`LIMIT ${params.limit}` : this.sql``
37
+
38
+ const rows = await this.sql<TimeseriesDatabaseRow[]>`
39
+ SELECT * FROM timeseries
40
+ WHERE project_id = ${params.projectId}
41
+ AND object_type_id = ${params.objectTypeId}
42
+ AND object_id = ${params.objectId}
43
+ AND property_id = ${params.propertyId}
44
+ ${fromFilter}
45
+ ${toFilter}
46
+ ORDER BY at ${order === "asc" ? this.sql`ASC` : this.sql`DESC`}
47
+ ${limitFilter}
48
+ `
49
+
50
+ return rows.map((row) => rowToPoint(row))
51
+ }
52
+
53
+ async getHistoryBatch(
54
+ input: TimeseriesHistoryBatchInput
55
+ ): Promise<readonly TimeseriesHistoryBatchResult[]> {
56
+ if (input.series.length === 0) {
57
+ return []
58
+ }
59
+
60
+ const order = input.order === "desc" ? "DESC" : "ASC"
61
+ const params: unknown[] = []
62
+ let nextIndex = 1
63
+ const values = input.series
64
+ .map((series, seriesIndex) => {
65
+ const placeholders = [
66
+ `$${nextIndex++}`,
67
+ `$${nextIndex++}`,
68
+ `$${nextIndex++}`,
69
+ `$${nextIndex++}`,
70
+ ]
71
+ params.push(seriesIndex, series.objectTypeId, series.objectId, series.propertyId)
72
+ return `(${placeholders.join(", ")})`
73
+ })
74
+ .join(", ")
75
+
76
+ const projectIdPlaceholder = `$${nextIndex++}`
77
+ params.push(input.projectId)
78
+
79
+ const whereClauses = ["1 = 1"]
80
+ if (input.from) {
81
+ whereClauses.push(`timeseries.at >= $${nextIndex++}`)
82
+ params.push(input.from)
83
+ }
84
+ if (input.to) {
85
+ whereClauses.push(`timeseries.at <= $${nextIndex++}`)
86
+ params.push(input.to)
87
+ }
88
+
89
+ let query = `
90
+ WITH requested(series_index, object_type_id, object_id, property_id) AS (
91
+ VALUES ${values}
92
+ ),
93
+ ranked AS (
94
+ SELECT
95
+ requested.series_index,
96
+ timeseries.*,
97
+ ROW_NUMBER() OVER (
98
+ PARTITION BY requested.series_index
99
+ ORDER BY timeseries.at ${order}
100
+ ) AS series_row_number
101
+ FROM requested
102
+ JOIN timeseries
103
+ ON timeseries.project_id = ${projectIdPlaceholder}
104
+ AND timeseries.object_type_id = requested.object_type_id
105
+ AND timeseries.object_id = requested.object_id
106
+ AND timeseries.property_id = requested.property_id
107
+ WHERE ${whereClauses.join(" AND ")}
108
+ )
109
+ SELECT * FROM ranked
110
+ `
111
+
112
+ if (input.limitPerSeries !== undefined) {
113
+ query += ` WHERE series_row_number <= $${nextIndex++}`
114
+ params.push(Math.max(0, input.limitPerSeries))
115
+ }
116
+
117
+ query += ` ORDER BY series_index ASC, at ${order}`
118
+
119
+ const rows = await this.sql.unsafe<
120
+ (TimeseriesDatabaseRow & {
121
+ series_index: number | string
122
+ series_row_number: number | string
123
+ })[]
124
+ >(query, params as SqlParameter[])
125
+ const results = input.series.map((series) => ({
126
+ ...series,
127
+ points: [] as TimeseriesPoint[],
128
+ }))
129
+
130
+ for (const row of rows) {
131
+ const result = results[Number(row.series_index)]
132
+ if (result) {
133
+ result.points.push(rowToPoint(row))
134
+ }
135
+ }
136
+
137
+ return results
138
+ }
139
+
140
+ async getLatest(params: {
141
+ projectId: string
142
+ objectTypeId: string
143
+ objectId: string
144
+ propertyId: string
145
+ }): Promise<TimeseriesPoint | null> {
146
+ const [row] = await this.sql<TimeseriesDatabaseRow[]>`
147
+ SELECT * FROM timeseries_latest
148
+ WHERE project_id = ${params.projectId}
149
+ AND object_type_id = ${params.objectTypeId}
150
+ AND object_id = ${params.objectId}
151
+ AND property_id = ${params.propertyId}
152
+ `
153
+
154
+ return row ? rowToPoint(row) : null
155
+ }
156
+ }
157
+
158
+ function rowToPoint(row: TimeseriesDatabaseRow): TimeseriesPoint {
159
+ return {
160
+ projectId: row.project_id,
161
+ objectTypeId: row.object_type_id,
162
+ objectId: row.object_id,
163
+ propertyId: row.property_id,
164
+ value: row.value,
165
+ unit: row.unit ?? undefined,
166
+ at: new Date(row.at),
167
+ lastCommitId: row.last_commit_id,
168
+ }
169
+ }
170
+
171
+ interface TimeseriesDatabaseRow {
172
+ project_id: string
173
+ object_type_id: string
174
+ object_id: string
175
+ property_id: string
176
+ value: unknown
177
+ unit: string | null
178
+ at: Date | string
179
+ last_commit_id: string
180
+ }
@@ -0,0 +1,180 @@
1
+ import type {
2
+ WebhookDeliveryClaimRecord,
3
+ WebhookDeliveryClaimResult,
4
+ WebhookDeliveryKey,
5
+ WebhookDeliveryRecord,
6
+ WebhookDeliveryStorage,
7
+ } from "@sixb/core/storage"
8
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
9
+
10
+ export class PgWebhookDeliveryStorage implements WebhookDeliveryStorage {
11
+ constructor(private readonly sql: PgStoreClient) {}
12
+
13
+ async claim(
14
+ input: WebhookDeliveryKey & { receivedAt: string }
15
+ ): Promise<WebhookDeliveryClaimRecord> {
16
+ return runPgTransaction(this.sql, async (tx) => {
17
+ // Atomic first claim: only one transaction can insert this scoped delivery key.
18
+ const [inserted] = await tx<WebhookDeliveryRow[]>`
19
+ INSERT INTO webhook_deliveries (
20
+ project_id,
21
+ connector_id,
22
+ webhook_id,
23
+ idempotency_key,
24
+ status,
25
+ received_at
26
+ ) VALUES (
27
+ ${input.projectId},
28
+ ${input.connectorId},
29
+ ${input.webhookId},
30
+ ${input.idempotencyKey},
31
+ ${"in_progress"},
32
+ ${input.receivedAt}
33
+ )
34
+ ON CONFLICT (project_id, connector_id, webhook_id, idempotency_key) DO NOTHING
35
+ RETURNING *
36
+ `
37
+
38
+ if (inserted) {
39
+ return toWebhookDeliveryClaimRecord(inserted, "claimed")
40
+ }
41
+
42
+ const [existing] = await tx<WebhookDeliveryRow[]>`
43
+ SELECT * FROM webhook_deliveries
44
+ WHERE project_id = ${input.projectId}
45
+ AND connector_id = ${input.connectorId}
46
+ AND webhook_id = ${input.webhookId}
47
+ AND idempotency_key = ${input.idempotencyKey}
48
+ FOR UPDATE
49
+ `
50
+
51
+ if (existing?.status === "completed") {
52
+ return toWebhookDeliveryClaimRecord(existing, "duplicate")
53
+ }
54
+
55
+ if (existing?.status === "in_progress") {
56
+ return toWebhookDeliveryClaimRecord(existing, "in_progress")
57
+ }
58
+
59
+ if (existing?.status !== "failed") {
60
+ throw new Error("[SixbPg] Webhook delivery row disappeared during claim.")
61
+ }
62
+
63
+ // Failed deliveries are intentionally claimable so provider retries can run again.
64
+ const [updated] = await tx<WebhookDeliveryRow[]>`
65
+ UPDATE webhook_deliveries
66
+ SET status = ${"in_progress"},
67
+ received_at = ${input.receivedAt},
68
+ completed_at = ${null},
69
+ failed_at = ${null},
70
+ error = ${null}
71
+ WHERE project_id = ${input.projectId}
72
+ AND connector_id = ${input.connectorId}
73
+ AND webhook_id = ${input.webhookId}
74
+ AND idempotency_key = ${input.idempotencyKey}
75
+ RETURNING *
76
+ `
77
+
78
+ if (!updated) {
79
+ throw new Error(
80
+ `[SixbPg] Missing webhook delivery row after claim for ${serializeKey(input)}.`
81
+ )
82
+ }
83
+
84
+ return toWebhookDeliveryClaimRecord(updated, "claimed")
85
+ })
86
+ }
87
+
88
+ async complete(
89
+ input: WebhookDeliveryKey & { completedAt: string }
90
+ ): Promise<WebhookDeliveryRecord> {
91
+ const [updated] = await this.sql<WebhookDeliveryRow[]>`
92
+ UPDATE webhook_deliveries
93
+ SET status = ${"completed"},
94
+ completed_at = ${input.completedAt},
95
+ failed_at = ${null},
96
+ error = ${null}
97
+ WHERE project_id = ${input.projectId}
98
+ AND connector_id = ${input.connectorId}
99
+ AND webhook_id = ${input.webhookId}
100
+ AND idempotency_key = ${input.idempotencyKey}
101
+ RETURNING *
102
+ `
103
+
104
+ if (!updated) {
105
+ throw new Error(
106
+ `[SixbPg] Missing webhook delivery row after complete for ${serializeKey(input)}.`
107
+ )
108
+ }
109
+
110
+ return toWebhookDeliveryRecord(updated)
111
+ }
112
+
113
+ async fail(
114
+ input: WebhookDeliveryKey & { failedAt: string; error: string }
115
+ ): Promise<WebhookDeliveryRecord> {
116
+ const [updated] = await this.sql<WebhookDeliveryRow[]>`
117
+ UPDATE webhook_deliveries
118
+ SET status = ${"failed"},
119
+ failed_at = ${input.failedAt},
120
+ error = ${input.error}
121
+ WHERE project_id = ${input.projectId}
122
+ AND connector_id = ${input.connectorId}
123
+ AND webhook_id = ${input.webhookId}
124
+ AND idempotency_key = ${input.idempotencyKey}
125
+ RETURNING *
126
+ `
127
+
128
+ if (!updated) {
129
+ throw new Error(
130
+ `[SixbPg] Missing webhook delivery row after fail for ${serializeKey(input)}.`
131
+ )
132
+ }
133
+
134
+ return toWebhookDeliveryRecord(updated)
135
+ }
136
+ }
137
+
138
+ function toWebhookDeliveryClaimRecord(
139
+ row: WebhookDeliveryRow,
140
+ claimResult: WebhookDeliveryClaimResult
141
+ ): WebhookDeliveryClaimRecord {
142
+ return {
143
+ ...toWebhookDeliveryRecord(row),
144
+ claimResult,
145
+ }
146
+ }
147
+
148
+ function toWebhookDeliveryRecord(row: WebhookDeliveryRow): WebhookDeliveryRecord {
149
+ return {
150
+ projectId: row.project_id,
151
+ connectorId: row.connector_id,
152
+ webhookId: row.webhook_id,
153
+ idempotencyKey: row.idempotency_key,
154
+ status: row.status,
155
+ receivedAt: toIsoString(row.received_at),
156
+ completedAt: row.completed_at ? toIsoString(row.completed_at) : undefined,
157
+ failedAt: row.failed_at ? toIsoString(row.failed_at) : undefined,
158
+ error: row.error ?? undefined,
159
+ }
160
+ }
161
+
162
+ function toIsoString(value: Date | string): string {
163
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString()
164
+ }
165
+
166
+ function serializeKey(key: WebhookDeliveryKey): string {
167
+ return JSON.stringify([key.projectId, key.connectorId, key.webhookId, key.idempotencyKey])
168
+ }
169
+
170
+ interface WebhookDeliveryRow {
171
+ readonly project_id: string
172
+ readonly connector_id: string
173
+ readonly webhook_id: string
174
+ readonly idempotency_key: string
175
+ readonly status: WebhookDeliveryRecord["status"]
176
+ readonly received_at: Date | string
177
+ readonly completed_at: Date | string | null
178
+ readonly failed_at: Date | string | null
179
+ readonly error: string | null
180
+ }
@@ -0,0 +1,185 @@
1
+ import type {
2
+ FinishWebhookRunInput,
3
+ ListWebhookRunsInput,
4
+ ListWebhookRunsResult,
5
+ StartWebhookRunInput,
6
+ WebhookRunRecord,
7
+ WebhookRunStorage,
8
+ } from "@sixb/core/storage"
9
+ import { WebhookRunError } from "@sixb/core/storage"
10
+ import type { SqlParameter } from "./pg-client"
11
+ import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
12
+ import { isUniqueViolation } from "./storage-errors"
13
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
14
+
15
+ export class PgWebhookRunStorage implements WebhookRunStorage {
16
+ constructor(private readonly sql: PgStoreClient) {}
17
+
18
+ async start(input: StartWebhookRunInput): Promise<WebhookRunRecord> {
19
+ try {
20
+ const [row] = await this.sql<WebhookRunDatabaseRow[]>`
21
+ INSERT INTO webhook_runs (
22
+ project_id,
23
+ id,
24
+ connector_id,
25
+ webhook_id,
26
+ status,
27
+ method,
28
+ route,
29
+ started_at
30
+ ) 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()}
39
+ )
40
+ RETURNING *
41
+ `
42
+
43
+ return rowToWebhookRunRecord(row)
44
+ } catch (error) {
45
+ if (isUniqueViolation(error)) {
46
+ throw new WebhookRunError(
47
+ `[SixbPg] Webhook run '${input.id}' already exists for project '${input.projectId}'.`
48
+ )
49
+ }
50
+
51
+ throw error
52
+ }
53
+ }
54
+
55
+ async finish(input: FinishWebhookRunInput): Promise<WebhookRunRecord> {
56
+ return runPgTransaction(this.sql, async (tx) => {
57
+ const [existing] = await tx<WebhookRunDatabaseRow[]>`
58
+ SELECT * FROM webhook_runs
59
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
60
+ FOR UPDATE
61
+ `
62
+
63
+ if (!existing) {
64
+ throw new WebhookRunError(
65
+ `[SixbPg] Webhook run '${input.id}' not found for project '${input.projectId}'.`
66
+ )
67
+ }
68
+
69
+ if (existing.status !== "running") {
70
+ throw new WebhookRunError(
71
+ `[SixbPg] Webhook run '${input.id}' for project '${input.projectId}' is already terminal.`
72
+ )
73
+ }
74
+
75
+ const [updated] = await tx<WebhookRunDatabaseRow[]>`
76
+ 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)}
85
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
86
+ RETURNING *
87
+ `
88
+
89
+ return rowToWebhookRunRecord(updated)
90
+ })
91
+ }
92
+
93
+ async getById(params: { projectId: string; id: string }): Promise<WebhookRunRecord | null> {
94
+ const [row] = await this.sql<WebhookRunDatabaseRow[]>`
95
+ SELECT * FROM webhook_runs
96
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
97
+ `
98
+
99
+ return row ? rowToWebhookRunRecord(row) : null
100
+ }
101
+
102
+ async list(input: ListWebhookRunsInput): Promise<ListWebhookRunsResult> {
103
+ if (hasEmptyStatuses(input)) {
104
+ return {
105
+ runs: [],
106
+ hasMore: false,
107
+ total: 0,
108
+ }
109
+ }
110
+
111
+ const whereClauses = ["project_id = $1"]
112
+ const params: SqlParameter[] = [input.projectId]
113
+ let index = 2
114
+
115
+ if (input.connectorId) {
116
+ whereClauses.push(`connector_id = $${index++}`)
117
+ params.push(input.connectorId)
118
+ }
119
+
120
+ if (input.webhookId) {
121
+ whereClauses.push(`webhook_id = $${index++}`)
122
+ params.push(input.webhookId)
123
+ }
124
+
125
+ if (input.idempotencyKey) {
126
+ whereClauses.push(`idempotency_key = $${index++}`)
127
+ params.push(input.idempotencyKey)
128
+ }
129
+
130
+ index = appendRunListFilters(whereClauses, params, index, input)
131
+
132
+ const { rows, total, hasMore } = await queryRunList<WebhookRunDatabaseRow>({
133
+ sql: this.sql,
134
+ tableName: "webhook_runs",
135
+ whereClauses,
136
+ params,
137
+ nextIndex: index,
138
+ order: input.order,
139
+ limit: input.limit,
140
+ offset: input.offset,
141
+ })
142
+
143
+ return {
144
+ runs: rows.map(rowToWebhookRunRecord),
145
+ hasMore,
146
+ total,
147
+ }
148
+ }
149
+ }
150
+
151
+ function rowToWebhookRunRecord(row: WebhookRunDatabaseRow): WebhookRunRecord {
152
+ return {
153
+ id: row.id,
154
+ projectId: row.project_id,
155
+ connectorId: row.connector_id,
156
+ webhookId: row.webhook_id,
157
+ status: row.status,
158
+ method: row.method,
159
+ route: row.route,
160
+ startedAt: new Date(row.started_at),
161
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
162
+ requestBodyBytes: row.request_body_bytes != null ? Number(row.request_body_bytes) : undefined,
163
+ responseStatus: row.response_status != null ? Number(row.response_status) : undefined,
164
+ idempotencyKey: row.idempotency_key ?? undefined,
165
+ deliveryClaimResult: row.delivery_claim_result ?? undefined,
166
+ error: row.error ?? undefined,
167
+ }
168
+ }
169
+
170
+ interface WebhookRunDatabaseRow {
171
+ readonly project_id: string
172
+ readonly id: string
173
+ readonly connector_id: string
174
+ readonly webhook_id: string
175
+ readonly status: WebhookRunRecord["status"]
176
+ readonly method: string
177
+ readonly route: string
178
+ readonly started_at: Date | string
179
+ readonly finished_at: Date | string | null
180
+ readonly request_body_bytes: number | string | null
181
+ readonly response_status: number | string | null
182
+ readonly idempotency_key: string | null
183
+ readonly delivery_claim_result: WebhookRunRecord["deliveryClaimResult"] | null
184
+ readonly error: string | null
185
+ }