@sixb/sqlite 0.1.21 → 0.1.23
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/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 +12 -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 +10 -1
- package/dist/artifact-storage.d.ts.map +1 -1
- 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/users.d.ts +7 -3
- package/dist/auth-storage/users.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.js +3348 -1515
- package/dist/index.js.map +32 -26
- 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/unique-values.d.ts +42 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/package.json +3 -3
- 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 +385 -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 +206 -9
- package/src/auth-storage/auth-storage.ts +58 -26
- package/src/auth-storage/users.ts +106 -60
- package/src/execution-storage.ts +39 -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 +75 -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/unique-values.ts +187 -0
- package/src/transactions.ts +1 -1
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
|
|
package/src/artifact-storage.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { Database, SQLQueryBindings } from "bun:sqlite"
|
|
2
2
|
import {
|
|
3
|
+
artifactStateBytes,
|
|
3
4
|
artifactStorageErrors,
|
|
5
|
+
checkArtifactStateWrite,
|
|
4
6
|
parseArtifactFrameGrantRow,
|
|
5
7
|
parseArtifactRow,
|
|
8
|
+
parseArtifactStateRow,
|
|
6
9
|
parseArtifactVersionRow,
|
|
7
10
|
} from "@sixb/core/internal/storage"
|
|
8
11
|
import type {
|
|
9
12
|
ArtifactFrameGrantRecord,
|
|
10
13
|
ArtifactRecord,
|
|
14
|
+
ArtifactStateEntry,
|
|
15
|
+
ArtifactStateKeyInput,
|
|
16
|
+
ArtifactStateScope,
|
|
11
17
|
ArtifactStorage,
|
|
12
18
|
ArtifactVersionInput,
|
|
13
19
|
ArtifactVersionRecord,
|
|
20
|
+
CopyArtifactInput,
|
|
14
21
|
CreateArtifactInput,
|
|
15
22
|
DeleteArtifactInput,
|
|
16
23
|
DeleteArtifactVersionsByOriginInput,
|
|
@@ -25,6 +32,7 @@ import type {
|
|
|
25
32
|
ListReferencingVersionsInput,
|
|
26
33
|
PublishArtifactVersionInput,
|
|
27
34
|
PublishedArtifactVersion,
|
|
35
|
+
SetArtifactStateInput,
|
|
28
36
|
} from "@sixb/core/storage"
|
|
29
37
|
import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
|
|
30
38
|
|
|
@@ -45,6 +53,15 @@ const FRAME_GRANT_COLUMNS = `
|
|
|
45
53
|
created_at AS createdAt, expires_at AS expiresAt
|
|
46
54
|
`
|
|
47
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
|
+
|
|
48
65
|
type Row = Record<string, unknown>
|
|
49
66
|
|
|
50
67
|
export class SqliteArtifactStorage implements ArtifactStorage {
|
|
@@ -88,16 +105,33 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
88
105
|
return runImmediateTransaction(this.db, () => {
|
|
89
106
|
const current = this.db
|
|
90
107
|
.query(
|
|
91
|
-
|
|
108
|
+
`SELECT kind, next_version AS nextVersion, latest_version AS latestVersion
|
|
109
|
+
FROM artifacts WHERE project_id = ? AND id = ?`
|
|
92
110
|
)
|
|
93
111
|
.get(input.projectId, input.artifactId) as {
|
|
94
112
|
readonly kind: ArtifactRecord["kind"]
|
|
95
113
|
readonly nextVersion: number
|
|
114
|
+
readonly latestVersion: number
|
|
96
115
|
} | null
|
|
97
116
|
if (!current) throw artifactStorageErrors.notFound(input.artifactId)
|
|
98
117
|
if (current.kind !== input.kind) {
|
|
99
118
|
throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
|
|
100
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
|
+
}
|
|
101
135
|
const version = this.insertVersion(
|
|
102
136
|
input.projectId,
|
|
103
137
|
input.artifactId,
|
|
@@ -137,14 +171,14 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
137
171
|
where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
|
|
138
172
|
params.push(input.owner.type, input.owner.id)
|
|
139
173
|
}
|
|
140
|
-
if (input.
|
|
174
|
+
if (input.threadId !== undefined) {
|
|
141
175
|
where.push(
|
|
142
176
|
`EXISTS (
|
|
143
|
-
SELECT 1 FROM
|
|
144
|
-
WHERE
|
|
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 = ?
|
|
145
179
|
)`
|
|
146
180
|
)
|
|
147
|
-
params.push(input.
|
|
181
|
+
params.push(input.threadId)
|
|
148
182
|
}
|
|
149
183
|
if (input.kind !== undefined) {
|
|
150
184
|
where.push("a.kind = ?")
|
|
@@ -221,6 +255,70 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
221
255
|
return rows.map(parseArtifactVersionRow)
|
|
222
256
|
}
|
|
223
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
|
+
|
|
224
322
|
async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
|
|
225
323
|
runImmediateTransaction(this.db, () => {
|
|
226
324
|
const exists = this.db
|
|
@@ -269,8 +367,65 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
269
367
|
.run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
|
|
270
368
|
}
|
|
271
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
|
+
|
|
272
427
|
async delete(input: DeleteArtifactInput): Promise<boolean> {
|
|
273
|
-
// Versions
|
|
428
|
+
// Versions, frame grants, and state go with the artifact through ON DELETE CASCADE.
|
|
274
429
|
return (
|
|
275
430
|
this.db
|
|
276
431
|
.query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
|
|
@@ -287,12 +442,35 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
287
442
|
)
|
|
288
443
|
.all(input.projectId, input.runId) as { readonly artifactId: string }[]
|
|
289
444
|
const deleted = this.db
|
|
290
|
-
.query(
|
|
291
|
-
|
|
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
|
|
292
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
|
+
)
|
|
293
471
|
this.repointLatest(input.projectId, artifactId)
|
|
294
472
|
}
|
|
295
|
-
return deleted
|
|
473
|
+
return deleted.length
|
|
296
474
|
})
|
|
297
475
|
}
|
|
298
476
|
|
|
@@ -351,6 +529,25 @@ export class SqliteArtifactStorage implements ArtifactStorage {
|
|
|
351
529
|
input.origin?.runId ?? null,
|
|
352
530
|
input.createdAt.toISOString()
|
|
353
531
|
) as Row
|
|
532
|
+
if (input.origin) {
|
|
533
|
+
this.linkThread(projectId, artifactId, input.origin.threadId, input.createdAt)
|
|
534
|
+
}
|
|
354
535
|
return parseArtifactVersionRow(row)
|
|
355
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
|
+
}
|
|
356
553
|
}
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
InvitationRecord,
|
|
13
13
|
SessionRecord,
|
|
14
14
|
SuspendUserAndRevokeSessionsInput,
|
|
15
|
+
UpdateAuthUserResult,
|
|
15
16
|
UserIdentityRecord,
|
|
16
17
|
UserRecord,
|
|
17
18
|
} from "@sixb/core/storage"
|
|
@@ -32,7 +33,7 @@ import { SqliteAuthUserIdentityStore } from "./identities"
|
|
|
32
33
|
import { SqliteAuthInvitationStore } from "./invitations"
|
|
33
34
|
import { SqliteAuthMagicLinkStore } from "./magic-links"
|
|
34
35
|
import { SqliteAuthOidcAuthorizationAttemptStore } from "./oidc-attempts"
|
|
35
|
-
import type { SqliteAuthMagicLinkRow, SqliteAuthOidcAttemptRow } from "./rows"
|
|
36
|
+
import type { SqliteAuthMagicLinkRow, SqliteAuthOidcAttemptRow, SqliteAuthUserRow } from "./rows"
|
|
36
37
|
import { rowToIdentityRecord, rowToUserRecord, serializeOptionalRecord } from "./rows"
|
|
37
38
|
import { SqliteAuthServiceAccountGroupMembershipStore } from "./service-account-group-memberships"
|
|
38
39
|
import { SqliteAuthServiceAccountStore } from "./service-accounts"
|
|
@@ -216,16 +217,17 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
216
217
|
consumedAt: completedAt,
|
|
217
218
|
})
|
|
218
219
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
const previousUser = existingUserRow ? rowToUserRecord(existingUserRow) : null
|
|
221
|
+
const user =
|
|
222
|
+
previousUser ??
|
|
223
|
+
this.insertUser({
|
|
224
|
+
id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
|
|
225
|
+
projectId,
|
|
226
|
+
email: magicLink.email,
|
|
227
|
+
displayName: input.newUserDisplayName,
|
|
228
|
+
avatarUrl: input.newUserAvatarUrl,
|
|
229
|
+
createdAt: completedAt,
|
|
230
|
+
})
|
|
229
231
|
|
|
230
232
|
const invitation = this.acceptInvitationAndApplyGroups({
|
|
231
233
|
activeInvitation,
|
|
@@ -253,6 +255,7 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
253
255
|
session,
|
|
254
256
|
invitation: invitation.invitation,
|
|
255
257
|
groupMemberships,
|
|
258
|
+
previousUser,
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
)
|
|
@@ -297,7 +300,7 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
297
300
|
email,
|
|
298
301
|
now: completedAt,
|
|
299
302
|
})
|
|
300
|
-
|
|
303
|
+
const userRow = identity
|
|
301
304
|
? getUserRowById(this.db, { projectId, id: identity.user_id })
|
|
302
305
|
: getUserRowByEmail(this.db, { projectId, email })
|
|
303
306
|
const shouldCreateUser = !identity && !userRow
|
|
@@ -348,8 +351,10 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
348
351
|
|
|
349
352
|
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
350
353
|
|
|
354
|
+
const previousUser = userRow ? rowToUserRecord(userRow) : null
|
|
355
|
+
let user: UserRecord
|
|
351
356
|
if (!userRow) {
|
|
352
|
-
|
|
357
|
+
user = this.insertUser({
|
|
353
358
|
id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
|
|
354
359
|
projectId,
|
|
355
360
|
email,
|
|
@@ -357,19 +362,10 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
357
362
|
avatarUrl: input.avatarUrl,
|
|
358
363
|
createdAt: completedAt,
|
|
359
364
|
})
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
id: user.id,
|
|
363
|
-
email: user.email,
|
|
364
|
-
display_name: user.displayName ?? null,
|
|
365
|
-
avatar_url: user.avatarUrl ?? null,
|
|
366
|
-
status: user.status,
|
|
367
|
-
created_at: toIso(user.createdAt),
|
|
368
|
-
updated_at: toIso(user.updatedAt),
|
|
369
|
-
}
|
|
365
|
+
} else {
|
|
366
|
+
user = this.refreshProviderProfile(userRow, input, completedAt)
|
|
370
367
|
}
|
|
371
368
|
|
|
372
|
-
const user = rowToUserRecord(userRow)
|
|
373
369
|
const invitation = this.acceptInvitationAndApplyGroups({
|
|
374
370
|
activeInvitation,
|
|
375
371
|
completedAt,
|
|
@@ -412,6 +408,7 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
412
408
|
identity: nextIdentity,
|
|
413
409
|
invitation: invitation.invitation,
|
|
414
410
|
groupMemberships,
|
|
411
|
+
previousUser,
|
|
415
412
|
}
|
|
416
413
|
}
|
|
417
414
|
)
|
|
@@ -425,7 +422,7 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
425
422
|
|
|
426
423
|
async suspendUserAndRevokeSessions(
|
|
427
424
|
input: SuspendUserAndRevokeSessionsInput
|
|
428
|
-
): Promise<
|
|
425
|
+
): Promise<UpdateAuthUserResult> {
|
|
429
426
|
return runImmediateTransaction(this.db, () => {
|
|
430
427
|
const existing = getUserRowById(this.db, {
|
|
431
428
|
projectId: input.projectId,
|
|
@@ -456,11 +453,12 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
456
453
|
revokedAt: input.suspendedAt,
|
|
457
454
|
})
|
|
458
455
|
|
|
459
|
-
|
|
456
|
+
const user = rowToUserRecord({
|
|
460
457
|
...existing,
|
|
461
458
|
status: "suspended",
|
|
462
459
|
updated_at: toIso(input.suspendedAt),
|
|
463
460
|
})
|
|
461
|
+
return { user, previousUser: rowToUserRecord(existing) }
|
|
464
462
|
})
|
|
465
463
|
}
|
|
466
464
|
|
|
@@ -526,6 +524,40 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
526
524
|
}
|
|
527
525
|
}
|
|
528
526
|
|
|
527
|
+
// The identity provider owns the name and picture it sends: every sign-in overwrites them, and
|
|
528
|
+
// one it leaves out keeps the stored value.
|
|
529
|
+
private refreshProviderProfile(
|
|
530
|
+
row: SqliteAuthUserRow,
|
|
531
|
+
input: CompleteOidcSignInInput,
|
|
532
|
+
updatedAt: Date
|
|
533
|
+
): UserRecord {
|
|
534
|
+
const displayName = input.displayName ?? row.display_name
|
|
535
|
+
const avatarUrl = input.avatarUrl ?? row.avatar_url
|
|
536
|
+
if (displayName === row.display_name && avatarUrl === row.avatar_url) {
|
|
537
|
+
return rowToUserRecord(row)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.db
|
|
541
|
+
.query(
|
|
542
|
+
`
|
|
543
|
+
UPDATE auth_users
|
|
544
|
+
SET display_name = ?,
|
|
545
|
+
avatar_url = ?,
|
|
546
|
+
updated_at = ?
|
|
547
|
+
WHERE project_id = ?
|
|
548
|
+
AND id = ?
|
|
549
|
+
`
|
|
550
|
+
)
|
|
551
|
+
.run(displayName, avatarUrl, toIso(updatedAt), row.project_id, row.id)
|
|
552
|
+
|
|
553
|
+
return rowToUserRecord({
|
|
554
|
+
...row,
|
|
555
|
+
display_name: displayName,
|
|
556
|
+
avatar_url: avatarUrl,
|
|
557
|
+
updated_at: toIso(updatedAt),
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
|
|
529
561
|
private insertUser(input: {
|
|
530
562
|
readonly id: string
|
|
531
563
|
readonly projectId: string
|