@sixb/pg 0.1.19 → 0.1.21
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/dist/agents/threads.d.ts +4 -0
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/magic-links.d.ts.map +1 -1
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +0 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -11
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -2
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +984 -80
- package/dist/index.js.map +20 -18
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-artifact-storage.d.ts +19 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/threads.ts +23 -0
- package/src/auth-storage/auth-storage.ts +1 -22
- package/src/auth-storage/magic-links.ts +0 -4
- package/src/auth-storage/oidc-attempts.ts +0 -4
- package/src/auth-storage/rows.ts +0 -6
- package/src/auth-storage/sessions.ts +18 -30
- package/src/auth-storage/shared.ts +3 -35
- package/src/index.ts +10 -0
- package/src/migrations/062-drop-auth-audience.sql +19 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations.ts +20 -0
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-artifact-storage.ts +325 -0
- package/src/pg-execution-storage.ts +52 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
artifactStorageErrors,
|
|
3
|
+
parseArtifactFrameGrantRow,
|
|
4
|
+
parseArtifactRow,
|
|
5
|
+
parseArtifactVersionRow,
|
|
6
|
+
} from "@sixb/core/internal/storage"
|
|
7
|
+
import type {
|
|
8
|
+
ArtifactFrameGrantRecord,
|
|
9
|
+
ArtifactRecord,
|
|
10
|
+
ArtifactStorage,
|
|
11
|
+
ArtifactVersionInput,
|
|
12
|
+
ArtifactVersionRecord,
|
|
13
|
+
CreateArtifactInput,
|
|
14
|
+
DeleteArtifactInput,
|
|
15
|
+
DeleteArtifactVersionsByOriginInput,
|
|
16
|
+
DeleteExpiredArtifactFrameGrantsInput,
|
|
17
|
+
FindArtifactFrameGrantInput,
|
|
18
|
+
GetArtifactInput,
|
|
19
|
+
GetArtifactVersionInput,
|
|
20
|
+
ListArtifactsInput,
|
|
21
|
+
ListArtifactsResult,
|
|
22
|
+
ListArtifactVersionsInput,
|
|
23
|
+
ListArtifactVersionsResult,
|
|
24
|
+
ListReferencingVersionsInput,
|
|
25
|
+
PublishArtifactVersionInput,
|
|
26
|
+
PublishedArtifactVersion,
|
|
27
|
+
} from "@sixb/core/storage"
|
|
28
|
+
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
29
|
+
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
30
|
+
|
|
31
|
+
interface PgRow {
|
|
32
|
+
readonly [column: string]: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Quoted aliases give the shared row parsers their camelCase keys; an unquoted alias would fold to
|
|
36
|
+
// lower case.
|
|
37
|
+
const ARTIFACT_COLUMNS = `
|
|
38
|
+
project_id AS "projectId", id, kind, title,
|
|
39
|
+
owner_principal_type AS "ownerPrincipalType", owner_principal_id AS "ownerPrincipalId",
|
|
40
|
+
latest_version AS "latestVersion", created_at AS "createdAt", updated_at AS "updatedAt"
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const VERSION_COLUMNS = `
|
|
44
|
+
project_id AS "projectId", artifact_id AS "artifactId", version, title, entry, files, outline,
|
|
45
|
+
refs, author_principal_type AS "authorPrincipalType", author_principal_id AS "authorPrincipalId",
|
|
46
|
+
origin_thread_id AS "originThreadId", origin_run_id AS "originRunId", created_at AS "createdAt"
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
const FRAME_GRANT_COLUMNS = `
|
|
50
|
+
project_id AS "projectId", token_hash AS "tokenHash", artifact_id AS "artifactId", version,
|
|
51
|
+
targets, created_at AS "createdAt", expires_at AS "expiresAt"
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
export class PgArtifactStorage implements ArtifactStorage {
|
|
55
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
56
|
+
|
|
57
|
+
async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
|
|
58
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
59
|
+
const [artifact] = await tx<PgRow[]>`
|
|
60
|
+
INSERT INTO artifacts (
|
|
61
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
62
|
+
latest_version, next_version, created_at, updated_at
|
|
63
|
+
) VALUES (
|
|
64
|
+
${input.projectId}, ${input.id}, ${input.kind}, ${input.version.title},
|
|
65
|
+
${input.owner.type}, ${input.owner.id}, 1, 2,
|
|
66
|
+
${input.version.createdAt}, ${input.version.createdAt}
|
|
67
|
+
)
|
|
68
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
69
|
+
RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
|
|
70
|
+
`
|
|
71
|
+
if (!artifact) throw artifactStorageErrors.exists(input.id)
|
|
72
|
+
return {
|
|
73
|
+
artifact: parseArtifactRow(artifact),
|
|
74
|
+
version: await insertVersion(tx, input.projectId, input.id, 1, input.version),
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
|
|
80
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
81
|
+
// The row lock serializes concurrent publishes of one artifact.
|
|
82
|
+
const [current] = await tx<PgRow[]>`
|
|
83
|
+
SELECT kind, next_version FROM artifacts
|
|
84
|
+
WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
85
|
+
FOR UPDATE
|
|
86
|
+
`
|
|
87
|
+
if (!current) throw artifactStorageErrors.notFound(input.artifactId)
|
|
88
|
+
const kind = current.kind as ArtifactRecord["kind"]
|
|
89
|
+
if (kind !== input.kind) {
|
|
90
|
+
throw artifactStorageErrors.kindMismatch(input.artifactId, kind, input.kind)
|
|
91
|
+
}
|
|
92
|
+
const version = await insertVersion(
|
|
93
|
+
tx,
|
|
94
|
+
input.projectId,
|
|
95
|
+
input.artifactId,
|
|
96
|
+
Number(current.next_version),
|
|
97
|
+
input.version
|
|
98
|
+
)
|
|
99
|
+
const [artifact] = await tx<PgRow[]>`
|
|
100
|
+
UPDATE artifacts
|
|
101
|
+
SET title = ${input.version.title}, latest_version = ${version.version},
|
|
102
|
+
next_version = ${version.version + 1}, updated_at = ${input.version.createdAt}
|
|
103
|
+
WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
104
|
+
RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
|
|
105
|
+
`
|
|
106
|
+
return { artifact: parseArtifactRow(artifact), version }
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
|
|
111
|
+
const [row] = await this.sql<PgRow[]>`
|
|
112
|
+
SELECT ${this.sql.unsafe(ARTIFACT_COLUMNS)} FROM artifacts
|
|
113
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
114
|
+
`
|
|
115
|
+
return row ? parseArtifactRow(row) : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
|
|
119
|
+
const where = ["a.project_id = $1"]
|
|
120
|
+
const params: SqlParameter[] = [input.projectId]
|
|
121
|
+
if (input.owner) {
|
|
122
|
+
where.push(`a.owner_principal_type = $${params.length + 1}`)
|
|
123
|
+
params.push(input.owner.type)
|
|
124
|
+
where.push(`a.owner_principal_id = $${params.length + 1}`)
|
|
125
|
+
params.push(input.owner.id)
|
|
126
|
+
}
|
|
127
|
+
if (input.originThreadId !== undefined) {
|
|
128
|
+
where.push(
|
|
129
|
+
`EXISTS (
|
|
130
|
+
SELECT 1 FROM artifact_versions v
|
|
131
|
+
WHERE v.project_id = a.project_id AND v.artifact_id = a.id
|
|
132
|
+
AND v.origin_thread_id = $${params.length + 1}
|
|
133
|
+
)`
|
|
134
|
+
)
|
|
135
|
+
params.push(input.originThreadId)
|
|
136
|
+
}
|
|
137
|
+
if (input.kind !== undefined) {
|
|
138
|
+
where.push(`a.kind = $${params.length + 1}`)
|
|
139
|
+
params.push(input.kind)
|
|
140
|
+
}
|
|
141
|
+
const clause = where.join(" AND ")
|
|
142
|
+
const [counted] = await this.sql.unsafe<{ total: number }[]>(
|
|
143
|
+
`SELECT COUNT(*)::int AS total FROM artifacts a WHERE ${clause}`,
|
|
144
|
+
params
|
|
145
|
+
)
|
|
146
|
+
const offset = input.offset ?? 0
|
|
147
|
+
const rows = await this.sql.unsafe<PgRow[]>(
|
|
148
|
+
`SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
|
|
149
|
+
ORDER BY a.updated_at DESC, a.id ASC
|
|
150
|
+
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
|
|
151
|
+
[...params, input.limit ?? null, offset]
|
|
152
|
+
)
|
|
153
|
+
const total = counted?.total ?? 0
|
|
154
|
+
return { artifacts: rows.map(parseArtifactRow), hasMore: offset + rows.length < total, total }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
|
|
158
|
+
const [row] = await this.sql<PgRow[]>`
|
|
159
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
|
|
160
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
161
|
+
AND version = ${input.version}
|
|
162
|
+
`
|
|
163
|
+
return row ? parseArtifactVersionRow(row) : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
|
|
167
|
+
const [counted] = await this.sql<{ total: number }[]>`
|
|
168
|
+
SELECT COUNT(*)::int AS total FROM artifact_versions
|
|
169
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
170
|
+
`
|
|
171
|
+
const offset = input.offset ?? 0
|
|
172
|
+
const rows = await this.sql<PgRow[]>`
|
|
173
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
|
|
174
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
175
|
+
ORDER BY version DESC
|
|
176
|
+
LIMIT ${input.limit ?? null} OFFSET ${offset}
|
|
177
|
+
`
|
|
178
|
+
const total = counted?.total ?? 0
|
|
179
|
+
return {
|
|
180
|
+
versions: rows.map(parseArtifactVersionRow),
|
|
181
|
+
hasMore: offset + rows.length < total,
|
|
182
|
+
total,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async listReferencingVersions(
|
|
187
|
+
input: ListReferencingVersionsInput
|
|
188
|
+
): Promise<ArtifactVersionRecord[]> {
|
|
189
|
+
// An address names the artifact when the id is followed by nothing, a version, a file, or a
|
|
190
|
+
// part.
|
|
191
|
+
const prefix = `artifact:${input.artifactId}`
|
|
192
|
+
const rows = await this.sql<PgRow[]>`
|
|
193
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions v
|
|
194
|
+
WHERE project_id = ${input.projectId} AND (artifact_id, version) IN (
|
|
195
|
+
SELECT id, latest_version FROM artifacts WHERE project_id = ${input.projectId}
|
|
196
|
+
) AND EXISTS (
|
|
197
|
+
SELECT 1 FROM jsonb_array_elements(v.refs) r
|
|
198
|
+
WHERE left(r ->> 'address', ${prefix.length}::int) = ${prefix}
|
|
199
|
+
AND substr(r ->> 'address', ${prefix.length + 1}::int, 1) IN ('', '@', '/', '#')
|
|
200
|
+
)
|
|
201
|
+
ORDER BY artifact_id
|
|
202
|
+
`
|
|
203
|
+
return rows.map(parseArtifactVersionRow)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
|
|
207
|
+
const rows = await this.sql`
|
|
208
|
+
INSERT INTO artifact_frame_grants
|
|
209
|
+
(project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
|
|
210
|
+
SELECT ${record.projectId}, ${record.tokenHash}, ${record.artifactId}, ${record.version},
|
|
211
|
+
${JSON.stringify(record.targets)}::text::jsonb, ${record.createdAt},
|
|
212
|
+
${record.expiresAt}
|
|
213
|
+
WHERE EXISTS (
|
|
214
|
+
SELECT 1 FROM artifacts WHERE project_id = ${record.projectId} AND id = ${record.artifactId}
|
|
215
|
+
)
|
|
216
|
+
RETURNING token_hash
|
|
217
|
+
`
|
|
218
|
+
if (rows.length === 0) throw artifactStorageErrors.notFound(record.artifactId)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async findFrameGrant(
|
|
222
|
+
input: FindArtifactFrameGrantInput
|
|
223
|
+
): Promise<ArtifactFrameGrantRecord | null> {
|
|
224
|
+
const [row] = await this.sql<PgRow[]>`
|
|
225
|
+
SELECT ${this.sql.unsafe(FRAME_GRANT_COLUMNS)} FROM artifact_frame_grants
|
|
226
|
+
WHERE project_id = ${input.projectId} AND token_hash = ${input.tokenHash}
|
|
227
|
+
`
|
|
228
|
+
return row ? parseArtifactFrameGrantRow(row) : null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
|
|
232
|
+
const rows = await this.sql`
|
|
233
|
+
DELETE FROM artifact_frame_grants
|
|
234
|
+
WHERE (project_id, token_hash) IN (
|
|
235
|
+
SELECT project_id, token_hash FROM artifact_frame_grants
|
|
236
|
+
WHERE project_id = ${input.projectId} AND expires_at < ${input.expiredBefore}
|
|
237
|
+
ORDER BY expires_at LIMIT ${input.limit}
|
|
238
|
+
)
|
|
239
|
+
RETURNING token_hash
|
|
240
|
+
`
|
|
241
|
+
return rows.length
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async delete(input: DeleteArtifactInput): Promise<boolean> {
|
|
245
|
+
// Versions and frame grants go with the artifact through ON DELETE CASCADE.
|
|
246
|
+
const rows = await this.sql`
|
|
247
|
+
DELETE FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
248
|
+
RETURNING id
|
|
249
|
+
`
|
|
250
|
+
return rows.length > 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
|
|
254
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
255
|
+
// Lock the run's artifacts first, in id order so two of these never deadlock. `publish`
|
|
256
|
+
// holds the same lock while it adds a version, and under READ COMMITTED each statement
|
|
257
|
+
// below then reads a snapshot taken after any publish that held it committed. Without the
|
|
258
|
+
// lock they would wait on a publish's row lock and then apply what they read before it: the
|
|
259
|
+
// UPDATE would point `latest_version` back past the new version, and the DELETE would drop
|
|
260
|
+
// an artifact whose versions were all the run's, cascading the new version with it.
|
|
261
|
+
const locked = await tx<{ id: string }[]>`
|
|
262
|
+
SELECT id FROM artifacts
|
|
263
|
+
WHERE project_id = ${input.projectId} AND id IN (
|
|
264
|
+
SELECT artifact_id FROM artifact_versions
|
|
265
|
+
WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
|
|
266
|
+
)
|
|
267
|
+
ORDER BY id
|
|
268
|
+
FOR UPDATE
|
|
269
|
+
`
|
|
270
|
+
const affected = locked.map((row) => row.id)
|
|
271
|
+
if (affected.length === 0) return 0
|
|
272
|
+
const deleted = await tx`
|
|
273
|
+
DELETE FROM artifact_versions
|
|
274
|
+
WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
|
|
275
|
+
AND artifact_id IN ${tx(affected)}
|
|
276
|
+
RETURNING version
|
|
277
|
+
`
|
|
278
|
+
// An artifact left without versions goes; the rest point at their newest remaining one.
|
|
279
|
+
await tx`
|
|
280
|
+
DELETE FROM artifacts a
|
|
281
|
+
WHERE a.project_id = ${input.projectId} AND a.id IN ${tx(affected)}
|
|
282
|
+
AND NOT EXISTS (
|
|
283
|
+
SELECT 1 FROM artifact_versions v
|
|
284
|
+
WHERE v.project_id = a.project_id AND v.artifact_id = a.id
|
|
285
|
+
)
|
|
286
|
+
`
|
|
287
|
+
await tx`
|
|
288
|
+
UPDATE artifacts a
|
|
289
|
+
SET latest_version = latest.version, title = latest.title, updated_at = latest.created_at
|
|
290
|
+
FROM (
|
|
291
|
+
SELECT DISTINCT ON (artifact_id) artifact_id, version, title, created_at
|
|
292
|
+
FROM artifact_versions
|
|
293
|
+
WHERE project_id = ${input.projectId} AND artifact_id IN ${tx(affected)}
|
|
294
|
+
ORDER BY artifact_id, version DESC
|
|
295
|
+
) latest
|
|
296
|
+
WHERE a.project_id = ${input.projectId} AND a.id = latest.artifact_id
|
|
297
|
+
`
|
|
298
|
+
return deleted.length
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function insertVersion(
|
|
304
|
+
sql: SQLClient,
|
|
305
|
+
projectId: string,
|
|
306
|
+
artifactId: string,
|
|
307
|
+
version: number,
|
|
308
|
+
input: ArtifactVersionInput
|
|
309
|
+
): Promise<ArtifactVersionRecord> {
|
|
310
|
+
const [row] = await sql<PgRow[]>`
|
|
311
|
+
INSERT INTO artifact_versions (
|
|
312
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
313
|
+
author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
|
|
314
|
+
) VALUES (
|
|
315
|
+
${projectId}, ${artifactId}, ${version}, ${input.title}, ${input.entry},
|
|
316
|
+
${JSON.stringify(input.files)}::text::jsonb,
|
|
317
|
+
${JSON.stringify(input.outline)}::text::jsonb,
|
|
318
|
+
${JSON.stringify(input.references)}::text::jsonb,
|
|
319
|
+
${input.author.type}, ${input.author.id},
|
|
320
|
+
${input.origin?.threadId ?? null}, ${input.origin?.runId ?? null}, ${input.createdAt}
|
|
321
|
+
)
|
|
322
|
+
RETURNING ${sql.unsafe(VERSION_COLUMNS)}
|
|
323
|
+
`
|
|
324
|
+
return parseArtifactVersionRow(row)
|
|
325
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
compareExecutionCreation,
|
|
2
3
|
type ExecutionStorageRow,
|
|
3
4
|
executionRecordFromStorageRow,
|
|
4
5
|
executionRecordToStorageRow,
|
|
@@ -8,6 +9,8 @@ import type {
|
|
|
8
9
|
CreateExecutionInput,
|
|
9
10
|
ExecutionRecord,
|
|
10
11
|
ExecutionStorage,
|
|
12
|
+
ListSessionExecutionsInput,
|
|
13
|
+
SessionExecutions,
|
|
11
14
|
ShareSessionStorage,
|
|
12
15
|
} from "@sixb/core/storage"
|
|
13
16
|
import { ExecutionStorageError } from "@sixb/core/storage"
|
|
@@ -111,6 +114,55 @@ export class PgExecutionStorage implements ExecutionStorage {
|
|
|
111
114
|
`
|
|
112
115
|
return row ? pgExecutionRecord(row) : null
|
|
113
116
|
}
|
|
117
|
+
|
|
118
|
+
async listBySession(input: ListSessionExecutionsInput): Promise<SessionExecutions> {
|
|
119
|
+
const session =
|
|
120
|
+
input.session.type === "user"
|
|
121
|
+
? this.sql`authority_session_id = ${input.session.id}`
|
|
122
|
+
: this.sql`authority_delegation_session_id = ${input.session.id}`
|
|
123
|
+
|
|
124
|
+
// Each iteration of a recursive query reads the rows the previous one produced, so the walk goes
|
|
125
|
+
// level by level, roots first; with no ORDER BY outside, PostgreSQL evaluates only the rows the
|
|
126
|
+
// limit returns. Within the last level reached, which rows come first is unspecified.
|
|
127
|
+
const rows = await this.sql<(PgExecutionRow & { readonly depth: number })[]>`
|
|
128
|
+
WITH RECURSIVE tree AS (
|
|
129
|
+
(
|
|
130
|
+
SELECT *, 0 AS depth
|
|
131
|
+
FROM executions
|
|
132
|
+
WHERE project_id = ${input.projectId}
|
|
133
|
+
AND ${session}
|
|
134
|
+
AND parent_execution_id IS NULL
|
|
135
|
+
AND created_at >= ${input.from}
|
|
136
|
+
AND created_at < ${input.to}
|
|
137
|
+
ORDER BY created_at, id
|
|
138
|
+
LIMIT ${input.limit + 1}
|
|
139
|
+
)
|
|
140
|
+
UNION ALL
|
|
141
|
+
SELECT child.*, tree.depth + 1
|
|
142
|
+
FROM tree
|
|
143
|
+
JOIN executions child
|
|
144
|
+
ON child.project_id = tree.project_id
|
|
145
|
+
AND child.parent_execution_id = tree.id
|
|
146
|
+
)
|
|
147
|
+
SELECT * FROM tree
|
|
148
|
+
LIMIT ${input.limit + 1}
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
// Keep the first `limit` by depth, then oldest first, as the contract keeps them.
|
|
152
|
+
const records = rows
|
|
153
|
+
.map((row) => ({ depth: row.depth, record: pgExecutionRecord(row) }))
|
|
154
|
+
.sort(
|
|
155
|
+
(left, right) =>
|
|
156
|
+
left.depth - right.depth || compareExecutionCreation(left.record, right.record)
|
|
157
|
+
)
|
|
158
|
+
.slice(0, input.limit)
|
|
159
|
+
.map(({ record }) => record)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
executions: records.sort(compareExecutionCreation),
|
|
163
|
+
truncated: rows.length > input.limit,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
114
166
|
}
|
|
115
167
|
|
|
116
168
|
/** An `executions` row as its record, for any store that reads executions alongside its own rows. */
|
|
@@ -314,6 +314,29 @@ export class PgWorkflowRunStorage implements WorkflowRunStorage {
|
|
|
314
314
|
return row ? rowToWorkflowRunRecord(row) : null
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
async getByExecutionIds(params: {
|
|
318
|
+
readonly projectId: string
|
|
319
|
+
readonly executionIds: readonly string[]
|
|
320
|
+
}): Promise<readonly WorkflowRunRecord[]> {
|
|
321
|
+
const executionIds = [...new Set(params.executionIds)]
|
|
322
|
+
if (executionIds.length === 0) {
|
|
323
|
+
return []
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const rows = await this.sql<WorkflowRunDatabaseRow[]>`
|
|
327
|
+
SELECT *
|
|
328
|
+
FROM workflow_runs
|
|
329
|
+
WHERE project_id = ${params.projectId}
|
|
330
|
+
AND execution_id = ANY(${this.sql.array(executionIds)}::text[])
|
|
331
|
+
`
|
|
332
|
+
|
|
333
|
+
const byExecution = new Map(rows.map((row) => [row.execution_id, row]))
|
|
334
|
+
return executionIds.flatMap((executionId) => {
|
|
335
|
+
const row = byExecution.get(executionId)
|
|
336
|
+
return row ? [rowToWorkflowRunRecord(row)] : []
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
317
340
|
async list(input: ListWorkflowRunsInput): Promise<ListWorkflowRunsResult> {
|
|
318
341
|
if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
|
|
319
342
|
return {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import type {
|
|
9
9
|
CreateShareGrantInput,
|
|
10
10
|
GetShareGrantByIdInput,
|
|
11
|
+
GetShareGrantsByIdsInput,
|
|
11
12
|
ListShareGrantsInput,
|
|
12
13
|
ListShareGrantsResult,
|
|
13
14
|
RevokeShareGrantInput,
|
|
@@ -85,6 +86,26 @@ export class PgShareGrantStorage implements ShareGrantStorage {
|
|
|
85
86
|
return row ? recordFromRow(row) : null
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
async getByIds(input: GetShareGrantsByIdsInput): Promise<readonly ShareGrantRecord[]> {
|
|
90
|
+
const ids = [...new Set(input.ids)]
|
|
91
|
+
if (ids.length === 0) {
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rows = await this.sql<PgShareGrantRow[]>`
|
|
96
|
+
SELECT *
|
|
97
|
+
FROM share_grants
|
|
98
|
+
WHERE project_id = ${input.projectId}
|
|
99
|
+
AND id = ANY(${this.sql.array(ids)}::text[])
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
const byId = new Map(rows.map((row) => [row.id, row]))
|
|
103
|
+
return ids.flatMap((id) => {
|
|
104
|
+
const row = byId.get(id)
|
|
105
|
+
return row ? [recordFromRow(row)] : []
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
async list(input: ListShareGrantsInput): Promise<ListShareGrantsResult> {
|
|
89
110
|
const normalized = normalizeListShareGrantsInput(input)
|
|
90
111
|
const where = ["project_id = $1"]
|