@sixb/sqlite 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +9 -5
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/checkpoints.d.ts +14 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/messages.d.ts +2 -1
  11. package/dist/agents/messages.d.ts.map +1 -1
  12. package/dist/agents/rows.d.ts +21 -11
  13. package/dist/agents/rows.d.ts.map +1 -1
  14. package/dist/agents/runs.d.ts +3 -2
  15. package/dist/agents/runs.d.ts.map +1 -1
  16. package/dist/ai-cost-storage.d.ts +15 -0
  17. package/dist/ai-cost-storage.d.ts.map +1 -0
  18. package/dist/ai-usage-storage.d.ts +22 -0
  19. package/dist/ai-usage-storage.d.ts.map +1 -0
  20. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  21. package/dist/auth-storage/shared.d.ts +1 -10
  22. package/dist/auth-storage/shared.d.ts.map +1 -1
  23. package/dist/connector-connection-storage.d.ts +9 -0
  24. package/dist/connector-connection-storage.d.ts.map +1 -0
  25. package/dist/execution-storage.d.ts +14 -0
  26. package/dist/execution-storage.d.ts.map +1 -0
  27. package/dist/index.d.ts +14 -9
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4633 -1066
  30. package/dist/index.js.map +33 -28
  31. package/dist/latest-run-query.d.ts.map +1 -1
  32. package/dist/migrations.d.ts +0 -1
  33. package/dist/migrations.d.ts.map +1 -1
  34. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  35. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  36. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  37. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  38. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  39. package/dist/ontology-storage/materializations.d.ts +0 -2
  40. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  41. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  42. package/dist/ontology-storage/shared.d.ts +3 -3
  43. package/dist/ontology-storage/shared.d.ts.map +1 -1
  44. package/dist/ontology-storage/sources.d.ts.map +1 -1
  45. package/dist/pipeline-run-storage.d.ts +7 -2
  46. package/dist/pipeline-run-storage.d.ts.map +1 -1
  47. package/dist/projection-run-storage.d.ts +9 -2
  48. package/dist/projection-run-storage.d.ts.map +1 -1
  49. package/dist/run-list-query.d.ts +1 -1
  50. package/dist/run-list-query.d.ts.map +1 -1
  51. package/dist/sync-run-storage.d.ts +7 -2
  52. package/dist/sync-run-storage.d.ts.map +1 -1
  53. package/dist/testing.d.ts +6 -0
  54. package/dist/testing.d.ts.map +1 -0
  55. package/dist/transactions.d.ts +3 -0
  56. package/dist/transactions.d.ts.map +1 -1
  57. package/dist/webhook-run-storage.d.ts +9 -2
  58. package/dist/webhook-run-storage.d.ts.map +1 -1
  59. package/dist/workflow-run-storage.d.ts +7 -3
  60. package/dist/workflow-run-storage.d.ts.map +1 -1
  61. package/package.json +3 -3
  62. package/src/action-run-storage.ts +51 -74
  63. package/src/agents/agent-storage.ts +11 -6
  64. package/src/agents/checkpoints.ts +175 -0
  65. package/src/agents/index.ts +1 -0
  66. package/src/agents/messages.ts +59 -0
  67. package/src/agents/rows.ts +52 -40
  68. package/src/agents/runs.ts +27 -21
  69. package/src/ai-cost-storage.ts +744 -0
  70. package/src/ai-usage-storage.ts +324 -0
  71. package/src/auth-storage/service-accounts.ts +14 -21
  72. package/src/auth-storage/shared.ts +0 -22
  73. package/src/connector-connection-storage.ts +784 -0
  74. package/src/execution-storage.ts +158 -0
  75. package/src/index.ts +69 -22
  76. package/src/latest-run-query.ts +7 -1
  77. package/src/migrations/004-executions.sql +142 -0
  78. package/src/migrations/005-workflow-executions.sql +7 -0
  79. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  80. package/src/migrations/007-split-overrides.sql +154 -0
  81. package/src/migrations/008-action-executions.sql +4 -0
  82. package/src/migrations/009-agent-executions.sql +115 -0
  83. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  84. package/src/migrations/011-sync-failure-record.sql +22 -0
  85. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  86. package/src/migrations/013-workflow-failure-record.sql +45 -0
  87. package/src/migrations/014-agent-failure-record.sql +59 -0
  88. package/src/migrations/015-projection-failure-record.sql +20 -0
  89. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  90. package/src/migrations/017-action-failure-record.sql +65 -0
  91. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  92. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  93. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  94. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  95. package/src/migrations/022-projection-executions.sql +244 -0
  96. package/src/migrations/023-webhook-executions.sql +51 -0
  97. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  98. package/src/migrations/025-connector-connections.sql +257 -0
  99. package/src/migrations/026-ai-cost-accounting.sql +57 -0
  100. package/src/migrations/027-agent-context-checkpoints.sql +28 -0
  101. package/src/migrations/028-object-override-edit-times.sql +3 -0
  102. package/src/migrations.ts +129 -4
  103. package/src/ontology-storage/materialization-session.ts +317 -143
  104. package/src/ontology-storage/materialization-state.ts +323 -112
  105. package/src/ontology-storage/materialization-writer.ts +145 -67
  106. package/src/ontology-storage/materializations.ts +67 -141
  107. package/src/ontology-storage/outbox.ts +10 -3
  108. package/src/ontology-storage/shared.ts +10 -4
  109. package/src/ontology-storage/sources.ts +4 -1
  110. package/src/pipeline-run-storage.ts +128 -43
  111. package/src/projection-run-storage.ts +209 -97
  112. package/src/run-list-query.ts +9 -4
  113. package/src/sync-run-storage.ts +118 -32
  114. package/src/testing.ts +20 -0
  115. package/src/transactions.ts +10 -3
  116. package/src/webhook-run-storage.ts +118 -74
  117. package/src/workflow-run-storage.ts +117 -131
  118. package/dist/validation.d.ts +0 -5
  119. package/dist/validation.d.ts.map +0 -1
  120. package/dist/webhook-delivery-storage.d.ts +0 -27
  121. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  122. package/src/validation.ts +0 -30
  123. package/src/webhook-delivery-storage.ts +0 -272
