@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- 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
|
+
}
|