@sixb/pg 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,196 @@
1
+ import type {
2
+ AuthUserStore,
3
+ CreateAuthUserInput,
4
+ ListAuthUsersInput,
5
+ ListAuthUsersResult,
6
+ UpdateAuthUserProfileInput,
7
+ UpdateAuthUserStatusInput,
8
+ UserRecord,
9
+ } from "@sixb/core/storage"
10
+ import { AuthStorageError } from "@sixb/core/storage"
11
+ import type { PgStoreClient } from "../transactions"
12
+ import type { PgAuthUserRow } from "./rows"
13
+ import { rowToUserRecord } from "./rows"
14
+ import {
15
+ appendPagination,
16
+ assertNonEmpty,
17
+ dateOrNow,
18
+ getUserRowByEmail,
19
+ getUserRowById,
20
+ hasEmptyFilter,
21
+ mapUniqueConstraintError,
22
+ normalizeEmail,
23
+ type PgValue,
24
+ } from "./shared"
25
+
26
+ export class PgAuthUserStore implements AuthUserStore {
27
+ constructor(private readonly sql: PgStoreClient) {}
28
+
29
+ async create(input: CreateAuthUserInput): Promise<UserRecord> {
30
+ const id = assertNonEmpty(input.id, "User id")
31
+ const projectId = assertNonEmpty(input.projectId, "Project id")
32
+ const email = normalizeEmail(input.email)
33
+
34
+ if (await getUserRowById(this.sql, { projectId, id })) {
35
+ throw new AuthStorageError(
36
+ "duplicate_user",
37
+ `[Sixb] User '${id}' already exists for project '${projectId}'.`
38
+ )
39
+ }
40
+
41
+ if (await getUserRowByEmail(this.sql, { projectId, email })) {
42
+ throw new AuthStorageError(
43
+ "duplicate_user",
44
+ `[Sixb] User email '${email}' already exists for project '${projectId}'.`
45
+ )
46
+ }
47
+
48
+ const createdAt = dateOrNow(input.createdAt)
49
+ const updatedAt = input.updatedAt ? new Date(input.updatedAt) : createdAt
50
+
51
+ try {
52
+ const [row] = await this.sql<PgAuthUserRow[]>`
53
+ INSERT INTO auth_users (
54
+ project_id,
55
+ id,
56
+ email,
57
+ display_name,
58
+ avatar_url,
59
+ status,
60
+ created_at,
61
+ updated_at
62
+ ) VALUES (
63
+ ${projectId},
64
+ ${id},
65
+ ${email},
66
+ ${input.displayName ?? null},
67
+ ${input.avatarUrl ?? null},
68
+ ${input.status ?? "active"},
69
+ ${createdAt},
70
+ ${updatedAt}
71
+ )
72
+ RETURNING *
73
+ `
74
+
75
+ return rowToUserRecord(row)
76
+ } catch (error) {
77
+ mapUniqueConstraintError(
78
+ error,
79
+ "duplicate_user",
80
+ `[Sixb] User '${id}' already exists for project '${projectId}'.`
81
+ )
82
+ }
83
+ }
84
+
85
+ async getById(params: {
86
+ readonly projectId: string
87
+ readonly id: string
88
+ }): Promise<UserRecord | null> {
89
+ const row = await getUserRowById(this.sql, params)
90
+ return row ? rowToUserRecord(row) : null
91
+ }
92
+
93
+ async getByEmail(params: {
94
+ readonly projectId: string
95
+ readonly email: string
96
+ }): Promise<UserRecord | null> {
97
+ const row = await getUserRowByEmail(this.sql, params)
98
+ return row ? rowToUserRecord(row) : null
99
+ }
100
+
101
+ async updateProfile(input: UpdateAuthUserProfileInput): Promise<UserRecord> {
102
+ const existing = await getUserRowById(this.sql, {
103
+ projectId: input.projectId,
104
+ id: input.id,
105
+ })
106
+
107
+ if (!existing) {
108
+ throw new AuthStorageError(
109
+ "missing_user",
110
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
111
+ )
112
+ }
113
+
114
+ const updatedAt = dateOrNow(input.updatedAt)
115
+ const [row] = await this.sql<PgAuthUserRow[]>`
116
+ UPDATE auth_users
117
+ SET display_name = ${input.displayName ?? null},
118
+ avatar_url = ${input.avatarUrl ?? null},
119
+ updated_at = ${updatedAt}
120
+ WHERE project_id = ${input.projectId}
121
+ AND id = ${input.id}
122
+ RETURNING *
123
+ `
124
+
125
+ return rowToUserRecord(row)
126
+ }
127
+
128
+ async updateStatus(input: UpdateAuthUserStatusInput): Promise<UserRecord> {
129
+ const existing = await getUserRowById(this.sql, {
130
+ projectId: input.projectId,
131
+ id: input.id,
132
+ })
133
+
134
+ if (!existing) {
135
+ throw new AuthStorageError(
136
+ "missing_user",
137
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
138
+ )
139
+ }
140
+
141
+ const updatedAt = dateOrNow(input.updatedAt)
142
+ const [row] = await this.sql<PgAuthUserRow[]>`
143
+ UPDATE auth_users
144
+ SET status = ${input.status},
145
+ updated_at = ${updatedAt}
146
+ WHERE project_id = ${input.projectId}
147
+ AND id = ${input.id}
148
+ RETURNING *
149
+ `
150
+
151
+ return rowToUserRecord(row)
152
+ }
153
+
154
+ async list(input: ListAuthUsersInput): Promise<ListAuthUsersResult> {
155
+ if (hasEmptyFilter(input.statuses)) {
156
+ return { users: [], hasMore: false, total: 0 }
157
+ }
158
+
159
+ const whereClauses = ["project_id = $1"]
160
+ const params: PgValue[] = [input.projectId]
161
+ let nextIndex = 2
162
+
163
+ if (input.statuses) {
164
+ whereClauses.push(`status IN (${input.statuses.map(() => `$${nextIndex++}`).join(", ")})`)
165
+ params.push(...input.statuses)
166
+ }
167
+
168
+ const where = `WHERE ${whereClauses.join(" AND ")}`
169
+ const order = input.order === "asc" ? "ASC" : "DESC"
170
+ const [totalRow] = await this.sql.unsafe<{ readonly count: string | number }[]>(
171
+ `SELECT COUNT(*)::bigint AS count FROM auth_users ${where}`,
172
+ [...params]
173
+ )
174
+
175
+ const queryParams = [...params]
176
+ const query = appendPagination(
177
+ `
178
+ SELECT *
179
+ FROM auth_users
180
+ ${where}
181
+ ORDER BY created_at ${order}, id ${order}
182
+ `,
183
+ queryParams,
184
+ nextIndex,
185
+ input
186
+ )
187
+ const rows = await this.sql.unsafe<PgAuthUserRow[]>(query, queryParams)
188
+ const total = Number(totalRow?.count ?? 0)
189
+
190
+ return {
191
+ users: rows.map(rowToUserRecord),
192
+ hasMore: (input.offset ?? 0) + rows.length < total,
193
+ total,
194
+ }
195
+ }
196
+ }
package/src/index.ts ADDED
@@ -0,0 +1,367 @@
1
+ // Keep SQL module declarations visible to downstream workspaces that typecheck this package from
2
+ // source.
3
+ /// <reference path="./sql.d.ts" />
4
+
5
+ import { AsyncLocalStorage } from "node:async_hooks"
6
+ import type {
7
+ MigrationCapableStorage,
8
+ Storage,
9
+ StorageMigrator,
10
+ StorageTransactionOptions,
11
+ } from "@sixb/core"
12
+ import { ProviderMaterializationTransactionLifecycle } from "@sixb/core/internal/ontology-storage-provider"
13
+ import {
14
+ createAgentOperationScope,
15
+ createAuthOperationScope,
16
+ createOntologyOperationScope,
17
+ createOperationScopedFacade,
18
+ createStorageOperationScope,
19
+ createWorkflowRunOperationScope,
20
+ } from "@sixb/core/internal/storage-operation-scope"
21
+ import {
22
+ createTransactionStorageProxy,
23
+ type ObjectStorage,
24
+ StorageTransactionError,
25
+ throwNestedStorageTransaction,
26
+ } from "@sixb/core/storage"
27
+ import { PgAgentStorage } from "./agents"
28
+ import { PgAuthStorage } from "./auth-storage"
29
+ import { createPostgresStorageMigrators, dropSchema } from "./migrations"
30
+ import { PgOntologyStorage, type PgOntologyTransactionContext } from "./ontology-storage"
31
+ import { PgActionRunStorage } from "./pg-action-run-storage"
32
+ import { createPgClient, type SQL, type SQLClient } from "./pg-client"
33
+ import { PgObjectStorage } from "./pg-object-storage"
34
+ import { PgPipelineRunStorage } from "./pg-pipeline-run-storage"
35
+ import { PgProjectionRunStorage } from "./pg-projection-run-storage"
36
+ import { PgRulesStorage } from "./pg-rules-storage"
37
+ import { PgSyncRunStorage } from "./pg-sync-run-storage"
38
+ import { PgTimeseriesStorage } from "./pg-timeseries-storage"
39
+ import { PgWebhookDeliveryStorage } from "./pg-webhook-delivery-storage"
40
+ import { PgWebhookRunStorage } from "./pg-webhook-run-storage"
41
+ import { PgWorkflowInterventionStorage } from "./pg-workflow-intervention-storage"
42
+ import { PgWorkflowRunStorage } from "./pg-workflow-run-storage"
43
+ import { isRetryableTransactionConflict } from "./storage-errors"
44
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
45
+
46
+ export interface PostgresStorageOptions {
47
+ /** Full connection string (e.g. DATABASE_URL). Takes precedence over individual fields. */
48
+ connectionString?: string
49
+
50
+ /** PostgreSQL host. Defaults to 'localhost'. */
51
+ host?: string
52
+ /** PostgreSQL port. Defaults to 5432. */
53
+ port?: number
54
+ /** Database name. */
55
+ database?: string
56
+ /** Database user. */
57
+ user?: string
58
+ /** Database password. */
59
+ password?: string
60
+
61
+ /** Maximum number of connections in the pool. Defaults to 10. */
62
+ max?: number
63
+ /** Idle connection timeout in milliseconds. Defaults to 30000. */
64
+ idleTimeoutMillis?: number
65
+
66
+ /**
67
+ * Server-side `statement_timeout` (ms) applied to every connection in the pool. A query
68
+ * exceeding it is cancelled by PostgreSQL, which frees its pooled connection instead of
69
+ * pinning it indefinitely. Without this, a single stalled query can starve a small pool
70
+ * until the process is restarted. Unset by default (no timeout). Set per role — keep it
71
+ * generous or unset for workers that run long migrations/bulk writes.
72
+ */
73
+ statementTimeoutMillis?: number
74
+ /**
75
+ * Server-side `idle_in_transaction_session_timeout` (ms) applied to every connection. A
76
+ * transaction left open and idle beyond it is aborted by PostgreSQL, releasing the
77
+ * connection. Unset by default (no timeout).
78
+ */
79
+ idleInTransactionSessionTimeoutMillis?: number
80
+
81
+ /**
82
+ * Grace period (ms) for {@link PostgresStorage.close}. On shutdown the pool stops
83
+ * accepting new queries and waits up to this long for in-flight queries to finish before
84
+ * destroying the connections, so a restart drains cleanly instead of severing live work.
85
+ * Defaults to 5000.
86
+ */
87
+ shutdownTimeoutMillis?: number
88
+
89
+ /** Seconds to wait when establishing a connection. Defaults to 10000. */
90
+ connectTimeoutMillis?: number
91
+ /**
92
+ * Whether to use server-side prepared statements (porsager's default, faster against a
93
+ * direct Postgres connection). Behind PgBouncer transaction mode they require PgBouncer
94
+ * >= 1.21 with `max_prepared_statements > 0`; set `false` for an older PgBouncer or when
95
+ * that setting is 0. Defaults to `true`.
96
+ */
97
+ prepare?: boolean
98
+
99
+ /** PostgreSQL schema name for all Sixb tables. Defaults to 'sixb'. */
100
+ schemaName?: string
101
+
102
+ /** SSL configuration. */
103
+ ssl?: boolean | "require" | "prefer"
104
+ }
105
+
106
+ /**
107
+ * PostgreSQL storage provider for Sixb.
108
+ *
109
+ * Bundles Sixb storage adapters backed by a shared PostgreSQL connection pool (porsager
110
+ * `postgres`), which reliably reclaims connections under load.
111
+ *
112
+ * The storage exposes a core `StorageMigrator`. Sixb CLI startup and
113
+ * `sixb db migrate` run it automatically through `migrateStorage(storage)`.
114
+ *
115
+ * Usage:
116
+ * ```ts
117
+ * const pg = new PostgresStorage({
118
+ * connectionString: process.env.DATABASE_URL!,
119
+ * })
120
+ *
121
+ * export const sixb = createSixb({
122
+ * broker: myBroker,
123
+ * storage: pg,
124
+ * })
125
+ * ```
126
+ */
127
+ export class PostgresStorage implements MigrationCapableStorage {
128
+ readonly objects: ObjectStorage
129
+ readonly ontology: PgOntologyStorage
130
+ readonly auth: PgAuthStorage
131
+ readonly agents: PgAgentStorage
132
+ readonly actionRuns: PgActionRunStorage
133
+ readonly pipelineRuns: PgPipelineRunStorage
134
+ readonly workflowRuns: PgWorkflowRunStorage
135
+ readonly workflowInterventions: PgWorkflowInterventionStorage
136
+ readonly syncRuns: PgSyncRunStorage
137
+ readonly projectionRuns: PgProjectionRunStorage
138
+ readonly timeseries: Storage["timeseries"]
139
+ readonly webhookDeliveries: PgWebhookDeliveryStorage
140
+ readonly webhookRuns: PgWebhookRunStorage
141
+ readonly rules: PgRulesStorage
142
+ readonly migrators: readonly StorageMigrator[]
143
+
144
+ private readonly sql: SQL
145
+ private readonly schemaName: string
146
+ private readonly shutdownTimeoutSeconds: number
147
+ private readonly transactionScope = new AsyncLocalStorage<boolean>()
148
+
149
+ constructor(options: PostgresStorageOptions) {
150
+ this.schemaName = options.schemaName ?? "sixb"
151
+ this.shutdownTimeoutSeconds = Math.max(
152
+ 1,
153
+ Math.round((options.shutdownTimeoutMillis ?? 5000) / 1000)
154
+ )
155
+
156
+ this.sql = createPgClient({
157
+ ...(options.connectionString !== undefined
158
+ ? { connectionString: options.connectionString }
159
+ : {}),
160
+ ...(options.host !== undefined ? { host: options.host } : {}),
161
+ ...(options.port !== undefined ? { port: options.port } : {}),
162
+ ...(options.database !== undefined ? { database: options.database } : {}),
163
+ ...(options.user !== undefined ? { user: options.user } : {}),
164
+ ...(options.password !== undefined ? { password: options.password } : {}),
165
+ max: options.max ?? 10,
166
+ schemaName: this.schemaName,
167
+ ...(options.idleTimeoutMillis !== undefined
168
+ ? { idleTimeoutMillis: options.idleTimeoutMillis }
169
+ : {}),
170
+ ...(resolveTimeoutMillis(options.statementTimeoutMillis, "statementTimeoutMillis") !==
171
+ undefined
172
+ ? { statementTimeoutMillis: options.statementTimeoutMillis }
173
+ : {}),
174
+ ...(resolveTimeoutMillis(
175
+ options.idleInTransactionSessionTimeoutMillis,
176
+ "idleInTransactionSessionTimeoutMillis"
177
+ ) !== undefined
178
+ ? { idleInTransactionSessionTimeoutMillis: options.idleInTransactionSessionTimeoutMillis }
179
+ : {}),
180
+ ...(resolveTimeoutMillis(options.connectTimeoutMillis, "connectTimeoutMillis") !== undefined
181
+ ? { connectTimeoutMillis: options.connectTimeoutMillis }
182
+ : {}),
183
+ ...(options.prepare !== undefined ? { prepare: options.prepare } : {}),
184
+ ...(options.ssl !== undefined ? { ssl: options.ssl } : {}),
185
+ })
186
+
187
+ this.migrators = createPostgresStorageMigrators(this.sql, this.schemaName)
188
+ const stores = createPostgresStores(this.sql, {
189
+ runOntologyOperation: (run) => runPgTransaction(this.sql, run),
190
+ transactionContext: null,
191
+ })
192
+ const scope = createStorageOperationScope(
193
+ async (run) => {
194
+ this.assertRootOperationAvailable()
195
+ return run()
196
+ },
197
+ () => this.assertRootOperationAvailable()
198
+ )
199
+ this.objects = createOperationScopedFacade(stores.objects, scope)
200
+ this.ontology = createOntologyOperationScope(stores.ontology, scope)
201
+ this.auth = createAuthOperationScope(stores.auth, scope)
202
+ this.agents = createAgentOperationScope(stores.agents, scope)
203
+ this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope)
204
+ this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope)
205
+ this.workflowRuns = createWorkflowRunOperationScope(stores.workflowRuns, scope)
206
+ this.workflowInterventions = createOperationScopedFacade(stores.workflowInterventions, scope)
207
+ this.syncRuns = createOperationScopedFacade(stores.syncRuns, scope)
208
+ this.projectionRuns = createOperationScopedFacade(stores.projectionRuns, scope)
209
+ this.timeseries = createOperationScopedFacade(stores.timeseries, scope)
210
+ this.webhookDeliveries = createOperationScopedFacade(stores.webhookDeliveries, scope)
211
+ this.webhookRuns = createOperationScopedFacade(stores.webhookRuns, scope)
212
+ this.rules = createOperationScopedFacade(stores.rules, scope)
213
+ }
214
+
215
+ async ping(): Promise<void> {
216
+ this.assertRootOperationAvailable()
217
+ await this.sql`SELECT 1`
218
+ }
219
+
220
+ async transaction<T>(
221
+ run: (tx: Storage) => Promise<T> | T,
222
+ options: StorageTransactionOptions = {}
223
+ ): Promise<T> {
224
+ if (this.transactionScope.getStore()) {
225
+ throwNestedStorageTransaction()
226
+ }
227
+
228
+ try {
229
+ return await runPgTransaction(
230
+ this.sql,
231
+ async (client) => {
232
+ let active = true
233
+ const transactionContext: PgOntologyTransactionContext = {
234
+ id: {},
235
+ materializations: new ProviderMaterializationTransactionLifecycle(),
236
+ active: true,
237
+ }
238
+ const txStorage = this.createTransactionStorage(client, transactionContext)
239
+ const tx = createTransactionStorageProxy(txStorage, () => active)
240
+
241
+ try {
242
+ const result = await this.transactionScope.run(true, () => run(tx))
243
+ transactionContext.materializations.assertCommittable()
244
+ return result
245
+ } finally {
246
+ transactionContext.active = false
247
+ txStorage.ontology.deactivateSessions()
248
+ transactionContext.materializations.deactivate()
249
+ active = false
250
+ }
251
+ },
252
+ { isolation: options.isolation === "serializable" ? "serializable" : undefined }
253
+ )
254
+ } catch (error) {
255
+ if (isRetryableTransactionConflict(error)) {
256
+ throw new StorageTransactionError(
257
+ "[SixbPg] Storage transaction failed due to a serialization conflict or deadlock and may be retried.",
258
+ { cause: error, code: "serialization_failure" }
259
+ )
260
+ }
261
+ throw error
262
+ }
263
+ }
264
+
265
+ private assertRootOperationAvailable(): void {
266
+ if (!this.transactionScope.getStore()) return
267
+ throw new StorageTransactionError(
268
+ "[SixbPg] Root storage cannot be used inside a transaction callback; use the provided tx storage."
269
+ )
270
+ }
271
+
272
+ /**
273
+ * Close the connection pool. Should be called on shutdown.
274
+ *
275
+ * Stops accepting new queries and waits up to `shutdownTimeoutMillis` for in-flight
276
+ * queries to finish before destroying connections.
277
+ */
278
+ async close(): Promise<void> {
279
+ await this.sql.end({ timeout: this.shutdownTimeoutSeconds })
280
+ }
281
+
282
+ /**
283
+ * Drop all Sixb tables and schema. Useful for test cleanup.
284
+ * WARNING: This permanently deletes all data.
285
+ */
286
+ async dropSchema(): Promise<void> {
287
+ await dropSchema(this.sql, this.schemaName)
288
+ }
289
+
290
+ private createTransactionStorage(
291
+ client: PgStoreClient,
292
+ transactionContext: PgOntologyTransactionContext
293
+ ): Storage & { readonly ontology: PgOntologyStorage } {
294
+ return {
295
+ ...createPostgresStores(client, {
296
+ runOntologyOperation: async (run) => run(client),
297
+ transactionContext,
298
+ }),
299
+ ping: async () => {
300
+ await client`SELECT 1`
301
+ },
302
+ transaction: async <T>(): Promise<T> => {
303
+ throwNestedStorageTransaction()
304
+ },
305
+ }
306
+ }
307
+ }
308
+
309
+ function createPostgresStores(
310
+ sql: PgStoreClient,
311
+ options: {
312
+ readonly runOntologyOperation: <T>(run: (sql: SQLClient) => Promise<T>) => Promise<T>
313
+ readonly transactionContext: PgOntologyTransactionContext | null
314
+ }
315
+ ): PostgresStoreSet {
316
+ return {
317
+ objects: new PgObjectStorage(sql),
318
+ ontology: new PgOntologyStorage({
319
+ sql,
320
+ runRootOperation: options.runOntologyOperation,
321
+ transactionContext: options.transactionContext,
322
+ }),
323
+ auth: new PgAuthStorage({ sql }),
324
+ agents: new PgAgentStorage({ sql }),
325
+ actionRuns: new PgActionRunStorage(sql),
326
+ pipelineRuns: new PgPipelineRunStorage(sql),
327
+ workflowRuns: new PgWorkflowRunStorage(sql),
328
+ workflowInterventions: new PgWorkflowInterventionStorage(sql),
329
+ syncRuns: new PgSyncRunStorage(sql),
330
+ projectionRuns: new PgProjectionRunStorage(sql),
331
+ timeseries: new PgTimeseriesStorage(sql),
332
+ webhookDeliveries: new PgWebhookDeliveryStorage(sql),
333
+ webhookRuns: new PgWebhookRunStorage(sql),
334
+ rules: new PgRulesStorage(sql),
335
+ }
336
+ }
337
+
338
+ interface PostgresStoreSet {
339
+ readonly objects: PgObjectStorage
340
+ readonly ontology: PgOntologyStorage
341
+ readonly auth: PgAuthStorage
342
+ readonly agents: PgAgentStorage
343
+ readonly actionRuns: PgActionRunStorage
344
+ readonly pipelineRuns: PgPipelineRunStorage
345
+ readonly workflowRuns: PgWorkflowRunStorage
346
+ readonly workflowInterventions: PgWorkflowInterventionStorage
347
+ readonly syncRuns: PgSyncRunStorage
348
+ readonly projectionRuns: PgProjectionRunStorage
349
+ readonly timeseries: PgTimeseriesStorage
350
+ readonly webhookDeliveries: PgWebhookDeliveryStorage
351
+ readonly webhookRuns: PgWebhookRunStorage
352
+ readonly rules: PgRulesStorage
353
+ }
354
+
355
+ function resolveTimeoutMillis(value: number | undefined, label: string): number | undefined {
356
+ if (value === undefined) {
357
+ return undefined
358
+ }
359
+
360
+ if (!Number.isFinite(value) || value < 0) {
361
+ throw new Error(`[Sixb] PostgresStorage ${label} must be a non-negative finite number.`)
362
+ }
363
+
364
+ return Math.trunc(value)
365
+ }
366
+
367
+ export { migratePostgresStorage } from "./migrations"
@@ -0,0 +1,42 @@
1
+ import type { SQLClient, SqlParameter } from "./pg-client"
2
+
3
+ type PgLatestRunTarget =
4
+ | { readonly tableName: "pipeline_runs"; readonly ownerColumn: "pipeline_id" }
5
+ | { readonly tableName: "projection_runs"; readonly ownerColumn: "projection_id" }
6
+ | { readonly tableName: "sync_runs"; readonly ownerColumn: "sync_id" }
7
+ | { readonly tableName: "workflow_runs"; readonly ownerColumn: "workflow_id" }
8
+
9
+ type PgLatestRunSelectList = "*" | "*, checkpoint IS NOT NULL AS checkpoint_present"
10
+
11
+ export async function queryLatestRunsByOwnerId<TRow>(
12
+ input: {
13
+ readonly sql: SQLClient
14
+ readonly ownerIds: readonly string[]
15
+ readonly projectId: string
16
+ readonly ownerIdFor: (row: TRow) => string
17
+ readonly selectList?: PgLatestRunSelectList
18
+ } & PgLatestRunTarget
19
+ ): Promise<readonly TRow[]> {
20
+ const ownerIds = [...new Set(input.ownerIds)]
21
+ if (ownerIds.length === 0) {
22
+ return []
23
+ }
24
+
25
+ const placeholders = ownerIds.map((_, index) => `$${index + 2}`).join(", ")
26
+ const rows = await input.sql.unsafe<TRow[]>(
27
+ `
28
+ SELECT DISTINCT ON (${input.ownerColumn}) ${input.selectList ?? "*"}
29
+ FROM ${input.tableName}
30
+ WHERE project_id = $1 AND ${input.ownerColumn} IN (${placeholders})
31
+ ORDER BY ${input.ownerColumn}, started_at DESC, id DESC
32
+ `,
33
+ [input.projectId, ...ownerIds] as SqlParameter[]
34
+ )
35
+
36
+ const latestByOwnerId = new Map(rows.map((row) => [input.ownerIdFor(row), row]))
37
+
38
+ return ownerIds.flatMap((ownerId) => {
39
+ const row = latestByOwnerId.get(ownerId)
40
+ return row ? [row] : []
41
+ })
42
+ }