@@ -0,0 +1,784 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite"
2
+ import {
3
+ type ConnectorConnectionPersistence,
4
+ type ConnectorConnectionPersistenceBackend,
5
+ type ConnectorConnectionPersistenceScope,
6
+ DurableConnectorConnectionStorage,
7
+ } from "@sixb/core/internal/connector-connection-storage-provider"
8
+ import type {
9
+ ConnectorAuthorizationAttemptRecord,
10
+ ConnectorAuthorizationRecord,
11
+ ConnectorConnectionRecord,
12
+ ConnectorConnectionRunRecord,
13
+ ConnectorCredentialMutation,
14
+ } from "@sixb/core/storage"
15
+ import { isUniqueConstraintError } from "./storage-errors"
16
+ import { runImmediateTransactionAsync, type SqliteStoreConnection } from "./transactions"
17
+
18
+ export class SqliteConnectorConnectionStorage extends DurableConnectorConnectionStorage {
19
+ private readonly clock: ConnectorConnectionTestClock
20
+
21
+ constructor(connection: SqliteStoreConnection) {
22
+ const clock = { offsetMs: 0 }
23
+ super(new SqliteConnectorConnectionPersistenceBackend(connection.db, clock))
24
+ this.clock = clock
25
+ }
26
+
27
+ /** Internal deterministic clock control used only by the provider contract suite. */
28
+ advanceTimeForTesting(durationMs: number): void {
29
+ this.clock.offsetMs += durationMs
30
+ }
31
+ }
32
+
33
+ class SqliteConnectorConnectionPersistenceBackend implements ConnectorConnectionPersistenceBackend {
34
+ constructor(
35
+ private readonly db: Database,
36
+ private readonly clock: ConnectorConnectionTestClock
37
+ ) {}
38
+
39
+ read<T>(
40
+ scope: ConnectorConnectionPersistenceScope,
41
+ run: (persistence: ConnectorConnectionPersistence) => Promise<T>
42
+ ): Promise<T> {
43
+ return run(new SqliteConnectorConnectionPersistence(this.db, scope, this.clock))
44
+ }
45
+
46
+ transaction<T>(
47
+ scope: ConnectorConnectionPersistenceScope,
48
+ run: (persistence: ConnectorConnectionPersistence) => Promise<T>
49
+ ): Promise<T> {
50
+ return runImmediateTransactionAsync(this.db, () =>
51
+ run(new SqliteConnectorConnectionPersistence(this.db, scope, this.clock))
52
+ )
53
+ }
54
+ }
55
+
56
+ class SqliteConnectorConnectionPersistence implements ConnectorConnectionPersistence {
57
+ constructor(
58
+ private readonly db: Database,
59
+ private readonly scope: ConnectorConnectionPersistenceScope,
60
+ private readonly clock: ConnectorConnectionTestClock
61
+ ) {}
62
+
63
+ async now(): Promise<Date> {
64
+ const row = this.db.query("SELECT strftime('%Y-%m-%dT%H:%M:%fZ', 'now') AS now").get() as {
65
+ readonly now: string
66
+ }
67
+ return new Date(new Date(row.now).getTime() + this.clock.offsetMs)
68
+ }
69
+
70
+ async insertAuthorizationAttempt(record: ConnectorAuthorizationAttemptRecord): Promise<boolean> {
71
+ try {
72
+ this.db
73
+ .query(
74
+ `
75
+ INSERT INTO connector_authorization_attempts (
76
+ project_id, connector_id, id, slot, initiated_by_execution_id,
77
+ state_hash, code_verifier, redirect_uri, connection_run_id, return_to,
78
+ callback_binding_hash, reauthorization_id, reauthorization_revision,
79
+ reauthorization_connection_ids, created_at, expires_at
80
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
81
+ `
82
+ )
83
+ .run(
84
+ record.projectId,
85
+ record.connectorId,
86
+ record.id,
87
+ record.slot,
88
+ record.initiatedByExecutionId,
89
+ record.stateHash,
90
+ json(record.codeVerifier),
91
+ record.redirectUri,
92
+ record.connectionRunId ?? null,
93
+ record.returnTo ?? null,
94
+ record.callbackBindingHash ?? null,
95
+ record.reauthorizationId ?? null,
96
+ record.reauthorizationRevision ?? null,
97
+ record.reauthorizationConnectionIds ? json(record.reauthorizationConnectionIds) : null,
98
+ iso(record.createdAt),
99
+ iso(record.expiresAt)
100
+ )
101
+ return true
102
+ } catch (error) {
103
+ if (isUniqueConstraintError(error)) return false
104
+ throw error
105
+ }
106
+ }
107
+
108
+ async getAuthorizationAttempt(id: string): Promise<ConnectorAuthorizationAttemptRecord | null> {
109
+ const row = this.getById<SqliteAuthorizationAttemptRow>("connector_authorization_attempts", id)
110
+ return row ? authorizationAttemptFromRow(row) : null
111
+ }
112
+
113
+ async getAuthorizationAttemptByConnectionRunId(
114
+ connectionRunId: string
115
+ ): Promise<ConnectorAuthorizationAttemptRecord | null> {
116
+ if (!this.scope.connectorId) return null
117
+ const row = this.db
118
+ .query(
119
+ `
120
+ SELECT * FROM connector_authorization_attempts
121
+ WHERE project_id = ? AND connector_id = ? AND connection_run_id = ?
122
+ `
123
+ )
124
+ .get(
125
+ this.scope.projectId,
126
+ this.scope.connectorId,
127
+ connectionRunId
128
+ ) as SqliteAuthorizationAttemptRow | null
129
+ return row ? authorizationAttemptFromRow(row) : null
130
+ }
131
+
132
+ async deleteAuthorizationAttempt(id: string): Promise<boolean> {
133
+ return this.deleteById("connector_authorization_attempts", id)
134
+ }
135
+
136
+ async insertConnectionRun(record: ConnectorConnectionRunRecord): Promise<boolean> {
137
+ try {
138
+ this.insertRun(record)
139
+ return true
140
+ } catch (error) {
141
+ if (isUniqueConstraintError(error)) return false
142
+ throw error
143
+ }
144
+ }
145
+
146
+ async getConnectionRun(id: string): Promise<ConnectorConnectionRunRecord | null> {
147
+ const row = this.getById<SqliteConnectionRunRow>("connector_connection_runs", id)
148
+ return row ? connectionRunFromRow(row) : null
149
+ }
150
+
151
+ async updateConnectionRun(record: ConnectorConnectionRunRecord): Promise<void> {
152
+ const values = runValues(record)
153
+ const result = this.db
154
+ .query(
155
+ `
156
+ UPDATE connector_connection_runs
157
+ SET kind = ?, slot = ?, initiated_by_execution_id = ?, status = ?,
158
+ waiting_for = ?, processing_id = ?,
159
+ callback_started_at = ?, expires_at = ?, authorization_id = ?,
160
+ cleanup_authorization_id = ?, connections = ?, error = ?, updated_at = ?, finished_at = ?
161
+ WHERE project_id = ? AND connector_id = ? AND id = ?
162
+ `
163
+ )
164
+ .run(...values, record.projectId, record.connectorId, record.id)
165
+ assertChanged(result.changes, "connector connection run", record.id)
166
+ }
167
+
168
+ async insertAuthorization(record: ConnectorAuthorizationRecord): Promise<boolean> {
169
+ try {
170
+ this.insertAuthorizationRow(record)
171
+ return true
172
+ } catch (error) {
173
+ if (isUniqueConstraintError(error)) return false
174
+ throw error
175
+ }
176
+ }
177
+
178
+ async getAuthorization(id: string): Promise<ConnectorAuthorizationRecord | null> {
179
+ const row = this.getById<SqliteAuthorizationRow>("connector_authorizations", id)
180
+ return row ? authorizationFromRow(row) : null
181
+ }
182
+
183
+ async updateAuthorization(record: ConnectorAuthorizationRecord): Promise<void> {
184
+ const result = this.db
185
+ .query(
186
+ `
187
+ UPDATE connector_authorizations
188
+ SET authorized_by = ?, credentials = ?, credential_expires_at = ?, scopes = ?, accounts = ?,
189
+ status = ?, selection_expires_at = ?, revision = ?, mutation_id = ?, mutation_kind = ?,
190
+ mutation_phase = ?, mutation_holder_id = ?, mutation_expires_at = ?,
191
+ mutation_deadline_at = ?, mutation_expected_connection_ids = ?, staged_credentials = ?,
192
+ staged_credential_expires_at = ?, staged_scopes = ?, updated_at = ?
193
+ WHERE project_id = ? AND connector_id = ? AND id = ?
194
+ `
195
+ )
196
+ .run(...authorizationValues(record), record.projectId, record.connectorId, record.id)
197
+ assertChanged(result.changes, "connector authorization", record.id)
198
+ }
199
+
200
+ async insertConnection(record: ConnectorConnectionRecord): Promise<boolean> {
201
+ try {
202
+ this.db
203
+ .query(
204
+ `
205
+ INSERT INTO connector_connections (
206
+ project_id, connector_id, id, authorization_id, slot, account,
207
+ account_id, status, disconnected_at, created_at, updated_at
208
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
209
+ `
210
+ )
211
+ .run(
212
+ record.projectId,
213
+ record.connectorId,
214
+ record.id,
215
+ record.authorizationId,
216
+ record.slot,
217
+ json(record.account),
218
+ record.account.id,
219
+ record.status,
220
+ optionalIso(record.disconnectedAt),
221
+ iso(record.createdAt),
222
+ iso(record.updatedAt)
223
+ )
224
+ return true
225
+ } catch (error) {
226
+ if (isUniqueConstraintError(error)) return false
227
+ throw error
228
+ }
229
+ }
230
+
231
+ async getConnectionById(id: string): Promise<ConnectorConnectionRecord | null> {
232
+ const row = this.getById<SqliteConnectionRow>("connector_connections", id)
233
+ return row ? connectionFromRow(row) : null
234
+ }
235
+
236
+ async getConnectionBySelector(input: {
237
+ readonly owner: { readonly type: "project" }
238
+ readonly slot: string
239
+ }): Promise<ConnectorConnectionRecord | null> {
240
+ if (!this.scope.connectorId) return null
241
+ const row = this.db
242
+ .query(
243
+ `
244
+ SELECT * FROM connector_connections
245
+ WHERE project_id = ? AND connector_id = ? AND slot = ?
246
+ `
247
+ )
248
+ .get(this.scope.projectId, this.scope.connectorId, input.slot) as SqliteConnectionRow | null
249
+ return row ? connectionFromRow(row) : null
250
+ }
251
+
252
+ async listConnections(): Promise<readonly ConnectorConnectionRecord[]> {
253
+ if (!this.scope.connectorId) return []
254
+ const rows = this.db
255
+ .query(
256
+ `
257
+ SELECT * FROM connector_connections
258
+ WHERE project_id = ? AND connector_id = ?
259
+ ORDER BY id
260
+ `
261
+ )
262
+ .all(this.scope.projectId, this.scope.connectorId) as SqliteConnectionRow[]
263
+ return rows.map(connectionFromRow)
264
+ }
265
+
266
+ async listConnectionsByAuthorization(
267
+ authorizationId: string,
268
+ options: { readonly connectedOnly?: boolean } = {}
269
+ ): Promise<readonly ConnectorConnectionRecord[]> {
270
+ if (!this.scope.connectorId) return []
271
+ const rows = options.connectedOnly
272
+ ? (this.db
273
+ .query(
274
+ `
275
+ SELECT * FROM connector_connections
276
+ WHERE project_id = ? AND connector_id = ? AND authorization_id = ?
277
+ AND status = 'connected'
278
+ ORDER BY id
279
+ `
280
+ )
281
+ .all(
282
+ this.scope.projectId,
283
+ this.scope.connectorId,
284
+ authorizationId
285
+ ) as SqliteConnectionRow[])
286
+ : (this.db
287
+ .query(
288
+ `
289
+ SELECT * FROM connector_connections
290
+ WHERE project_id = ? AND connector_id = ? AND authorization_id = ?
291
+ ORDER BY id
292
+ `
293
+ )
294
+ .all(
295
+ this.scope.projectId,
296
+ this.scope.connectorId,
297
+ authorizationId
298
+ ) as SqliteConnectionRow[])
299
+ return rows.map(connectionFromRow)
300
+ }
301
+
302
+ async updateConnection(record: ConnectorConnectionRecord): Promise<void> {
303
+ const result = this.db
304
+ .query(
305
+ `
306
+ UPDATE connector_connections
307
+ SET authorization_id = ?, slot = ?, account = ?, account_id = ?,
308
+ status = ?, disconnected_at = ?, updated_at = ?
309
+ WHERE project_id = ? AND connector_id = ? AND id = ?
310
+ `
311
+ )
312
+ .run(
313
+ record.authorizationId,
314
+ record.slot,
315
+ json(record.account),
316
+ record.account.id,
317
+ record.status,
318
+ optionalIso(record.disconnectedAt),
319
+ iso(record.updatedAt),
320
+ record.projectId,
321
+ record.connectorId,
322
+ record.id
323
+ )
324
+ assertChanged(result.changes, "connector connection", record.id)
325
+ }
326
+
327
+ private insertRun(record: ConnectorConnectionRunRecord): void {
328
+ const values = runValues(record)
329
+ this.db
330
+ .query(
331
+ `
332
+ INSERT INTO connector_connection_runs (
333
+ project_id, connector_id, id, kind, slot, initiated_by_execution_id,
334
+ status, waiting_for, processing_id, callback_started_at,
335
+ expires_at, authorization_id, cleanup_authorization_id, connections, error,
336
+ created_at, updated_at, finished_at
337
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
338
+ `
339
+ )
340
+ .run(
341
+ record.projectId,
342
+ record.connectorId,
343
+ record.id,
344
+ ...values.slice(0, -2),
345
+ iso(record.createdAt),
346
+ ...values.slice(-2)
347
+ )
348
+ }
349
+
350
+ private insertAuthorizationRow(record: ConnectorAuthorizationRecord): void {
351
+ const values = authorizationValues(record)
352
+ this.db
353
+ .query(
354
+ `
355
+ INSERT INTO connector_authorizations (
356
+ project_id, connector_id, id, authorized_by, credentials, credential_expires_at,
357
+ scopes, accounts, status, selection_expires_at, revision, mutation_id, mutation_kind,
358
+ mutation_phase, mutation_holder_id, mutation_expires_at, mutation_deadline_at,
359
+ mutation_expected_connection_ids, staged_credentials, staged_credential_expires_at,
360
+ staged_scopes, created_at, updated_at
361
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
362
+ `
363
+ )
364
+ .run(
365
+ record.projectId,
366
+ record.connectorId,
367
+ record.id,
368
+ ...values.slice(0, -1),
369
+ iso(record.createdAt),
370
+ ...values.slice(-1)
371
+ )
372
+ }
373
+
374
+ private getById<TRow>(table: ConnectorTable, id: string): TRow | null {
375
+ if (this.scope.connectorId) {
376
+ return this.db
377
+ .query(`SELECT * FROM ${table} WHERE project_id = ? AND connector_id = ? AND id = ?`)
378
+ .get(this.scope.projectId, this.scope.connectorId, id) as TRow | null
379
+ }
380
+ return this.db
381
+ .query(`SELECT * FROM ${table} WHERE project_id = ? AND id = ?`)
382
+ .get(this.scope.projectId, id) as TRow | null
383
+ }
384
+
385
+ private deleteById(table: ConnectorTable, id: string): boolean {
386
+ const result = this.scope.connectorId
387
+ ? this.db
388
+ .query(`DELETE FROM ${table} WHERE project_id = ? AND connector_id = ? AND id = ?`)
389
+ .run(this.scope.projectId, this.scope.connectorId, id)
390
+ : this.db
391
+ .query(`DELETE FROM ${table} WHERE project_id = ? AND id = ?`)
392
+ .run(this.scope.projectId, id)
393
+ return result.changes === 1
394
+ }
395
+ }
396
+
397
+ type ConnectorTable =
398
+ | "connector_authorization_attempts"
399
+ | "connector_connection_runs"
400
+ | "connector_authorizations"
401
+ | "connector_connections"
402
+
403
+ function authorizationAttemptFromRow(
404
+ row: SqliteAuthorizationAttemptRow
405
+ ): ConnectorAuthorizationAttemptRecord {
406
+ return {
407
+ id: row.id,
408
+ projectId: row.project_id,
409
+ connectorId: row.connector_id,
410
+ owner: { type: "project" },
411
+ slot: row.slot,
412
+ initiatedByExecutionId: row.initiated_by_execution_id,
413
+ stateHash: row.state_hash,
414
+ codeVerifier: parseJson(row.code_verifier),
415
+ redirectUri: row.redirect_uri,
416
+ ...(row.connection_run_id === null ? {} : { connectionRunId: row.connection_run_id }),
417
+ ...(row.return_to === null ? {} : { returnTo: row.return_to }),
418
+ ...(row.callback_binding_hash === null
419
+ ? {}
420
+ : { callbackBindingHash: row.callback_binding_hash }),
421
+ ...(row.reauthorization_id === null ? {} : { reauthorizationId: row.reauthorization_id }),
422
+ ...(row.reauthorization_revision === null
423
+ ? {}
424
+ : { reauthorizationRevision: row.reauthorization_revision }),
425
+ ...(row.reauthorization_connection_ids === null
426
+ ? {}
427
+ : { reauthorizationConnectionIds: parseJson(row.reauthorization_connection_ids) }),
428
+ createdAt: new Date(row.created_at),
429
+ expiresAt: new Date(row.expires_at),
430
+ }
431
+ }
432
+
433
+ function runValues(record: ConnectorConnectionRunRecord): readonly SQLQueryBindings[] {
434
+ const waitingFor = record.status === "waiting" ? record.waitingFor : null
435
+ const processingId = record.status === "running" ? record.processingId : null
436
+ const callbackStartedAt = record.status === "running" ? iso(record.callbackStartedAt) : null
437
+ const expiresAt =
438
+ record.status === "waiting" || record.status === "running" ? iso(record.expiresAt) : null
439
+ const authorizationId = "authorizationId" in record ? (record.authorizationId ?? null) : null
440
+ const cleanupAuthorizationId =
441
+ record.status === "succeeded" ? (record.cleanupAuthorizationId ?? null) : null
442
+ const connections = record.status === "succeeded" ? json(record.connections) : null
443
+ const error = record.status === "failed" ? json(record.error) : null
444
+ const finishedAt =
445
+ record.status === "succeeded" ||
446
+ record.status === "failed" ||
447
+ record.status === "cancelled" ||
448
+ record.status === "expired"
449
+ ? iso(record.finishedAt)
450
+ : null
451
+ return [
452
+ record.kind,
453
+ record.slot,
454
+ record.initiatedByExecutionId,
455
+ record.status,
456
+ waitingFor,
457
+ processingId,
458
+ callbackStartedAt,
459
+ expiresAt,
460
+ authorizationId,
461
+ cleanupAuthorizationId,
462
+ connections,
463
+ error,
464
+ iso(record.updatedAt),
465
+ finishedAt,
466
+ ]
467
+ }
468
+
469
+ function connectionRunFromRow(row: SqliteConnectionRunRow): ConnectorConnectionRunRecord {
470
+ const base = {
471
+ id: row.id,
472
+ projectId: row.project_id,
473
+ connectorId: row.connector_id,
474
+ kind: row.kind,
475
+ owner: { type: "project" as const },
476
+ slot: row.slot,
477
+ initiatedByExecutionId: row.initiated_by_execution_id,
478
+ createdAt: new Date(row.created_at),
479
+ updatedAt: new Date(row.updated_at),
480
+ }
481
+ if (row.status === "waiting" && row.waiting_for === "provider_authorization") {
482
+ return {
483
+ ...base,
484
+ status: "waiting",
485
+ waitingFor: "provider_authorization",
486
+ expiresAt: new Date(required(row.expires_at, "run expiry")),
487
+ }
488
+ }
489
+ if (row.status === "waiting" && row.waiting_for === "account_selection") {
490
+ return {
491
+ ...base,
492
+ status: "waiting",
493
+ waitingFor: "account_selection",
494
+ authorizationId: required(row.authorization_id, "authorization id"),
495
+ expiresAt: new Date(required(row.expires_at, "run expiry")),
496
+ }
497
+ }
498
+ if (row.status === "running") {
499
+ return {
500
+ ...base,
501
+ status: "running",
502
+ processingId: required(row.processing_id, "processing id"),
503
+ callbackStartedAt: new Date(required(row.callback_started_at, "callback start")),
504
+ expiresAt: new Date(required(row.expires_at, "run expiry")),
505
+ ...(row.authorization_id === null ? {} : { authorizationId: row.authorization_id }),
506
+ }
507
+ }
508
+ const finishedAt = new Date(required(row.finished_at, "run finish"))
509
+ if (row.status === "succeeded") {
510
+ return {
511
+ ...base,
512
+ status: "succeeded",
513
+ authorizationId: required(row.authorization_id, "authorization id"),
514
+ ...(row.cleanup_authorization_id === null
515
+ ? {}
516
+ : { cleanupAuthorizationId: row.cleanup_authorization_id }),
517
+ connections: parseJson<readonly SerializedConnectorConnection[]>(
518
+ required(row.connections, "connections")
519
+ ).map(connectionFromSerialized),
520
+ finishedAt,
521
+ }
522
+ }
523
+ if (row.status === "failed") {
524
+ return {
525
+ ...base,
526
+ status: "failed",
527
+ error: parseJson(required(row.error, "run error")),
528
+ ...(row.authorization_id === null ? {} : { authorizationId: row.authorization_id }),
529
+ finishedAt,
530
+ }
531
+ }
532
+ if (row.status === "cancelled") {
533
+ return {
534
+ ...base,
535
+ status: "cancelled",
536
+ ...(row.authorization_id === null ? {} : { authorizationId: row.authorization_id }),
537
+ finishedAt,
538
+ }
539
+ }
540
+ if (row.status === "expired") {
541
+ return {
542
+ ...base,
543
+ status: "expired",
544
+ ...(row.authorization_id === null ? {} : { authorizationId: row.authorization_id }),
545
+ finishedAt,
546
+ }
547
+ }
548
+ throw invalidStoredValue("connector connection run status", row.status)
549
+ }
550
+
551
+ function authorizationValues(record: ConnectorAuthorizationRecord): readonly SQLQueryBindings[] {
552
+ const mutation = record.credentialMutation
553
+ const staged = mutation?.stagedCredentials
554
+ return [
555
+ json(record.authorizedBy),
556
+ record.credentials ? json(record.credentials) : null,
557
+ optionalIso(record.credentialExpiresAt),
558
+ json(record.scopes),
559
+ json(record.accounts),
560
+ record.status,
561
+ optionalIso(record.selectionExpiresAt),
562
+ record.revision,
563
+ mutation?.id ?? null,
564
+ mutation?.kind ?? null,
565
+ mutation?.phase ?? null,
566
+ mutation?.holderId ?? null,
567
+ optionalIso(mutation?.expiresAt),
568
+ optionalIso(mutation?.deadlineAt),
569
+ mutation?.expectedConnectionIds ? json(mutation.expectedConnectionIds) : null,
570
+ staged ? json(staged.credentials) : null,
571
+ optionalIso(staged?.credentialExpiresAt),
572
+ staged ? json(staged.scopes) : null,
573
+ iso(record.updatedAt),
574
+ ]
575
+ }
576
+
577
+ function authorizationFromRow(row: SqliteAuthorizationRow): ConnectorAuthorizationRecord {
578
+ const mutation = mutationFromRow(row)
579
+ return {
580
+ id: row.id,
581
+ projectId: row.project_id,
582
+ connectorId: row.connector_id,
583
+ authorizedBy: parseJson(row.authorized_by),
584
+ ...(row.credentials === null ? {} : { credentials: parseJson(row.credentials) }),
585
+ ...(row.credential_expires_at === null
586
+ ? {}
587
+ : { credentialExpiresAt: new Date(row.credential_expires_at) }),
588
+ scopes: parseJson(row.scopes),
589
+ accounts: parseJson(row.accounts),
590
+ status: row.status,
591
+ ...(row.selection_expires_at === null
592
+ ? {}
593
+ : { selectionExpiresAt: new Date(row.selection_expires_at) }),
594
+ revision: row.revision,
595
+ ...(mutation === undefined ? {} : { credentialMutation: mutation }),
596
+ createdAt: new Date(row.created_at),
597
+ updatedAt: new Date(row.updated_at),
598
+ }
599
+ }
600
+
601
+ function mutationFromRow(row: SqliteAuthorizationRow): ConnectorCredentialMutation | undefined {
602
+ if (row.mutation_id === null) return undefined
603
+ const kind = required(row.mutation_kind, "credential mutation kind")
604
+ const phase = required(row.mutation_phase, "credential mutation phase")
605
+ const stagedCredentials: ConnectorCredentialMutation["stagedCredentials"] =
606
+ row.staged_credentials === null
607
+ ? undefined
608
+ : {
609
+ credentials: parseJson<
610
+ NonNullable<ConnectorCredentialMutation["stagedCredentials"]>["credentials"]
611
+ >(row.staged_credentials),
612
+ ...(row.staged_credential_expires_at === null
613
+ ? {}
614
+ : { credentialExpiresAt: new Date(row.staged_credential_expires_at) }),
615
+ scopes: parseJson<readonly string[]>(
616
+ required(row.staged_scopes, "staged credential scopes")
617
+ ),
618
+ }
619
+ return {
620
+ id: row.mutation_id,
621
+ kind,
622
+ phase,
623
+ holderId: required(row.mutation_holder_id, "credential mutation holder"),
624
+ expiresAt: new Date(required(row.mutation_expires_at, "credential mutation expiry")),
625
+ deadlineAt: new Date(required(row.mutation_deadline_at, "credential mutation deadline")),
626
+ ...(row.mutation_expected_connection_ids === null
627
+ ? {}
628
+ : {
629
+ expectedConnectionIds: parseJson<readonly string[]>(row.mutation_expected_connection_ids),
630
+ }),
631
+ ...(stagedCredentials === undefined ? {} : { stagedCredentials }),
632
+ }
633
+ }
634
+
635
+ function connectionFromRow(row: SqliteConnectionRow): ConnectorConnectionRecord {
636
+ return {
637
+ id: row.id,
638
+ projectId: row.project_id,
639
+ connectorId: row.connector_id,
640
+ authorizationId: row.authorization_id,
641
+ owner: { type: "project" },
642
+ slot: row.slot,
643
+ account: parseJson(row.account),
644
+ status: row.status,
645
+ ...(row.disconnected_at === null ? {} : { disconnectedAt: new Date(row.disconnected_at) }),
646
+ createdAt: new Date(row.created_at),
647
+ updatedAt: new Date(row.updated_at),
648
+ }
649
+ }
650
+
651
+ type SerializedConnectorConnection = Omit<
652
+ ConnectorConnectionRecord,
653
+ "createdAt" | "updatedAt" | "disconnectedAt"
654
+ > & {
655
+ readonly createdAt: string
656
+ readonly updatedAt: string
657
+ readonly disconnectedAt?: string
658
+ }
659
+
660
+ function connectionFromSerialized(
661
+ record: SerializedConnectorConnection
662
+ ): ConnectorConnectionRecord {
663
+ const { createdAt, updatedAt, disconnectedAt, ...connection } = record
664
+ return {
665
+ ...connection,
666
+ createdAt: new Date(createdAt),
667
+ updatedAt: new Date(updatedAt),
668
+ ...(disconnectedAt === undefined ? {} : { disconnectedAt: new Date(disconnectedAt) }),
669
+ }
670
+ }
671
+
672
+ function json(value: unknown): string {
673
+ return JSON.stringify(value)
674
+ }
675
+
676
+ function parseJson<T>(value: string): T {
677
+ return JSON.parse(value) as T
678
+ }
679
+
680
+ function iso(value: Date): string {
681
+ return value.toISOString()
682
+ }
683
+
684
+ function optionalIso(value: Date | undefined): string | null {
685
+ return value?.toISOString() ?? null
686
+ }
687
+
688
+ function required<T>(value: T | null, label: string): T {
689
+ if (value !== null) return value
690
+ throw new Error(`[SixbSqlite] Stored ${label} is missing.`)
691
+ }
692
+
693
+ function invalidStoredValue(label: string, value: unknown): Error {
694
+ return new Error(`[SixbSqlite] Stored ${label} '${String(value)}' is invalid.`)
695
+ }
696
+
697
+ function assertChanged(changes: number, label: string, id: string): void {
698
+ if (changes === 1) return
699
+ throw new Error(`[SixbSqlite] Stored ${label} '${id}' is unavailable.`)
700
+ }
701
+
702
+ interface SqliteAuthorizationAttemptRow {
703
+ readonly project_id: string
704
+ readonly connector_id: string
705
+ readonly id: string
706
+ readonly slot: string
707
+ readonly initiated_by_execution_id: string
708
+ readonly state_hash: string
709
+ readonly code_verifier: string
710
+ readonly redirect_uri: string
711
+ readonly connection_run_id: string | null
712
+ readonly return_to: string | null
713
+ readonly callback_binding_hash: string | null
714
+ readonly reauthorization_id: string | null
715
+ readonly reauthorization_revision: number | null
716
+ readonly reauthorization_connection_ids: string | null
717
+ readonly created_at: string
718
+ readonly expires_at: string
719
+ }
720
+
721
+ interface SqliteConnectionRunRow {
722
+ readonly project_id: string
723
+ readonly connector_id: string
724
+ readonly id: string
725
+ readonly kind: ConnectorConnectionRunRecord["kind"]
726
+ readonly slot: string
727
+ readonly initiated_by_execution_id: string
728
+ readonly status: ConnectorConnectionRunRecord["status"]
729
+ readonly waiting_for: "provider_authorization" | "account_selection" | null
730
+ readonly processing_id: string | null
731
+ readonly callback_started_at: string | null
732
+ readonly expires_at: string | null
733
+ readonly authorization_id: string | null
734
+ readonly cleanup_authorization_id: string | null
735
+ readonly connections: string | null
736
+ readonly error: string | null
737
+ readonly created_at: string
738
+ readonly updated_at: string
739
+ readonly finished_at: string | null
740
+ }
741
+
742
+ interface SqliteAuthorizationRow {
743
+ readonly project_id: string
744
+ readonly connector_id: string
745
+ readonly id: string
746
+ readonly authorized_by: string
747
+ readonly credentials: string | null
748
+ readonly credential_expires_at: string | null
749
+ readonly scopes: string
750
+ readonly accounts: string
751
+ readonly status: ConnectorAuthorizationRecord["status"]
752
+ readonly selection_expires_at: string | null
753
+ readonly revision: number
754
+ readonly mutation_id: string | null
755
+ readonly mutation_kind: ConnectorCredentialMutation["kind"] | null
756
+ readonly mutation_phase: ConnectorCredentialMutation["phase"] | null
757
+ readonly mutation_holder_id: string | null
758
+ readonly mutation_expires_at: string | null
759
+ readonly mutation_deadline_at: string | null
760
+ readonly mutation_expected_connection_ids: string | null
761
+ readonly staged_credentials: string | null
762
+ readonly staged_credential_expires_at: string | null
763
+ readonly staged_scopes: string | null
764
+ readonly created_at: string
765
+ readonly updated_at: string
766
+ }
767
+
768
+ interface SqliteConnectionRow {
769
+ readonly project_id: string
770
+ readonly connector_id: string
771
+ readonly id: string
772
+ readonly authorization_id: string
773
+ readonly slot: string
774
+ readonly account: string
775
+ readonly account_id: string
776
+ readonly status: ConnectorConnectionRecord["status"]
777
+ readonly disconnected_at: string | null
778
+ readonly created_at: string
779
+ readonly updated_at: string
780
+ }
781
+
782
+ interface ConnectorConnectionTestClock {
783
+ offsetMs: number
784
+ }