@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- 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
|
+
}
|