@sixb/sqlite 0.1.20 → 0.1.22
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/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +51 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +21 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +14 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +16 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts +4 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/artifact-storage.d.ts +31 -0
- package/dist/artifact-storage.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +7 -3
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -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 +5216 -2517
- package/dist/index.js.map +39 -31
- package/dist/migrations.d.ts +0 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +8 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +26 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +15 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +6 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +2 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.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/ontology-storage/unique-values.d.ts +42 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- 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/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +456 -0
- package/src/agents/checkpoints.ts +9 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +195 -0
- package/src/agents/messages.ts +21 -18
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +202 -21
- package/src/agents/settings.ts +112 -0
- package/src/agents/threads.ts +412 -1
- package/src/ai-cost-groups.ts +11 -1
- package/src/ai-cost-storage.ts +54 -12
- package/src/ai-usage-storage.ts +79 -2
- package/src/artifact-storage.ts +553 -0
- package/src/auth-storage/auth-storage.ts +58 -26
- package/src/auth-storage/sessions.ts +27 -0
- package/src/auth-storage/users.ts +106 -60
- package/src/execution-storage.ts +95 -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/067-artifact-kinds.sql +32 -0
- package/src/migrations/068-artifact-state.sql +15 -0
- package/src/migrations/069-agent-thread-participants.sql +16 -0
- package/src/migrations/070-forked-context-checkpoints.sql +45 -0
- package/src/migrations/071-unique-values.sql +22 -0
- package/src/migrations/072-generated-media-usage.sql +32 -0
- package/src/migrations/073-model-call-files.sql +11 -0
- package/src/migrations/074-agent-memory.sql +32 -0
- package/src/migrations/075-artifact-threads.sql +21 -0
- package/src/migrations/076-agent-approvals.sql +59 -0
- package/src/migrations/077-agent-approval-resume.sql +20 -0
- package/src/migrations/078-subagent-threads.sql +241 -0
- package/src/migrations/079-subagent-questions.sql +6 -0
- package/src/migrations.ts +89 -1
- package/src/objects/README.md +7 -0
- package/src/objects/conditional-reader.ts +27 -0
- package/src/objects/query-compiler.ts +36 -6
- package/src/objects/read-scope.ts +140 -23
- package/src/objects/reader.ts +47 -14
- package/src/objects/scoped-reader.ts +103 -0
- package/src/objects/selected-reader.ts +12 -75
- package/src/objects/storage.ts +16 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-writer.ts +14 -0
- package/src/ontology-storage/materializations.ts +16 -1
- package/src/ontology-storage/sources.ts +41 -0
- package/src/ontology-storage/unique-values.ts +187 -0
- package/src/share-grant-storage.ts +25 -0
- package/src/transactions.ts +1 -1
- package/src/workflow-run-storage.ts +27 -0
package/src/ai-usage-storage.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FileRef } from "@sixb/core/blob-storage/server"
|
|
2
|
+
import {
|
|
3
|
+
assertAiUsageExecutionId,
|
|
4
|
+
assertSameCallExecution,
|
|
5
|
+
normalizeAiModelCallFilesRecord,
|
|
6
|
+
normalizeAiModelCallRecord,
|
|
7
|
+
} from "@sixb/core/internal/storage"
|
|
2
8
|
import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
|
|
3
9
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
4
10
|
import {
|
|
11
|
+
type AiModelCallFilesRecord,
|
|
5
12
|
type AiModelCallUsageInput,
|
|
6
13
|
type AiModelCallUsageRecord,
|
|
7
14
|
type AiUsageExecutionSummary,
|
|
8
15
|
type AiUsageStorage,
|
|
9
16
|
AiUsageStorageError,
|
|
10
17
|
aggregateAiModelCallUsage,
|
|
18
|
+
type GetAiModelCallFilesInput,
|
|
11
19
|
type GetLatestAiModelCallForExecutionInput,
|
|
20
|
+
type RecordAiModelCallFilesInput,
|
|
12
21
|
type RecordAiModelCallInput,
|
|
13
22
|
type RecordAiModelCallResult,
|
|
14
23
|
type SummarizeAiUsageExecutionInput,
|
|
@@ -110,10 +119,54 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
110
119
|
return aggregateExecutionRows(input.executionIds, rows)
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
async recordModelCallFiles(input: RecordAiModelCallFilesInput): Promise<AiModelCallFilesRecord> {
|
|
123
|
+
const record = normalizeAiModelCallFilesRecord(input)
|
|
124
|
+
return runImmediateTransaction(this.connection.db, () => {
|
|
125
|
+
const existing = this.findCallFiles(record.projectId, record.callId)
|
|
126
|
+
if (existing) return assertSameCallExecution(existing, record)
|
|
127
|
+
this.assertExecutionExists(record.projectId, record.executionId)
|
|
128
|
+
this.connection.db
|
|
129
|
+
.query(
|
|
130
|
+
`
|
|
131
|
+
INSERT INTO ai_model_call_files (project_id, call_id, execution_id, files, recorded_at)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?)
|
|
133
|
+
`
|
|
134
|
+
)
|
|
135
|
+
.run(
|
|
136
|
+
record.projectId,
|
|
137
|
+
record.callId,
|
|
138
|
+
record.executionId,
|
|
139
|
+
JSON.stringify(record.files),
|
|
140
|
+
record.recordedAt.toISOString()
|
|
141
|
+
)
|
|
142
|
+
return record
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getModelCallFiles(input: GetAiModelCallFilesInput): Promise<AiModelCallFilesRecord | null> {
|
|
147
|
+
assertNonBlankProjectId(input.projectId)
|
|
148
|
+
return this.findCallFiles(input.projectId, input.callId)
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
close(): void {
|
|
114
152
|
closeSqliteStoreConnection(this.connection)
|
|
115
153
|
}
|
|
116
154
|
|
|
155
|
+
private findCallFiles(projectId: string, callId: string): AiModelCallFilesRecord | null {
|
|
156
|
+
const row = this.connection.db
|
|
157
|
+
.query("SELECT * FROM ai_model_call_files WHERE project_id = ? AND call_id = ?")
|
|
158
|
+
.get(projectId, callId) as CallFilesRow | null
|
|
159
|
+
return row
|
|
160
|
+
? {
|
|
161
|
+
projectId: row.project_id,
|
|
162
|
+
executionId: row.execution_id,
|
|
163
|
+
callId: row.call_id,
|
|
164
|
+
files: JSON.parse(row.files) as FileRef[],
|
|
165
|
+
recordedAt: new Date(row.recorded_at),
|
|
166
|
+
}
|
|
167
|
+
: null
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
private insertRecord(record: AiModelCallUsageRecord): void {
|
|
118
171
|
this.connection.db
|
|
119
172
|
.query(
|
|
@@ -140,12 +193,16 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
140
193
|
text_output_tokens,
|
|
141
194
|
reasoning_output_tokens,
|
|
142
195
|
audio_duration_ms,
|
|
196
|
+
image_output_tokens,
|
|
197
|
+
audio_output_tokens,
|
|
198
|
+
output_images,
|
|
199
|
+
input_characters,
|
|
143
200
|
reporting_status,
|
|
144
201
|
raw_usage,
|
|
145
202
|
occurred_at,
|
|
146
203
|
recorded_at
|
|
147
204
|
) VALUES (
|
|
148
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
205
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
149
206
|
)
|
|
150
207
|
`
|
|
151
208
|
)
|
|
@@ -171,6 +228,10 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
171
228
|
record.usage.textOutputTokens ?? null,
|
|
172
229
|
record.usage.reasoningOutputTokens ?? null,
|
|
173
230
|
record.usage.audioDurationMs ?? null,
|
|
231
|
+
record.usage.imageOutputTokens ?? null,
|
|
232
|
+
record.usage.audioOutputTokens ?? null,
|
|
233
|
+
record.usage.outputImages ?? null,
|
|
234
|
+
record.usage.inputCharacters ?? null,
|
|
174
235
|
record.usage.reportingStatus,
|
|
175
236
|
record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage),
|
|
176
237
|
record.occurredAt.toISOString(),
|
|
@@ -285,6 +346,14 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
285
346
|
}
|
|
286
347
|
}
|
|
287
348
|
|
|
349
|
+
interface CallFilesRow {
|
|
350
|
+
readonly project_id: string
|
|
351
|
+
readonly call_id: string
|
|
352
|
+
readonly execution_id: string
|
|
353
|
+
readonly files: string
|
|
354
|
+
readonly recorded_at: string
|
|
355
|
+
}
|
|
356
|
+
|
|
288
357
|
interface AiUsageRow {
|
|
289
358
|
readonly project_id: string
|
|
290
359
|
readonly id: string
|
|
@@ -307,6 +376,10 @@ interface AiUsageRow {
|
|
|
307
376
|
readonly text_output_tokens: number | null
|
|
308
377
|
readonly reasoning_output_tokens: number | null
|
|
309
378
|
readonly audio_duration_ms: number | null
|
|
379
|
+
readonly image_output_tokens: number | null
|
|
380
|
+
readonly audio_output_tokens: number | null
|
|
381
|
+
readonly output_images: number | null
|
|
382
|
+
readonly input_characters: number | null
|
|
310
383
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
311
384
|
readonly raw_usage: string | null
|
|
312
385
|
readonly occurred_at: string
|
|
@@ -349,6 +422,10 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
|
|
|
349
422
|
? {}
|
|
350
423
|
: { reasoningOutputTokens: row.reasoning_output_tokens }),
|
|
351
424
|
...(row.audio_duration_ms === null ? {} : { audioDurationMs: row.audio_duration_ms }),
|
|
425
|
+
...(row.image_output_tokens === null ? {} : { imageOutputTokens: row.image_output_tokens }),
|
|
426
|
+
...(row.audio_output_tokens === null ? {} : { audioOutputTokens: row.audio_output_tokens }),
|
|
427
|
+
...(row.output_images === null ? {} : { outputImages: row.output_images }),
|
|
428
|
+
...(row.input_characters === null ? {} : { inputCharacters: row.input_characters }),
|
|
352
429
|
}
|
|
353
430
|
}
|
|
354
431
|
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import type { Database, SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
artifactStateBytes,
|
|
4
|
+
artifactStorageErrors,
|
|
5
|
+
checkArtifactStateWrite,
|
|
6
|
+
parseArtifactFrameGrantRow,
|
|
7
|
+
parseArtifactRow,
|
|
8
|
+
parseArtifactStateRow,
|
|
9
|
+
parseArtifactVersionRow,
|
|
10
|
+
} from "@sixb/core/internal/storage"
|
|
11
|
+
import type {
|
|
12
|
+
ArtifactFrameGrantRecord,
|
|
13
|
+
ArtifactRecord,
|
|
14
|
+
ArtifactStateEntry,
|
|
15
|
+
ArtifactStateKeyInput,
|
|
16
|
+
ArtifactStateScope,
|
|
17
|
+
ArtifactStorage,
|
|
18
|
+
ArtifactVersionInput,
|
|
19
|
+
ArtifactVersionRecord,
|
|
20
|
+
CopyArtifactInput,
|
|
21
|
+
CreateArtifactInput,
|
|
22
|
+
DeleteArtifactInput,
|
|
23
|
+
DeleteArtifactVersionsByOriginInput,
|
|
24
|
+
DeleteExpiredArtifactFrameGrantsInput,
|
|
25
|
+
FindArtifactFrameGrantInput,
|
|
26
|
+
GetArtifactInput,
|
|
27
|
+
GetArtifactVersionInput,
|
|
28
|
+
ListArtifactsInput,
|
|
29
|
+
ListArtifactsResult,
|
|
30
|
+
ListArtifactVersionsInput,
|
|
31
|
+
ListArtifactVersionsResult,
|
|
32
|
+
ListReferencingVersionsInput,
|
|
33
|
+
PublishArtifactVersionInput,
|
|
34
|
+
PublishedArtifactVersion,
|
|
35
|
+
SetArtifactStateInput,
|
|
36
|
+
} from "@sixb/core/storage"
|
|
37
|
+
import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
|
|
38
|
+
|
|
39
|
+
const ARTIFACT_COLUMNS = `
|
|
40
|
+
project_id AS projectId, id, kind, title,
|
|
41
|
+
owner_principal_type AS ownerPrincipalType, owner_principal_id AS ownerPrincipalId,
|
|
42
|
+
latest_version AS latestVersion, created_at AS createdAt, updated_at AS updatedAt
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
const VERSION_COLUMNS = `
|
|
46
|
+
project_id AS projectId, artifact_id AS artifactId, version, title, entry, files, outline, refs,
|
|
47
|
+
author_principal_type AS authorPrincipalType, author_principal_id AS authorPrincipalId,
|
|
48
|
+
origin_thread_id AS originThreadId, origin_run_id AS originRunId, created_at AS createdAt
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
const FRAME_GRANT_COLUMNS = `
|
|
52
|
+
project_id AS projectId, token_hash AS tokenHash, artifact_id AS artifactId, version, targets,
|
|
53
|
+
created_at AS createdAt, expires_at AS expiresAt
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
const STATE_COLUMNS = "key, value, size_bytes AS sizeBytes, updated_at AS updatedAt"
|
|
57
|
+
|
|
58
|
+
/** One reader of one artifact: the bindings that follow `STATE_SCOPE`. */
|
|
59
|
+
const STATE_SCOPE = "project_id = ? AND artifact_id = ? AND principal_type = ? AND principal_id = ?"
|
|
60
|
+
|
|
61
|
+
function stateScope(input: ArtifactStateScope): SQLQueryBindings[] {
|
|
62
|
+
return [input.projectId, input.artifactId, input.principal.type, input.principal.id]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type Row = Record<string, unknown>
|
|
66
|
+
|
|
67
|
+
export class SqliteArtifactStorage implements ArtifactStorage {
|
|
68
|
+
private readonly db: Database
|
|
69
|
+
|
|
70
|
+
constructor(connection: SqliteStoreConnection) {
|
|
71
|
+
this.db = connection.db
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
|
|
75
|
+
return runImmediateTransaction(this.db, () => {
|
|
76
|
+
const createdAt = input.version.createdAt.toISOString()
|
|
77
|
+
const artifact = this.db
|
|
78
|
+
.query(
|
|
79
|
+
`INSERT INTO artifacts (
|
|
80
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
81
|
+
latest_version, next_version, created_at, updated_at
|
|
82
|
+
) VALUES (?, ?, ?, ?, ?, ?, 1, 2, ?, ?)
|
|
83
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
84
|
+
RETURNING ${ARTIFACT_COLUMNS}`
|
|
85
|
+
)
|
|
86
|
+
.get(
|
|
87
|
+
input.projectId,
|
|
88
|
+
input.id,
|
|
89
|
+
input.kind,
|
|
90
|
+
input.version.title,
|
|
91
|
+
input.owner.type,
|
|
92
|
+
input.owner.id,
|
|
93
|
+
createdAt,
|
|
94
|
+
createdAt
|
|
95
|
+
) as Row | null
|
|
96
|
+
if (!artifact) throw artifactStorageErrors.exists(input.id)
|
|
97
|
+
return {
|
|
98
|
+
artifact: parseArtifactRow(artifact),
|
|
99
|
+
version: this.insertVersion(input.projectId, input.id, 1, input.version),
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
|
|
105
|
+
return runImmediateTransaction(this.db, () => {
|
|
106
|
+
const current = this.db
|
|
107
|
+
.query(
|
|
108
|
+
`SELECT kind, next_version AS nextVersion, latest_version AS latestVersion
|
|
109
|
+
FROM artifacts WHERE project_id = ? AND id = ?`
|
|
110
|
+
)
|
|
111
|
+
.get(input.projectId, input.artifactId) as {
|
|
112
|
+
readonly kind: ArtifactRecord["kind"]
|
|
113
|
+
readonly nextVersion: number
|
|
114
|
+
readonly latestVersion: number
|
|
115
|
+
} | null
|
|
116
|
+
if (!current) throw artifactStorageErrors.notFound(input.artifactId)
|
|
117
|
+
if (current.kind !== input.kind) {
|
|
118
|
+
throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
|
|
119
|
+
}
|
|
120
|
+
// Inside the write transaction: a publish from the same base after this one finds it moved.
|
|
121
|
+
if (input.baseVersion !== undefined && current.latestVersion !== input.baseVersion) {
|
|
122
|
+
const latest = parseArtifactVersionRow(
|
|
123
|
+
this.db
|
|
124
|
+
.query(
|
|
125
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions
|
|
126
|
+
WHERE project_id = ? AND artifact_id = ? AND version = ?`
|
|
127
|
+
)
|
|
128
|
+
.get(input.projectId, input.artifactId, current.latestVersion) as Row
|
|
129
|
+
)
|
|
130
|
+
throw artifactStorageErrors.moved(input.artifactId, input.baseVersion, {
|
|
131
|
+
version: latest.version,
|
|
132
|
+
author: latest.author,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
const version = this.insertVersion(
|
|
136
|
+
input.projectId,
|
|
137
|
+
input.artifactId,
|
|
138
|
+
current.nextVersion,
|
|
139
|
+
input.version
|
|
140
|
+
)
|
|
141
|
+
const artifact = this.db
|
|
142
|
+
.query(
|
|
143
|
+
`UPDATE artifacts
|
|
144
|
+
SET title = ?, latest_version = ?, next_version = ?, updated_at = ?
|
|
145
|
+
WHERE project_id = ? AND id = ?
|
|
146
|
+
RETURNING ${ARTIFACT_COLUMNS}`
|
|
147
|
+
)
|
|
148
|
+
.get(
|
|
149
|
+
input.version.title,
|
|
150
|
+
version.version,
|
|
151
|
+
version.version + 1,
|
|
152
|
+
input.version.createdAt.toISOString(),
|
|
153
|
+
input.projectId,
|
|
154
|
+
input.artifactId
|
|
155
|
+
) as Row
|
|
156
|
+
return { artifact: parseArtifactRow(artifact), version }
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
|
|
161
|
+
const row = this.db
|
|
162
|
+
.query(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE project_id = ? AND id = ?`)
|
|
163
|
+
.get(input.projectId, input.id) as Row | null
|
|
164
|
+
return row ? parseArtifactRow(row) : null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
|
|
168
|
+
const where = ["a.project_id = ?"]
|
|
169
|
+
const params: SQLQueryBindings[] = [input.projectId]
|
|
170
|
+
if (input.owner) {
|
|
171
|
+
where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
|
|
172
|
+
params.push(input.owner.type, input.owner.id)
|
|
173
|
+
}
|
|
174
|
+
if (input.threadId !== undefined) {
|
|
175
|
+
where.push(
|
|
176
|
+
`EXISTS (
|
|
177
|
+
SELECT 1 FROM artifact_threads t
|
|
178
|
+
WHERE t.project_id = a.project_id AND t.artifact_id = a.id AND t.thread_id = ?
|
|
179
|
+
)`
|
|
180
|
+
)
|
|
181
|
+
params.push(input.threadId)
|
|
182
|
+
}
|
|
183
|
+
if (input.kind !== undefined) {
|
|
184
|
+
where.push("a.kind = ?")
|
|
185
|
+
params.push(input.kind)
|
|
186
|
+
}
|
|
187
|
+
const clause = where.join(" AND ")
|
|
188
|
+
const { total } = this.db
|
|
189
|
+
.query(`SELECT COUNT(*) AS total FROM artifacts a WHERE ${clause}`)
|
|
190
|
+
.get(...params) as { readonly total: number }
|
|
191
|
+
const offset = input.offset ?? 0
|
|
192
|
+
const rows = this.db
|
|
193
|
+
.query(
|
|
194
|
+
`SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
|
|
195
|
+
ORDER BY a.updated_at DESC, a.id ASC LIMIT ? OFFSET ?`
|
|
196
|
+
)
|
|
197
|
+
.all(...params, input.limit ?? -1, offset) as Row[]
|
|
198
|
+
return {
|
|
199
|
+
artifacts: rows.map(parseArtifactRow),
|
|
200
|
+
hasMore: offset + rows.length < total,
|
|
201
|
+
total,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
|
|
206
|
+
const row = this.db
|
|
207
|
+
.query(
|
|
208
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions
|
|
209
|
+
WHERE project_id = ? AND artifact_id = ? AND version = ?`
|
|
210
|
+
)
|
|
211
|
+
.get(input.projectId, input.artifactId, input.version) as Row | null
|
|
212
|
+
return row ? parseArtifactVersionRow(row) : null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
|
|
216
|
+
const { total } = this.db
|
|
217
|
+
.query(
|
|
218
|
+
"SELECT COUNT(*) AS total FROM artifact_versions WHERE project_id = ? AND artifact_id = ?"
|
|
219
|
+
)
|
|
220
|
+
.get(input.projectId, input.artifactId) as { readonly total: number }
|
|
221
|
+
const offset = input.offset ?? 0
|
|
222
|
+
const rows = this.db
|
|
223
|
+
.query(
|
|
224
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions
|
|
225
|
+
WHERE project_id = ? AND artifact_id = ?
|
|
226
|
+
ORDER BY version DESC LIMIT ? OFFSET ?`
|
|
227
|
+
)
|
|
228
|
+
.all(input.projectId, input.artifactId, input.limit ?? -1, offset) as Row[]
|
|
229
|
+
return {
|
|
230
|
+
versions: rows.map(parseArtifactVersionRow),
|
|
231
|
+
hasMore: offset + rows.length < total,
|
|
232
|
+
total,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async listReferencingVersions(
|
|
237
|
+
input: ListReferencingVersionsInput
|
|
238
|
+
): Promise<ArtifactVersionRecord[]> {
|
|
239
|
+
// An address names the artifact when the id is followed by nothing, a version, a file, or a
|
|
240
|
+
// part.
|
|
241
|
+
const prefix = `artifact:${input.artifactId}`
|
|
242
|
+
const rows = this.db
|
|
243
|
+
.query(
|
|
244
|
+
`SELECT ${VERSION_COLUMNS} FROM artifact_versions v
|
|
245
|
+
WHERE (project_id, artifact_id, version) IN (
|
|
246
|
+
SELECT project_id, id, latest_version FROM artifacts WHERE project_id = ?
|
|
247
|
+
) AND EXISTS (
|
|
248
|
+
SELECT 1 FROM json_each(v.refs) r
|
|
249
|
+
WHERE substr(json_extract(r.value, '$.address'), 1, ?) = ?
|
|
250
|
+
AND substr(json_extract(r.value, '$.address'), ? + 1, 1) IN ('', '@', '/', '#')
|
|
251
|
+
)
|
|
252
|
+
ORDER BY artifact_id`
|
|
253
|
+
)
|
|
254
|
+
.all(input.projectId, prefix.length, prefix, prefix.length) as Row[]
|
|
255
|
+
return rows.map(parseArtifactVersionRow)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async listThreadIds(input: GetArtifactInput): Promise<string[]> {
|
|
259
|
+
const rows = this.db
|
|
260
|
+
.query(
|
|
261
|
+
`SELECT thread_id AS threadId FROM artifact_threads
|
|
262
|
+
WHERE project_id = ? AND artifact_id = ? ORDER BY thread_id`
|
|
263
|
+
)
|
|
264
|
+
.all(input.projectId, input.id) as { readonly threadId: string }[]
|
|
265
|
+
return rows.map((row) => row.threadId)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async copy(input: CopyArtifactInput): Promise<ArtifactRecord> {
|
|
269
|
+
return runImmediateTransaction(this.db, () => {
|
|
270
|
+
const source = this.db
|
|
271
|
+
.query("SELECT id FROM artifacts WHERE project_id = ? AND id = ?")
|
|
272
|
+
.get(input.projectId, input.sourceId)
|
|
273
|
+
if (!source) throw artifactStorageErrors.notFound(input.sourceId)
|
|
274
|
+
|
|
275
|
+
const artifact = this.db
|
|
276
|
+
.query(
|
|
277
|
+
`INSERT INTO artifacts (
|
|
278
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
279
|
+
latest_version, next_version, created_at, updated_at
|
|
280
|
+
)
|
|
281
|
+
SELECT project_id, ?, kind, title, ?, ?, latest_version, next_version, created_at,
|
|
282
|
+
updated_at
|
|
283
|
+
FROM artifacts WHERE project_id = ? AND id = ?
|
|
284
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
285
|
+
RETURNING ${ARTIFACT_COLUMNS}`
|
|
286
|
+
)
|
|
287
|
+
.get(
|
|
288
|
+
input.id,
|
|
289
|
+
input.owner.type,
|
|
290
|
+
input.owner.id,
|
|
291
|
+
input.projectId,
|
|
292
|
+
input.sourceId
|
|
293
|
+
) as Row | null
|
|
294
|
+
if (!artifact) throw artifactStorageErrors.exists(input.id)
|
|
295
|
+
|
|
296
|
+
this.db
|
|
297
|
+
.query(
|
|
298
|
+
`INSERT INTO artifact_versions (
|
|
299
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
300
|
+
author_principal_type, author_principal_id, created_at
|
|
301
|
+
)
|
|
302
|
+
SELECT project_id, ?, version, title, entry, files, outline, refs,
|
|
303
|
+
author_principal_type, author_principal_id, created_at
|
|
304
|
+
FROM artifact_versions WHERE project_id = ? AND artifact_id = ?`
|
|
305
|
+
)
|
|
306
|
+
.run(input.id, input.projectId, input.sourceId)
|
|
307
|
+
this.db
|
|
308
|
+
.query(
|
|
309
|
+
`INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at, copied)
|
|
310
|
+
VALUES (?, ?, ?, ?, 1)`
|
|
311
|
+
)
|
|
312
|
+
.run(
|
|
313
|
+
input.projectId,
|
|
314
|
+
input.id,
|
|
315
|
+
input.threadId,
|
|
316
|
+
(input.copiedAt ?? new Date()).toISOString()
|
|
317
|
+
)
|
|
318
|
+
return parseArtifactRow(artifact)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
|
|
323
|
+
runImmediateTransaction(this.db, () => {
|
|
324
|
+
const exists = this.db
|
|
325
|
+
.query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
|
|
326
|
+
.get(record.projectId, record.artifactId)
|
|
327
|
+
if (!exists) throw artifactStorageErrors.notFound(record.artifactId)
|
|
328
|
+
this.db
|
|
329
|
+
.query(
|
|
330
|
+
`INSERT INTO artifact_frame_grants
|
|
331
|
+
(project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
|
|
332
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
333
|
+
)
|
|
334
|
+
.run(
|
|
335
|
+
record.projectId,
|
|
336
|
+
record.tokenHash,
|
|
337
|
+
record.artifactId,
|
|
338
|
+
record.version,
|
|
339
|
+
JSON.stringify(record.targets),
|
|
340
|
+
record.createdAt.toISOString(),
|
|
341
|
+
record.expiresAt.toISOString()
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async findFrameGrant(
|
|
347
|
+
input: FindArtifactFrameGrantInput
|
|
348
|
+
): Promise<ArtifactFrameGrantRecord | null> {
|
|
349
|
+
const row = this.db
|
|
350
|
+
.query(
|
|
351
|
+
`SELECT ${FRAME_GRANT_COLUMNS} FROM artifact_frame_grants
|
|
352
|
+
WHERE project_id = ? AND token_hash = ?`
|
|
353
|
+
)
|
|
354
|
+
.get(input.projectId, input.tokenHash) as Row | null
|
|
355
|
+
return row ? parseArtifactFrameGrantRow(row) : null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
|
|
359
|
+
return this.db
|
|
360
|
+
.query(
|
|
361
|
+
`DELETE FROM artifact_frame_grants WHERE rowid IN (
|
|
362
|
+
SELECT rowid FROM artifact_frame_grants
|
|
363
|
+
WHERE project_id = ? AND expires_at < ?
|
|
364
|
+
ORDER BY expires_at LIMIT ?
|
|
365
|
+
)`
|
|
366
|
+
)
|
|
367
|
+
.run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async getState(input: ArtifactStateKeyInput): Promise<ArtifactStateEntry | null> {
|
|
371
|
+
const row = this.db
|
|
372
|
+
.query(`SELECT ${STATE_COLUMNS} FROM artifact_state WHERE ${STATE_SCOPE} AND key = ?`)
|
|
373
|
+
.get(...stateScope(input), input.key) as Row | null
|
|
374
|
+
return row ? parseArtifactStateRow(row) : null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async setState(input: SetArtifactStateInput): Promise<void> {
|
|
378
|
+
runImmediateTransaction(this.db, () => {
|
|
379
|
+
const artifact = this.db
|
|
380
|
+
.query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
|
|
381
|
+
.get(input.projectId, input.artifactId)
|
|
382
|
+
if (!artifact) throw artifactStorageErrors.notFound(input.artifactId)
|
|
383
|
+
// In the write transaction: two writes at once cannot both fit under the limits.
|
|
384
|
+
const others = this.db
|
|
385
|
+
.query(
|
|
386
|
+
`SELECT count(*) AS keys, coalesce(sum(size_bytes), 0) AS bytes FROM artifact_state
|
|
387
|
+
WHERE ${STATE_SCOPE} AND key <> ?`
|
|
388
|
+
)
|
|
389
|
+
.get(...stateScope(input), input.key) as { readonly keys: number; readonly bytes: number }
|
|
390
|
+
const sizeBytes = artifactStateBytes(input.value)
|
|
391
|
+
checkArtifactStateWrite({ key: input.key, valueBytes: sizeBytes, others })
|
|
392
|
+
this.db
|
|
393
|
+
.query(
|
|
394
|
+
`INSERT INTO artifact_state (
|
|
395
|
+
project_id, artifact_id, principal_type, principal_id, key, value, size_bytes,
|
|
396
|
+
updated_at
|
|
397
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
398
|
+
ON CONFLICT (project_id, artifact_id, principal_type, principal_id, key) DO UPDATE SET
|
|
399
|
+
value = excluded.value, size_bytes = excluded.size_bytes,
|
|
400
|
+
updated_at = excluded.updated_at`
|
|
401
|
+
)
|
|
402
|
+
.run(...stateScope(input), input.key, input.value, sizeBytes, input.updatedAt.toISOString())
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async deleteState(input: ArtifactStateKeyInput): Promise<boolean> {
|
|
407
|
+
return (
|
|
408
|
+
this.db
|
|
409
|
+
.query(`DELETE FROM artifact_state WHERE ${STATE_SCOPE} AND key = ?`)
|
|
410
|
+
.run(...stateScope(input), input.key).changes > 0
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async listState(input: ArtifactStateScope): Promise<ArtifactStateEntry[]> {
|
|
415
|
+
const rows = this.db
|
|
416
|
+
.query(`SELECT ${STATE_COLUMNS} FROM artifact_state WHERE ${STATE_SCOPE} ORDER BY key`)
|
|
417
|
+
.all(...stateScope(input)) as Row[]
|
|
418
|
+
return rows.map(parseArtifactStateRow)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async clearState(input: ArtifactStateScope): Promise<number> {
|
|
422
|
+
return this.db
|
|
423
|
+
.query(`DELETE FROM artifact_state WHERE ${STATE_SCOPE}`)
|
|
424
|
+
.run(...stateScope(input)).changes
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async delete(input: DeleteArtifactInput): Promise<boolean> {
|
|
428
|
+
// Versions, frame grants, and state go with the artifact through ON DELETE CASCADE.
|
|
429
|
+
return (
|
|
430
|
+
this.db
|
|
431
|
+
.query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
|
|
432
|
+
.run(input.projectId, input.id).changes > 0
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
|
|
437
|
+
return runImmediateTransaction(this.db, () => {
|
|
438
|
+
const affected = this.db
|
|
439
|
+
.query(
|
|
440
|
+
`SELECT DISTINCT artifact_id AS artifactId FROM artifact_versions
|
|
441
|
+
WHERE project_id = ? AND origin_run_id = ?`
|
|
442
|
+
)
|
|
443
|
+
.all(input.projectId, input.runId) as { readonly artifactId: string }[]
|
|
444
|
+
const deleted = this.db
|
|
445
|
+
.query(
|
|
446
|
+
`DELETE FROM artifact_versions WHERE project_id = ? AND origin_run_id = ?
|
|
447
|
+
RETURNING origin_thread_id AS threadId`
|
|
448
|
+
)
|
|
449
|
+
.all(input.projectId, input.runId) as { readonly threadId: string }[]
|
|
450
|
+
// A run publishes from one thread: its artifacts stay shared there only while a version
|
|
451
|
+
// from it remains, or when they were copied for it.
|
|
452
|
+
const threadId = deleted[0]?.threadId
|
|
453
|
+
for (const { artifactId } of affected) {
|
|
454
|
+
this.db
|
|
455
|
+
.query(
|
|
456
|
+
`DELETE FROM artifact_threads
|
|
457
|
+
WHERE project_id = ? AND artifact_id = ? AND thread_id = ? AND copied = 0
|
|
458
|
+
AND NOT EXISTS (
|
|
459
|
+
SELECT 1 FROM artifact_versions
|
|
460
|
+
WHERE project_id = ? AND artifact_id = ? AND origin_thread_id = ?
|
|
461
|
+
)`
|
|
462
|
+
)
|
|
463
|
+
.run(
|
|
464
|
+
input.projectId,
|
|
465
|
+
artifactId,
|
|
466
|
+
threadId ?? null,
|
|
467
|
+
input.projectId,
|
|
468
|
+
artifactId,
|
|
469
|
+
threadId ?? null
|
|
470
|
+
)
|
|
471
|
+
this.repointLatest(input.projectId, artifactId)
|
|
472
|
+
}
|
|
473
|
+
return deleted.length
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Point an artifact at its newest remaining version, or delete it when none remains. */
|
|
478
|
+
private repointLatest(projectId: string, artifactId: string): void {
|
|
479
|
+
const latest = this.db
|
|
480
|
+
.query(
|
|
481
|
+
`SELECT version, title, created_at AS createdAt FROM artifact_versions
|
|
482
|
+
WHERE project_id = ? AND artifact_id = ? ORDER BY version DESC LIMIT 1`
|
|
483
|
+
)
|
|
484
|
+
.get(projectId, artifactId) as {
|
|
485
|
+
readonly version: number
|
|
486
|
+
readonly title: string
|
|
487
|
+
readonly createdAt: string
|
|
488
|
+
} | null
|
|
489
|
+
if (!latest) {
|
|
490
|
+
this.db
|
|
491
|
+
.query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
|
|
492
|
+
.run(projectId, artifactId)
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
this.db
|
|
496
|
+
.query(
|
|
497
|
+
`UPDATE artifacts SET title = ?, latest_version = ?, updated_at = ?
|
|
498
|
+
WHERE project_id = ? AND id = ?`
|
|
499
|
+
)
|
|
500
|
+
.run(latest.title, latest.version, latest.createdAt, projectId, artifactId)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private insertVersion(
|
|
504
|
+
projectId: string,
|
|
505
|
+
artifactId: string,
|
|
506
|
+
version: number,
|
|
507
|
+
input: ArtifactVersionInput
|
|
508
|
+
): ArtifactVersionRecord {
|
|
509
|
+
const row = this.db
|
|
510
|
+
.query(
|
|
511
|
+
`INSERT INTO artifact_versions (
|
|
512
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
513
|
+
author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
|
|
514
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
515
|
+
RETURNING ${VERSION_COLUMNS}`
|
|
516
|
+
)
|
|
517
|
+
.get(
|
|
518
|
+
projectId,
|
|
519
|
+
artifactId,
|
|
520
|
+
version,
|
|
521
|
+
input.title,
|
|
522
|
+
input.entry,
|
|
523
|
+
JSON.stringify(input.files),
|
|
524
|
+
JSON.stringify(input.outline),
|
|
525
|
+
JSON.stringify(input.references),
|
|
526
|
+
input.author.type,
|
|
527
|
+
input.author.id,
|
|
528
|
+
input.origin?.threadId ?? null,
|
|
529
|
+
input.origin?.runId ?? null,
|
|
530
|
+
input.createdAt.toISOString()
|
|
531
|
+
) as Row
|
|
532
|
+
if (input.origin) {
|
|
533
|
+
this.linkThread(projectId, artifactId, input.origin.threadId, input.createdAt)
|
|
534
|
+
}
|
|
535
|
+
return parseArtifactVersionRow(row)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Shares an artifact in a thread, keeping when it was first shared there. */
|
|
539
|
+
private linkThread(
|
|
540
|
+
projectId: string,
|
|
541
|
+
artifactId: string,
|
|
542
|
+
threadId: string,
|
|
543
|
+
sharedAt: Date
|
|
544
|
+
): void {
|
|
545
|
+
this.db
|
|
546
|
+
.query(
|
|
547
|
+
`INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at)
|
|
548
|
+
VALUES (?, ?, ?, ?)
|
|
549
|
+
ON CONFLICT (project_id, artifact_id, thread_id) DO NOTHING`
|
|
550
|
+
)
|
|
551
|
+
.run(projectId, artifactId, threadId, sharedAt.toISOString())
|
|
552
|
+
}
|
|
553
|
+
}
|