@sixb/sqlite 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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.map +1 -1
- package/dist/agents/threads.d.ts +4 -0
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-limit-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.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/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/magic-links.d.ts.map +1 -1
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +0 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -11
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +0 -2
- package/dist/auth-storage/shared.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 +933 -128
- package/dist/index.js.map +32 -30
- package/dist/migrations.d.ts +2 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/storage.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/pipeline-run-storage.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/dist/rules-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/share-session-storage.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/dist/timeseries-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +0 -1
- package/dist/transactions.d.ts.map +1 -1
- package/dist/webhook-run-storage.d.ts.map +1 -1
- package/dist/workflow-intervention-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 -5
- package/src/activity-storage.ts +588 -0
- package/src/agents/agent-storage.ts +0 -5
- package/src/agents/threads.ts +27 -0
- package/src/ai-limit-storage.ts +0 -2
- package/src/ai-usage-storage.ts +0 -4
- package/src/artifact-storage.ts +356 -0
- package/src/auth-storage/auth-storage.ts +0 -23
- package/src/auth-storage/magic-links.ts +1 -6
- package/src/auth-storage/oidc-attempts.ts +1 -6
- package/src/auth-storage/rows.ts +0 -6
- package/src/auth-storage/sessions.ts +21 -42
- package/src/auth-storage/shared.ts +3 -20
- package/src/execution-storage.ts +56 -0
- package/src/index.ts +11 -10
- package/src/migrations/061-drop-auth-audience.sql +16 -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 +36 -0
- package/src/objects/storage.ts +0 -5
- package/src/ontology-storage/sources.ts +41 -0
- package/src/pipeline-run-storage.ts +0 -5
- package/src/projection-run-storage.ts +0 -5
- package/src/rules-storage.ts +0 -5
- package/src/share-grant-storage.ts +25 -2
- package/src/share-session-storage.ts +0 -2
- package/src/sync-run-storage.ts +0 -5
- package/src/timeseries-storage.ts +0 -5
- package/src/transactions.ts +8 -8
- package/src/webhook-run-storage.ts +0 -3
- package/src/workflow-intervention-storage.ts +0 -5
- package/src/workflow-run-storage.ts +27 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
|
-
import type { AuthSessionAudience } from "@sixb/core"
|
|
3
2
|
import type {
|
|
4
3
|
CompleteAuthSessionInput,
|
|
5
4
|
CreateAuthSessionInput,
|
|
@@ -385,7 +384,6 @@ export function validateCompleteSessionInput(
|
|
|
385
384
|
session: CompleteAuthSessionInput
|
|
386
385
|
): void {
|
|
387
386
|
const sessionId = assertNonEmpty(session.id, "Session id")
|
|
388
|
-
assertNonEmpty(session.audience, "Session audience")
|
|
389
387
|
assertNonEmpty(session.tokenHash, "Session token hash")
|
|
390
388
|
assertSessionIdAvailable(db, projectId, sessionId)
|
|
391
389
|
}
|
|
@@ -407,7 +405,6 @@ export function createSession(
|
|
|
407
405
|
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
408
406
|
const userId = assertNonEmpty(input.userId, "User id")
|
|
409
407
|
const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
|
|
410
|
-
const audience = assertNonEmpty(input.audience, "Session audience")
|
|
411
408
|
const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash")
|
|
412
409
|
|
|
413
410
|
assertSessionIdAvailable(db, projectId, id)
|
|
@@ -419,7 +416,6 @@ export function createSession(
|
|
|
419
416
|
id,
|
|
420
417
|
user_id,
|
|
421
418
|
strategy_id,
|
|
422
|
-
audience,
|
|
423
419
|
token_hash,
|
|
424
420
|
created_at,
|
|
425
421
|
expires_at,
|
|
@@ -429,14 +425,13 @@ export function createSession(
|
|
|
429
425
|
client_name,
|
|
430
426
|
access_expires_at,
|
|
431
427
|
refresh_token_hash
|
|
432
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
428
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
433
429
|
`
|
|
434
430
|
).run(
|
|
435
431
|
projectId,
|
|
436
432
|
id,
|
|
437
433
|
userId,
|
|
438
434
|
strategyId,
|
|
439
|
-
audience,
|
|
440
435
|
tokenHash,
|
|
441
436
|
toIso(input.createdAt),
|
|
442
437
|
toIso(input.expiresAt),
|
|
@@ -463,20 +458,10 @@ export function revokeActiveSessionsForUser(
|
|
|
463
458
|
params: {
|
|
464
459
|
readonly projectId: string
|
|
465
460
|
readonly userId: string
|
|
466
|
-
readonly audience?: AuthSessionAudience
|
|
467
461
|
readonly revokedAt: Date
|
|
468
462
|
}
|
|
469
463
|
): readonly ReturnType<typeof rowToSessionRecord>[] {
|
|
470
464
|
const revokedAt = toIso(params.revokedAt)
|
|
471
|
-
const audienceCondition = params.audience === undefined ? "" : "AND audience = ?"
|
|
472
|
-
const selectArgs =
|
|
473
|
-
params.audience === undefined
|
|
474
|
-
? [params.projectId, params.userId, revokedAt, revokedAt]
|
|
475
|
-
: [params.projectId, params.userId, params.audience, revokedAt, revokedAt]
|
|
476
|
-
const updateArgs =
|
|
477
|
-
params.audience === undefined
|
|
478
|
-
? [revokedAt, params.projectId, params.userId, revokedAt, revokedAt]
|
|
479
|
-
: [revokedAt, params.projectId, params.userId, params.audience, revokedAt, revokedAt]
|
|
480
465
|
const rows = db
|
|
481
466
|
.query(
|
|
482
467
|
`
|
|
@@ -484,14 +469,13 @@ export function revokeActiveSessionsForUser(
|
|
|
484
469
|
FROM auth_sessions
|
|
485
470
|
WHERE project_id = ?
|
|
486
471
|
AND user_id = ?
|
|
487
|
-
${audienceCondition}
|
|
488
472
|
AND revoked_at IS NULL
|
|
489
473
|
AND expires_at > ?
|
|
490
474
|
AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
|
|
491
475
|
ORDER BY created_at ASC, id ASC
|
|
492
476
|
`
|
|
493
477
|
)
|
|
494
|
-
.all(
|
|
478
|
+
.all(params.projectId, params.userId, revokedAt, revokedAt) as SqliteAuthSessionRow[]
|
|
495
479
|
|
|
496
480
|
db.query(
|
|
497
481
|
`
|
|
@@ -499,12 +483,11 @@ export function revokeActiveSessionsForUser(
|
|
|
499
483
|
SET revoked_at = ?
|
|
500
484
|
WHERE project_id = ?
|
|
501
485
|
AND user_id = ?
|
|
502
|
-
${audienceCondition}
|
|
503
486
|
AND revoked_at IS NULL
|
|
504
487
|
AND expires_at > ?
|
|
505
488
|
AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
|
|
506
489
|
`
|
|
507
|
-
).run(
|
|
490
|
+
).run(revokedAt, params.projectId, params.userId, revokedAt, revokedAt)
|
|
508
491
|
|
|
509
492
|
return rows.map((row) =>
|
|
510
493
|
rowToSessionRecord({
|
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,20 +32,18 @@ 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"
|
|
42
44
|
import { SqliteFileDownloadGrantStorage } from "./file-download-grant-storage"
|
|
43
45
|
import { SqliteFileUploadSessionStorage } from "./file-upload-session-storage"
|
|
44
|
-
import {
|
|
45
|
-
createSqliteStorageMigrators,
|
|
46
|
-
installFreshSqliteSchema,
|
|
47
|
-
sqliteStoragePath,
|
|
48
|
-
} from "./migrations"
|
|
46
|
+
import { createSqliteStorageMigrators, sqliteStoragePath } from "./migrations"
|
|
49
47
|
import { SqliteObjectStorage } from "./objects"
|
|
50
48
|
import { SqliteOntologyStorage, type SqliteOntologyTransactionContext } from "./ontology-storage"
|
|
51
49
|
import { SqlitePipelineRunStorage } from "./pipeline-run-storage"
|
|
@@ -93,6 +91,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
93
91
|
readonly auth: SqliteAuthStorage
|
|
94
92
|
readonly executions: SqliteExecutionStorage
|
|
95
93
|
readonly agents: SqliteAgentStorage
|
|
94
|
+
readonly artifacts: SqliteArtifactStorage
|
|
96
95
|
readonly aiUsage: AiUsageStorage
|
|
97
96
|
readonly aiCosts: AiCostStorage
|
|
98
97
|
readonly aiLimits: AiLimitStorage
|
|
@@ -110,6 +109,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
110
109
|
readonly connectorConnections: SqliteConnectorConnectionStorage
|
|
111
110
|
readonly fileUploadSessions: SqliteFileUploadSessionStorage
|
|
112
111
|
readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
|
|
112
|
+
readonly activity: SqliteActivityStorage
|
|
113
113
|
readonly migrators: readonly StorageMigrator[]
|
|
114
114
|
|
|
115
115
|
private readonly connection: SqliteStoreConnection
|
|
@@ -127,10 +127,6 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
127
127
|
this.readConnection = this.connection
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
if (this.connection.installFreshSchema) {
|
|
131
|
-
installFreshSqliteSchema(this.connection.db)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
130
|
const stores = createSqliteStores(this.connection, {
|
|
135
131
|
runOntologyOperation: async (run) => run(),
|
|
136
132
|
transactionContext: null,
|
|
@@ -161,6 +157,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
161
157
|
this.auth = createAuthOperationScope(stores.auth, scope)
|
|
162
158
|
this.executions = createOperationScopedFacade(stores.executions, scope)
|
|
163
159
|
this.agents = createAgentOperationScope(stores.agents, scope)
|
|
160
|
+
this.artifacts = createOperationScopedFacade(stores.artifacts, scope)
|
|
164
161
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
|
|
165
162
|
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
|
|
166
163
|
this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
|
|
@@ -178,6 +175,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
178
175
|
this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope)
|
|
179
176
|
this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
|
|
180
177
|
this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope)
|
|
178
|
+
this.activity = createOperationScopedFacade(stores.activity, scope)
|
|
181
179
|
this.migrators = options.path ? createSqliteStorageMigrators(options.path) : []
|
|
182
180
|
registerSqliteStorageTestingAdapter(this, {
|
|
183
181
|
advanceConnectorConnectionTime: (durationMs) =>
|
|
@@ -256,7 +254,6 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
256
254
|
{
|
|
257
255
|
db: this.connection.db,
|
|
258
256
|
ownsConnection: false,
|
|
259
|
-
installFreshSchema: false,
|
|
260
257
|
},
|
|
261
258
|
{
|
|
262
259
|
runOntologyOperation: async (run) => run(),
|
|
@@ -326,6 +323,7 @@ function createSqliteStores(
|
|
|
326
323
|
auth,
|
|
327
324
|
executions,
|
|
328
325
|
agents: new SqliteAgentStorage({ connection, executions }),
|
|
326
|
+
artifacts: new SqliteArtifactStorage(connection),
|
|
329
327
|
aiUsage: new SqliteAiUsageStorage({ connection }),
|
|
330
328
|
aiCosts: new SqliteAiCostStorage(connection),
|
|
331
329
|
aiLimits: new SqliteAiLimitStorage({ connection }),
|
|
@@ -343,6 +341,7 @@ function createSqliteStores(
|
|
|
343
341
|
connectorConnections: new SqliteConnectorConnectionStorage(connection),
|
|
344
342
|
fileUploadSessions: new SqliteFileUploadSessionStorage(connection),
|
|
345
343
|
fileDownloadGrants: new SqliteFileDownloadGrantStorage(connection),
|
|
344
|
+
activity: new SqliteActivityStorage(connection),
|
|
346
345
|
}
|
|
347
346
|
}
|
|
348
347
|
|
|
@@ -352,6 +351,7 @@ interface SqliteStoreSet {
|
|
|
352
351
|
readonly auth: SqliteAuthStorage
|
|
353
352
|
readonly executions: SqliteExecutionStorage
|
|
354
353
|
readonly agents: SqliteAgentStorage
|
|
354
|
+
readonly artifacts: SqliteArtifactStorage
|
|
355
355
|
readonly aiUsage: SqliteAiUsageStorage
|
|
356
356
|
readonly aiCosts: SqliteAiCostStorage
|
|
357
357
|
readonly aiLimits: SqliteAiLimitStorage
|
|
@@ -369,6 +369,7 @@ interface SqliteStoreSet {
|
|
|
369
369
|
readonly connectorConnections: SqliteConnectorConnectionStorage
|
|
370
370
|
readonly fileUploadSessions: SqliteFileUploadSessionStorage
|
|
371
371
|
readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
|
|
372
|
+
readonly activity: SqliteActivityStorage
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
export { migrateSqliteStorage } from "./migrations"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- A project has one browser app, so sessions, magic links and sign-in attempts no longer name the
|
|
2
|
+
-- one they belong to. Atlas's browser sessions end here: their cookie is no longer read. Native
|
|
3
|
+
-- sessions carried Atlas's name only because the column needed a value, so they stay.
|
|
4
|
+
UPDATE auth_sessions
|
|
5
|
+
SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
6
|
+
WHERE audience = 'atlas'
|
|
7
|
+
AND refresh_token_hash IS NULL
|
|
8
|
+
AND revoked_at IS NULL;
|
|
9
|
+
|
|
10
|
+
DROP INDEX idx_auth_sessions_user_active;
|
|
11
|
+
ALTER TABLE auth_sessions DROP COLUMN audience;
|
|
12
|
+
CREATE INDEX idx_auth_sessions_user_active
|
|
13
|
+
ON auth_sessions(project_id, user_id, revoked_at, expires_at);
|
|
14
|
+
|
|
15
|
+
ALTER TABLE auth_magic_links DROP COLUMN audience;
|
|
16
|
+
ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
|
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- App activity: one source per verified session, the visits it opened, and their page views.
|
|
2
|
+
-- Foreground activity moves a visit's `last_seen_at` and stores no row, unless it opens a visit.
|
|
3
|
+
-- A session id names an `auth_sessions` row for a user and a `share_sessions` row for a share; it
|
|
4
|
+
-- is not a secret, and no foreign key holds it, so history outlives its sessions. Timestamps are
|
|
5
|
+
-- ISO-8601 UTC text and compare lexicographically.
|
|
6
|
+
CREATE TABLE activity_sources (
|
|
7
|
+
project_id TEXT NOT NULL,
|
|
8
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
|
|
9
|
+
session_id TEXT NOT NULL CHECK (length(session_id) > 0),
|
|
10
|
+
visit_id TEXT NOT NULL,
|
|
11
|
+
last_seen_at TEXT NOT NULL,
|
|
12
|
+
sequence INTEGER NOT NULL CHECK (sequence >= 0),
|
|
13
|
+
window_start TEXT NOT NULL,
|
|
14
|
+
window_count INTEGER NOT NULL CHECK (window_count >= 0),
|
|
15
|
+
PRIMARY KEY (project_id, actor_type, session_id)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE activity_visits (
|
|
19
|
+
project_id TEXT NOT NULL,
|
|
20
|
+
id TEXT NOT NULL,
|
|
21
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
|
|
22
|
+
user_id TEXT,
|
|
23
|
+
grant_id TEXT,
|
|
24
|
+
session_id TEXT NOT NULL CHECK (length(session_id) > 0),
|
|
25
|
+
started_at TEXT NOT NULL,
|
|
26
|
+
last_seen_at TEXT NOT NULL CHECK (last_seen_at >= started_at),
|
|
27
|
+
page_views INTEGER NOT NULL CHECK (page_views >= 0),
|
|
28
|
+
attribution TEXT CHECK (attribution IS NULL OR json_type(attribution) = 'object'),
|
|
29
|
+
PRIMARY KEY (project_id, id),
|
|
30
|
+
CHECK (
|
|
31
|
+
(actor_type = 'user' AND user_id IS NOT NULL AND grant_id IS NULL)
|
|
32
|
+
OR (actor_type = 'share' AND user_id IS NULL AND grant_id IS NOT NULL)
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- A period reads the visits it overlaps. A visit lasts less than a day, so they started in
|
|
37
|
+
-- `[from - 1 day, to)`: both period indexes bound `started_at` on both sides. `last_seen_at` is
|
|
38
|
+
-- left out of every index, so the update each batch makes touches none of them. The user index
|
|
39
|
+
-- also serves deleting one user's history.
|
|
40
|
+
CREATE INDEX idx_activity_visits_started
|
|
41
|
+
ON activity_visits (project_id, started_at, id);
|
|
42
|
+
CREATE INDEX idx_activity_visits_user
|
|
43
|
+
ON activity_visits (project_id, user_id, started_at, id);
|
|
44
|
+
-- A session's visits, to join them with the session and the executions it authorized.
|
|
45
|
+
CREATE INDEX idx_activity_visits_session
|
|
46
|
+
ON activity_visits (project_id, actor_type, session_id);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE activity_page_views (
|
|
49
|
+
project_id TEXT NOT NULL,
|
|
50
|
+
visit_id TEXT NOT NULL,
|
|
51
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
52
|
+
id TEXT NOT NULL,
|
|
53
|
+
route TEXT NOT NULL,
|
|
54
|
+
received_at TEXT NOT NULL,
|
|
55
|
+
attribution TEXT CHECK (attribution IS NULL OR json_type(attribution) = 'object'),
|
|
56
|
+
PRIMARY KEY (project_id, visit_id, sequence),
|
|
57
|
+
FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- Finds a retried page view by the id its browser chose.
|
|
61
|
+
CREATE INDEX idx_activity_page_views_id ON activity_page_views (project_id, id);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- A visit's timeline reads what its session started: the roots it authorized in a window, then
|
|
2
|
+
-- everything they caused. A signed-in session is the credential of its roots, a shared link's
|
|
3
|
+
-- session their delegation; a child names its parent. Agent executions copy their parent's
|
|
4
|
+
-- session: the root indexes hold only executions without a parent, so they never see those, and
|
|
5
|
+
-- end with `id` so the root read's order needs no sort.
|
|
6
|
+
CREATE INDEX idx_executions_session_roots
|
|
7
|
+
ON executions (project_id, authority_session_id, created_at, id)
|
|
8
|
+
WHERE authority_session_id IS NOT NULL AND parent_execution_id IS NULL;
|
|
9
|
+
CREATE INDEX idx_executions_delegation_roots
|
|
10
|
+
ON executions (project_id, authority_delegation_session_id, created_at, id)
|
|
11
|
+
WHERE authority_delegation_session_id IS NOT NULL AND parent_execution_id IS NULL;
|
|
12
|
+
CREATE INDEX idx_executions_parent
|
|
13
|
+
ON executions (project_id, parent_execution_id)
|
|
14
|
+
WHERE parent_execution_id IS NOT NULL;
|
|
15
|
+
|
|
16
|
+
-- A user's connection history lists all their sessions, revoked and expired ones too, newest
|
|
17
|
+
-- first.
|
|
18
|
+
CREATE INDEX idx_auth_sessions_user_created
|
|
19
|
+
ON auth_sessions (project_id, user_id, created_at, id);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Interactions an app marks, such as a button: their name and route, never a value. They share
|
|
2
|
+
-- the visit's sequence with its page views, so a visit reads both in the order they happened, and
|
|
3
|
+
-- cascade with the visit, which counts them as it counts its page views.
|
|
4
|
+
ALTER TABLE activity_visits
|
|
5
|
+
ADD COLUMN interactions INTEGER NOT NULL DEFAULT 0 CHECK (interactions >= 0);
|
|
6
|
+
|
|
7
|
+
CREATE TABLE activity_interactions (
|
|
8
|
+
project_id TEXT NOT NULL,
|
|
9
|
+
visit_id TEXT NOT NULL,
|
|
10
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
11
|
+
id TEXT NOT NULL,
|
|
12
|
+
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 100),
|
|
13
|
+
route TEXT NOT NULL,
|
|
14
|
+
received_at TEXT NOT NULL,
|
|
15
|
+
PRIMARY KEY (project_id, visit_id, sequence),
|
|
16
|
+
FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Finds a retried interaction by the id its browser chose.
|
|
20
|
+
CREATE INDEX idx_activity_interactions_id ON activity_interactions (project_id, id);
|
package/src/migrations.ts
CHANGED
|
@@ -272,6 +272,12 @@ export const sqliteStorageMigrations = defineMigrations({
|
|
|
272
272
|
sqliteSql("058-file-download-grants", fileDownloadGrantsSql),
|
|
273
273
|
sqliteSql("059-drained-source-versions", drainedSourceVersionsSql),
|
|
274
274
|
sqliteSql("060-terminal-action-runs", terminalActionRunsSql),
|
|
275
|
+
sqliteSql("061-drop-auth-audience", dropAuthAudienceSql),
|
|
276
|
+
sqliteSql("062-artifacts", artifactsSql),
|
|
277
|
+
sqliteSql("063-drop-output-diagnostics", dropOutputDiagnosticsSql),
|
|
278
|
+
sqliteSql("064-activity", activitySql),
|
|
279
|
+
sqliteSql("065-activity-timeline", activityTimelineSql),
|
|
280
|
+
sqliteSql("066-activity-interactions", activityInteractionsSql),
|
|
275
281
|
],
|
|
276
282
|
})
|
|
277
283
|
|
|
@@ -297,6 +303,26 @@ export function sqliteStep(
|
|
|
297
303
|
return step<Database>(id, up, options)
|
|
298
304
|
}
|
|
299
305
|
|
|
306
|
+
// Replaying every migration costs a few hundred milliseconds, and in-memory databases are made
|
|
307
|
+
// often (each test's storage, for one). The schema is built once per process and each new database
|
|
308
|
+
// starts as a copy of it.
|
|
309
|
+
let freshSchema: Uint8Array | undefined
|
|
310
|
+
|
|
311
|
+
/** A new in-memory database holding the current schema, as replaying every migration leaves it. */
|
|
312
|
+
export function freshSqliteDatabase(): Database {
|
|
313
|
+
if (!freshSchema) {
|
|
314
|
+
const template = new Database(":memory:")
|
|
315
|
+
try {
|
|
316
|
+
template.run("PRAGMA foreign_keys = ON")
|
|
317
|
+
installFreshSqliteSchema(template)
|
|
318
|
+
freshSchema = template.serialize()
|
|
319
|
+
} finally {
|
|
320
|
+
template.close()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return Database.deserialize(freshSchema)
|
|
324
|
+
}
|
|
325
|
+
|
|
300
326
|
export function installFreshSqliteSchema(db: Database): void {
|
|
301
327
|
for (const migration of sqliteStorageMigrations.steps) {
|
|
302
328
|
const installed = migration.up(db)
|
|
@@ -599,3 +625,13 @@ import drainedSourceVersionsSql from "./migrations/059-drained-source-versions.s
|
|
|
599
625
|
type: "text",
|
|
600
626
|
}
|
|
601
627
|
import terminalActionRunsSql from "./migrations/060-terminal-action-runs.sql" with { type: "text" }
|
|
628
|
+
import dropAuthAudienceSql from "./migrations/061-drop-auth-audience.sql" with { type: "text" }
|
|
629
|
+
import artifactsSql from "./migrations/062-artifacts.sql" with { type: "text" }
|
|
630
|
+
import dropOutputDiagnosticsSql from "./migrations/063-drop-output-diagnostics.sql" with {
|
|
631
|
+
type: "text",
|
|
632
|
+
}
|
|
633
|
+
import activitySql from "./migrations/064-activity.sql" with { type: "text" }
|
|
634
|
+
import activityTimelineSql from "./migrations/065-activity-timeline.sql" with { type: "text" }
|
|
635
|
+
import activityInteractionsSql from "./migrations/066-activity-interactions.sql" with {
|
|
636
|
+
type: "text",
|
|
637
|
+
}
|
package/src/objects/storage.ts
CHANGED
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
QueryObjectsInput,
|
|
23
23
|
QueryObjectsResult,
|
|
24
24
|
} from "@sixb/core/storage"
|
|
25
|
-
import { installFreshSqliteSchema } from "../migrations"
|
|
26
25
|
import {
|
|
27
26
|
closeSqliteStoreConnection,
|
|
28
27
|
openSqliteStoreConnection,
|
|
@@ -124,10 +123,6 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
124
123
|
this.connection = openSqliteStoreConnection(options)
|
|
125
124
|
this.db = this.connection.db
|
|
126
125
|
this.reader = new SqliteObjectReader(this.db, DEFAULT_OBJECT_QUERY_SOURCE)
|
|
127
|
-
|
|
128
|
-
if (this.connection.installFreshSchema) {
|
|
129
|
-
installFreshSqliteSchema(this.db)
|
|
130
|
-
}
|
|
131
126
|
}
|
|
132
127
|
|
|
133
128
|
queryCapabilities(): ObjectQueryCapabilities {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
AbandonRunSourceMaterializationInput,
|
|
21
21
|
AbandonSourceMaterializationCandidateInput,
|
|
22
22
|
AbandonSourceMaterializationInput,
|
|
23
|
+
ActiveOntologySource,
|
|
23
24
|
AdoptedSourceMaterialization,
|
|
24
25
|
AdoptSourceMaterializationInput,
|
|
25
26
|
AssertSourceMaterializationExecutionInput,
|
|
@@ -27,6 +28,7 @@ import type {
|
|
|
27
28
|
CleanupTerminalSourceMaterializationsInput,
|
|
28
29
|
CleanupTerminalSourceMaterializationsResult,
|
|
29
30
|
GetActiveOntologySourceInput,
|
|
31
|
+
ListActiveOntologySourcesInput,
|
|
30
32
|
MarkSourceMaterializationReadyInput,
|
|
31
33
|
OntologySourceRecord,
|
|
32
34
|
OntologySourceStorage,
|
|
@@ -238,6 +240,36 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
238
240
|
})
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
async listActive(
|
|
244
|
+
input: ListActiveOntologySourcesInput
|
|
245
|
+
): Promise<readonly ActiveOntologySource[]> {
|
|
246
|
+
return this.runRootOperation(() => {
|
|
247
|
+
assertNonblank(input.projectId, "Source project id")
|
|
248
|
+
|
|
249
|
+
// Served by the partial index on active sources.
|
|
250
|
+
const rows = this.db
|
|
251
|
+
.query(
|
|
252
|
+
`
|
|
253
|
+
SELECT source_id, dataset_id, dataset_version_id, dataset_version_created_at,
|
|
254
|
+
projection_revision
|
|
255
|
+
FROM ontology_sources
|
|
256
|
+
WHERE project_id = ? AND status = 'active'
|
|
257
|
+
`
|
|
258
|
+
)
|
|
259
|
+
.all(input.projectId) as ActiveSourceRow[]
|
|
260
|
+
|
|
261
|
+
return rows.map((row) => ({
|
|
262
|
+
source: { projectionId: row.source_id },
|
|
263
|
+
datasetVersion: {
|
|
264
|
+
datasetId: row.dataset_id,
|
|
265
|
+
versionId: row.dataset_version_id,
|
|
266
|
+
createdAt: row.dataset_version_created_at,
|
|
267
|
+
},
|
|
268
|
+
projectionRevision: row.projection_revision,
|
|
269
|
+
}))
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
241
273
|
async adopt(
|
|
242
274
|
input: AdoptSourceMaterializationInput
|
|
243
275
|
): Promise<AdoptedSourceMaterialization | null> {
|
|
@@ -626,3 +658,12 @@ export class SqliteOntologySourceStorage implements OntologySourceStorage {
|
|
|
626
658
|
return row
|
|
627
659
|
}
|
|
628
660
|
}
|
|
661
|
+
|
|
662
|
+
type ActiveSourceRow = Pick<
|
|
663
|
+
SqliteOntologySourceRow,
|
|
664
|
+
| "source_id"
|
|
665
|
+
| "dataset_id"
|
|
666
|
+
| "dataset_version_id"
|
|
667
|
+
| "dataset_version_created_at"
|
|
668
|
+
| "projection_revision"
|
|
669
|
+
>
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
} from "@sixb/core/storage"
|
|
23
23
|
import { PIPELINE_RUN_FAILURE_CODES, PipelineRunError } from "@sixb/core/storage"
|
|
24
24
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
25
|
-
import { installFreshSqliteSchema } from "./migrations"
|
|
26
25
|
import {
|
|
27
26
|
appendRunListFilters,
|
|
28
27
|
hasEmptyStatuses,
|
|
@@ -54,10 +53,6 @@ export class SqlitePipelineRunStorage implements PipelineRunStorage {
|
|
|
54
53
|
this.connection = openSqliteStoreConnection(options)
|
|
55
54
|
this.db = this.connection.db
|
|
56
55
|
this.executions = options.executions
|
|
57
|
-
|
|
58
|
-
if (this.connection.installFreshSchema) {
|
|
59
|
-
installFreshSqliteSchema(this.db)
|
|
60
|
-
}
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
async queue(input: QueuePipelineRunInput): Promise<PipelineRunRecord> {
|
|
@@ -56,7 +56,6 @@ import type {
|
|
|
56
56
|
} from "@sixb/core/storage"
|
|
57
57
|
import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage"
|
|
58
58
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
59
|
-
import { installFreshSqliteSchema } from "./migrations"
|
|
60
59
|
import {
|
|
61
60
|
closeSqliteStoreConnection,
|
|
62
61
|
openSqliteStoreConnection,
|
|
@@ -82,10 +81,6 @@ export class SqliteProjectionRunStorage implements ProjectionRunStorage {
|
|
|
82
81
|
this.connection = openSqliteStoreConnection(options)
|
|
83
82
|
this.db = this.connection.db
|
|
84
83
|
this.executions = options.executions
|
|
85
|
-
|
|
86
|
-
if (this.connection.installFreshSchema) {
|
|
87
|
-
installFreshSqliteSchema(this.db)
|
|
88
|
-
}
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
async queue(input: QueueProjectionRunInput): Promise<ProjectionRunRecord> {
|