@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,657 @@
|
|
|
1
|
+
import type { ActionSubject, JsonValue } from "@sixb/core"
|
|
2
|
+
import type {
|
|
3
|
+
ActionRunEffectsRecord,
|
|
4
|
+
ActionRunFailure,
|
|
5
|
+
ActionRunParams,
|
|
6
|
+
ActionRunPhase,
|
|
7
|
+
ActionRunRecord,
|
|
8
|
+
ActionRunStorage,
|
|
9
|
+
ActionRunWritebackRecord,
|
|
10
|
+
EnterActionRunPhaseInput,
|
|
11
|
+
FinishActionRunInput,
|
|
12
|
+
ListActionRunsInput,
|
|
13
|
+
ListActionRunsResult,
|
|
14
|
+
LockActionMaterializationRunInput,
|
|
15
|
+
QueueActionRunInput,
|
|
16
|
+
RecordActionEffectsInput,
|
|
17
|
+
RecordActionWritebackInput,
|
|
18
|
+
StartActionRunInput,
|
|
19
|
+
} from "@sixb/core/storage"
|
|
20
|
+
import {
|
|
21
|
+
ActionRunError,
|
|
22
|
+
actionRunPhaseRecordsEqual,
|
|
23
|
+
canRequeueActionRunAfterEnqueueFailure,
|
|
24
|
+
finishActionRunPhase,
|
|
25
|
+
isTerminalActionRun,
|
|
26
|
+
} from "@sixb/core/storage"
|
|
27
|
+
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
28
|
+
import { isUniqueViolation } from "./storage-errors"
|
|
29
|
+
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
30
|
+
|
|
31
|
+
export class PgActionRunStorage implements ActionRunStorage {
|
|
32
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
33
|
+
|
|
34
|
+
async lockForMaterialization(input: LockActionMaterializationRunInput): Promise<ActionRunRecord> {
|
|
35
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
36
|
+
SELECT *
|
|
37
|
+
FROM action_runs
|
|
38
|
+
WHERE project_id = ${input.projectId} AND id = ${input.runId}
|
|
39
|
+
FOR UPDATE
|
|
40
|
+
`
|
|
41
|
+
if (!row) {
|
|
42
|
+
throw new ActionRunError(
|
|
43
|
+
`[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
if (row.action_id !== input.actionId) {
|
|
47
|
+
throw new ActionRunError(
|
|
48
|
+
`[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
if (row.status !== "running") {
|
|
52
|
+
throw new ActionRunError(
|
|
53
|
+
`[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return rowToActionRunRecord(row)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async queue(input: QueueActionRunInput): Promise<ActionRunRecord> {
|
|
60
|
+
try {
|
|
61
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
62
|
+
INSERT INTO action_runs (
|
|
63
|
+
project_id,
|
|
64
|
+
id,
|
|
65
|
+
action_id,
|
|
66
|
+
subject_kind,
|
|
67
|
+
object_type_id,
|
|
68
|
+
primary_id,
|
|
69
|
+
status,
|
|
70
|
+
phase,
|
|
71
|
+
queued_at,
|
|
72
|
+
started_at,
|
|
73
|
+
finished_at,
|
|
74
|
+
params,
|
|
75
|
+
idempotency_key,
|
|
76
|
+
writeback_status,
|
|
77
|
+
writeback_completed_at,
|
|
78
|
+
writeback_result,
|
|
79
|
+
writeback_error_name,
|
|
80
|
+
writeback_error_message,
|
|
81
|
+
writeback_error_phase,
|
|
82
|
+
effects_status,
|
|
83
|
+
effects_completed_at,
|
|
84
|
+
effects_error_name,
|
|
85
|
+
effects_error_message,
|
|
86
|
+
effects_error_phase,
|
|
87
|
+
error_name,
|
|
88
|
+
error_message,
|
|
89
|
+
error_phase
|
|
90
|
+
) VALUES (
|
|
91
|
+
${input.projectId},
|
|
92
|
+
${input.id},
|
|
93
|
+
${input.actionId},
|
|
94
|
+
${input.subject.kind},
|
|
95
|
+
${input.subject.kind === "object" ? input.subject.objectTypeId : null},
|
|
96
|
+
${input.subject.kind === "object" ? input.subject.primaryId : null},
|
|
97
|
+
${"queued"},
|
|
98
|
+
${"request"},
|
|
99
|
+
${input.queuedAt ?? new Date()},
|
|
100
|
+
${null},
|
|
101
|
+
${null},
|
|
102
|
+
${JSON.stringify(input.params)}::text::jsonb,
|
|
103
|
+
${input.idempotencyKey},
|
|
104
|
+
${null},
|
|
105
|
+
${null},
|
|
106
|
+
${null},
|
|
107
|
+
${null},
|
|
108
|
+
${null},
|
|
109
|
+
${null},
|
|
110
|
+
${null},
|
|
111
|
+
${null},
|
|
112
|
+
${null},
|
|
113
|
+
${null},
|
|
114
|
+
${null},
|
|
115
|
+
${null},
|
|
116
|
+
${null},
|
|
117
|
+
${null}
|
|
118
|
+
)
|
|
119
|
+
RETURNING *
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
return rowToActionRunRecord(row)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (isUniqueViolation(error)) {
|
|
125
|
+
return this.requeueAfterEnqueueFailure(input)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw error
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async requeueAfterEnqueueFailure(input: QueueActionRunInput): Promise<ActionRunRecord> {
|
|
133
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
134
|
+
const [existing] = await tx<DatabaseRow[]>`
|
|
135
|
+
SELECT * FROM action_runs
|
|
136
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
137
|
+
FOR UPDATE
|
|
138
|
+
`
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
!existing ||
|
|
142
|
+
!canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)
|
|
143
|
+
) {
|
|
144
|
+
throw new ActionRunError(
|
|
145
|
+
`[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
150
|
+
UPDATE action_runs
|
|
151
|
+
SET
|
|
152
|
+
status = ${"queued"},
|
|
153
|
+
phase = ${"request"},
|
|
154
|
+
queued_at = ${input.queuedAt ?? new Date()},
|
|
155
|
+
started_at = ${null},
|
|
156
|
+
finished_at = ${null},
|
|
157
|
+
writeback_status = ${null},
|
|
158
|
+
writeback_completed_at = ${null},
|
|
159
|
+
writeback_result = ${null},
|
|
160
|
+
writeback_error_name = ${null},
|
|
161
|
+
writeback_error_message = ${null},
|
|
162
|
+
writeback_error_phase = ${null},
|
|
163
|
+
effects_status = ${null},
|
|
164
|
+
effects_completed_at = ${null},
|
|
165
|
+
effects_error_name = ${null},
|
|
166
|
+
effects_error_message = ${null},
|
|
167
|
+
effects_error_phase = ${null},
|
|
168
|
+
error_name = ${null},
|
|
169
|
+
error_message = ${null},
|
|
170
|
+
error_phase = ${null}
|
|
171
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
172
|
+
RETURNING *
|
|
173
|
+
`
|
|
174
|
+
|
|
175
|
+
return rowToActionRunRecord(updated)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async start(input: StartActionRunInput): Promise<ActionRunRecord> {
|
|
180
|
+
const [updated] = await this.sql<DatabaseRow[]>`
|
|
181
|
+
UPDATE action_runs
|
|
182
|
+
SET
|
|
183
|
+
status = ${"running"},
|
|
184
|
+
phase = ${input.phase ?? "validation"},
|
|
185
|
+
started_at = ${input.startedAt ?? new Date()},
|
|
186
|
+
error_name = ${null},
|
|
187
|
+
error_message = ${null},
|
|
188
|
+
error_phase = ${null}
|
|
189
|
+
WHERE project_id = ${input.projectId}
|
|
190
|
+
AND id = ${input.id}
|
|
191
|
+
AND status = ${"queued"}
|
|
192
|
+
RETURNING *
|
|
193
|
+
`
|
|
194
|
+
|
|
195
|
+
if (updated) {
|
|
196
|
+
return rowToActionRunRecord(updated)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const existing = await this.getById({ projectId: input.projectId, id: input.id })
|
|
200
|
+
if (!existing) {
|
|
201
|
+
throw new ActionRunError(
|
|
202
|
+
`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new ActionRunError(
|
|
207
|
+
`[SixbPg] Action run '${input.id}' cannot start from status '${existing.status}'.`
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async enterPhase(input: EnterActionRunPhaseInput): Promise<ActionRunRecord> {
|
|
212
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
213
|
+
await this.requireRunningRun(tx, input.projectId, input.id, "transition phase")
|
|
214
|
+
|
|
215
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
216
|
+
UPDATE action_runs
|
|
217
|
+
SET phase = ${input.phase}
|
|
218
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
219
|
+
RETURNING *
|
|
220
|
+
`
|
|
221
|
+
|
|
222
|
+
return rowToActionRunRecord(updated)
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async recordWriteback(input: RecordActionWritebackInput): Promise<ActionRunRecord> {
|
|
227
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
228
|
+
const existing = await this.requireRunningRun(
|
|
229
|
+
tx,
|
|
230
|
+
input.projectId,
|
|
231
|
+
input.id,
|
|
232
|
+
"record writeback"
|
|
233
|
+
)
|
|
234
|
+
const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date()))
|
|
235
|
+
const currentWriteback = toActionRunWritebackRecord(existing)
|
|
236
|
+
|
|
237
|
+
if (currentWriteback) {
|
|
238
|
+
if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
|
|
239
|
+
return rowToActionRunRecord(existing)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new ActionRunError(
|
|
243
|
+
`[SixbPg] Action run '${input.id}' already has a different writeback record.`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
248
|
+
UPDATE action_runs
|
|
249
|
+
SET
|
|
250
|
+
phase = ${"writeback"},
|
|
251
|
+
writeback_status = ${input.status},
|
|
252
|
+
writeback_completed_at = ${nextWriteback.completedAt},
|
|
253
|
+
writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
|
|
254
|
+
writeback_error_name = ${input.status === "failed" ? (input.error.name ?? null) : null},
|
|
255
|
+
writeback_error_message = ${input.status === "failed" ? input.error.message : null},
|
|
256
|
+
writeback_error_phase = ${input.status === "failed" ? (input.error.phase ?? "writeback") : null}
|
|
257
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
258
|
+
RETURNING *
|
|
259
|
+
`
|
|
260
|
+
|
|
261
|
+
return rowToActionRunRecord(updated)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async recordEffects(input: RecordActionEffectsInput): Promise<ActionRunRecord> {
|
|
266
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
267
|
+
const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects")
|
|
268
|
+
const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date()))
|
|
269
|
+
const currentEffects = toActionRunEffectsRecord(existing)
|
|
270
|
+
|
|
271
|
+
if (currentEffects) {
|
|
272
|
+
if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
|
|
273
|
+
return rowToActionRunRecord(existing)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new ActionRunError(
|
|
277
|
+
`[SixbPg] Action run '${input.id}' already has a different effects record.`
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
282
|
+
UPDATE action_runs
|
|
283
|
+
SET
|
|
284
|
+
phase = ${"effects"},
|
|
285
|
+
effects_status = ${input.status},
|
|
286
|
+
effects_completed_at = ${nextEffects.completedAt},
|
|
287
|
+
effects_error_name = ${input.status === "failed" ? (input.error.name ?? null) : null},
|
|
288
|
+
effects_error_message = ${input.status === "failed" ? input.error.message : null},
|
|
289
|
+
effects_error_phase = ${input.status === "failed" ? (input.error.phase ?? "effects") : null}
|
|
290
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
291
|
+
RETURNING *
|
|
292
|
+
`
|
|
293
|
+
|
|
294
|
+
return rowToActionRunRecord(updated)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async finish(input: FinishActionRunInput): Promise<ActionRunRecord> {
|
|
299
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
300
|
+
const [existing] = await tx<DatabaseRow[]>`
|
|
301
|
+
SELECT * FROM action_runs
|
|
302
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
303
|
+
FOR UPDATE
|
|
304
|
+
`
|
|
305
|
+
|
|
306
|
+
if (!existing) {
|
|
307
|
+
throw new ActionRunError(
|
|
308
|
+
`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (isTerminalActionRun({ status: existing.status })) {
|
|
313
|
+
throw new ActionRunError(
|
|
314
|
+
`[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const phase = finishActionRunPhase(input, existing.phase)
|
|
319
|
+
|
|
320
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
321
|
+
UPDATE action_runs
|
|
322
|
+
SET
|
|
323
|
+
status = ${input.status},
|
|
324
|
+
phase = ${phase},
|
|
325
|
+
finished_at = ${input.finishedAt ?? new Date()},
|
|
326
|
+
error_name = ${input.status === "succeeded" ? null : (input.error?.name ?? null)},
|
|
327
|
+
error_message = ${input.status === "succeeded" ? null : (input.error?.message ?? null)},
|
|
328
|
+
error_phase = ${input.status === "succeeded" ? null : (input.error?.phase ?? phase)}
|
|
329
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
330
|
+
RETURNING *
|
|
331
|
+
`
|
|
332
|
+
|
|
333
|
+
return rowToActionRunRecord(updated)
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async getById(params: { projectId: string; id: string }): Promise<ActionRunRecord | null> {
|
|
338
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
339
|
+
SELECT * FROM action_runs
|
|
340
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
341
|
+
`
|
|
342
|
+
|
|
343
|
+
return row ? rowToActionRunRecord(row) : null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async list(input: ListActionRunsInput): Promise<ListActionRunsResult> {
|
|
347
|
+
if ((input.statuses && input.statuses.length === 0) || input.actionIds?.length === 0) {
|
|
348
|
+
return {
|
|
349
|
+
runs: [],
|
|
350
|
+
hasMore: false,
|
|
351
|
+
total: 0,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const whereClauses = ["project_id = $1"]
|
|
356
|
+
const params: SqlParameter[] = [input.projectId]
|
|
357
|
+
let index = 2
|
|
358
|
+
|
|
359
|
+
if (input.actionId) {
|
|
360
|
+
whereClauses.push(`action_id = $${index++}`)
|
|
361
|
+
params.push(input.actionId)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (input.actionIds) {
|
|
365
|
+
const placeholders = input.actionIds.map(() => `$${index++}`)
|
|
366
|
+
whereClauses.push(`action_id IN (${placeholders.join(", ")})`)
|
|
367
|
+
params.push(...input.actionIds)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (input.objectTypeIds) {
|
|
371
|
+
if (input.objectTypeIds.length === 0) {
|
|
372
|
+
whereClauses.push(`subject_kind <> $${index++}`)
|
|
373
|
+
params.push("object")
|
|
374
|
+
} else {
|
|
375
|
+
const subjectKindIndex = index++
|
|
376
|
+
const placeholders = input.objectTypeIds.map(() => `$${index++}`)
|
|
377
|
+
whereClauses.push(
|
|
378
|
+
`(subject_kind <> $${subjectKindIndex} OR object_type_id IN (${placeholders.join(", ")}))`
|
|
379
|
+
)
|
|
380
|
+
params.push("object", ...input.objectTypeIds)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (input.objectTypeId) {
|
|
385
|
+
whereClauses.push(`subject_kind = $${index++}`)
|
|
386
|
+
params.push("object")
|
|
387
|
+
whereClauses.push(`object_type_id = $${index++}`)
|
|
388
|
+
params.push(input.objectTypeId)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (input.primaryId) {
|
|
392
|
+
whereClauses.push(`subject_kind = $${index++}`)
|
|
393
|
+
params.push("object")
|
|
394
|
+
whereClauses.push(`primary_id = $${index++}`)
|
|
395
|
+
params.push(input.primaryId)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (input.subject) {
|
|
399
|
+
whereClauses.push(`subject_kind = $${index++}`)
|
|
400
|
+
params.push(input.subject.kind)
|
|
401
|
+
if (input.subject.kind === "object") {
|
|
402
|
+
whereClauses.push(`object_type_id = $${index++}`)
|
|
403
|
+
params.push(input.subject.objectTypeId)
|
|
404
|
+
whereClauses.push(`primary_id = $${index++}`)
|
|
405
|
+
params.push(input.subject.primaryId)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (input.statuses) {
|
|
410
|
+
const placeholders = input.statuses.map(() => `$${index++}`)
|
|
411
|
+
whereClauses.push(`status IN (${placeholders.join(", ")})`)
|
|
412
|
+
params.push(...input.statuses)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (input.startedAfter) {
|
|
416
|
+
whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`)
|
|
417
|
+
params.push(input.startedAfter)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (input.startedBefore) {
|
|
421
|
+
whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`)
|
|
422
|
+
params.push(input.startedBefore)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const where = `WHERE ${whereClauses.join(" AND ")}`
|
|
426
|
+
const order = input.order === "asc" ? "ASC" : "DESC"
|
|
427
|
+
const offset = input.offset ?? 0
|
|
428
|
+
|
|
429
|
+
const [totalRow] = await this.sql.unsafe<{ count: string | number }[]>(
|
|
430
|
+
`SELECT COUNT(*)::bigint AS count FROM action_runs ${where}`,
|
|
431
|
+
params
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
const queryParams = [...params]
|
|
435
|
+
let query = `
|
|
436
|
+
SELECT * FROM action_runs
|
|
437
|
+
${where}
|
|
438
|
+
ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
|
|
439
|
+
`
|
|
440
|
+
|
|
441
|
+
if (input.limit !== undefined) {
|
|
442
|
+
query += ` LIMIT $${index++} OFFSET $${index++}`
|
|
443
|
+
queryParams.push(input.limit, offset)
|
|
444
|
+
} else if (offset > 0) {
|
|
445
|
+
query += ` OFFSET $${index++}`
|
|
446
|
+
queryParams.push(offset)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const rows = await this.sql.unsafe<DatabaseRow[]>(query, queryParams)
|
|
450
|
+
const total = Number(totalRow?.count ?? 0)
|
|
451
|
+
const runs = rows.map((row) => rowToActionRunRecord(row))
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
runs,
|
|
455
|
+
hasMore: offset + runs.length < total,
|
|
456
|
+
total,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async requireRunningRun(
|
|
461
|
+
runner: SQLClient,
|
|
462
|
+
projectId: string,
|
|
463
|
+
id: string,
|
|
464
|
+
operation: string
|
|
465
|
+
): Promise<DatabaseRow> {
|
|
466
|
+
const [existing] = await runner<DatabaseRow[]>`
|
|
467
|
+
SELECT * FROM action_runs
|
|
468
|
+
WHERE project_id = ${projectId} AND id = ${id}
|
|
469
|
+
FOR UPDATE
|
|
470
|
+
`
|
|
471
|
+
|
|
472
|
+
if (!existing) {
|
|
473
|
+
throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (existing.status !== "running") {
|
|
477
|
+
throw new ActionRunError(
|
|
478
|
+
`[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return existing
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function toActionRunFailure(row: DatabaseRow): ActionRunFailure | undefined {
|
|
487
|
+
return toFailure(row.error_name, row.error_message, row.error_phase)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function toFailure(
|
|
491
|
+
name: string | null,
|
|
492
|
+
message: string | null,
|
|
493
|
+
phase: ActionRunPhase | null
|
|
494
|
+
): ActionRunFailure | undefined {
|
|
495
|
+
if (!message) {
|
|
496
|
+
return undefined
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
name: name ?? undefined,
|
|
501
|
+
message,
|
|
502
|
+
phase: phase ?? undefined,
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord | undefined {
|
|
507
|
+
if (!row.writeback_status) {
|
|
508
|
+
return undefined
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const completedAt = row.writeback_completed_at
|
|
512
|
+
? new Date(row.writeback_completed_at)
|
|
513
|
+
: new Date(row.queued_at)
|
|
514
|
+
|
|
515
|
+
if (row.writeback_status === "succeeded") {
|
|
516
|
+
return {
|
|
517
|
+
status: "succeeded",
|
|
518
|
+
completedAt,
|
|
519
|
+
result: (row.writeback_result ?? null) as JsonValue,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
status: "failed",
|
|
525
|
+
completedAt,
|
|
526
|
+
error: toFailure(
|
|
527
|
+
row.writeback_error_name,
|
|
528
|
+
row.writeback_error_message,
|
|
529
|
+
row.writeback_error_phase
|
|
530
|
+
),
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | undefined {
|
|
535
|
+
if (!row.effects_status) {
|
|
536
|
+
return undefined
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const completedAt = row.effects_completed_at
|
|
540
|
+
? new Date(row.effects_completed_at)
|
|
541
|
+
: new Date(row.queued_at)
|
|
542
|
+
|
|
543
|
+
if (row.effects_status === "succeeded") {
|
|
544
|
+
return {
|
|
545
|
+
status: "succeeded",
|
|
546
|
+
completedAt,
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
status: "failed",
|
|
552
|
+
completedAt,
|
|
553
|
+
error: toFailure(row.effects_error_name, row.effects_error_message, row.effects_error_phase),
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function toWritebackRecord(
|
|
558
|
+
input: RecordActionWritebackInput,
|
|
559
|
+
completedAt: Date
|
|
560
|
+
): ActionRunWritebackRecord {
|
|
561
|
+
if (input.status === "succeeded") {
|
|
562
|
+
return {
|
|
563
|
+
status: "succeeded",
|
|
564
|
+
completedAt,
|
|
565
|
+
result: input.result,
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
status: "failed",
|
|
571
|
+
completedAt,
|
|
572
|
+
error: input.error,
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function toEffectsRecord(
|
|
577
|
+
input: RecordActionEffectsInput,
|
|
578
|
+
completedAt: Date
|
|
579
|
+
): ActionRunEffectsRecord {
|
|
580
|
+
if (input.status === "succeeded") {
|
|
581
|
+
return {
|
|
582
|
+
status: "succeeded",
|
|
583
|
+
completedAt,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
status: "failed",
|
|
589
|
+
completedAt,
|
|
590
|
+
error: input.error,
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function rowToActionRunRecord(row: DatabaseRow): ActionRunRecord {
|
|
595
|
+
return {
|
|
596
|
+
id: row.id,
|
|
597
|
+
projectId: row.project_id,
|
|
598
|
+
actionId: row.action_id,
|
|
599
|
+
subject: rowToActionSubject(row),
|
|
600
|
+
status: row.status,
|
|
601
|
+
phase: row.phase ?? undefined,
|
|
602
|
+
queuedAt: new Date(row.queued_at),
|
|
603
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
604
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
605
|
+
params: row.params,
|
|
606
|
+
idempotencyKey: row.idempotency_key,
|
|
607
|
+
writeback: toActionRunWritebackRecord(row),
|
|
608
|
+
effects: toActionRunEffectsRecord(row),
|
|
609
|
+
error: toActionRunFailure(row),
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function rowToActionSubject(row: DatabaseRow): ActionSubject {
|
|
614
|
+
if (row.subject_kind === "none") {
|
|
615
|
+
return { kind: "none" }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!row.object_type_id || !row.primary_id) {
|
|
619
|
+
throw new ActionRunError(`[SixbPg] Action run '${row.id}' has an invalid object subject.`)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
kind: "object",
|
|
624
|
+
objectTypeId: row.object_type_id,
|
|
625
|
+
primaryId: row.primary_id,
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
interface DatabaseRow {
|
|
630
|
+
project_id: string
|
|
631
|
+
id: string
|
|
632
|
+
action_id: string
|
|
633
|
+
subject_kind: ActionSubject["kind"]
|
|
634
|
+
object_type_id: string | null
|
|
635
|
+
primary_id: string | null
|
|
636
|
+
status: ActionRunRecord["status"]
|
|
637
|
+
phase: ActionRunPhase | null
|
|
638
|
+
queued_at: Date | string
|
|
639
|
+
started_at: Date | string | null
|
|
640
|
+
finished_at: Date | string | null
|
|
641
|
+
params: ActionRunParams
|
|
642
|
+
idempotency_key: string
|
|
643
|
+
writeback_status: ActionRunWritebackRecord["status"] | null
|
|
644
|
+
writeback_completed_at: Date | string | null
|
|
645
|
+
writeback_result: JsonValue | null
|
|
646
|
+
writeback_error_name: string | null
|
|
647
|
+
writeback_error_message: string | null
|
|
648
|
+
writeback_error_phase: ActionRunPhase | null
|
|
649
|
+
effects_status: ActionRunEffectsRecord["status"] | null
|
|
650
|
+
effects_completed_at: Date | string | null
|
|
651
|
+
effects_error_name: string | null
|
|
652
|
+
effects_error_message: string | null
|
|
653
|
+
effects_error_phase: ActionRunPhase | null
|
|
654
|
+
error_name: string | null
|
|
655
|
+
error_message: string | null
|
|
656
|
+
error_phase: ActionRunPhase | null
|
|
657
|
+
}
|