@sixb/sqlite 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 +57 -0
- package/dist/action-run-storage.d.ts +29 -0
- package/dist/action-run-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +25 -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 +14 -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 +26 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +14 -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 +46 -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 +28 -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 +134 -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 +131 -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 +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10204 -0
- package/dist/index.js.map +54 -0
- package/dist/latest-run-query.d.ts +22 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +18 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/object-query-compiler.d.ts +44 -0
- package/dist/object-query-compiler.d.ts.map +1 -0
- package/dist/object-storage.d.ts +93 -0
- package/dist/object-storage.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +14 -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 +49 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +63 -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 +32 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +14 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +91 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +26 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts +26 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -0
- package/dist/projection-run-storage.d.ts +32 -0
- package/dist/projection-run-storage.d.ts.map +1 -0
- package/dist/rules-storage.d.ts +33 -0
- package/dist/rules-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +27 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/sync-run-storage.d.ts +23 -0
- package/dist/sync-run-storage.d.ts.map +1 -0
- package/dist/timeseries-storage.d.ts +41 -0
- package/dist/timeseries-storage.d.ts.map +1 -0
- package/dist/transactions.d.ts +15 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/webhook-delivery-storage.d.ts +27 -0
- package/dist/webhook-delivery-storage.d.ts.map +1 -0
- package/dist/webhook-run-storage.d.ts +22 -0
- package/dist/webhook-run-storage.d.ts.map +1 -0
- package/dist/workflow-intervention-storage.d.ts +26 -0
- package/dist/workflow-intervention-storage.d.ts.map +1 -0
- package/dist/workflow-run-storage.d.ts +63 -0
- package/dist/workflow-run-storage.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/action-run-storage.ts +761 -0
- package/src/agents/agent-storage.ts +46 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +162 -0
- package/src/agents/rows.ts +205 -0
- package/src/agents/runs.ts +389 -0
- package/src/agents/threads.ts +133 -0
- package/src/auth-storage/access-tokens.ts +289 -0
- package/src/auth-storage/auth-storage.ts +784 -0
- package/src/auth-storage/group-memberships.ts +52 -0
- package/src/auth-storage/identities.ts +98 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +258 -0
- package/src/auth-storage/magic-links.ts +149 -0
- package/src/auth-storage/oidc-attempts.ts +109 -0
- package/src/auth-storage/rows.ts +338 -0
- package/src/auth-storage/service-account-group-memberships.ts +161 -0
- package/src/auth-storage/service-accounts.ts +168 -0
- package/src/auth-storage/sessions.ts +230 -0
- package/src/auth-storage/shared.ts +707 -0
- package/src/auth-storage/users.ts +226 -0
- package/src/index.ts +292 -0
- package/src/latest-run-query.ts +49 -0
- package/src/migrations/001-initial-schema.sql +1068 -0
- package/src/migrations.ts +245 -0
- package/src/object-query-compiler.ts +1198 -0
- package/src/object-storage.ts +605 -0
- package/src/ontology-storage/commits.ts +127 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +393 -0
- package/src/ontology-storage/materialization-state.ts +901 -0
- package/src/ontology-storage/materialization-writer.ts +394 -0
- package/src/ontology-storage/materializations.ts +838 -0
- package/src/ontology-storage/outbox.ts +237 -0
- package/src/ontology-storage/shared.ts +269 -0
- package/src/ontology-storage/sources.ts +653 -0
- package/src/pipeline-run-storage.ts +506 -0
- package/src/projection-run-storage.ts +592 -0
- package/src/rules-storage.ts +375 -0
- package/src/run-list-query.ts +98 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +3 -0
- package/src/sync-run-storage.ts +299 -0
- package/src/timeseries-storage.ts +212 -0
- package/src/transactions.ts +96 -0
- package/src/validation.ts +30 -0
- package/src/webhook-delivery-storage.ts +272 -0
- package/src/webhook-run-storage.ts +240 -0
- package/src/workflow-intervention-storage.ts +398 -0
- package/src/workflow-run-storage.ts +1148 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { ActionSubject, JsonValue } from "@sixb/core"
|
|
3
|
+
import type {
|
|
4
|
+
ActionRunEffectsRecord,
|
|
5
|
+
ActionRunFailure,
|
|
6
|
+
ActionRunParams,
|
|
7
|
+
ActionRunPhase,
|
|
8
|
+
ActionRunRecord,
|
|
9
|
+
ActionRunStorage,
|
|
10
|
+
ActionRunWritebackRecord,
|
|
11
|
+
EnterActionRunPhaseInput,
|
|
12
|
+
FinishActionRunInput,
|
|
13
|
+
ListActionRunsInput,
|
|
14
|
+
ListActionRunsResult,
|
|
15
|
+
LockActionMaterializationRunInput,
|
|
16
|
+
QueueActionRunInput,
|
|
17
|
+
RecordActionEffectsInput,
|
|
18
|
+
RecordActionWritebackInput,
|
|
19
|
+
StartActionRunInput,
|
|
20
|
+
} from "@sixb/core/storage"
|
|
21
|
+
import {
|
|
22
|
+
ActionRunError,
|
|
23
|
+
actionRunPhaseRecordsEqual,
|
|
24
|
+
canRequeueActionRunAfterEnqueueFailure,
|
|
25
|
+
finishActionRunPhase,
|
|
26
|
+
isTerminalActionRun,
|
|
27
|
+
} from "@sixb/core/storage"
|
|
28
|
+
import { installFreshSqliteSchema } from "./migrations"
|
|
29
|
+
import {
|
|
30
|
+
closeSqliteStoreConnection,
|
|
31
|
+
openSqliteStoreConnection,
|
|
32
|
+
type SqliteStoreConnection,
|
|
33
|
+
} from "./transactions"
|
|
34
|
+
|
|
35
|
+
export interface SqliteActionRunStorageOptions {
|
|
36
|
+
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
37
|
+
path?: string
|
|
38
|
+
/** Internal shared connection used by bundled SqliteStorage. */
|
|
39
|
+
connection?: SqliteStoreConnection
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class SqliteActionRunStorage implements ActionRunStorage {
|
|
43
|
+
private readonly connection: SqliteStoreConnection
|
|
44
|
+
private readonly db: Database
|
|
45
|
+
|
|
46
|
+
constructor(options: SqliteActionRunStorageOptions = {}) {
|
|
47
|
+
this.connection = openSqliteStoreConnection(options)
|
|
48
|
+
this.db = this.connection.db
|
|
49
|
+
|
|
50
|
+
if (this.connection.installFreshSchema) {
|
|
51
|
+
installFreshSqliteSchema(this.db)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async lockForMaterialization(input: LockActionMaterializationRunInput): Promise<ActionRunRecord> {
|
|
56
|
+
const row = this.db
|
|
57
|
+
.query(`SELECT action_id, status FROM action_runs WHERE project_id = ? AND id = ?`)
|
|
58
|
+
.get(input.projectId, input.runId) as {
|
|
59
|
+
readonly action_id: string
|
|
60
|
+
readonly status: string
|
|
61
|
+
} | null
|
|
62
|
+
if (!row) {
|
|
63
|
+
throw new ActionRunError(
|
|
64
|
+
`[SixbSqlite] Action run '${input.runId}' not found for project '${input.projectId}'.`
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
if (row.action_id !== input.actionId) {
|
|
68
|
+
throw new ActionRunError(
|
|
69
|
+
`[SixbSqlite] Action run '${input.runId}' does not belong to action '${input.actionId}'.`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (row.status !== "running") {
|
|
73
|
+
throw new ActionRunError(
|
|
74
|
+
`[SixbSqlite] Action run '${input.runId}' cannot materialize from status '${row.status}'.`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const record = await this.getById({ projectId: input.projectId, id: input.runId })
|
|
78
|
+
if (!record) {
|
|
79
|
+
throw new ActionRunError(
|
|
80
|
+
`[SixbSqlite] Action run '${input.runId}' disappeared while locking materialization.`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
return record
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async queue(input: QueueActionRunInput): Promise<ActionRunRecord> {
|
|
87
|
+
const queuedAt = input.queuedAt ?? new Date()
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
this.db
|
|
91
|
+
.query(
|
|
92
|
+
`
|
|
93
|
+
INSERT INTO action_runs (
|
|
94
|
+
project_id,
|
|
95
|
+
id,
|
|
96
|
+
action_id,
|
|
97
|
+
subject_kind,
|
|
98
|
+
object_type_id,
|
|
99
|
+
primary_id,
|
|
100
|
+
status,
|
|
101
|
+
phase,
|
|
102
|
+
queued_at,
|
|
103
|
+
started_at,
|
|
104
|
+
finished_at,
|
|
105
|
+
params,
|
|
106
|
+
idempotency_key,
|
|
107
|
+
writeback_status,
|
|
108
|
+
writeback_completed_at,
|
|
109
|
+
writeback_result,
|
|
110
|
+
writeback_error_name,
|
|
111
|
+
writeback_error_message,
|
|
112
|
+
writeback_error_phase,
|
|
113
|
+
effects_status,
|
|
114
|
+
effects_completed_at,
|
|
115
|
+
effects_error_name,
|
|
116
|
+
effects_error_message,
|
|
117
|
+
effects_error_phase,
|
|
118
|
+
error_name,
|
|
119
|
+
error_message,
|
|
120
|
+
error_phase
|
|
121
|
+
) VALUES (
|
|
122
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?,
|
|
123
|
+
NULL, NULL, NULL, NULL, NULL, NULL,
|
|
124
|
+
NULL, NULL, NULL, NULL, NULL,
|
|
125
|
+
NULL, NULL, NULL
|
|
126
|
+
)
|
|
127
|
+
`
|
|
128
|
+
)
|
|
129
|
+
.run(
|
|
130
|
+
input.projectId,
|
|
131
|
+
input.id,
|
|
132
|
+
input.actionId,
|
|
133
|
+
input.subject.kind,
|
|
134
|
+
input.subject.kind === "object" ? input.subject.objectTypeId : null,
|
|
135
|
+
input.subject.kind === "object" ? input.subject.primaryId : null,
|
|
136
|
+
"queued",
|
|
137
|
+
"request",
|
|
138
|
+
queuedAt.toISOString(),
|
|
139
|
+
JSON.stringify(input.params),
|
|
140
|
+
input.idempotencyKey
|
|
141
|
+
)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (isUniqueConstraintError(error)) {
|
|
144
|
+
return this.requeueAfterEnqueueFailure(input, queuedAt)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw error
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const record = await this.getById({ projectId: input.projectId, id: input.id })
|
|
151
|
+
if (!record) {
|
|
152
|
+
throw new ActionRunError(
|
|
153
|
+
`[SixbSqlite] Failed to load action run '${input.id}' for project '${input.projectId}'.`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return record
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async requeueAfterEnqueueFailure(
|
|
161
|
+
input: QueueActionRunInput,
|
|
162
|
+
queuedAt: Date
|
|
163
|
+
): Promise<ActionRunRecord> {
|
|
164
|
+
return this.db.transaction(() => {
|
|
165
|
+
const existing = this.db
|
|
166
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
167
|
+
.get(input.projectId, input.id) as DatabaseRow | null
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
!existing ||
|
|
171
|
+
!canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)
|
|
172
|
+
) {
|
|
173
|
+
throw new ActionRunError(
|
|
174
|
+
`[SixbSqlite] Action run '${input.id}' already exists for project '${input.projectId}'.`
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.db
|
|
179
|
+
.query(
|
|
180
|
+
`
|
|
181
|
+
UPDATE action_runs
|
|
182
|
+
SET
|
|
183
|
+
status = ?,
|
|
184
|
+
phase = ?,
|
|
185
|
+
queued_at = ?,
|
|
186
|
+
started_at = NULL,
|
|
187
|
+
finished_at = NULL,
|
|
188
|
+
writeback_status = NULL,
|
|
189
|
+
writeback_completed_at = NULL,
|
|
190
|
+
writeback_result = NULL,
|
|
191
|
+
writeback_error_name = NULL,
|
|
192
|
+
writeback_error_message = NULL,
|
|
193
|
+
writeback_error_phase = NULL,
|
|
194
|
+
effects_status = NULL,
|
|
195
|
+
effects_completed_at = NULL,
|
|
196
|
+
effects_error_name = NULL,
|
|
197
|
+
effects_error_message = NULL,
|
|
198
|
+
effects_error_phase = NULL,
|
|
199
|
+
error_name = NULL,
|
|
200
|
+
error_message = NULL,
|
|
201
|
+
error_phase = NULL
|
|
202
|
+
WHERE project_id = ? AND id = ?
|
|
203
|
+
`
|
|
204
|
+
)
|
|
205
|
+
.run("queued", "request", queuedAt.toISOString(), input.projectId, input.id)
|
|
206
|
+
|
|
207
|
+
const updated = this.db
|
|
208
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
209
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
210
|
+
|
|
211
|
+
return rowToActionRunRecord(updated)
|
|
212
|
+
})()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async start(input: StartActionRunInput): Promise<ActionRunRecord> {
|
|
216
|
+
return this.db.transaction(() => {
|
|
217
|
+
const existing = this.db
|
|
218
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
219
|
+
.get(input.projectId, input.id) as DatabaseRow | null
|
|
220
|
+
|
|
221
|
+
if (!existing) {
|
|
222
|
+
throw new ActionRunError(
|
|
223
|
+
`[SixbSqlite] Action run '${input.id}' not found for project '${input.projectId}'.`
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (existing.status !== "queued") {
|
|
228
|
+
throw new ActionRunError(
|
|
229
|
+
`[SixbSqlite] Action run '${input.id}' cannot start from status '${existing.status}'.`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.db
|
|
234
|
+
.query(
|
|
235
|
+
`
|
|
236
|
+
UPDATE action_runs
|
|
237
|
+
SET
|
|
238
|
+
status = ?,
|
|
239
|
+
phase = ?,
|
|
240
|
+
started_at = ?,
|
|
241
|
+
error_name = NULL,
|
|
242
|
+
error_message = NULL,
|
|
243
|
+
error_phase = NULL
|
|
244
|
+
WHERE project_id = ? AND id = ?
|
|
245
|
+
`
|
|
246
|
+
)
|
|
247
|
+
.run(
|
|
248
|
+
"running",
|
|
249
|
+
input.phase ?? "validation",
|
|
250
|
+
(input.startedAt ?? new Date()).toISOString(),
|
|
251
|
+
input.projectId,
|
|
252
|
+
input.id
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const updated = this.db
|
|
256
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
257
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
258
|
+
|
|
259
|
+
return rowToActionRunRecord(updated)
|
|
260
|
+
})()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async enterPhase(input: EnterActionRunPhaseInput): Promise<ActionRunRecord> {
|
|
264
|
+
return this.db.transaction(() => {
|
|
265
|
+
this.requireRunningRun(input.projectId, input.id, "transition phase")
|
|
266
|
+
|
|
267
|
+
this.db
|
|
268
|
+
.query(
|
|
269
|
+
`
|
|
270
|
+
UPDATE action_runs
|
|
271
|
+
SET phase = ?
|
|
272
|
+
WHERE project_id = ? AND id = ?
|
|
273
|
+
`
|
|
274
|
+
)
|
|
275
|
+
.run(input.phase, input.projectId, input.id)
|
|
276
|
+
|
|
277
|
+
const updated = this.db
|
|
278
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
279
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
280
|
+
|
|
281
|
+
return rowToActionRunRecord(updated)
|
|
282
|
+
})()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async recordWriteback(input: RecordActionWritebackInput): Promise<ActionRunRecord> {
|
|
286
|
+
return this.db.transaction(() => {
|
|
287
|
+
const existing = this.requireRunningRun(input.projectId, input.id, "record writeback")
|
|
288
|
+
const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date()))
|
|
289
|
+
const currentWriteback = toActionRunWritebackRecord(existing)
|
|
290
|
+
|
|
291
|
+
if (currentWriteback) {
|
|
292
|
+
if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
|
|
293
|
+
return rowToActionRunRecord(existing)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw new ActionRunError(
|
|
297
|
+
`[SixbSqlite] Action run '${input.id}' already has a different writeback record.`
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.db
|
|
302
|
+
.query(
|
|
303
|
+
`
|
|
304
|
+
UPDATE action_runs
|
|
305
|
+
SET
|
|
306
|
+
phase = ?,
|
|
307
|
+
writeback_status = ?,
|
|
308
|
+
writeback_completed_at = ?,
|
|
309
|
+
writeback_result = ?,
|
|
310
|
+
writeback_error_name = ?,
|
|
311
|
+
writeback_error_message = ?,
|
|
312
|
+
writeback_error_phase = ?
|
|
313
|
+
WHERE project_id = ? AND id = ?
|
|
314
|
+
`
|
|
315
|
+
)
|
|
316
|
+
.run(
|
|
317
|
+
"writeback",
|
|
318
|
+
input.status,
|
|
319
|
+
nextWriteback.completedAt.toISOString(),
|
|
320
|
+
input.status === "succeeded" ? serializeJsonValue(input.result) : null,
|
|
321
|
+
input.status === "failed" ? (input.error.name ?? null) : null,
|
|
322
|
+
input.status === "failed" ? input.error.message : null,
|
|
323
|
+
input.status === "failed" ? (input.error.phase ?? "writeback") : null,
|
|
324
|
+
input.projectId,
|
|
325
|
+
input.id
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const updated = this.db
|
|
329
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
330
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
331
|
+
|
|
332
|
+
return rowToActionRunRecord(updated)
|
|
333
|
+
})()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async recordEffects(input: RecordActionEffectsInput): Promise<ActionRunRecord> {
|
|
337
|
+
return this.db.transaction(() => {
|
|
338
|
+
const existing = this.requireRunningRun(input.projectId, input.id, "record effects")
|
|
339
|
+
const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date()))
|
|
340
|
+
const currentEffects = toActionRunEffectsRecord(existing)
|
|
341
|
+
|
|
342
|
+
if (currentEffects) {
|
|
343
|
+
if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
|
|
344
|
+
return rowToActionRunRecord(existing)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new ActionRunError(
|
|
348
|
+
`[SixbSqlite] Action run '${input.id}' already has a different effects record.`
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.db
|
|
353
|
+
.query(
|
|
354
|
+
`
|
|
355
|
+
UPDATE action_runs
|
|
356
|
+
SET
|
|
357
|
+
phase = ?,
|
|
358
|
+
effects_status = ?,
|
|
359
|
+
effects_completed_at = ?,
|
|
360
|
+
effects_error_name = ?,
|
|
361
|
+
effects_error_message = ?,
|
|
362
|
+
effects_error_phase = ?
|
|
363
|
+
WHERE project_id = ? AND id = ?
|
|
364
|
+
`
|
|
365
|
+
)
|
|
366
|
+
.run(
|
|
367
|
+
"effects",
|
|
368
|
+
input.status,
|
|
369
|
+
nextEffects.completedAt.toISOString(),
|
|
370
|
+
input.status === "failed" ? (input.error.name ?? null) : null,
|
|
371
|
+
input.status === "failed" ? input.error.message : null,
|
|
372
|
+
input.status === "failed" ? (input.error.phase ?? "effects") : null,
|
|
373
|
+
input.projectId,
|
|
374
|
+
input.id
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const updated = this.db
|
|
378
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
379
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
380
|
+
|
|
381
|
+
return rowToActionRunRecord(updated)
|
|
382
|
+
})()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async finish(input: FinishActionRunInput): Promise<ActionRunRecord> {
|
|
386
|
+
return this.db.transaction(() => {
|
|
387
|
+
const existing = this.db
|
|
388
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
389
|
+
.get(input.projectId, input.id) as DatabaseRow | null
|
|
390
|
+
|
|
391
|
+
if (!existing) {
|
|
392
|
+
throw new ActionRunError(
|
|
393
|
+
`[SixbSqlite] Action run '${input.id}' not found for project '${input.projectId}'.`
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (isTerminalActionRun({ status: existing.status })) {
|
|
398
|
+
throw new ActionRunError(
|
|
399
|
+
`[SixbSqlite] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const phase = finishActionRunPhase(input, existing.phase)
|
|
404
|
+
|
|
405
|
+
this.db
|
|
406
|
+
.query(
|
|
407
|
+
`
|
|
408
|
+
UPDATE action_runs
|
|
409
|
+
SET
|
|
410
|
+
status = ?,
|
|
411
|
+
phase = ?,
|
|
412
|
+
finished_at = ?,
|
|
413
|
+
error_name = ?,
|
|
414
|
+
error_message = ?,
|
|
415
|
+
error_phase = ?
|
|
416
|
+
WHERE project_id = ? AND id = ?
|
|
417
|
+
`
|
|
418
|
+
)
|
|
419
|
+
.run(
|
|
420
|
+
input.status,
|
|
421
|
+
phase,
|
|
422
|
+
(input.finishedAt ?? new Date()).toISOString(),
|
|
423
|
+
input.status === "succeeded" ? null : (input.error?.name ?? null),
|
|
424
|
+
input.status === "succeeded" ? null : (input.error?.message ?? null),
|
|
425
|
+
input.status === "succeeded" ? null : (input.error?.phase ?? phase),
|
|
426
|
+
input.projectId,
|
|
427
|
+
input.id
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
const updated = this.db
|
|
431
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
432
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
433
|
+
|
|
434
|
+
return rowToActionRunRecord(updated)
|
|
435
|
+
})()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async getById(params: { projectId: string; id: string }): Promise<ActionRunRecord | null> {
|
|
439
|
+
const row = this.db
|
|
440
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
441
|
+
.get(params.projectId, params.id) as DatabaseRow | null
|
|
442
|
+
|
|
443
|
+
return row ? rowToActionRunRecord(row) : null
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async list(input: ListActionRunsInput): Promise<ListActionRunsResult> {
|
|
447
|
+
if ((input.statuses && input.statuses.length === 0) || input.actionIds?.length === 0) {
|
|
448
|
+
return {
|
|
449
|
+
runs: [],
|
|
450
|
+
hasMore: false,
|
|
451
|
+
total: 0,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const whereClauses = ["project_id = ?"]
|
|
456
|
+
const args: (string | number)[] = [input.projectId]
|
|
457
|
+
|
|
458
|
+
if (input.actionId) {
|
|
459
|
+
whereClauses.push("action_id = ?")
|
|
460
|
+
args.push(input.actionId)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (input.actionIds) {
|
|
464
|
+
whereClauses.push(`action_id IN (${input.actionIds.map(() => "?").join(", ")})`)
|
|
465
|
+
args.push(...input.actionIds)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (input.objectTypeIds) {
|
|
469
|
+
if (input.objectTypeIds.length === 0) {
|
|
470
|
+
whereClauses.push("subject_kind <> ?")
|
|
471
|
+
args.push("object")
|
|
472
|
+
} else {
|
|
473
|
+
whereClauses.push(
|
|
474
|
+
`(subject_kind <> ? OR object_type_id IN (${input.objectTypeIds
|
|
475
|
+
.map(() => "?")
|
|
476
|
+
.join(", ")}))`
|
|
477
|
+
)
|
|
478
|
+
args.push("object", ...input.objectTypeIds)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (input.objectTypeId) {
|
|
483
|
+
whereClauses.push("subject_kind = ?")
|
|
484
|
+
args.push("object")
|
|
485
|
+
whereClauses.push("object_type_id = ?")
|
|
486
|
+
args.push(input.objectTypeId)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (input.primaryId) {
|
|
490
|
+
whereClauses.push("subject_kind = ?")
|
|
491
|
+
args.push("object")
|
|
492
|
+
whereClauses.push("primary_id = ?")
|
|
493
|
+
args.push(input.primaryId)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (input.subject) {
|
|
497
|
+
whereClauses.push("subject_kind = ?")
|
|
498
|
+
args.push(input.subject.kind)
|
|
499
|
+
if (input.subject.kind === "object") {
|
|
500
|
+
whereClauses.push("object_type_id = ?")
|
|
501
|
+
args.push(input.subject.objectTypeId)
|
|
502
|
+
whereClauses.push("primary_id = ?")
|
|
503
|
+
args.push(input.subject.primaryId)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (input.statuses) {
|
|
508
|
+
whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
|
|
509
|
+
args.push(...input.statuses)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (input.startedAfter) {
|
|
513
|
+
whereClauses.push("COALESCE(started_at, queued_at) >= ?")
|
|
514
|
+
args.push(input.startedAfter.toISOString())
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (input.startedBefore) {
|
|
518
|
+
whereClauses.push("COALESCE(started_at, queued_at) <= ?")
|
|
519
|
+
args.push(input.startedBefore.toISOString())
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const where = `WHERE ${whereClauses.join(" AND ")}`
|
|
523
|
+
const order = input.order === "asc" ? "ASC" : "DESC"
|
|
524
|
+
const offset = input.offset ?? 0
|
|
525
|
+
const limit = input.limit
|
|
526
|
+
|
|
527
|
+
const totalRow = this.db
|
|
528
|
+
.query(`SELECT COUNT(*) AS count FROM action_runs ${where}`)
|
|
529
|
+
.get(...args) as { count: number }
|
|
530
|
+
|
|
531
|
+
let query = `
|
|
532
|
+
SELECT * FROM action_runs
|
|
533
|
+
${where}
|
|
534
|
+
ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
|
|
535
|
+
`
|
|
536
|
+
const queryArgs = [...args]
|
|
537
|
+
|
|
538
|
+
if (limit !== undefined) {
|
|
539
|
+
query += " LIMIT ? OFFSET ?"
|
|
540
|
+
queryArgs.push(limit, offset)
|
|
541
|
+
} else if (offset > 0) {
|
|
542
|
+
query += " LIMIT -1 OFFSET ?"
|
|
543
|
+
queryArgs.push(offset)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const rows = this.db.query(query).all(...queryArgs) as DatabaseRow[]
|
|
547
|
+
const runs = rows.map((row) => rowToActionRunRecord(row))
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
runs,
|
|
551
|
+
hasMore: offset + runs.length < totalRow.count,
|
|
552
|
+
total: totalRow.count,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
close(): void {
|
|
557
|
+
closeSqliteStoreConnection(this.connection)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private requireRunningRun(projectId: string, id: string, operation: string): DatabaseRow {
|
|
561
|
+
const existing = this.db
|
|
562
|
+
.query("SELECT * FROM action_runs WHERE project_id = ? AND id = ?")
|
|
563
|
+
.get(projectId, id) as DatabaseRow | null
|
|
564
|
+
|
|
565
|
+
if (!existing) {
|
|
566
|
+
throw new ActionRunError(
|
|
567
|
+
`[SixbSqlite] Action run '${id}' not found for project '${projectId}'.`
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (existing.status !== "running") {
|
|
572
|
+
throw new ActionRunError(
|
|
573
|
+
`[SixbSqlite] Action run '${id}' cannot ${operation} from status '${existing.status}'.`
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return existing
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function serializeJsonValue(value: JsonValue): string {
|
|
582
|
+
return JSON.stringify(value)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function toActionRunFailure(row: DatabaseRow): ActionRunFailure | undefined {
|
|
586
|
+
return toFailure(row.error_name, row.error_message, row.error_phase)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function toFailure(
|
|
590
|
+
name: string | null,
|
|
591
|
+
message: string | null,
|
|
592
|
+
phase: ActionRunPhase | null
|
|
593
|
+
): ActionRunFailure | undefined {
|
|
594
|
+
if (!message) {
|
|
595
|
+
return undefined
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
name: name ?? undefined,
|
|
600
|
+
message,
|
|
601
|
+
phase: phase ?? undefined,
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function toActionRunWritebackRecord(row: DatabaseRow): ActionRunWritebackRecord | undefined {
|
|
606
|
+
if (!row.writeback_status) {
|
|
607
|
+
return undefined
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const completedAt = row.writeback_completed_at
|
|
611
|
+
? new Date(row.writeback_completed_at)
|
|
612
|
+
: new Date(row.queued_at)
|
|
613
|
+
|
|
614
|
+
if (row.writeback_status === "succeeded") {
|
|
615
|
+
return {
|
|
616
|
+
status: "succeeded",
|
|
617
|
+
completedAt,
|
|
618
|
+
result:
|
|
619
|
+
row.writeback_result === null ? null : (JSON.parse(row.writeback_result) as JsonValue),
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
status: "failed",
|
|
625
|
+
completedAt,
|
|
626
|
+
error: toFailure(
|
|
627
|
+
row.writeback_error_name,
|
|
628
|
+
row.writeback_error_message,
|
|
629
|
+
row.writeback_error_phase
|
|
630
|
+
),
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function toActionRunEffectsRecord(row: DatabaseRow): ActionRunEffectsRecord | undefined {
|
|
635
|
+
if (!row.effects_status) {
|
|
636
|
+
return undefined
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const completedAt = row.effects_completed_at
|
|
640
|
+
? new Date(row.effects_completed_at)
|
|
641
|
+
: new Date(row.queued_at)
|
|
642
|
+
|
|
643
|
+
if (row.effects_status === "succeeded") {
|
|
644
|
+
return {
|
|
645
|
+
status: "succeeded",
|
|
646
|
+
completedAt,
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
status: "failed",
|
|
652
|
+
completedAt,
|
|
653
|
+
error: toFailure(row.effects_error_name, row.effects_error_message, row.effects_error_phase),
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function toWritebackRecord(
|
|
658
|
+
input: RecordActionWritebackInput,
|
|
659
|
+
completedAt: Date
|
|
660
|
+
): ActionRunWritebackRecord {
|
|
661
|
+
if (input.status === "succeeded") {
|
|
662
|
+
return {
|
|
663
|
+
status: "succeeded",
|
|
664
|
+
completedAt,
|
|
665
|
+
result: input.result,
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
status: "failed",
|
|
671
|
+
completedAt,
|
|
672
|
+
error: input.error,
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function toEffectsRecord(
|
|
677
|
+
input: RecordActionEffectsInput,
|
|
678
|
+
completedAt: Date
|
|
679
|
+
): ActionRunEffectsRecord {
|
|
680
|
+
if (input.status === "succeeded") {
|
|
681
|
+
return {
|
|
682
|
+
status: "succeeded",
|
|
683
|
+
completedAt,
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
status: "failed",
|
|
689
|
+
completedAt,
|
|
690
|
+
error: input.error,
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function rowToActionRunRecord(row: DatabaseRow): ActionRunRecord {
|
|
695
|
+
return {
|
|
696
|
+
id: row.id,
|
|
697
|
+
projectId: row.project_id,
|
|
698
|
+
actionId: row.action_id,
|
|
699
|
+
subject: rowToActionSubject(row),
|
|
700
|
+
status: row.status,
|
|
701
|
+
phase: row.phase ?? undefined,
|
|
702
|
+
queuedAt: new Date(row.queued_at),
|
|
703
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
704
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
705
|
+
params: JSON.parse(row.params) as ActionRunParams,
|
|
706
|
+
idempotencyKey: row.idempotency_key,
|
|
707
|
+
writeback: toActionRunWritebackRecord(row),
|
|
708
|
+
effects: toActionRunEffectsRecord(row),
|
|
709
|
+
error: toActionRunFailure(row),
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function rowToActionSubject(row: DatabaseRow): ActionSubject {
|
|
714
|
+
if (row.subject_kind === "none") {
|
|
715
|
+
return { kind: "none" }
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!row.object_type_id || !row.primary_id) {
|
|
719
|
+
throw new ActionRunError(`[SixbSqlite] Action run '${row.id}' has an invalid object subject.`)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
kind: "object",
|
|
724
|
+
objectTypeId: row.object_type_id,
|
|
725
|
+
primaryId: row.primary_id,
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function isUniqueConstraintError(error: unknown): boolean {
|
|
730
|
+
return error instanceof Error && error.message.includes("UNIQUE constraint failed")
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
interface DatabaseRow {
|
|
734
|
+
project_id: string
|
|
735
|
+
id: string
|
|
736
|
+
action_id: string
|
|
737
|
+
subject_kind: ActionSubject["kind"]
|
|
738
|
+
object_type_id: string | null
|
|
739
|
+
primary_id: string | null
|
|
740
|
+
status: ActionRunRecord["status"]
|
|
741
|
+
phase: ActionRunPhase | null
|
|
742
|
+
queued_at: string
|
|
743
|
+
started_at: string | null
|
|
744
|
+
finished_at: string | null
|
|
745
|
+
params: string
|
|
746
|
+
idempotency_key: string
|
|
747
|
+
writeback_status: ActionRunWritebackRecord["status"] | null
|
|
748
|
+
writeback_completed_at: string | null
|
|
749
|
+
writeback_result: string | null
|
|
750
|
+
writeback_error_name: string | null
|
|
751
|
+
writeback_error_message: string | null
|
|
752
|
+
writeback_error_phase: ActionRunPhase | null
|
|
753
|
+
effects_status: ActionRunEffectsRecord["status"] | null
|
|
754
|
+
effects_completed_at: string | null
|
|
755
|
+
effects_error_name: string | null
|
|
756
|
+
effects_error_message: string | null
|
|
757
|
+
effects_error_phase: ActionRunPhase | null
|
|
758
|
+
error_name: string | null
|
|
759
|
+
error_message: string | null
|
|
760
|
+
error_phase: ActionRunPhase | null
|
|
761
|
+
}
|