@sixb/sqlite 0.1.20 → 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/action-run-storage.d.ts +4 -0
- package/dist/action-run-storage.d.ts.map +1 -1
- package/dist/activity-storage.d.ts +20 -0
- package/dist/activity-storage.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +4 -0
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/artifact-storage.d.ts +22 -0
- package/dist/artifact-storage.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +2 -1
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +880 -14
- package/dist/index.js.map +14 -12
- package/dist/migrations.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/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts +4 -0
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/action-run-storage.ts +27 -0
- package/src/activity-storage.ts +588 -0
- package/src/agents/threads.ts +27 -0
- package/src/artifact-storage.ts +356 -0
- package/src/auth-storage/sessions.ts +27 -0
- package/src/execution-storage.ts +56 -0
- package/src/index.ts +10 -0
- package/src/migrations/062-artifacts.sql +71 -0
- package/src/migrations/063-drop-output-diagnostics.sql +19 -0
- package/src/migrations/064-activity.sql +61 -0
- package/src/migrations/065-activity-timeline.sql +19 -0
- package/src/migrations/066-activity-interactions.sql +20 -0
- package/src/migrations.ts +14 -0
- package/src/ontology-storage/sources.ts +41 -0
- package/src/share-grant-storage.ts +25 -0
- package/src/workflow-run-storage.ts +27 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { Database, SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
artifactStorageErrors,
|
|
4
|
+
parseArtifactFrameGrantRow,
|
|
5
|
+
parseArtifactRow,
|
|
6
|
+
parseArtifactVersionRow,
|
|
7
|
+
} from "@sixb/core/internal/storage"
|
|
8
|
+
import type {
|
|
9
|
+
ArtifactFrameGrantRecord,
|
|
10
|
+
ArtifactRecord,
|
|
11
|
+
ArtifactStorage,
|
|
12
|
+
ArtifactVersionInput,
|
|
13
|
+
ArtifactVersionRecord,
|
|
14
|
+
CreateArtifactInput,
|
|
15
|
+
DeleteArtifactInput,
|
|
16
|
+
DeleteArtifactVersionsByOriginInput,
|
|
17
|
+
DeleteExpiredArtifactFrameGrantsInput,
|
|
18
|
+
FindArtifactFrameGrantInput,
|
|
19
|
+
GetArtifactInput,
|
|
20
|
+
GetArtifactVersionInput,
|
|
21
|
+
ListArtifactsInput,
|
|
22
|
+
ListArtifactsResult,
|
|
23
|
+
ListArtifactVersionsInput,
|
|
24
|
+
ListArtifactVersionsResult,
|
|
25
|
+
ListReferencingVersionsInput,
|
|
26
|
+
PublishArtifactVersionInput,
|
|
27
|
+
PublishedArtifactVersion,
|
|
28
|
+
} from "@sixb/core/storage"
|
|
29
|
+
import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
|
|
30
|
+
|
|
31
|
+
const ARTIFACT_COLUMNS = `
|
|
32
|
+
project_id AS projectId, id, kind, title,
|
|
33
|
+
owner_principal_type AS ownerPrincipalType, owner_principal_id AS ownerPrincipalId,
|
|
34
|
+
latest_version AS latestVersion, created_at AS createdAt, updated_at AS updatedAt
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
const VERSION_COLUMNS = `
|
|
38
|
+
project_id AS projectId, artifact_id AS artifactId, version, title, entry, files, outline, refs,
|
|
39
|
+
author_principal_type AS authorPrincipalType, author_principal_id AS authorPrincipalId,
|
|
40
|
+
origin_thread_id AS originThreadId, origin_run_id AS originRunId, created_at AS createdAt
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const FRAME_GRANT_COLUMNS = `
|
|
44
|
+
project_id AS projectId, token_hash AS tokenHash, artifact_id AS artifactId, version, targets,
|
|
45
|
+
created_at AS createdAt, expires_at AS expiresAt
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
type Row = Record<string, unknown>
|
|
49
|
+
|
|
50
|
+
export class SqliteArtifactStorage implements ArtifactStorage {
|
|
51
|
+
private readonly db: Database
|
|
52
|
+
|
|
53
|
+
constructor(connection: SqliteStoreConnection) {
|
|
54
|
+
this.db = connection.db
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
|
|
58
|
+
return runImmediateTransaction(this.db, () => {
|
|
59
|
+
const createdAt = input.version.createdAt.toISOString()
|
|
60
|
+
const artifact = this.db
|
|
61
|
+
.query(
|
|
62
|
+
`INSERT INTO artifacts (
|
|
63
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
64
|
+
latest_version, next_version, created_at, updated_at
|
|
65
|
+
) VALUES (?, ?, ?, ?, ?, ?, 1, 2, ?, ?)
|
|
66
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
67
|
+
RETURNING ${ARTIFACT_COLUMNS}`
|
|
68
|
+
)
|
|
69
|
+
.get(
|
|
70
|
+
input.projectId,
|
|
71
|
+
input.id,
|
|
72
|
+
input.kind,
|
|
73
|
+
input.version.title,
|
|
74
|
+
input.owner.type,
|
|
75
|
+
input.owner.id,
|
|
76
|
+
createdAt,
|
|
77
|
+
createdAt
|
|
78
|
+
) as Row | null
|
|
79
|
+
if (!artifact) throw artifactStorageErrors.exists(input.id)
|
|
80
|
+
return {
|
|
81
|
+
artifact: parseArtifactRow(artifact),
|
|
82
|
+
version: this.insertVersion(input.projectId, input.id, 1, input.version),
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
|
|
88
|
+
return runImmediateTransaction(this.db, () => {
|
|
89
|
+
const current = this.db
|
|
90
|
+
.query(
|
|
91
|
+
"SELECT kind, next_version AS nextVersion FROM artifacts WHERE project_id = ? AND id = ?"
|
|
92
|
+
)
|
|
93
|
+
.get(input.projectId, input.artifactId) as {
|
|
94
|
+
readonly kind: ArtifactRecord["kind"]
|
|
95
|
+
readonly nextVersion: number
|
|
96
|
+
} | null
|
|
97
|
+
if (!current) throw artifactStorageErrors.notFound(input.artifactId)
|
|
98
|
+
if (current.kind !== input.kind) {
|
|
99
|
+
throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
|
|
100
|
+
}
|
|
101
|
+
const version = this.insertVersion(
|
|
102
|
+
input.projectId,
|
|
103
|
+
input.artifactId,
|
|
104
|
+
current.nextVersion,
|
|
105
|
+
input.version
|
|
106
|
+
)
|
|
107
|
+
const artifact = this.db
|
|
108
|
+
.query(
|
|
109
|
+
`UPDATE artifacts
|
|
110
|
+
SET title = ?, latest_version = ?, next_version = ?, updated_at = ?
|
|
111
|
+
WHERE project_id = ? AND id = ?
|
|
112
|
+
RETURNING ${ARTIFACT_COLUMNS}`
|
|
113
|
+
)
|
|
114
|
+
.get(
|
|
115
|
+
input.version.title,
|
|
116
|
+
version.version,
|
|
117
|
+
version.version + 1,
|
|
118
|
+
input.version.createdAt.toISOString(),
|
|
119
|
+
input.projectId,
|
|
120
|
+
input.artifactId
|
|
121
|
+
) as Row
|
|
122
|
+
return { artifact: parseArtifactRow(artifact), version }
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
|
|
127
|
+
const row = this.db
|
|
128
|
+
.query(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE project_id = ? AND id = ?`)
|
|
129
|
+
.get(input.projectId, input.id) as Row | null
|
|
130
|
+
return row ? parseArtifactRow(row) : null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
|
|
134
|
+
const where = ["a.project_id = ?"]
|
|
135
|
+
const params: SQLQueryBindings[] = [input.projectId]
|
|
136
|
+
if (input.owner) {
|
|
137
|
+
where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
|
|
138
|
+
params.push(input.owner.type, input.owner.id)
|
|
139
|
+
}
|
|
140
|
+
if (input.originThreadId !== undefined) {
|
|
141
|
+
where.push(
|
|
142
|
+
`EXISTS (
|
|
143
|
+
SELECT 1 FROM artifact_versions v
|
|
144
|
+
WHERE v.project_id = a.project_id AND v.artifact_id = a.id AND v.origin_thread_id = ?
|
|
145
|
+
)`
|
|
146
|
+
)
|
|
147
|
+
params.push(input.originThreadId)
|
|
148
|
+
}
|
|
149
|
+
if (input.kind !== undefined) {
|
|
150
|
+
where.push("a.kind = ?")
|
|
151
|
+
params.push(input.kind)
|
|
152
|
+
}
|
|
153
|
+
const clause = where.join(" AND ")
|
|
154
|
+
const { total } = this.db
|
|
155
|
+
.query(`SELECT COUNT(*) AS total FROM artifacts a WHERE ${clause}`)
|
|
156
|
+
.get(...params) as { readonly total: number }
|
|
157
|
+
const offset = input.offset ?? 0
|
|
158
|
+
const rows = this.db
|
|
159
|
+
.query(
|
|
160
|
+
`SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
|
|
161
|
+
ORDER BY a.updated_at DESC, a.id ASC LIMIT ? OFFSET ?`
|
|
162
|
+
)
|
|
163
|
+
.all(...params, input.limit ?? -1, offset) as Row[]
|
|
164
|
+
return {
|
|
165
|
+
artifacts: rows.map(parseArtifactRow),
|
|
166
|
+
hasMore: offset + rows.length < total,
|
|
167
|
+
total,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
|
|
172
|
+
const row = this.db
|
|
173
|
+
.query(
|
|
174
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions
|
|
175
|
+
WHERE project_id = ? AND artifact_id = ? AND version = ?`
|
|
176
|
+
)
|
|
177
|
+
.get(input.projectId, input.artifactId, input.version) as Row | null
|
|
178
|
+
return row ? parseArtifactVersionRow(row) : null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
|
|
182
|
+
const { total } = this.db
|
|
183
|
+
.query(
|
|
184
|
+
"SELECT COUNT(*) AS total FROM artifact_versions WHERE project_id = ? AND artifact_id = ?"
|
|
185
|
+
)
|
|
186
|
+
.get(input.projectId, input.artifactId) as { readonly total: number }
|
|
187
|
+
const offset = input.offset ?? 0
|
|
188
|
+
const rows = this.db
|
|
189
|
+
.query(
|
|
190
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions
|
|
191
|
+
WHERE project_id = ? AND artifact_id = ?
|
|
192
|
+
ORDER BY version DESC LIMIT ? OFFSET ?`
|
|
193
|
+
)
|
|
194
|
+
.all(input.projectId, input.artifactId, input.limit ?? -1, offset) as Row[]
|
|
195
|
+
return {
|
|
196
|
+
versions: rows.map(parseArtifactVersionRow),
|
|
197
|
+
hasMore: offset + rows.length < total,
|
|
198
|
+
total,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async listReferencingVersions(
|
|
203
|
+
input: ListReferencingVersionsInput
|
|
204
|
+
): Promise<ArtifactVersionRecord[]> {
|
|
205
|
+
// An address names the artifact when the id is followed by nothing, a version, a file, or a
|
|
206
|
+
// part.
|
|
207
|
+
const prefix = `artifact:${input.artifactId}`
|
|
208
|
+
const rows = this.db
|
|
209
|
+
.query(
|
|
210
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions v
|
|
211
|
+
WHERE (project_id, artifact_id, version) IN (
|
|
212
|
+
SELECT project_id, id, latest_version FROM artifacts WHERE project_id = ?
|
|
213
|
+
) AND EXISTS (
|
|
214
|
+
SELECT 1 FROM json_each(v.refs) r
|
|
215
|
+
WHERE substr(json_extract(r.value, '$.address'), 1, ?) = ?
|
|
216
|
+
AND substr(json_extract(r.value, '$.address'), ? + 1, 1) IN ('', '@', '/', '#')
|
|
217
|
+
)
|
|
218
|
+
ORDER BY artifact_id`
|
|
219
|
+
)
|
|
220
|
+
.all(input.projectId, prefix.length, prefix, prefix.length) as Row[]
|
|
221
|
+
return rows.map(parseArtifactVersionRow)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
|
|
225
|
+
runImmediateTransaction(this.db, () => {
|
|
226
|
+
const exists = this.db
|
|
227
|
+
.query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
|
|
228
|
+
.get(record.projectId, record.artifactId)
|
|
229
|
+
if (!exists) throw artifactStorageErrors.notFound(record.artifactId)
|
|
230
|
+
this.db
|
|
231
|
+
.query(
|
|
232
|
+
`INSERT INTO artifact_frame_grants
|
|
233
|
+
(project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
|
|
234
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
235
|
+
)
|
|
236
|
+
.run(
|
|
237
|
+
record.projectId,
|
|
238
|
+
record.tokenHash,
|
|
239
|
+
record.artifactId,
|
|
240
|
+
record.version,
|
|
241
|
+
JSON.stringify(record.targets),
|
|
242
|
+
record.createdAt.toISOString(),
|
|
243
|
+
record.expiresAt.toISOString()
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async findFrameGrant(
|
|
249
|
+
input: FindArtifactFrameGrantInput
|
|
250
|
+
): Promise<ArtifactFrameGrantRecord | null> {
|
|
251
|
+
const row = this.db
|
|
252
|
+
.query(
|
|
253
|
+
`SELECT ${FRAME_GRANT_COLUMNS} FROM artifact_frame_grants
|
|
254
|
+
WHERE project_id = ? AND token_hash = ?`
|
|
255
|
+
)
|
|
256
|
+
.get(input.projectId, input.tokenHash) as Row | null
|
|
257
|
+
return row ? parseArtifactFrameGrantRow(row) : null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
|
|
261
|
+
return this.db
|
|
262
|
+
.query(
|
|
263
|
+
`DELETE FROM artifact_frame_grants WHERE rowid IN (
|
|
264
|
+
SELECT rowid FROM artifact_frame_grants
|
|
265
|
+
WHERE project_id = ? AND expires_at < ?
|
|
266
|
+
ORDER BY expires_at LIMIT ?
|
|
267
|
+
)`
|
|
268
|
+
)
|
|
269
|
+
.run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async delete(input: DeleteArtifactInput): Promise<boolean> {
|
|
273
|
+
// Versions and frame grants go with the artifact through ON DELETE CASCADE.
|
|
274
|
+
return (
|
|
275
|
+
this.db
|
|
276
|
+
.query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
|
|
277
|
+
.run(input.projectId, input.id).changes > 0
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
|
|
282
|
+
return runImmediateTransaction(this.db, () => {
|
|
283
|
+
const affected = this.db
|
|
284
|
+
.query(
|
|
285
|
+
`SELECT DISTINCT artifact_id AS artifactId FROM artifact_versions
|
|
286
|
+
WHERE project_id = ? AND origin_run_id = ?`
|
|
287
|
+
)
|
|
288
|
+
.all(input.projectId, input.runId) as { readonly artifactId: string }[]
|
|
289
|
+
const deleted = this.db
|
|
290
|
+
.query("DELETE FROM artifact_versions WHERE project_id = ? AND origin_run_id = ?")
|
|
291
|
+
.run(input.projectId, input.runId).changes
|
|
292
|
+
for (const { artifactId } of affected) {
|
|
293
|
+
this.repointLatest(input.projectId, artifactId)
|
|
294
|
+
}
|
|
295
|
+
return deleted
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Point an artifact at its newest remaining version, or delete it when none remains. */
|
|
300
|
+
private repointLatest(projectId: string, artifactId: string): void {
|
|
301
|
+
const latest = this.db
|
|
302
|
+
.query(
|
|
303
|
+
`SELECT version, title, created_at AS createdAt FROM artifact_versions
|
|
304
|
+
WHERE project_id = ? AND artifact_id = ? ORDER BY version DESC LIMIT 1`
|
|
305
|
+
)
|
|
306
|
+
.get(projectId, artifactId) as {
|
|
307
|
+
readonly version: number
|
|
308
|
+
readonly title: string
|
|
309
|
+
readonly createdAt: string
|
|
310
|
+
} | null
|
|
311
|
+
if (!latest) {
|
|
312
|
+
this.db
|
|
313
|
+
.query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
|
|
314
|
+
.run(projectId, artifactId)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
this.db
|
|
318
|
+
.query(
|
|
319
|
+
`UPDATE artifacts SET title = ?, latest_version = ?, updated_at = ?
|
|
320
|
+
WHERE project_id = ? AND id = ?`
|
|
321
|
+
)
|
|
322
|
+
.run(latest.title, latest.version, latest.createdAt, projectId, artifactId)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private insertVersion(
|
|
326
|
+
projectId: string,
|
|
327
|
+
artifactId: string,
|
|
328
|
+
version: number,
|
|
329
|
+
input: ArtifactVersionInput
|
|
330
|
+
): ArtifactVersionRecord {
|
|
331
|
+
const row = this.db
|
|
332
|
+
.query(
|
|
333
|
+
`INSERT INTO artifact_versions (
|
|
334
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
335
|
+
author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
|
|
336
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
337
|
+
RETURNING ${VERSION_COLUMNS}`
|
|
338
|
+
)
|
|
339
|
+
.get(
|
|
340
|
+
projectId,
|
|
341
|
+
artifactId,
|
|
342
|
+
version,
|
|
343
|
+
input.title,
|
|
344
|
+
input.entry,
|
|
345
|
+
JSON.stringify(input.files),
|
|
346
|
+
JSON.stringify(input.outline),
|
|
347
|
+
JSON.stringify(input.references),
|
|
348
|
+
input.author.type,
|
|
349
|
+
input.author.id,
|
|
350
|
+
input.origin?.threadId ?? null,
|
|
351
|
+
input.origin?.runId ?? null,
|
|
352
|
+
input.createdAt.toISOString()
|
|
353
|
+
) as Row
|
|
354
|
+
return parseArtifactVersionRow(row)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -2,6 +2,8 @@ import type { Database } from "bun:sqlite"
|
|
|
2
2
|
import type {
|
|
3
3
|
AuthSessionStore,
|
|
4
4
|
CreateAuthSessionInput,
|
|
5
|
+
ListAuthSessionsInput,
|
|
6
|
+
ListAuthSessionsResult,
|
|
5
7
|
RotateSessionRefreshTokenResult,
|
|
6
8
|
SessionRecord,
|
|
7
9
|
} from "@sixb/core/storage"
|
|
@@ -60,6 +62,31 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
|
|
|
60
62
|
return rows.map(rowToSessionRecord)
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
async listByUserId(params: ListAuthSessionsInput): Promise<ListAuthSessionsResult> {
|
|
66
|
+
const rows = this.db
|
|
67
|
+
.query(
|
|
68
|
+
`
|
|
69
|
+
SELECT *
|
|
70
|
+
FROM auth_sessions
|
|
71
|
+
WHERE project_id = ? AND user_id = ? AND created_at <= ?
|
|
72
|
+
ORDER BY created_at DESC, id DESC
|
|
73
|
+
LIMIT ? OFFSET ?
|
|
74
|
+
`
|
|
75
|
+
)
|
|
76
|
+
.all(
|
|
77
|
+
params.projectId,
|
|
78
|
+
params.userId,
|
|
79
|
+
params.before ? toIso(params.before.createdAt) : "9999-12-31T23:59:59.999Z",
|
|
80
|
+
params.limit + 1,
|
|
81
|
+
params.before?.skip ?? 0
|
|
82
|
+
) as SqliteAuthSessionRow[]
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
|
|
86
|
+
hasMore: rows.length > params.limit,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
async findValidByTokenHash(params: {
|
|
64
91
|
readonly projectId: string
|
|
65
92
|
readonly id: string
|
package/src/execution-storage.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import {
|
|
3
|
+
compareExecutionCreation,
|
|
3
4
|
type ExecutionStorageRow,
|
|
4
5
|
executionRecordFromStorageRow,
|
|
5
6
|
executionRecordToStorageRow,
|
|
@@ -9,6 +10,8 @@ import type {
|
|
|
9
10
|
CreateExecutionInput,
|
|
10
11
|
ExecutionRecord,
|
|
11
12
|
ExecutionStorage,
|
|
13
|
+
ListSessionExecutionsInput,
|
|
14
|
+
SessionExecutions,
|
|
12
15
|
ShareSessionStorage,
|
|
13
16
|
} from "@sixb/core/storage"
|
|
14
17
|
import { ExecutionStorageError } from "@sixb/core/storage"
|
|
@@ -115,6 +118,59 @@ export class SqliteExecutionStorage implements ExecutionStorage {
|
|
|
115
118
|
|
|
116
119
|
return row ? sqliteExecutionRecord(row) : null
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
async listBySession(input: ListSessionExecutionsInput): Promise<SessionExecutions> {
|
|
123
|
+
const sessionColumn =
|
|
124
|
+
input.session.type === "user" ? "authority_session_id" : "authority_delegation_session_id"
|
|
125
|
+
|
|
126
|
+
// The queue is ordered by depth, so the walk takes roots, then their children level by level,
|
|
127
|
+
// the earliest first, and the limit stops it: a large tree costs no more than the rows
|
|
128
|
+
// returned. CROSS JOIN keeps the walk driven by the rows found: statistics taken on a small
|
|
129
|
+
// ledger choose to scan every child.
|
|
130
|
+
const rows = this.db
|
|
131
|
+
.query(
|
|
132
|
+
`
|
|
133
|
+
WITH RECURSIVE tree AS (
|
|
134
|
+
SELECT roots.*, 0 AS depth
|
|
135
|
+
FROM (
|
|
136
|
+
SELECT *
|
|
137
|
+
FROM executions
|
|
138
|
+
WHERE project_id = ?
|
|
139
|
+
AND ${sessionColumn} = ?
|
|
140
|
+
AND parent_execution_id IS NULL
|
|
141
|
+
AND created_at >= ?
|
|
142
|
+
AND created_at < ?
|
|
143
|
+
ORDER BY created_at, id
|
|
144
|
+
LIMIT ?
|
|
145
|
+
) roots
|
|
146
|
+
UNION ALL
|
|
147
|
+
SELECT child.*, tree.depth + 1
|
|
148
|
+
FROM tree
|
|
149
|
+
CROSS JOIN executions child
|
|
150
|
+
ON child.project_id = tree.project_id
|
|
151
|
+
AND child.parent_execution_id = tree.id
|
|
152
|
+
ORDER BY depth, created_at, id
|
|
153
|
+
LIMIT ?
|
|
154
|
+
)
|
|
155
|
+
SELECT * FROM tree
|
|
156
|
+
`
|
|
157
|
+
)
|
|
158
|
+
.all(
|
|
159
|
+
input.projectId,
|
|
160
|
+
input.session.id,
|
|
161
|
+
input.from.toISOString(),
|
|
162
|
+
input.to.toISOString(),
|
|
163
|
+
input.limit + 1,
|
|
164
|
+
input.limit + 1
|
|
165
|
+
) as SqliteExecutionRow[]
|
|
166
|
+
|
|
167
|
+
// Rows come out in walk order, so the first `limit` are the ones the contract keeps.
|
|
168
|
+
const kept = rows.slice(0, input.limit).map(sqliteExecutionRecord)
|
|
169
|
+
return {
|
|
170
|
+
executions: kept.sort(compareExecutionCreation),
|
|
171
|
+
truncated: rows.length > input.limit,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
118
174
|
}
|
|
119
175
|
|
|
120
176
|
/** An `executions` row as its record, for any store that reads executions alongside its own rows. */
|
package/src/index.ts
CHANGED
|
@@ -32,10 +32,12 @@ import {
|
|
|
32
32
|
StorageTransactionError,
|
|
33
33
|
} from "@sixb/core/storage"
|
|
34
34
|
import { SqliteActionRunStorage } from "./action-run-storage"
|
|
35
|
+
import { SqliteActivityStorage } from "./activity-storage"
|
|
35
36
|
import { SqliteAgentStorage } from "./agents"
|
|
36
37
|
import { SqliteAiCostStorage } from "./ai-cost-storage"
|
|
37
38
|
import { SqliteAiLimitStorage } from "./ai-limit-storage"
|
|
38
39
|
import { SqliteAiUsageStorage } from "./ai-usage-storage"
|
|
40
|
+
import { SqliteArtifactStorage } from "./artifact-storage"
|
|
39
41
|
import { SqliteAuthStorage } from "./auth-storage"
|
|
40
42
|
import { SqliteConnectorConnectionStorage } from "./connector-connection-storage"
|
|
41
43
|
import { SqliteExecutionStorage } from "./execution-storage"
|
|
@@ -89,6 +91,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
89
91
|
readonly auth: SqliteAuthStorage
|
|
90
92
|
readonly executions: SqliteExecutionStorage
|
|
91
93
|
readonly agents: SqliteAgentStorage
|
|
94
|
+
readonly artifacts: SqliteArtifactStorage
|
|
92
95
|
readonly aiUsage: AiUsageStorage
|
|
93
96
|
readonly aiCosts: AiCostStorage
|
|
94
97
|
readonly aiLimits: AiLimitStorage
|
|
@@ -106,6 +109,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
106
109
|
readonly connectorConnections: SqliteConnectorConnectionStorage
|
|
107
110
|
readonly fileUploadSessions: SqliteFileUploadSessionStorage
|
|
108
111
|
readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
|
|
112
|
+
readonly activity: SqliteActivityStorage
|
|
109
113
|
readonly migrators: readonly StorageMigrator[]
|
|
110
114
|
|
|
111
115
|
private readonly connection: SqliteStoreConnection
|
|
@@ -153,6 +157,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
153
157
|
this.auth = createAuthOperationScope(stores.auth, scope)
|
|
154
158
|
this.executions = createOperationScopedFacade(stores.executions, scope)
|
|
155
159
|
this.agents = createAgentOperationScope(stores.agents, scope)
|
|
160
|
+
this.artifacts = createOperationScopedFacade(stores.artifacts, scope)
|
|
156
161
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
|
|
157
162
|
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
|
|
158
163
|
this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
|
|
@@ -170,6 +175,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
170
175
|
this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope)
|
|
171
176
|
this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
|
|
172
177
|
this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope)
|
|
178
|
+
this.activity = createOperationScopedFacade(stores.activity, scope)
|
|
173
179
|
this.migrators = options.path ? createSqliteStorageMigrators(options.path) : []
|
|
174
180
|
registerSqliteStorageTestingAdapter(this, {
|
|
175
181
|
advanceConnectorConnectionTime: (durationMs) =>
|
|
@@ -317,6 +323,7 @@ function createSqliteStores(
|
|
|
317
323
|
auth,
|
|
318
324
|
executions,
|
|
319
325
|
agents: new SqliteAgentStorage({ connection, executions }),
|
|
326
|
+
artifacts: new SqliteArtifactStorage(connection),
|
|
320
327
|
aiUsage: new SqliteAiUsageStorage({ connection }),
|
|
321
328
|
aiCosts: new SqliteAiCostStorage(connection),
|
|
322
329
|
aiLimits: new SqliteAiLimitStorage({ connection }),
|
|
@@ -334,6 +341,7 @@ function createSqliteStores(
|
|
|
334
341
|
connectorConnections: new SqliteConnectorConnectionStorage(connection),
|
|
335
342
|
fileUploadSessions: new SqliteFileUploadSessionStorage(connection),
|
|
336
343
|
fileDownloadGrants: new SqliteFileDownloadGrantStorage(connection),
|
|
344
|
+
activity: new SqliteActivityStorage(connection),
|
|
337
345
|
}
|
|
338
346
|
}
|
|
339
347
|
|
|
@@ -343,6 +351,7 @@ interface SqliteStoreSet {
|
|
|
343
351
|
readonly auth: SqliteAuthStorage
|
|
344
352
|
readonly executions: SqliteExecutionStorage
|
|
345
353
|
readonly agents: SqliteAgentStorage
|
|
354
|
+
readonly artifacts: SqliteArtifactStorage
|
|
346
355
|
readonly aiUsage: SqliteAiUsageStorage
|
|
347
356
|
readonly aiCosts: SqliteAiCostStorage
|
|
348
357
|
readonly aiLimits: SqliteAiLimitStorage
|
|
@@ -360,6 +369,7 @@ interface SqliteStoreSet {
|
|
|
360
369
|
readonly connectorConnections: SqliteConnectorConnectionStorage
|
|
361
370
|
readonly fileUploadSessions: SqliteFileUploadSessionStorage
|
|
362
371
|
readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
|
|
372
|
+
readonly activity: SqliteActivityStorage
|
|
363
373
|
}
|
|
364
374
|
|
|
365
375
|
export { migrateSqliteStorage } from "./migrations"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
-- Artifacts: versioned documents, sheets, decks, and files. An artifact's kind never
|
|
2
|
+
-- changes. Versions take `next_version`, which only grows, so a number is never reused after its
|
|
3
|
+
-- version is deleted. Timestamps are ISO-8601 UTC text and compare lexicographically.
|
|
4
|
+
CREATE TABLE artifacts (
|
|
5
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
6
|
+
id TEXT NOT NULL CHECK (length(trim(id)) > 0),
|
|
7
|
+
kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'file')),
|
|
8
|
+
title TEXT NOT NULL CHECK (length(trim(title)) > 0),
|
|
9
|
+
owner_principal_type TEXT NOT NULL
|
|
10
|
+
CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
11
|
+
owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
|
|
12
|
+
latest_version INTEGER NOT NULL CHECK (latest_version > 0),
|
|
13
|
+
next_version INTEGER NOT NULL CHECK (next_version > latest_version),
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
updated_at TEXT NOT NULL,
|
|
16
|
+
PRIMARY KEY (project_id, id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
|
|
20
|
+
CREATE INDEX idx_artifacts_owner_updated
|
|
21
|
+
ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
|
|
22
|
+
|
|
23
|
+
-- `refs`, not `references`: that word is reserved in SQL.
|
|
24
|
+
CREATE TABLE artifact_versions (
|
|
25
|
+
project_id TEXT NOT NULL,
|
|
26
|
+
artifact_id TEXT NOT NULL,
|
|
27
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
28
|
+
title TEXT NOT NULL CHECK (length(trim(title)) > 0),
|
|
29
|
+
entry TEXT NOT NULL CHECK (length(entry) > 0),
|
|
30
|
+
files TEXT NOT NULL CHECK (json_type(files) = 'array'),
|
|
31
|
+
outline TEXT NOT NULL CHECK (json_type(outline) = 'object'),
|
|
32
|
+
refs TEXT NOT NULL CHECK (json_type(refs) = 'array'),
|
|
33
|
+
author_principal_type TEXT NOT NULL
|
|
34
|
+
CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
35
|
+
author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
|
|
36
|
+
origin_thread_id TEXT,
|
|
37
|
+
origin_run_id TEXT,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
PRIMARY KEY (project_id, artifact_id, version),
|
|
40
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
|
|
41
|
+
CHECK ((origin_thread_id IS NULL) = (origin_run_id IS NULL))
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE INDEX idx_artifact_versions_origin_run
|
|
45
|
+
ON artifact_versions (project_id, origin_run_id) WHERE origin_run_id IS NOT NULL;
|
|
46
|
+
CREATE INDEX idx_artifact_versions_origin_thread
|
|
47
|
+
ON artifact_versions (project_id, origin_thread_id, artifact_id) WHERE origin_thread_id IS NOT NULL;
|
|
48
|
+
|
|
49
|
+
-- Short-lived links that a sandboxed frame loads a deck's files through. Only the SHA-256 of each
|
|
50
|
+
-- link's token is stored. `targets` is what the frame may show from other artifacts, decided for
|
|
51
|
+
-- its reader when the link was issued: a JSON array of { address, artifactId, version }.
|
|
52
|
+
CREATE TABLE artifact_frame_grants (
|
|
53
|
+
project_id TEXT NOT NULL,
|
|
54
|
+
token_hash TEXT NOT NULL CHECK (
|
|
55
|
+
length(token_hash) = 64
|
|
56
|
+
AND token_hash = lower(token_hash)
|
|
57
|
+
AND token_hash NOT GLOB '*[^0-9a-f]*'
|
|
58
|
+
),
|
|
59
|
+
artifact_id TEXT NOT NULL,
|
|
60
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
61
|
+
targets TEXT NOT NULL CHECK (json_type(targets) = 'array'),
|
|
62
|
+
created_at TEXT NOT NULL,
|
|
63
|
+
expires_at TEXT NOT NULL,
|
|
64
|
+
PRIMARY KEY (project_id, token_hash),
|
|
65
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
|
|
66
|
+
CHECK (expires_at > created_at)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX idx_artifact_frame_grants_expiry ON artifact_frame_grants (project_id, expires_at);
|
|
70
|
+
-- Deleting an artifact cascades here.
|
|
71
|
+
CREATE INDEX idx_artifact_frame_grants_artifact ON artifact_frame_grants (project_id, artifact_id);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- The Agent delivers files by publishing artifacts, so a run no longer collects an output
|
|
2
|
+
-- directory or reports on it. Stored `output_*` diagnostics go; a run left with none stores none.
|
|
3
|
+
UPDATE agent_runs
|
|
4
|
+
SET diagnostics = (
|
|
5
|
+
SELECT CASE WHEN count(*) = 0 THEN NULL ELSE json_group_array(json(value)) END
|
|
6
|
+
FROM (
|
|
7
|
+
SELECT value
|
|
8
|
+
FROM json_each(agent_runs.diagnostics)
|
|
9
|
+
WHERE json_extract(value, '$.code') NOT LIKE 'output\_%' ESCAPE '\'
|
|
10
|
+
ORDER BY key
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
WHERE diagnostics IS NOT NULL
|
|
14
|
+
AND json_valid(diagnostics)
|
|
15
|
+
AND EXISTS (
|
|
16
|
+
SELECT 1
|
|
17
|
+
FROM json_each(agent_runs.diagnostics)
|
|
18
|
+
WHERE json_extract(value, '$.code') LIKE 'output\_%' ESCAPE '\'
|
|
19
|
+
);
|