@sixb/pg 0.1.18 → 0.1.19
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/dist/index.js +237 -276
- package/dist/index.js.map +4 -4
- package/dist/migrations.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +2 -9
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/migrations/061-terminal-action-runs.sql +150 -0
- package/src/migrations.ts +2 -0
- package/src/pg-action-run-storage.ts +97 -374
|
@@ -4,33 +4,23 @@ import {
|
|
|
4
4
|
serializeActionRunFailure,
|
|
5
5
|
} from "@sixb/core/internal/action-run-storage"
|
|
6
6
|
import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider"
|
|
7
|
-
import {
|
|
8
|
-
actionRunPhaseRecordsEqual,
|
|
9
|
-
canRequeueActionRunAfterEnqueueFailure,
|
|
10
|
-
finishActionRunPhase,
|
|
11
|
-
} from "@sixb/core/internal/storage"
|
|
7
|
+
import { normalizeRecordActionRunInput, resolveActionRunEffects } from "@sixb/core/internal/storage"
|
|
12
8
|
import type {
|
|
13
9
|
ActionRunEffectsRecord,
|
|
14
|
-
ActionRunFailure,
|
|
15
10
|
ActionRunParams,
|
|
16
11
|
ActionRunPhase,
|
|
17
12
|
ActionRunRecord,
|
|
13
|
+
ActionRunStatus,
|
|
18
14
|
ActionRunStorage,
|
|
19
15
|
ActionRunWritebackRecord,
|
|
20
|
-
EnterActionRunPhaseInput,
|
|
21
16
|
ExecutionStorage,
|
|
22
|
-
FinishActionRunInput,
|
|
23
17
|
ListActionRunsInput,
|
|
24
18
|
ListActionRunsResult,
|
|
25
|
-
LockActionMaterializationRunInput,
|
|
26
|
-
QueueActionRunInput,
|
|
27
19
|
RecordActionEffectsInput,
|
|
28
|
-
|
|
29
|
-
StartActionRunInput,
|
|
20
|
+
RecordActionRunInput,
|
|
30
21
|
} from "@sixb/core/storage"
|
|
31
|
-
import { ActionRunError
|
|
32
|
-
import type {
|
|
33
|
-
import { isUniqueViolation } from "./storage-errors"
|
|
22
|
+
import { ActionRunError } from "@sixb/core/storage"
|
|
23
|
+
import type { SqlParameter } from "./pg-client"
|
|
34
24
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
35
25
|
|
|
36
26
|
export class PgActionRunStorage implements ActionRunStorage {
|
|
@@ -39,289 +29,100 @@ export class PgActionRunStorage implements ActionRunStorage {
|
|
|
39
29
|
private readonly executions: ExecutionStorage
|
|
40
30
|
) {}
|
|
41
31
|
|
|
42
|
-
async
|
|
43
|
-
const
|
|
44
|
-
SELECT *
|
|
45
|
-
FROM action_runs
|
|
46
|
-
WHERE project_id = ${input.projectId} AND id = ${input.runId}
|
|
47
|
-
FOR UPDATE
|
|
48
|
-
`
|
|
49
|
-
if (!row) {
|
|
50
|
-
throw new ActionRunError(
|
|
51
|
-
`[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
if (row.action_id !== input.actionId) {
|
|
55
|
-
throw new ActionRunError(
|
|
56
|
-
`[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
if (row.status !== "running") {
|
|
60
|
-
throw new ActionRunError(
|
|
61
|
-
`[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
return rowToActionRunRecord(row)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async queue(input: QueueActionRunInput): Promise<ActionRunRecord> {
|
|
32
|
+
async record(input: RecordActionRunInput): Promise<ActionRunRecord> {
|
|
33
|
+
const record = normalizeRecordActionRunInput(input)
|
|
68
34
|
await assertActionRunExecution({
|
|
69
35
|
executions: this.executions,
|
|
70
|
-
projectId:
|
|
71
|
-
executionId:
|
|
72
|
-
runId:
|
|
73
|
-
actionId:
|
|
36
|
+
projectId: record.projectId,
|
|
37
|
+
executionId: record.executionId,
|
|
38
|
+
runId: record.id,
|
|
39
|
+
actionId: record.actionId,
|
|
74
40
|
})
|
|
75
41
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
${null},
|
|
123
|
-
${null}
|
|
124
|
-
)
|
|
125
|
-
RETURNING *
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
return rowToActionRunRecord(row)
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (isUniqueViolation(error)) {
|
|
131
|
-
return this.requeueAfterEnqueueFailure(input)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
throw error
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private async requeueAfterEnqueueFailure(input: QueueActionRunInput): Promise<ActionRunRecord> {
|
|
139
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
140
|
-
const [existing] = await tx<DatabaseRow[]>`
|
|
141
|
-
SELECT * FROM action_runs
|
|
142
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
143
|
-
FOR UPDATE
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
!existing ||
|
|
148
|
-
!canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)
|
|
149
|
-
) {
|
|
150
|
-
throw new ActionRunError(
|
|
151
|
-
`[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
156
|
-
UPDATE action_runs
|
|
157
|
-
SET
|
|
158
|
-
status = ${"queued"},
|
|
159
|
-
phase = ${"request"},
|
|
160
|
-
queued_at = ${input.queuedAt ?? new Date()},
|
|
161
|
-
started_at = ${null},
|
|
162
|
-
finished_at = ${null},
|
|
163
|
-
writeback_status = ${null},
|
|
164
|
-
writeback_completed_at = ${null},
|
|
165
|
-
writeback_result = ${null},
|
|
166
|
-
writeback_error = ${null},
|
|
167
|
-
effects_status = ${null},
|
|
168
|
-
effects_completed_at = ${null},
|
|
169
|
-
effects_error = ${null},
|
|
170
|
-
error = ${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 = ${null}
|
|
187
|
-
WHERE project_id = ${input.projectId}
|
|
188
|
-
AND id = ${input.id}
|
|
189
|
-
AND status = ${"queued"}
|
|
42
|
+
// A conflict with a committed record is answered as a refusal rather than raised: a raised
|
|
43
|
+
// unique violation would abort the caller's transaction before the error could say what
|
|
44
|
+
// conflicted. A conflict with a concurrent transaction's insert waits for that transaction;
|
|
45
|
+
// under the serializable isolation of an Action commit, it then fails with a serialization
|
|
46
|
+
// error, which the Materializer retries into a replay of the commit that won.
|
|
47
|
+
const [row] = await this.sql<DatabaseRow[]>`
|
|
48
|
+
INSERT INTO action_runs (
|
|
49
|
+
project_id,
|
|
50
|
+
id,
|
|
51
|
+
execution_id,
|
|
52
|
+
action_id,
|
|
53
|
+
subject_kind,
|
|
54
|
+
object_type_id,
|
|
55
|
+
primary_id,
|
|
56
|
+
status,
|
|
57
|
+
phase,
|
|
58
|
+
started_at,
|
|
59
|
+
finished_at,
|
|
60
|
+
params,
|
|
61
|
+
idempotency_key,
|
|
62
|
+
writeback_status,
|
|
63
|
+
writeback_completed_at,
|
|
64
|
+
writeback_result,
|
|
65
|
+
writeback_error,
|
|
66
|
+
error
|
|
67
|
+
) VALUES (
|
|
68
|
+
${record.projectId},
|
|
69
|
+
${record.id},
|
|
70
|
+
${record.executionId},
|
|
71
|
+
${record.actionId},
|
|
72
|
+
${record.subject.kind},
|
|
73
|
+
${record.subject.kind === "object" ? record.subject.objectTypeId : null},
|
|
74
|
+
${record.subject.kind === "object" ? record.subject.primaryId : null},
|
|
75
|
+
${record.status},
|
|
76
|
+
${record.phase},
|
|
77
|
+
${record.startedAt},
|
|
78
|
+
${record.finishedAt},
|
|
79
|
+
${JSON.stringify(record.params)}::text::jsonb,
|
|
80
|
+
${record.idempotencyKey},
|
|
81
|
+
${record.writeback?.status ?? null},
|
|
82
|
+
${record.writeback?.completedAt ?? null},
|
|
83
|
+
${record.writeback?.status === "succeeded" ? JSON.stringify(record.writeback.result) : null}::text::jsonb,
|
|
84
|
+
${record.writeback?.status === "failed" ? serializeActionRunFailure(record.writeback.error, "writeback") : null}::text::jsonb,
|
|
85
|
+
${record.status === "failed" ? serializeActionRunFailure(record.error) : null}::text::jsonb
|
|
86
|
+
)
|
|
87
|
+
ON CONFLICT DO NOTHING
|
|
190
88
|
RETURNING *
|
|
191
89
|
`
|
|
90
|
+
if (row) return rowToActionRunRecord(row)
|
|
192
91
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const existing = await this.getById({ projectId: input.projectId, id: input.id })
|
|
198
|
-
if (!existing) {
|
|
199
|
-
throw new ActionRunError(
|
|
200
|
-
`[SixbPg] Action run '${input.id}' not found for project '${input.projectId}'.`
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
|
|
92
|
+
const [conflict] = await this.sql<{ id: string }[]>`
|
|
93
|
+
SELECT id FROM action_runs
|
|
94
|
+
WHERE project_id = ${record.projectId} AND id = ${record.id}
|
|
95
|
+
`
|
|
204
96
|
throw new ActionRunError(
|
|
205
|
-
|
|
97
|
+
conflict
|
|
98
|
+
? `[SixbPg] Action run '${record.id}' is already recorded for project '${record.projectId}'.`
|
|
99
|
+
: `[SixbPg] Execution '${record.executionId}' already belongs to another Action run.`
|
|
206
100
|
)
|
|
207
101
|
}
|
|
208
102
|
|
|
209
|
-
async enterPhase(input: EnterActionRunPhaseInput): Promise<ActionRunRecord> {
|
|
210
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
211
|
-
await this.requireRunningRun(tx, input.projectId, input.id, "transition phase")
|
|
212
|
-
|
|
213
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
214
|
-
UPDATE action_runs
|
|
215
|
-
SET phase = ${input.phase}
|
|
216
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
217
|
-
RETURNING *
|
|
218
|
-
`
|
|
219
|
-
|
|
220
|
-
return rowToActionRunRecord(updated)
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async recordWriteback(input: RecordActionWritebackInput): Promise<ActionRunRecord> {
|
|
225
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
226
|
-
const existing = await this.requireRunningRun(
|
|
227
|
-
tx,
|
|
228
|
-
input.projectId,
|
|
229
|
-
input.id,
|
|
230
|
-
"record writeback"
|
|
231
|
-
)
|
|
232
|
-
const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date()))
|
|
233
|
-
const currentWriteback = toActionRunWritebackRecord(existing)
|
|
234
|
-
|
|
235
|
-
if (currentWriteback) {
|
|
236
|
-
if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
|
|
237
|
-
return rowToActionRunRecord(existing)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
throw new ActionRunError(
|
|
241
|
-
`[SixbPg] Action run '${input.id}' already has a different writeback record.`
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
246
|
-
UPDATE action_runs
|
|
247
|
-
SET
|
|
248
|
-
phase = ${"writeback"},
|
|
249
|
-
writeback_status = ${input.status},
|
|
250
|
-
writeback_completed_at = ${nextWriteback.completedAt},
|
|
251
|
-
writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
|
|
252
|
-
writeback_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "writeback") : null}::text::jsonb
|
|
253
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
254
|
-
RETURNING *
|
|
255
|
-
`
|
|
256
|
-
|
|
257
|
-
return rowToActionRunRecord(updated)
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
|
|
261
103
|
async recordEffects(input: RecordActionEffectsInput): Promise<ActionRunRecord> {
|
|
262
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
263
|
-
const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects")
|
|
264
|
-
const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date()))
|
|
265
|
-
const currentEffects = toActionRunEffectsRecord(existing)
|
|
266
|
-
|
|
267
|
-
if (currentEffects) {
|
|
268
|
-
if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
|
|
269
|
-
return rowToActionRunRecord(existing)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
throw new ActionRunError(
|
|
273
|
-
`[SixbPg] Action run '${input.id}' already has a different effects record.`
|
|
274
|
-
)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
278
|
-
UPDATE action_runs
|
|
279
|
-
SET
|
|
280
|
-
phase = ${"effects"},
|
|
281
|
-
effects_status = ${input.status},
|
|
282
|
-
effects_completed_at = ${nextEffects.completedAt},
|
|
283
|
-
effects_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "effects") : null}::text::jsonb
|
|
284
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
285
|
-
RETURNING *
|
|
286
|
-
`
|
|
287
|
-
|
|
288
|
-
return rowToActionRunRecord(updated)
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async finish(input: FinishActionRunInput): Promise<ActionRunRecord> {
|
|
293
104
|
return runPgTransaction(this.sql, async (tx) => {
|
|
294
105
|
const [existing] = await tx<DatabaseRow[]>`
|
|
295
106
|
SELECT * FROM action_runs
|
|
296
107
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
297
108
|
FOR UPDATE
|
|
298
109
|
`
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (isTerminalActionRun({ status: existing.status })) {
|
|
307
|
-
throw new ActionRunError(
|
|
308
|
-
`[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const phase = finishActionRunPhase(input, existing.phase)
|
|
110
|
+
const { run, effects } = resolveActionRunEffects(
|
|
111
|
+
existing ? rowToActionRunRecord(existing) : null,
|
|
112
|
+
input
|
|
113
|
+
)
|
|
114
|
+
if (!effects) return run
|
|
313
115
|
|
|
314
116
|
const [updated] = await tx<DatabaseRow[]>`
|
|
315
117
|
UPDATE action_runs
|
|
316
118
|
SET
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
119
|
+
phase = ${"effects"},
|
|
120
|
+
effects_status = ${effects.status},
|
|
121
|
+
effects_completed_at = ${effects.completedAt},
|
|
122
|
+
effects_error = ${effects.status === "failed" ? serializeActionRunFailure(effects.error, "effects") : null}::text::jsonb
|
|
321
123
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
322
124
|
RETURNING *
|
|
323
125
|
`
|
|
324
|
-
|
|
325
126
|
return rowToActionRunRecord(updated)
|
|
326
127
|
})
|
|
327
128
|
}
|
|
@@ -405,12 +206,12 @@ export class PgActionRunStorage implements ActionRunStorage {
|
|
|
405
206
|
}
|
|
406
207
|
|
|
407
208
|
if (input.startedAfter) {
|
|
408
|
-
whereClauses.push(`
|
|
209
|
+
whereClauses.push(`started_at >= $${index++}`)
|
|
409
210
|
params.push(input.startedAfter)
|
|
410
211
|
}
|
|
411
212
|
|
|
412
213
|
if (input.startedBefore) {
|
|
413
|
-
whereClauses.push(`
|
|
214
|
+
whereClauses.push(`started_at <= $${index++}`)
|
|
414
215
|
params.push(input.startedBefore)
|
|
415
216
|
}
|
|
416
217
|
|
|
@@ -427,7 +228,7 @@ export class PgActionRunStorage implements ActionRunStorage {
|
|
|
427
228
|
let query = `
|
|
428
229
|
SELECT * FROM action_runs
|
|
429
230
|
${where}
|
|
430
|
-
ORDER BY
|
|
231
|
+
ORDER BY started_at ${order}, id ${order}
|
|
431
232
|
`
|
|
432
233
|
|
|
433
234
|
if (input.limit !== undefined) {
|
|
@@ -448,42 +249,6 @@ export class PgActionRunStorage implements ActionRunStorage {
|
|
|
448
249
|
total,
|
|
449
250
|
}
|
|
450
251
|
}
|
|
451
|
-
|
|
452
|
-
private async requireRunningRun(
|
|
453
|
-
runner: SQLClient,
|
|
454
|
-
projectId: string,
|
|
455
|
-
id: string,
|
|
456
|
-
operation: string
|
|
457
|
-
): Promise<DatabaseRow> {
|
|
458
|
-
const [existing] = await runner<DatabaseRow[]>`
|
|
459
|
-
SELECT * FROM action_runs
|
|
460
|
-
WHERE project_id = ${projectId} AND id = ${id}
|
|
461
|
-
FOR UPDATE
|
|
462
|
-
`
|
|
463
|
-
|
|
464
|
-
if (!existing) {
|
|
465
|
-
throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (existing.status !== "running") {
|
|
469
|
-
throw new ActionRunError(
|
|
470
|
-
`[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`
|
|
471
|
-
)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return existing
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function toActionRunFailure(row: DatabaseRow): ActionRunFailure | undefined {
|
|
479
|
-
return row.error === null ? undefined : parseActionRunFailure(row.error)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function parsePhaseFailure<TPhase extends Extract<ActionRunPhase, "writeback" | "effects">>(
|
|
483
|
-
value: unknown,
|
|
484
|
-
expectedPhase: TPhase
|
|
485
|
-
): ActionRunFailure<TPhase> {
|
|
486
|
-
return parseActionRunFailure(value, expectedPhase)
|
|
487
252
|
}
|
|
488
253
|
|
|
489
254
|
function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord | undefined {
|
|
@@ -491,22 +256,19 @@ function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord
|
|
|
491
256
|
return undefined
|
|
492
257
|
}
|
|
493
258
|
|
|
494
|
-
const completedAt = row.writeback_completed_at
|
|
495
|
-
? new Date(row.writeback_completed_at)
|
|
496
|
-
: new Date(row.queued_at)
|
|
497
|
-
|
|
259
|
+
const completedAt = new Date(row.writeback_completed_at ?? row.finished_at)
|
|
498
260
|
if (row.writeback_status === "succeeded") {
|
|
499
261
|
return {
|
|
500
262
|
status: "succeeded",
|
|
501
263
|
completedAt,
|
|
502
|
-
result:
|
|
264
|
+
result: row.writeback_result ?? null,
|
|
503
265
|
}
|
|
504
266
|
}
|
|
505
267
|
|
|
506
268
|
return {
|
|
507
269
|
status: "failed",
|
|
508
270
|
completedAt,
|
|
509
|
-
error:
|
|
271
|
+
error: parseActionRunFailure(row.writeback_error, "writeback"),
|
|
510
272
|
}
|
|
511
273
|
}
|
|
512
274
|
|
|
@@ -515,10 +277,7 @@ function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | un
|
|
|
515
277
|
return undefined
|
|
516
278
|
}
|
|
517
279
|
|
|
518
|
-
const completedAt = row.effects_completed_at
|
|
519
|
-
? new Date(row.effects_completed_at)
|
|
520
|
-
: new Date(row.queued_at)
|
|
521
|
-
|
|
280
|
+
const completedAt = new Date(row.effects_completed_at ?? row.finished_at)
|
|
522
281
|
if (row.effects_status === "succeeded") {
|
|
523
282
|
return {
|
|
524
283
|
status: "succeeded",
|
|
@@ -529,65 +288,30 @@ function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | un
|
|
|
529
288
|
return {
|
|
530
289
|
status: "failed",
|
|
531
290
|
completedAt,
|
|
532
|
-
error:
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function toWritebackRecord(
|
|
537
|
-
input: RecordActionWritebackInput,
|
|
538
|
-
completedAt: Date
|
|
539
|
-
): ActionRunWritebackRecord {
|
|
540
|
-
if (input.status === "succeeded") {
|
|
541
|
-
return {
|
|
542
|
-
status: "succeeded",
|
|
543
|
-
completedAt,
|
|
544
|
-
result: input.result,
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
status: "failed",
|
|
550
|
-
completedAt,
|
|
551
|
-
error: input.error,
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function toEffectsRecord(
|
|
556
|
-
input: RecordActionEffectsInput,
|
|
557
|
-
completedAt: Date
|
|
558
|
-
): ActionRunEffectsRecord {
|
|
559
|
-
if (input.status === "succeeded") {
|
|
560
|
-
return {
|
|
561
|
-
status: "succeeded",
|
|
562
|
-
completedAt,
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
status: "failed",
|
|
568
|
-
completedAt,
|
|
569
|
-
error: input.error,
|
|
291
|
+
error: parseActionRunFailure(row.effects_error, "effects"),
|
|
570
292
|
}
|
|
571
293
|
}
|
|
572
294
|
|
|
573
295
|
function rowToActionRunRecord(row: DatabaseRow): ActionRunRecord {
|
|
574
|
-
|
|
296
|
+
const writeback = toActionRunWritebackRecord(row)
|
|
297
|
+
const effects = toActionRunEffectsRecord(row)
|
|
298
|
+
const fields = {
|
|
575
299
|
id: row.id,
|
|
576
300
|
projectId: row.project_id,
|
|
577
301
|
executionId: row.execution_id,
|
|
578
302
|
actionId: row.action_id,
|
|
579
303
|
subject: rowToActionSubject(row),
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
584
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
304
|
+
phase: row.phase,
|
|
305
|
+
startedAt: new Date(row.started_at),
|
|
306
|
+
finishedAt: new Date(row.finished_at),
|
|
585
307
|
params: row.params,
|
|
586
308
|
idempotencyKey: row.idempotency_key,
|
|
587
|
-
writeback:
|
|
588
|
-
effects:
|
|
589
|
-
error: toActionRunFailure(row),
|
|
309
|
+
...(writeback === undefined ? {} : { writeback }),
|
|
310
|
+
...(effects === undefined ? {} : { effects }),
|
|
590
311
|
}
|
|
312
|
+
|
|
313
|
+
if (row.status === "succeeded") return { ...fields, status: "succeeded" }
|
|
314
|
+
return { ...fields, status: "failed", error: parseActionRunFailure(row.error) }
|
|
591
315
|
}
|
|
592
316
|
|
|
593
317
|
function rowToActionSubject(row: DatabaseRow): ActionSubject {
|
|
@@ -614,11 +338,10 @@ interface DatabaseRow {
|
|
|
614
338
|
subject_kind: ActionSubject["kind"]
|
|
615
339
|
object_type_id: string | null
|
|
616
340
|
primary_id: string | null
|
|
617
|
-
status:
|
|
618
|
-
phase: ActionRunPhase
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
finished_at: Date | string | null
|
|
341
|
+
status: ActionRunStatus
|
|
342
|
+
phase: ActionRunPhase
|
|
343
|
+
started_at: Date | string
|
|
344
|
+
finished_at: Date | string
|
|
622
345
|
params: ActionRunParams
|
|
623
346
|
idempotency_key: string
|
|
624
347
|
writeback_status: ActionRunWritebackRecord["status"] | null
|