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