@sixb/pg 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/threads.d.ts +4 -0
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/magic-links.d.ts.map +1 -1
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +0 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -11
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -2
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +984 -80
- package/dist/index.js.map +20 -18
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-artifact-storage.d.ts +19 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +2 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/threads.ts +23 -0
- package/src/auth-storage/auth-storage.ts +1 -22
- package/src/auth-storage/magic-links.ts +0 -4
- package/src/auth-storage/oidc-attempts.ts +0 -4
- package/src/auth-storage/rows.ts +0 -6
- package/src/auth-storage/sessions.ts +18 -30
- package/src/auth-storage/shared.ts +3 -35
- package/src/index.ts +10 -0
- package/src/migrations/062-drop-auth-audience.sql +19 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations.ts +20 -0
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-artifact-storage.ts +325 -0
- package/src/pg-execution-storage.ts +52 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
package/dist/index.js
CHANGED
|
@@ -1142,6 +1142,23 @@ class PgAgentThreadStore {
|
|
|
1142
1142
|
`;
|
|
1143
1143
|
return row ? rowToThreadRecord(row) : null;
|
|
1144
1144
|
}
|
|
1145
|
+
async getByIds(params) {
|
|
1146
|
+
const ids = [...new Set(params.ids)];
|
|
1147
|
+
if (ids.length === 0) {
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
const rows = await this.sql`
|
|
1151
|
+
SELECT *
|
|
1152
|
+
FROM agent_threads
|
|
1153
|
+
WHERE project_id = ${params.projectId}
|
|
1154
|
+
AND id = ANY(${this.sql.array(ids)}::text[])
|
|
1155
|
+
`;
|
|
1156
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
1157
|
+
return ids.flatMap((id) => {
|
|
1158
|
+
const row = byId.get(id);
|
|
1159
|
+
return row ? [rowToThreadRecord(row)] : [];
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1145
1162
|
async list(input) {
|
|
1146
1163
|
if (input.statuses !== undefined && input.statuses.length === 0) {
|
|
1147
1164
|
return { threads: [], hasMore: false, total: 0 };
|
|
@@ -1244,7 +1261,6 @@ function rowToSessionRecord(row) {
|
|
|
1244
1261
|
projectId: row.project_id,
|
|
1245
1262
|
userId: row.user_id,
|
|
1246
1263
|
strategyId: row.strategy_id,
|
|
1247
|
-
audience: row.audience,
|
|
1248
1264
|
tokenHash: row.token_hash,
|
|
1249
1265
|
createdAt: toDate(row.created_at),
|
|
1250
1266
|
expiresAt: toDate(row.expires_at),
|
|
@@ -1311,7 +1327,6 @@ function rowToMagicLinkRecord(row) {
|
|
|
1311
1327
|
id: row.id,
|
|
1312
1328
|
projectId: row.project_id,
|
|
1313
1329
|
strategyId: row.strategy_id,
|
|
1314
|
-
audience: row.audience,
|
|
1315
1330
|
email: row.email,
|
|
1316
1331
|
tokenHash: row.token_hash,
|
|
1317
1332
|
returnTo: row.return_to ?? undefined,
|
|
@@ -1326,7 +1341,6 @@ function rowToOidcAuthorizationAttemptRecord(row) {
|
|
|
1326
1341
|
id: row.id,
|
|
1327
1342
|
projectId: row.project_id,
|
|
1328
1343
|
strategyId: row.strategy_id,
|
|
1329
|
-
audience: row.audience,
|
|
1330
1344
|
stateHash: row.state_hash,
|
|
1331
1345
|
nonceHash: row.nonce_hash,
|
|
1332
1346
|
codeVerifier: row.code_verifier,
|
|
@@ -1622,7 +1636,6 @@ async function consumeOidcAttempt(sql, params) {
|
|
|
1622
1636
|
}
|
|
1623
1637
|
async function validateCompleteSessionInput(sql, projectId, session) {
|
|
1624
1638
|
const sessionId = assertNonEmpty(session.id, "Session id");
|
|
1625
|
-
assertNonEmpty(session.audience, "Session audience");
|
|
1626
1639
|
assertNonEmpty(session.tokenHash, "Session token hash");
|
|
1627
1640
|
await assertSessionIdAvailable(sql, projectId, sessionId);
|
|
1628
1641
|
}
|
|
@@ -1636,7 +1649,6 @@ async function createSession(sql, input) {
|
|
|
1636
1649
|
const projectId = assertNonEmpty(input.projectId, "Project id");
|
|
1637
1650
|
const userId = assertNonEmpty(input.userId, "User id");
|
|
1638
1651
|
const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
|
|
1639
|
-
const audience = assertNonEmpty(input.audience, "Session audience");
|
|
1640
1652
|
const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash");
|
|
1641
1653
|
await requireUserById(sql, { projectId, id: userId });
|
|
1642
1654
|
await assertSessionIdAvailable(sql, projectId, id);
|
|
@@ -1647,7 +1659,6 @@ async function createSession(sql, input) {
|
|
|
1647
1659
|
id,
|
|
1648
1660
|
user_id,
|
|
1649
1661
|
strategy_id,
|
|
1650
|
-
audience,
|
|
1651
1662
|
token_hash,
|
|
1652
1663
|
created_at,
|
|
1653
1664
|
expires_at,
|
|
@@ -1662,7 +1673,6 @@ async function createSession(sql, input) {
|
|
|
1662
1673
|
${id},
|
|
1663
1674
|
${userId},
|
|
1664
1675
|
${strategyId},
|
|
1665
|
-
${audience},
|
|
1666
1676
|
${tokenHash},
|
|
1667
1677
|
${input.createdAt},
|
|
1668
1678
|
${input.expiresAt},
|
|
@@ -1681,50 +1691,26 @@ async function createSession(sql, input) {
|
|
|
1681
1691
|
}
|
|
1682
1692
|
}
|
|
1683
1693
|
async function revokeActiveSessionsForUser(sql, params) {
|
|
1684
|
-
const rows =
|
|
1685
|
-
SELECT *
|
|
1686
|
-
FROM auth_sessions
|
|
1687
|
-
WHERE project_id = ${params.projectId}
|
|
1688
|
-
AND user_id = ${params.userId}
|
|
1689
|
-
AND revoked_at IS NULL
|
|
1690
|
-
AND expires_at > ${params.revokedAt}
|
|
1691
|
-
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
|
|
1692
|
-
ORDER BY created_at ASC, id ASC
|
|
1693
|
-
FOR UPDATE
|
|
1694
|
-
` : await sql`
|
|
1694
|
+
const rows = await sql`
|
|
1695
1695
|
SELECT *
|
|
1696
1696
|
FROM auth_sessions
|
|
1697
1697
|
WHERE project_id = ${params.projectId}
|
|
1698
1698
|
AND user_id = ${params.userId}
|
|
1699
|
-
AND audience = ${params.audience}
|
|
1700
1699
|
AND revoked_at IS NULL
|
|
1701
1700
|
AND expires_at > ${params.revokedAt}
|
|
1702
1701
|
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
|
|
1703
1702
|
ORDER BY created_at ASC, id ASC
|
|
1704
1703
|
FOR UPDATE
|
|
1705
1704
|
`;
|
|
1706
|
-
|
|
1707
|
-
await sql`
|
|
1708
|
-
UPDATE auth_sessions
|
|
1709
|
-
SET revoked_at = ${params.revokedAt}
|
|
1710
|
-
WHERE project_id = ${params.projectId}
|
|
1711
|
-
AND user_id = ${params.userId}
|
|
1712
|
-
AND revoked_at IS NULL
|
|
1713
|
-
AND expires_at > ${params.revokedAt}
|
|
1714
|
-
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
|
|
1715
|
-
`;
|
|
1716
|
-
} else {
|
|
1717
|
-
await sql`
|
|
1705
|
+
await sql`
|
|
1718
1706
|
UPDATE auth_sessions
|
|
1719
1707
|
SET revoked_at = ${params.revokedAt}
|
|
1720
1708
|
WHERE project_id = ${params.projectId}
|
|
1721
1709
|
AND user_id = ${params.userId}
|
|
1722
|
-
AND audience = ${params.audience}
|
|
1723
1710
|
AND revoked_at IS NULL
|
|
1724
1711
|
AND expires_at > ${params.revokedAt}
|
|
1725
1712
|
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
|
|
1726
1713
|
`;
|
|
1727
|
-
}
|
|
1728
1714
|
return rows.map((row) => rowToSessionRecord({
|
|
1729
1715
|
...row,
|
|
1730
1716
|
revoked_at: params.revokedAt
|
|
@@ -2384,7 +2370,6 @@ class PgAuthInvitationStore {
|
|
|
2384
2370
|
}
|
|
2385
2371
|
|
|
2386
2372
|
// src/auth-storage/magic-links.ts
|
|
2387
|
-
import { resolveAuthSessionAudience } from "@sixb/core";
|
|
2388
2373
|
import { AuthStorageError as AuthStorageError5 } from "@sixb/core/storage";
|
|
2389
2374
|
class PgAuthMagicLinkStore {
|
|
2390
2375
|
sql;
|
|
@@ -2396,7 +2381,6 @@ class PgAuthMagicLinkStore {
|
|
|
2396
2381
|
const id = assertNonEmpty(input.id, "Magic link id");
|
|
2397
2382
|
const projectId = assertNonEmpty(input.projectId, "Project id");
|
|
2398
2383
|
const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
|
|
2399
|
-
const audience = resolveAuthSessionAudience(input.audience);
|
|
2400
2384
|
const email = normalizeEmail(input.email);
|
|
2401
2385
|
const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash");
|
|
2402
2386
|
await lockAdvisoryKeys(tx, [authLockKey("magic-links", projectId, email)]);
|
|
@@ -2414,7 +2398,6 @@ class PgAuthMagicLinkStore {
|
|
|
2414
2398
|
project_id,
|
|
2415
2399
|
id,
|
|
2416
2400
|
strategy_id,
|
|
2417
|
-
audience,
|
|
2418
2401
|
email,
|
|
2419
2402
|
token_hash,
|
|
2420
2403
|
return_to,
|
|
@@ -2424,7 +2407,6 @@ class PgAuthMagicLinkStore {
|
|
|
2424
2407
|
${projectId},
|
|
2425
2408
|
${id},
|
|
2426
2409
|
${strategyId},
|
|
2427
|
-
${audience},
|
|
2428
2410
|
${email},
|
|
2429
2411
|
${tokenHash},
|
|
2430
2412
|
${input.returnTo ?? null},
|
|
@@ -2472,7 +2454,6 @@ class PgAuthMagicLinkStore {
|
|
|
2472
2454
|
}
|
|
2473
2455
|
|
|
2474
2456
|
// src/auth-storage/oidc-attempts.ts
|
|
2475
|
-
import { resolveAuthSessionAudience as resolveAuthSessionAudience2 } from "@sixb/core";
|
|
2476
2457
|
import { AuthStorageError as AuthStorageError6 } from "@sixb/core/storage";
|
|
2477
2458
|
class PgAuthOidcAuthorizationAttemptStore {
|
|
2478
2459
|
sql;
|
|
@@ -2483,7 +2464,6 @@ class PgAuthOidcAuthorizationAttemptStore {
|
|
|
2483
2464
|
const id = assertNonEmpty(input.id, "OIDC authorization attempt id");
|
|
2484
2465
|
const projectId = assertNonEmpty(input.projectId, "Project id");
|
|
2485
2466
|
const strategyId = assertNonEmpty(input.strategyId, "Strategy id");
|
|
2486
|
-
const audience = resolveAuthSessionAudience2(input.audience);
|
|
2487
2467
|
const stateHash = assertNonEmpty(input.stateHash, "OIDC state hash");
|
|
2488
2468
|
const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash");
|
|
2489
2469
|
const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier");
|
|
@@ -2496,7 +2476,6 @@ class PgAuthOidcAuthorizationAttemptStore {
|
|
|
2496
2476
|
project_id,
|
|
2497
2477
|
id,
|
|
2498
2478
|
strategy_id,
|
|
2499
|
-
audience,
|
|
2500
2479
|
state_hash,
|
|
2501
2480
|
nonce_hash,
|
|
2502
2481
|
code_verifier,
|
|
@@ -2507,7 +2486,6 @@ class PgAuthOidcAuthorizationAttemptStore {
|
|
|
2507
2486
|
${projectId},
|
|
2508
2487
|
${id},
|
|
2509
2488
|
${strategyId},
|
|
2510
|
-
${audience},
|
|
2511
2489
|
${stateHash},
|
|
2512
2490
|
${nonceHash},
|
|
2513
2491
|
${codeVerifier},
|
|
@@ -2765,37 +2743,38 @@ class PgAuthSessionStore {
|
|
|
2765
2743
|
async getById(params) {
|
|
2766
2744
|
return getSessionById(this.sql, params);
|
|
2767
2745
|
}
|
|
2768
|
-
async
|
|
2769
|
-
const
|
|
2746
|
+
async listActiveByUserId(params) {
|
|
2747
|
+
const rows = await this.sql`
|
|
2770
2748
|
SELECT *
|
|
2771
2749
|
FROM auth_sessions
|
|
2772
2750
|
WHERE project_id = ${params.projectId}
|
|
2773
2751
|
AND user_id = ${params.userId}
|
|
2774
|
-
AND audience = ${params.audience}
|
|
2775
2752
|
AND revoked_at IS NULL
|
|
2776
2753
|
AND expires_at > ${params.now}
|
|
2777
2754
|
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.now})
|
|
2778
|
-
ORDER BY created_at DESC, id DESC
|
|
2779
|
-
LIMIT 1
|
|
2755
|
+
ORDER BY COALESCE(last_seen_at, created_at) DESC, created_at DESC, id DESC
|
|
2780
2756
|
`;
|
|
2781
|
-
return
|
|
2757
|
+
return rows.map(rowToSessionRecord);
|
|
2782
2758
|
}
|
|
2783
|
-
async
|
|
2759
|
+
async listByUserId(params) {
|
|
2784
2760
|
const rows = await this.sql`
|
|
2785
2761
|
SELECT *
|
|
2786
2762
|
FROM auth_sessions
|
|
2787
2763
|
WHERE project_id = ${params.projectId}
|
|
2788
2764
|
AND user_id = ${params.userId}
|
|
2789
|
-
AND
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2765
|
+
${params.before ? this.sql`AND created_at <= ${params.before.createdAt}` : this.sql``}
|
|
2766
|
+
ORDER BY created_at DESC, id DESC
|
|
2767
|
+
LIMIT ${params.limit + 1}
|
|
2768
|
+
OFFSET ${params.before?.skip ?? 0}
|
|
2793
2769
|
`;
|
|
2794
|
-
return
|
|
2770
|
+
return {
|
|
2771
|
+
sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
|
|
2772
|
+
hasMore: rows.length > params.limit
|
|
2773
|
+
};
|
|
2795
2774
|
}
|
|
2796
2775
|
async findValidByTokenHash(params) {
|
|
2797
2776
|
const row = await getSessionRowById(this.sql, params);
|
|
2798
|
-
if (!row || row.refresh_token_hash !== null || row.
|
|
2777
|
+
if (!row || row.refresh_token_hash !== null || row.token_hash !== params.tokenHash || !isLiveSessionRow(row, params.now)) {
|
|
2799
2778
|
return null;
|
|
2800
2779
|
}
|
|
2801
2780
|
return rowToSessionRecord(row);
|
|
@@ -2855,7 +2834,6 @@ class PgAuthSessionStore {
|
|
|
2855
2834
|
last_seen_at = ${params.now}
|
|
2856
2835
|
WHERE project_id = ${params.projectId}
|
|
2857
2836
|
AND id = ${params.id}
|
|
2858
|
-
AND audience = ${params.audience}
|
|
2859
2837
|
AND token_hash = ${params.tokenHash}
|
|
2860
2838
|
AND refresh_token_hash IS NULL
|
|
2861
2839
|
AND revoked_at IS NULL
|
|
@@ -2883,11 +2861,7 @@ class PgAuthSessionStore {
|
|
|
2883
2861
|
}
|
|
2884
2862
|
async revokeActiveForUser(params) {
|
|
2885
2863
|
return runPgTransaction(this.sql, async (tx) => {
|
|
2886
|
-
|
|
2887
|
-
if (params.audience) {
|
|
2888
|
-
locks.push(authLockKey("sessions", params.projectId, params.userId, params.audience));
|
|
2889
|
-
}
|
|
2890
|
-
await lockAdvisoryKeys(tx, locks);
|
|
2864
|
+
await lockAdvisoryKeys(tx, [authLockKey("sessions", params.projectId, params.userId)]);
|
|
2891
2865
|
return revokeActiveSessionsForUser(tx, params);
|
|
2892
2866
|
});
|
|
2893
2867
|
}
|
|
@@ -3146,7 +3120,6 @@ class PgAuthStorage {
|
|
|
3146
3120
|
};
|
|
3147
3121
|
}
|
|
3148
3122
|
await validateCompleteSessionInput(tx, projectId, input.session);
|
|
3149
|
-
assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience);
|
|
3150
3123
|
let newUserId;
|
|
3151
3124
|
if (shouldCreateUser) {
|
|
3152
3125
|
newUserId = assertNonEmpty(input.newUserId, "User id");
|
|
@@ -3251,7 +3224,6 @@ class PgAuthStorage {
|
|
|
3251
3224
|
};
|
|
3252
3225
|
}
|
|
3253
3226
|
await validateCompleteSessionInput(tx, projectId, input.session);
|
|
3254
|
-
assertSignInSessionAudience(projectId, input.session.audience, attempt.audience);
|
|
3255
3227
|
let newUserId;
|
|
3256
3228
|
if (shouldCreateUser) {
|
|
3257
3229
|
newUserId = assertNonEmpty(input.newUserId, "User id");
|
|
@@ -3459,16 +3431,12 @@ class PgAuthStorage {
|
|
|
3459
3431
|
return this.applyGroups(sql, { ...input, groupIds, source: "directory" });
|
|
3460
3432
|
}
|
|
3461
3433
|
async createSignInSession(sql, input) {
|
|
3462
|
-
await lockAdvisoryKeys(sql, [
|
|
3463
|
-
authLockKey("sessions", input.projectId, input.userId),
|
|
3464
|
-
authLockKey("sessions", input.projectId, input.userId, input.session.audience)
|
|
3465
|
-
]);
|
|
3434
|
+
await lockAdvisoryKeys(sql, [authLockKey("sessions", input.projectId, input.userId)]);
|
|
3466
3435
|
return createSession(sql, {
|
|
3467
3436
|
id: input.session.id,
|
|
3468
3437
|
projectId: input.projectId,
|
|
3469
3438
|
userId: input.userId,
|
|
3470
3439
|
strategyId: input.strategyId,
|
|
3471
|
-
audience: input.session.audience,
|
|
3472
3440
|
tokenHash: input.session.tokenHash,
|
|
3473
3441
|
createdAt: input.session.createdAt,
|
|
3474
3442
|
expiresAt: input.session.expiresAt,
|
|
@@ -3514,12 +3482,6 @@ class PgAuthStorage {
|
|
|
3514
3482
|
return rowToIdentityRecord(row);
|
|
3515
3483
|
}
|
|
3516
3484
|
}
|
|
3517
|
-
function assertSignInSessionAudience(projectId, sessionAudience, storedAudience) {
|
|
3518
|
-
if (sessionAudience === storedAudience) {
|
|
3519
|
-
return;
|
|
3520
|
-
}
|
|
3521
|
-
throw new AuthStorageError11("invalid_input", `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`);
|
|
3522
|
-
}
|
|
3523
3485
|
function assertCompletableDeviceAuthorization(authorization, input) {
|
|
3524
3486
|
if (authorization.deviceCodeHash !== input.deviceCodeHash) {
|
|
3525
3487
|
throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Invalid device authorization.");
|
|
@@ -8168,6 +8130,223 @@ CREATE INDEX idx_action_runs_project_status_started
|
|
|
8168
8130
|
ON action_runs (project_id, status, started_at DESC);
|
|
8169
8131
|
`;
|
|
8170
8132
|
|
|
8133
|
+
// src/migrations/062-drop-auth-audience.sql
|
|
8134
|
+
var _062_drop_auth_audience_default = `-- A project has one browser app, so sessions, magic links and sign-in attempts no longer name the
|
|
8135
|
+
-- one they belong to. Atlas's browser sessions end here: their cookie is no longer read. Native
|
|
8136
|
+
-- sessions carried Atlas's name only because the column needed a value, so they stay.
|
|
8137
|
+
UPDATE auth_sessions
|
|
8138
|
+
SET revoked_at = now()
|
|
8139
|
+
WHERE audience = 'atlas'
|
|
8140
|
+
AND refresh_token_hash IS NULL
|
|
8141
|
+
AND revoked_at IS NULL;
|
|
8142
|
+
|
|
8143
|
+
-- Dropping the column drops the two indexes that name it.
|
|
8144
|
+
ALTER TABLE auth_sessions DROP COLUMN audience;
|
|
8145
|
+
CREATE INDEX idx_auth_sessions_user_active
|
|
8146
|
+
ON auth_sessions (project_id, user_id, expires_at DESC, created_at DESC, id DESC)
|
|
8147
|
+
WHERE revoked_at IS NULL;
|
|
8148
|
+
CREATE INDEX idx_auth_sessions_user_created
|
|
8149
|
+
ON auth_sessions (project_id, user_id, created_at DESC, id DESC);
|
|
8150
|
+
|
|
8151
|
+
ALTER TABLE auth_magic_links DROP COLUMN audience;
|
|
8152
|
+
ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
|
|
8153
|
+
`;
|
|
8154
|
+
|
|
8155
|
+
// src/migrations/063-artifacts.sql
|
|
8156
|
+
var _063_artifacts_default = `-- Artifacts: versioned documents, sheets, decks, and files. An artifact's kind never
|
|
8157
|
+
-- changes. Versions take \`next_version\`, which only grows, so a number is never reused after its
|
|
8158
|
+
-- version is deleted.
|
|
8159
|
+
CREATE TABLE artifacts (
|
|
8160
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
8161
|
+
id TEXT NOT NULL CHECK (length(trim(id)) > 0),
|
|
8162
|
+
kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'file')),
|
|
8163
|
+
title TEXT NOT NULL CHECK (length(trim(title)) > 0),
|
|
8164
|
+
owner_principal_type TEXT NOT NULL
|
|
8165
|
+
CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
8166
|
+
owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
|
|
8167
|
+
latest_version INTEGER NOT NULL CHECK (latest_version > 0),
|
|
8168
|
+
next_version INTEGER NOT NULL CHECK (next_version > latest_version),
|
|
8169
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8170
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8171
|
+
PRIMARY KEY (project_id, id)
|
|
8172
|
+
);
|
|
8173
|
+
|
|
8174
|
+
CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
|
|
8175
|
+
CREATE INDEX idx_artifacts_owner_updated
|
|
8176
|
+
ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
|
|
8177
|
+
|
|
8178
|
+
-- \`refs\`, not \`references\`: that word is reserved in SQL.
|
|
8179
|
+
CREATE TABLE artifact_versions (
|
|
8180
|
+
project_id TEXT NOT NULL,
|
|
8181
|
+
artifact_id TEXT NOT NULL,
|
|
8182
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
8183
|
+
title TEXT NOT NULL CHECK (length(trim(title)) > 0),
|
|
8184
|
+
entry TEXT NOT NULL CHECK (length(entry) > 0),
|
|
8185
|
+
files JSONB NOT NULL CHECK (jsonb_typeof(files) = 'array'),
|
|
8186
|
+
outline JSONB NOT NULL CHECK (jsonb_typeof(outline) = 'object'),
|
|
8187
|
+
refs JSONB NOT NULL CHECK (jsonb_typeof(refs) = 'array'),
|
|
8188
|
+
author_principal_type TEXT NOT NULL
|
|
8189
|
+
CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
8190
|
+
author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
|
|
8191
|
+
origin_thread_id TEXT,
|
|
8192
|
+
origin_run_id TEXT,
|
|
8193
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8194
|
+
PRIMARY KEY (project_id, artifact_id, version),
|
|
8195
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
|
|
8196
|
+
CHECK ((origin_thread_id IS NULL) = (origin_run_id IS NULL))
|
|
8197
|
+
);
|
|
8198
|
+
|
|
8199
|
+
CREATE INDEX idx_artifact_versions_origin_run
|
|
8200
|
+
ON artifact_versions (project_id, origin_run_id) WHERE origin_run_id IS NOT NULL;
|
|
8201
|
+
CREATE INDEX idx_artifact_versions_origin_thread
|
|
8202
|
+
ON artifact_versions (project_id, origin_thread_id, artifact_id) WHERE origin_thread_id IS NOT NULL;
|
|
8203
|
+
|
|
8204
|
+
-- Short-lived links that a sandboxed frame loads a deck's files through. Only the SHA-256 of each
|
|
8205
|
+
-- link's token is stored. \`targets\` is what the frame may show from other artifacts, decided for
|
|
8206
|
+
-- its reader when the link was issued: a JSON array of { address, artifactId, version }.
|
|
8207
|
+
CREATE TABLE artifact_frame_grants (
|
|
8208
|
+
project_id TEXT NOT NULL,
|
|
8209
|
+
token_hash TEXT NOT NULL CHECK (token_hash ~ '^[0-9a-f]{64}$'),
|
|
8210
|
+
artifact_id TEXT NOT NULL,
|
|
8211
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
8212
|
+
targets JSONB NOT NULL CHECK (jsonb_typeof(targets) = 'array'),
|
|
8213
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8214
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
8215
|
+
PRIMARY KEY (project_id, token_hash),
|
|
8216
|
+
FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
|
|
8217
|
+
CHECK (expires_at > created_at)
|
|
8218
|
+
);
|
|
8219
|
+
|
|
8220
|
+
CREATE INDEX idx_artifact_frame_grants_expiry ON artifact_frame_grants (project_id, expires_at);
|
|
8221
|
+
-- Deleting an artifact cascades here, and Postgres does not index a foreign key by itself.
|
|
8222
|
+
CREATE INDEX idx_artifact_frame_grants_artifact ON artifact_frame_grants (project_id, artifact_id);
|
|
8223
|
+
`;
|
|
8224
|
+
|
|
8225
|
+
// src/migrations/064-drop-output-diagnostics.sql
|
|
8226
|
+
var _064_drop_output_diagnostics_default = `-- The Agent delivers files by publishing artifacts, so a run no longer collects an output
|
|
8227
|
+
-- directory or reports on it. Stored \`output_*\` diagnostics go; a run left with none stores none.
|
|
8228
|
+
UPDATE agent_runs
|
|
8229
|
+
SET diagnostics = (
|
|
8230
|
+
SELECT jsonb_agg(item.value ORDER BY item.position)
|
|
8231
|
+
FROM jsonb_array_elements(agent_runs.diagnostics) WITH ORDINALITY AS item(value, position)
|
|
8232
|
+
WHERE item.value->>'code' NOT LIKE 'output\\_%'
|
|
8233
|
+
)
|
|
8234
|
+
WHERE jsonb_typeof(diagnostics) = 'array'
|
|
8235
|
+
AND EXISTS (
|
|
8236
|
+
SELECT 1
|
|
8237
|
+
FROM jsonb_array_elements(agent_runs.diagnostics) AS item(value)
|
|
8238
|
+
WHERE item.value->>'code' LIKE 'output\\_%'
|
|
8239
|
+
);
|
|
8240
|
+
`;
|
|
8241
|
+
|
|
8242
|
+
// src/migrations/065-plan-work-payload-statistics.sql
|
|
8243
|
+
var _065_plan_work_payload_statistics_default = "-- A plan's work is analyzed whenever a stream has staged enough of it, and most of that ANALYZE\n-- went into the statistics of `payload`, which no query reads: the plan queries filter on the\n-- typed columns and only extract values from `payload`, and an extracted value has no statistics.\n-- A query that filters `payload` itself (`@>`, `?`) would need them back.\nALTER TABLE ontology_replacement_plan_work ALTER COLUMN payload SET STATISTICS 0;\n";
|
|
8244
|
+
|
|
8245
|
+
// src/migrations/066-activity.sql
|
|
8246
|
+
var _066_activity_default = `-- App activity: one source per verified session, the visits it opened, and their page views.
|
|
8247
|
+
-- Foreground activity moves a visit's \`last_seen_at\` and stores no row, unless it opens a visit.
|
|
8248
|
+
-- A session id names an \`auth_sessions\` row for a user and a \`share_sessions\` row for a share; it
|
|
8249
|
+
-- is not a secret, and no foreign key holds it, so history outlives its sessions. Ids compare
|
|
8250
|
+
-- byte by byte, as the other providers sort them.
|
|
8251
|
+
CREATE TABLE activity_sources (
|
|
8252
|
+
project_id TEXT NOT NULL,
|
|
8253
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
|
|
8254
|
+
session_id TEXT NOT NULL CHECK (length(session_id) > 0),
|
|
8255
|
+
visit_id TEXT NOT NULL,
|
|
8256
|
+
last_seen_at TIMESTAMPTZ NOT NULL,
|
|
8257
|
+
sequence INTEGER NOT NULL CHECK (sequence >= 0),
|
|
8258
|
+
window_start TIMESTAMPTZ NOT NULL,
|
|
8259
|
+
window_count INTEGER NOT NULL CHECK (window_count >= 0),
|
|
8260
|
+
PRIMARY KEY (project_id, actor_type, session_id)
|
|
8261
|
+
);
|
|
8262
|
+
|
|
8263
|
+
CREATE TABLE activity_visits (
|
|
8264
|
+
project_id TEXT NOT NULL,
|
|
8265
|
+
id TEXT COLLATE "C" NOT NULL,
|
|
8266
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
|
|
8267
|
+
user_id TEXT COLLATE "C",
|
|
8268
|
+
grant_id TEXT,
|
|
8269
|
+
session_id TEXT NOT NULL CHECK (length(session_id) > 0),
|
|
8270
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
8271
|
+
last_seen_at TIMESTAMPTZ NOT NULL CHECK (last_seen_at >= started_at),
|
|
8272
|
+
page_views INTEGER NOT NULL CHECK (page_views >= 0),
|
|
8273
|
+
attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
|
|
8274
|
+
PRIMARY KEY (project_id, id),
|
|
8275
|
+
CHECK (
|
|
8276
|
+
(actor_type = 'user' AND user_id IS NOT NULL AND grant_id IS NULL)
|
|
8277
|
+
OR (actor_type = 'share' AND user_id IS NULL AND grant_id IS NOT NULL)
|
|
8278
|
+
)
|
|
8279
|
+
);
|
|
8280
|
+
|
|
8281
|
+
-- A period reads the visits it overlaps. A visit lasts less than a day, so they started in
|
|
8282
|
+
-- \`[from - 1 day, to)\`: both period indexes bound \`started_at\` on both sides. \`last_seen_at\` is
|
|
8283
|
+
-- left out of every index, so the update each batch makes touches none of them. The user index
|
|
8284
|
+
-- also serves deleting one user's history.
|
|
8285
|
+
CREATE INDEX idx_activity_visits_started
|
|
8286
|
+
ON activity_visits (project_id, started_at, id);
|
|
8287
|
+
CREATE INDEX idx_activity_visits_user
|
|
8288
|
+
ON activity_visits (project_id, user_id, started_at, id);
|
|
8289
|
+
-- A session's visits, to join them with the session and the executions it authorized.
|
|
8290
|
+
CREATE INDEX idx_activity_visits_session
|
|
8291
|
+
ON activity_visits (project_id, actor_type, session_id);
|
|
8292
|
+
|
|
8293
|
+
CREATE TABLE activity_page_views (
|
|
8294
|
+
project_id TEXT NOT NULL,
|
|
8295
|
+
visit_id TEXT COLLATE "C" NOT NULL,
|
|
8296
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
8297
|
+
id TEXT NOT NULL,
|
|
8298
|
+
route TEXT NOT NULL,
|
|
8299
|
+
received_at TIMESTAMPTZ NOT NULL,
|
|
8300
|
+
attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
|
|
8301
|
+
PRIMARY KEY (project_id, visit_id, sequence),
|
|
8302
|
+
FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
|
|
8303
|
+
);
|
|
8304
|
+
|
|
8305
|
+
-- Finds a retried page view by the id its browser chose.
|
|
8306
|
+
CREATE INDEX idx_activity_page_views_id ON activity_page_views (project_id, id);
|
|
8307
|
+
`;
|
|
8308
|
+
|
|
8309
|
+
// src/migrations/067-activity-timeline.sql
|
|
8310
|
+
var _067_activity_timeline_default = `-- A visit's timeline reads what its session started: the roots it authorized in a window, then
|
|
8311
|
+
-- everything they caused. A signed-in session is the credential of its roots, a shared link's
|
|
8312
|
+
-- session their delegation; a child names its parent. Agent executions copy their parent's
|
|
8313
|
+
-- session: the root indexes hold only executions without a parent, so they never see those, and
|
|
8314
|
+
-- end with \`id\` so the root read's order needs no sort. A user's connection history reads
|
|
8315
|
+
-- \`idx_auth_sessions_user_created\`, which already exists.
|
|
8316
|
+
CREATE INDEX idx_executions_session_roots
|
|
8317
|
+
ON executions (project_id, authority_session_id, created_at, id)
|
|
8318
|
+
WHERE authority_session_id IS NOT NULL AND parent_execution_id IS NULL;
|
|
8319
|
+
CREATE INDEX idx_executions_delegation_roots
|
|
8320
|
+
ON executions (project_id, authority_delegation_session_id, created_at, id)
|
|
8321
|
+
WHERE authority_delegation_session_id IS NOT NULL AND parent_execution_id IS NULL;
|
|
8322
|
+
CREATE INDEX idx_executions_parent
|
|
8323
|
+
ON executions (project_id, parent_execution_id)
|
|
8324
|
+
WHERE parent_execution_id IS NOT NULL;
|
|
8325
|
+
`;
|
|
8326
|
+
|
|
8327
|
+
// src/migrations/068-activity-interactions.sql
|
|
8328
|
+
var _068_activity_interactions_default = `-- Interactions an app marks, such as a button: their name and route, never a value. They share
|
|
8329
|
+
-- the visit's sequence with its page views, so a visit reads both in the order they happened, and
|
|
8330
|
+
-- cascade with the visit, which counts them as it counts its page views.
|
|
8331
|
+
ALTER TABLE activity_visits
|
|
8332
|
+
ADD COLUMN interactions INTEGER NOT NULL DEFAULT 0 CHECK (interactions >= 0);
|
|
8333
|
+
|
|
8334
|
+
CREATE TABLE activity_interactions (
|
|
8335
|
+
project_id TEXT NOT NULL,
|
|
8336
|
+
visit_id TEXT COLLATE "C" NOT NULL,
|
|
8337
|
+
sequence INTEGER NOT NULL CHECK (sequence > 0),
|
|
8338
|
+
id TEXT NOT NULL,
|
|
8339
|
+
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 100),
|
|
8340
|
+
route TEXT NOT NULL,
|
|
8341
|
+
received_at TIMESTAMPTZ NOT NULL,
|
|
8342
|
+
PRIMARY KEY (project_id, visit_id, sequence),
|
|
8343
|
+
FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
|
|
8344
|
+
);
|
|
8345
|
+
|
|
8346
|
+
-- Finds a retried interaction by the id its browser chose.
|
|
8347
|
+
CREATE INDEX idx_activity_interactions_id ON activity_interactions (project_id, id);
|
|
8348
|
+
`;
|
|
8349
|
+
|
|
8171
8350
|
// src/migrations.ts
|
|
8172
8351
|
var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
|
|
8173
8352
|
function pgSql(id, sqlText) {
|
|
@@ -8395,7 +8574,14 @@ var postgresStorageMigrations = defineMigrations({
|
|
|
8395
8574
|
pgSql("058-reranking-model-kind", _058_reranking_model_kind_default),
|
|
8396
8575
|
pgSql("059-file-download-grants", _059_file_download_grants_default),
|
|
8397
8576
|
pgSql("060-drained-source-versions", _060_drained_source_versions_default),
|
|
8398
|
-
pgSql("061-terminal-action-runs", _061_terminal_action_runs_default)
|
|
8577
|
+
pgSql("061-terminal-action-runs", _061_terminal_action_runs_default),
|
|
8578
|
+
pgSql("062-drop-auth-audience", _062_drop_auth_audience_default),
|
|
8579
|
+
pgSql("063-artifacts", _063_artifacts_default),
|
|
8580
|
+
pgSql("064-drop-output-diagnostics", _064_drop_output_diagnostics_default),
|
|
8581
|
+
pgSql("065-plan-work-payload-statistics", _065_plan_work_payload_statistics_default),
|
|
8582
|
+
pgSql("066-activity", _066_activity_default),
|
|
8583
|
+
pgSql("067-activity-timeline", _067_activity_timeline_default),
|
|
8584
|
+
pgSql("068-activity-interactions", _068_activity_interactions_default)
|
|
8399
8585
|
]
|
|
8400
8586
|
});
|
|
8401
8587
|
|
|
@@ -12723,7 +12909,7 @@ import {
|
|
|
12723
12909
|
var PG_PLAN_WORK_TABLE = "ontology_replacement_plan_work";
|
|
12724
12910
|
var IDENTITIES = "ontology_replacement_plan_identities";
|
|
12725
12911
|
var SORT_KEY = "encode(convert_to(identity_key, 'UTF8'), 'hex')";
|
|
12726
|
-
var ANALYZE_ROWS =
|
|
12912
|
+
var ANALYZE_ROWS = 1000;
|
|
12727
12913
|
function objectRevision(type, id) {
|
|
12728
12914
|
return `jsonb_build_array(
|
|
12729
12915
|
(SELECT jsonb_build_array(version, last_commit_id) FROM objects
|
|
@@ -12860,7 +13046,7 @@ class PgOntologyReplacementPlanStorage {
|
|
|
12860
13046
|
}
|
|
12861
13047
|
async* streamState(input) {
|
|
12862
13048
|
assertPageRows(input.pageRows);
|
|
12863
|
-
await runPgTransaction(this.sql, async (sql) => {
|
|
13049
|
+
const versionId = await runPgTransaction(this.sql, async (sql) => {
|
|
12864
13050
|
const { candidate } = await requirePlan(sql, input);
|
|
12865
13051
|
if (input.entityKind === "object" && candidate.projection_kind !== "object") {
|
|
12866
13052
|
throw new MaterializationConflictError4("source-materialization", "Link projection replacement cannot stream object state.");
|
|
@@ -12873,6 +13059,7 @@ class PgOntologyReplacementPlanStorage {
|
|
|
12873
13059
|
if (expanded >= ANALYZE_ROWS && !isWithinPgTransaction(this.sql))
|
|
12874
13060
|
await analyzePlans(sql);
|
|
12875
13061
|
}
|
|
13062
|
+
return candidate.version_id;
|
|
12876
13063
|
});
|
|
12877
13064
|
let after = "";
|
|
12878
13065
|
let streamed = 0;
|
|
@@ -12902,8 +13089,8 @@ class PgOntologyReplacementPlanStorage {
|
|
|
12902
13089
|
return state;
|
|
12903
13090
|
}, { isolation: "repeatableRead" });
|
|
12904
13091
|
if (!page) {
|
|
12905
|
-
if (streamed
|
|
12906
|
-
await
|
|
13092
|
+
if (streamed > 0 && !isWithinPgTransaction(this.sql)) {
|
|
13093
|
+
await analyzeStagedWork(this.sql, versionId);
|
|
12907
13094
|
}
|
|
12908
13095
|
return;
|
|
12909
13096
|
}
|
|
@@ -13431,6 +13618,16 @@ async function purgeTouches(sql, projectId, limit) {
|
|
|
13431
13618
|
async function analyzePlans(sql) {
|
|
13432
13619
|
await sql`ANALYZE ${sql(IDENTITIES)}, ${sql(PG_PLAN_WORK_TABLE)}`;
|
|
13433
13620
|
}
|
|
13621
|
+
async function analyzeStagedWork(sql, versionId) {
|
|
13622
|
+
const [staged] = await sql`
|
|
13623
|
+
SELECT COUNT(*) AS rows FROM (
|
|
13624
|
+
SELECT 1 FROM ${sql(PG_PLAN_WORK_TABLE)} WHERE work_id = ${versionId} LIMIT ${ANALYZE_ROWS}
|
|
13625
|
+
) AS work
|
|
13626
|
+
`;
|
|
13627
|
+
if (Number(staged?.rows ?? 0) < ANALYZE_ROWS)
|
|
13628
|
+
return;
|
|
13629
|
+
await analyzePlans(sql);
|
|
13630
|
+
}
|
|
13434
13631
|
async function dropUnplannedWork(sql, versionId, kind) {
|
|
13435
13632
|
await sql`
|
|
13436
13633
|
DELETE FROM ${sql(PG_PLAN_WORK_TABLE)}
|
|
@@ -14698,6 +14895,7 @@ import { ONTOLOGY_OUTBOX_FAILURE_CODES as ONTOLOGY_OUTBOX_FAILURE_CODES2 } from
|
|
|
14698
14895
|
|
|
14699
14896
|
// src/pg-execution-storage.ts
|
|
14700
14897
|
import {
|
|
14898
|
+
compareExecutionCreation,
|
|
14701
14899
|
executionRecordFromStorageRow,
|
|
14702
14900
|
executionRecordToStorageRow,
|
|
14703
14901
|
prepareExecutionRecord
|
|
@@ -14791,6 +14989,37 @@ class PgExecutionStorage {
|
|
|
14791
14989
|
`;
|
|
14792
14990
|
return row ? pgExecutionRecord(row) : null;
|
|
14793
14991
|
}
|
|
14992
|
+
async listBySession(input) {
|
|
14993
|
+
const session = input.session.type === "user" ? this.sql`authority_session_id = ${input.session.id}` : this.sql`authority_delegation_session_id = ${input.session.id}`;
|
|
14994
|
+
const rows = await this.sql`
|
|
14995
|
+
WITH RECURSIVE tree AS (
|
|
14996
|
+
(
|
|
14997
|
+
SELECT *, 0 AS depth
|
|
14998
|
+
FROM executions
|
|
14999
|
+
WHERE project_id = ${input.projectId}
|
|
15000
|
+
AND ${session}
|
|
15001
|
+
AND parent_execution_id IS NULL
|
|
15002
|
+
AND created_at >= ${input.from}
|
|
15003
|
+
AND created_at < ${input.to}
|
|
15004
|
+
ORDER BY created_at, id
|
|
15005
|
+
LIMIT ${input.limit + 1}
|
|
15006
|
+
)
|
|
15007
|
+
UNION ALL
|
|
15008
|
+
SELECT child.*, tree.depth + 1
|
|
15009
|
+
FROM tree
|
|
15010
|
+
JOIN executions child
|
|
15011
|
+
ON child.project_id = tree.project_id
|
|
15012
|
+
AND child.parent_execution_id = tree.id
|
|
15013
|
+
)
|
|
15014
|
+
SELECT * FROM tree
|
|
15015
|
+
LIMIT ${input.limit + 1}
|
|
15016
|
+
`;
|
|
15017
|
+
const records = rows.map((row) => ({ depth: row.depth, record: pgExecutionRecord(row) })).sort((left, right) => left.depth - right.depth || compareExecutionCreation(left.record, right.record)).slice(0, input.limit).map(({ record }) => record);
|
|
15018
|
+
return {
|
|
15019
|
+
executions: records.sort(compareExecutionCreation),
|
|
15020
|
+
truncated: rows.length > input.limit
|
|
15021
|
+
};
|
|
15022
|
+
}
|
|
14794
15023
|
}
|
|
14795
15024
|
function pgExecutionRecord(row) {
|
|
14796
15025
|
return executionRecordFromStorageRow(toStorageRow(row));
|
|
@@ -15263,6 +15492,26 @@ class PgOntologySourceStorage {
|
|
|
15263
15492
|
return row ? sourceRecord(row) : null;
|
|
15264
15493
|
});
|
|
15265
15494
|
}
|
|
15495
|
+
async listActive(input) {
|
|
15496
|
+
return this.runRootOperation(async (sql) => {
|
|
15497
|
+
assertNonblank(input.projectId, "Source project id");
|
|
15498
|
+
const rows = await sql`
|
|
15499
|
+
SELECT source_id, dataset_id, dataset_version_id, dataset_version_created_at,
|
|
15500
|
+
projection_revision
|
|
15501
|
+
FROM ontology_sources
|
|
15502
|
+
WHERE project_id = ${input.projectId} AND status = 'active'
|
|
15503
|
+
`;
|
|
15504
|
+
return rows.map((row) => ({
|
|
15505
|
+
source: { projectionId: row.source_id },
|
|
15506
|
+
datasetVersion: {
|
|
15507
|
+
datasetId: row.dataset_id,
|
|
15508
|
+
versionId: row.dataset_version_id,
|
|
15509
|
+
createdAt: toIsoString2(row.dataset_version_created_at)
|
|
15510
|
+
},
|
|
15511
|
+
projectionRevision: row.projection_revision
|
|
15512
|
+
}));
|
|
15513
|
+
});
|
|
15514
|
+
}
|
|
15266
15515
|
async adopt(input) {
|
|
15267
15516
|
return this.runRootOperation(async (sql) => {
|
|
15268
15517
|
assertProjectAndSource(input);
|
|
@@ -15934,6 +16183,23 @@ class PgActionRunStorage {
|
|
|
15934
16183
|
`;
|
|
15935
16184
|
return row ? rowToActionRunRecord(row) : null;
|
|
15936
16185
|
}
|
|
16186
|
+
async getByExecutionIds(params) {
|
|
16187
|
+
const executionIds = [...new Set(params.executionIds)];
|
|
16188
|
+
if (executionIds.length === 0) {
|
|
16189
|
+
return [];
|
|
16190
|
+
}
|
|
16191
|
+
const rows = await this.sql`
|
|
16192
|
+
SELECT *
|
|
16193
|
+
FROM action_runs
|
|
16194
|
+
WHERE project_id = ${params.projectId}
|
|
16195
|
+
AND execution_id = ANY(${this.sql.array(executionIds)}::text[])
|
|
16196
|
+
`;
|
|
16197
|
+
const byExecution = new Map(rows.map((row) => [row.execution_id, row]));
|
|
16198
|
+
return executionIds.flatMap((executionId) => {
|
|
16199
|
+
const row = byExecution.get(executionId);
|
|
16200
|
+
return row ? [rowToActionRunRecord(row)] : [];
|
|
16201
|
+
});
|
|
16202
|
+
}
|
|
15937
16203
|
async list(input) {
|
|
15938
16204
|
if (input.statuses && input.statuses.length === 0 || input.actionIds?.length === 0) {
|
|
15939
16205
|
return {
|
|
@@ -16097,6 +16363,350 @@ function rowToActionSubject(row) {
|
|
|
16097
16363
|
};
|
|
16098
16364
|
}
|
|
16099
16365
|
|
|
16366
|
+
// src/pg-activity-storage.ts
|
|
16367
|
+
import {
|
|
16368
|
+
ACTIVITY_MAX_VISIT_MS,
|
|
16369
|
+
activityActor,
|
|
16370
|
+
activityCalendarDays,
|
|
16371
|
+
parseActivityAttribution,
|
|
16372
|
+
planActivityAppend,
|
|
16373
|
+
serializeActivityAttribution,
|
|
16374
|
+
validateActivityAppend
|
|
16375
|
+
} from "@sixb/core/internal/storage";
|
|
16376
|
+
var NO_CURSOR = { at: new Date("9999-12-31T23:59:59.999Z"), id: "" };
|
|
16377
|
+
|
|
16378
|
+
class PgActivityStorage {
|
|
16379
|
+
sql;
|
|
16380
|
+
constructor(sql) {
|
|
16381
|
+
this.sql = sql;
|
|
16382
|
+
}
|
|
16383
|
+
async append(input) {
|
|
16384
|
+
validateActivityAppend(input);
|
|
16385
|
+
return runPgTransaction(this.sql, async (sql) => {
|
|
16386
|
+
await lockAdvisoryKeys(sql, [
|
|
16387
|
+
JSON.stringify(["activity", input.projectId, input.actor.type, input.sessionId])
|
|
16388
|
+
]);
|
|
16389
|
+
const plan = planActivityAppend(input, await readSource(sql, input), await readStoredEvents(sql, input));
|
|
16390
|
+
if (plan.status !== "append") {
|
|
16391
|
+
return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 };
|
|
16392
|
+
}
|
|
16393
|
+
const { state } = plan;
|
|
16394
|
+
const lastSeenAt = new Date(state.lastSeenAt);
|
|
16395
|
+
if (plan.newVisit) {
|
|
16396
|
+
const attribution = plan.newVisit.attribution === null ? null : serializeActivityAttribution(plan.newVisit.attribution);
|
|
16397
|
+
await sql`
|
|
16398
|
+
INSERT INTO activity_visits (
|
|
16399
|
+
project_id, id, actor_type, user_id, grant_id, session_id,
|
|
16400
|
+
started_at, last_seen_at, page_views, interactions, attribution
|
|
16401
|
+
) VALUES (
|
|
16402
|
+
${input.projectId}, ${state.visitId}, ${input.actor.type},
|
|
16403
|
+
${input.actor.type === "user" ? input.actor.userId : null},
|
|
16404
|
+
${input.actor.type === "share" ? input.actor.grantId : null},
|
|
16405
|
+
${input.sessionId}, ${lastSeenAt}, ${lastSeenAt}, ${plan.pageViews.length},
|
|
16406
|
+
${plan.interactions.length}, ${attribution}::text::jsonb
|
|
16407
|
+
)
|
|
16408
|
+
`;
|
|
16409
|
+
} else {
|
|
16410
|
+
await sql`
|
|
16411
|
+
UPDATE activity_visits
|
|
16412
|
+
SET
|
|
16413
|
+
last_seen_at = ${lastSeenAt},
|
|
16414
|
+
page_views = page_views + ${plan.pageViews.length},
|
|
16415
|
+
interactions = interactions + ${plan.interactions.length}
|
|
16416
|
+
WHERE project_id = ${input.projectId} AND id = ${state.visitId}
|
|
16417
|
+
`;
|
|
16418
|
+
}
|
|
16419
|
+
for (const pageView of plan.pageViews) {
|
|
16420
|
+
await sql`
|
|
16421
|
+
INSERT INTO activity_page_views (
|
|
16422
|
+
project_id, visit_id, sequence, id, route, received_at, attribution
|
|
16423
|
+
) VALUES (
|
|
16424
|
+
${input.projectId}, ${pageView.visitId}, ${pageView.sequence}, ${pageView.id},
|
|
16425
|
+
${pageView.route}, ${pageView.receivedAt},
|
|
16426
|
+
${serializeActivityAttribution(pageView.attribution)}::text::jsonb
|
|
16427
|
+
)
|
|
16428
|
+
`;
|
|
16429
|
+
}
|
|
16430
|
+
for (const interaction of plan.interactions) {
|
|
16431
|
+
await sql`
|
|
16432
|
+
INSERT INTO activity_interactions (
|
|
16433
|
+
project_id, visit_id, sequence, id, name, route, received_at
|
|
16434
|
+
) VALUES (
|
|
16435
|
+
${input.projectId}, ${interaction.visitId}, ${interaction.sequence},
|
|
16436
|
+
${interaction.id}, ${interaction.name}, ${interaction.route}, ${interaction.receivedAt}
|
|
16437
|
+
)
|
|
16438
|
+
`;
|
|
16439
|
+
}
|
|
16440
|
+
await sql`
|
|
16441
|
+
INSERT INTO activity_sources (
|
|
16442
|
+
project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
|
|
16443
|
+
window_start, window_count
|
|
16444
|
+
) VALUES (
|
|
16445
|
+
${input.projectId}, ${input.actor.type}, ${input.sessionId}, ${state.visitId},
|
|
16446
|
+
${lastSeenAt}, ${state.sequence}, ${new Date(state.windowStart)}, ${state.windowCount}
|
|
16447
|
+
)
|
|
16448
|
+
ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
|
|
16449
|
+
visit_id = excluded.visit_id,
|
|
16450
|
+
last_seen_at = excluded.last_seen_at,
|
|
16451
|
+
sequence = excluded.sequence,
|
|
16452
|
+
window_start = excluded.window_start,
|
|
16453
|
+
window_count = excluded.window_count
|
|
16454
|
+
`;
|
|
16455
|
+
return { status: "accepted", accepted: plan.accepted };
|
|
16456
|
+
});
|
|
16457
|
+
}
|
|
16458
|
+
async list(input) {
|
|
16459
|
+
const before = input.before ?? NO_CURSOR;
|
|
16460
|
+
const rows = await this.sql`
|
|
16461
|
+
SELECT ${this.visitColumns()}
|
|
16462
|
+
FROM activity_visits v
|
|
16463
|
+
WHERE ${this.overlapping(input)}
|
|
16464
|
+
AND (
|
|
16465
|
+
v.last_seen_at < ${before.at}
|
|
16466
|
+
OR (v.last_seen_at = ${before.at} AND v.id < ${before.id})
|
|
16467
|
+
)
|
|
16468
|
+
ORDER BY v.last_seen_at DESC, v.id DESC
|
|
16469
|
+
LIMIT ${input.limit + 1}
|
|
16470
|
+
`;
|
|
16471
|
+
return {
|
|
16472
|
+
visits: rows.slice(0, input.limit).map(visitFromRow),
|
|
16473
|
+
hasMore: rows.length > input.limit
|
|
16474
|
+
};
|
|
16475
|
+
}
|
|
16476
|
+
async users(input) {
|
|
16477
|
+
const before = input.before ?? NO_CURSOR;
|
|
16478
|
+
const rows = await this.sql`
|
|
16479
|
+
SELECT
|
|
16480
|
+
v.user_id,
|
|
16481
|
+
MAX(v.last_seen_at) AS last_seen_at,
|
|
16482
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
16483
|
+
COUNT(p.id) AS page_views
|
|
16484
|
+
FROM activity_visits v
|
|
16485
|
+
LEFT JOIN activity_page_views p
|
|
16486
|
+
ON p.project_id = v.project_id
|
|
16487
|
+
AND p.visit_id = v.id
|
|
16488
|
+
AND p.received_at >= ${input.from}
|
|
16489
|
+
AND p.received_at < ${input.to}
|
|
16490
|
+
WHERE ${this.overlapping(input)} AND v.actor_type = 'user'
|
|
16491
|
+
GROUP BY v.user_id
|
|
16492
|
+
HAVING MAX(v.last_seen_at) < ${before.at}
|
|
16493
|
+
OR (MAX(v.last_seen_at) = ${before.at} AND v.user_id < ${before.id})
|
|
16494
|
+
ORDER BY last_seen_at DESC, v.user_id DESC
|
|
16495
|
+
LIMIT ${input.limit + 1}
|
|
16496
|
+
`;
|
|
16497
|
+
return {
|
|
16498
|
+
users: rows.slice(0, input.limit).map((row) => ({
|
|
16499
|
+
userId: row.user_id,
|
|
16500
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
16501
|
+
visits: Number(row.visits),
|
|
16502
|
+
pageViews: Number(row.page_views)
|
|
16503
|
+
})),
|
|
16504
|
+
hasMore: rows.length > input.limit
|
|
16505
|
+
};
|
|
16506
|
+
}
|
|
16507
|
+
async summarize(input) {
|
|
16508
|
+
const [row] = await this.sql`
|
|
16509
|
+
SELECT
|
|
16510
|
+
COUNT(DISTINCT v.user_id) AS users,
|
|
16511
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
16512
|
+
COUNT(DISTINCT v.id) FILTER (WHERE v.actor_type = 'share') AS shared_visits,
|
|
16513
|
+
COUNT(p.id) AS page_views
|
|
16514
|
+
FROM activity_visits v
|
|
16515
|
+
LEFT JOIN activity_page_views p
|
|
16516
|
+
ON p.project_id = v.project_id
|
|
16517
|
+
AND p.visit_id = v.id
|
|
16518
|
+
AND p.received_at >= ${input.from}
|
|
16519
|
+
AND p.received_at < ${input.to}
|
|
16520
|
+
WHERE ${this.overlapping(input)}
|
|
16521
|
+
`;
|
|
16522
|
+
return {
|
|
16523
|
+
users: Number(row?.users ?? 0),
|
|
16524
|
+
visits: Number(row?.visits ?? 0),
|
|
16525
|
+
sharedVisits: Number(row?.shared_visits ?? 0),
|
|
16526
|
+
pageViews: Number(row?.page_views ?? 0)
|
|
16527
|
+
};
|
|
16528
|
+
}
|
|
16529
|
+
async calendar(input) {
|
|
16530
|
+
const visits = await this.sql`
|
|
16531
|
+
SELECT v.started_at, v.last_seen_at
|
|
16532
|
+
FROM activity_visits v
|
|
16533
|
+
WHERE ${this.overlapping(input)}
|
|
16534
|
+
`;
|
|
16535
|
+
const days = await this.sql`
|
|
16536
|
+
SELECT
|
|
16537
|
+
to_char(p.received_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
|
|
16538
|
+
COUNT(*) AS page_views
|
|
16539
|
+
FROM activity_visits v
|
|
16540
|
+
JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
|
|
16541
|
+
WHERE ${this.overlapping(input)}
|
|
16542
|
+
AND p.received_at >= ${input.from}
|
|
16543
|
+
AND p.received_at < ${input.to}
|
|
16544
|
+
GROUP BY day
|
|
16545
|
+
`;
|
|
16546
|
+
return activityCalendarDays(input, visits.map((visit) => ({
|
|
16547
|
+
startedAt: new Date(visit.started_at),
|
|
16548
|
+
lastSeenAt: new Date(visit.last_seen_at)
|
|
16549
|
+
})), new Map(days.map((row) => [row.day, Number(row.page_views)])));
|
|
16550
|
+
}
|
|
16551
|
+
async read(input) {
|
|
16552
|
+
const [visit] = await this.sql`
|
|
16553
|
+
SELECT ${this.visitColumns()}, v.session_id
|
|
16554
|
+
FROM activity_visits v
|
|
16555
|
+
WHERE v.project_id = ${input.projectId} AND v.id = ${input.id}
|
|
16556
|
+
`;
|
|
16557
|
+
if (!visit) {
|
|
16558
|
+
return null;
|
|
16559
|
+
}
|
|
16560
|
+
const rows = await this.sql`
|
|
16561
|
+
SELECT id, visit_id, sequence, route, NULL::text AS name, received_at
|
|
16562
|
+
FROM activity_page_views
|
|
16563
|
+
WHERE project_id = ${input.projectId}
|
|
16564
|
+
AND visit_id = ${input.id}
|
|
16565
|
+
UNION ALL
|
|
16566
|
+
SELECT id, visit_id, sequence, route, name, received_at
|
|
16567
|
+
FROM activity_interactions
|
|
16568
|
+
WHERE project_id = ${input.projectId}
|
|
16569
|
+
AND visit_id = ${input.id}
|
|
16570
|
+
ORDER BY sequence
|
|
16571
|
+
LIMIT ${input.limit + 1}
|
|
16572
|
+
`;
|
|
16573
|
+
const pageViews = [];
|
|
16574
|
+
const interactions = [];
|
|
16575
|
+
for (const row of rows.slice(0, input.limit)) {
|
|
16576
|
+
if (row.name === null) {
|
|
16577
|
+
pageViews.push(pageViewFromRow(row));
|
|
16578
|
+
} else {
|
|
16579
|
+
interactions.push({ ...pageViewFromRow(row), name: row.name });
|
|
16580
|
+
}
|
|
16581
|
+
}
|
|
16582
|
+
return {
|
|
16583
|
+
visit: visitFromRow(visit),
|
|
16584
|
+
sessionId: visit.session_id,
|
|
16585
|
+
pageViews,
|
|
16586
|
+
interactions,
|
|
16587
|
+
hasMore: rows.length > input.limit
|
|
16588
|
+
};
|
|
16589
|
+
}
|
|
16590
|
+
async sessionVisits(input) {
|
|
16591
|
+
if (input.sessionIds.length === 0) {
|
|
16592
|
+
return [];
|
|
16593
|
+
}
|
|
16594
|
+
const rows = await this.sql`
|
|
16595
|
+
SELECT *
|
|
16596
|
+
FROM (
|
|
16597
|
+
SELECT
|
|
16598
|
+
${this.visitColumns()},
|
|
16599
|
+
v.session_id,
|
|
16600
|
+
ROW_NUMBER() OVER (
|
|
16601
|
+
PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
|
|
16602
|
+
) AS position,
|
|
16603
|
+
COUNT(*) OVER (PARTITION BY v.session_id) AS total
|
|
16604
|
+
FROM activity_visits v
|
|
16605
|
+
WHERE v.project_id = ${input.projectId}
|
|
16606
|
+
AND v.actor_type = 'user'
|
|
16607
|
+
AND v.session_id = ANY(${this.sql.array([...input.sessionIds])}::text[])
|
|
16608
|
+
AND v.user_id = ${input.userId}
|
|
16609
|
+
) ranked
|
|
16610
|
+
WHERE position <= ${input.limit}
|
|
16611
|
+
ORDER BY session_id, position
|
|
16612
|
+
`;
|
|
16613
|
+
const bySession = new Map;
|
|
16614
|
+
for (const row of rows) {
|
|
16615
|
+
const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] };
|
|
16616
|
+
session.latest.push(visitFromRow(row));
|
|
16617
|
+
bySession.set(row.session_id, session);
|
|
16618
|
+
}
|
|
16619
|
+
return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }));
|
|
16620
|
+
}
|
|
16621
|
+
overlapping(input) {
|
|
16622
|
+
const user = input.userId === undefined ? this.sql`` : this.sql`AND v.user_id = ${input.userId}`;
|
|
16623
|
+
const earliestStart = new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS);
|
|
16624
|
+
return this.sql`
|
|
16625
|
+
v.project_id = ${input.projectId}
|
|
16626
|
+
AND v.started_at >= ${earliestStart}
|
|
16627
|
+
AND v.started_at < ${input.to}
|
|
16628
|
+
AND v.last_seen_at >= ${input.from}
|
|
16629
|
+
${user}
|
|
16630
|
+
`;
|
|
16631
|
+
}
|
|
16632
|
+
visitColumns() {
|
|
16633
|
+
return this.sql`
|
|
16634
|
+
v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
|
|
16635
|
+
v.interactions, v.attribution::text AS attribution
|
|
16636
|
+
`;
|
|
16637
|
+
}
|
|
16638
|
+
}
|
|
16639
|
+
async function readSource(sql, input) {
|
|
16640
|
+
const [row] = await sql`
|
|
16641
|
+
SELECT
|
|
16642
|
+
s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
|
|
16643
|
+
s.window_start, s.window_count
|
|
16644
|
+
FROM activity_sources s
|
|
16645
|
+
LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
|
|
16646
|
+
WHERE s.project_id = ${input.projectId}
|
|
16647
|
+
AND s.actor_type = ${input.actor.type}
|
|
16648
|
+
AND s.session_id = ${input.sessionId}
|
|
16649
|
+
`;
|
|
16650
|
+
if (!row) {
|
|
16651
|
+
return;
|
|
16652
|
+
}
|
|
16653
|
+
return {
|
|
16654
|
+
visitId: row.visit_id,
|
|
16655
|
+
visitStartedAt: row.visit_started_at === null ? null : new Date(row.visit_started_at).getTime(),
|
|
16656
|
+
lastSeenAt: new Date(row.last_seen_at).getTime(),
|
|
16657
|
+
sequence: Number(row.sequence),
|
|
16658
|
+
windowStart: new Date(row.window_start).getTime(),
|
|
16659
|
+
windowCount: Number(row.window_count)
|
|
16660
|
+
};
|
|
16661
|
+
}
|
|
16662
|
+
async function readStoredEvents(sql, input) {
|
|
16663
|
+
const ids = input.events.map((event) => event.id);
|
|
16664
|
+
const rows = await sql`
|
|
16665
|
+
SELECT p.id, p.route, NULL::text AS name, p.attribution::text AS attribution
|
|
16666
|
+
FROM activity_page_views p
|
|
16667
|
+
JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
|
|
16668
|
+
WHERE p.project_id = ${input.projectId}
|
|
16669
|
+
AND p.id = ANY(${sql.array(ids)}::text[])
|
|
16670
|
+
AND v.actor_type = ${input.actor.type}
|
|
16671
|
+
AND v.session_id = ${input.sessionId}
|
|
16672
|
+
UNION ALL
|
|
16673
|
+
SELECT i.id, i.route, i.name, NULL::text AS attribution
|
|
16674
|
+
FROM activity_interactions i
|
|
16675
|
+
JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
|
|
16676
|
+
WHERE i.project_id = ${input.projectId}
|
|
16677
|
+
AND i.id = ANY(${sql.array(ids)}::text[])
|
|
16678
|
+
AND v.actor_type = ${input.actor.type}
|
|
16679
|
+
AND v.session_id = ${input.sessionId}
|
|
16680
|
+
`;
|
|
16681
|
+
return rows.map((row) => {
|
|
16682
|
+
if (row.name !== null) {
|
|
16683
|
+
return { id: row.id, route: row.route, name: row.name };
|
|
16684
|
+
}
|
|
16685
|
+
const attribution = parseActivityAttribution(row.attribution);
|
|
16686
|
+
return { id: row.id, route: row.route, ...attribution ? { attribution } : {} };
|
|
16687
|
+
});
|
|
16688
|
+
}
|
|
16689
|
+
function visitFromRow(row) {
|
|
16690
|
+
return {
|
|
16691
|
+
id: row.id,
|
|
16692
|
+
actor: activityActor(row.actor_type, row.user_id, row.grant_id),
|
|
16693
|
+
attribution: parseActivityAttribution(row.attribution),
|
|
16694
|
+
startedAt: new Date(row.started_at),
|
|
16695
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
16696
|
+
pageViews: Number(row.page_views),
|
|
16697
|
+
interactions: Number(row.interactions)
|
|
16698
|
+
};
|
|
16699
|
+
}
|
|
16700
|
+
function pageViewFromRow(row) {
|
|
16701
|
+
return {
|
|
16702
|
+
id: row.id,
|
|
16703
|
+
visitId: row.visit_id,
|
|
16704
|
+
sequence: Number(row.sequence),
|
|
16705
|
+
route: row.route,
|
|
16706
|
+
receivedAt: new Date(row.received_at)
|
|
16707
|
+
};
|
|
16708
|
+
}
|
|
16709
|
+
|
|
16100
16710
|
// src/pg-ai-cost-storage.ts
|
|
16101
16711
|
import {
|
|
16102
16712
|
aiModelCallCostDetails,
|
|
@@ -17580,6 +18190,260 @@ function invalidState(reservation, operation) {
|
|
|
17580
18190
|
return new AiLimitStorageError("invalid_reservation_state", `[SixbPg] Cannot ${operation} AI model-call reservation '${reservation.callId}' in state '${reservation.state}'.`);
|
|
17581
18191
|
}
|
|
17582
18192
|
|
|
18193
|
+
// src/pg-artifact-storage.ts
|
|
18194
|
+
import {
|
|
18195
|
+
artifactStorageErrors,
|
|
18196
|
+
parseArtifactFrameGrantRow,
|
|
18197
|
+
parseArtifactRow,
|
|
18198
|
+
parseArtifactVersionRow
|
|
18199
|
+
} from "@sixb/core/internal/storage";
|
|
18200
|
+
var ARTIFACT_COLUMNS = `
|
|
18201
|
+
project_id AS "projectId", id, kind, title,
|
|
18202
|
+
owner_principal_type AS "ownerPrincipalType", owner_principal_id AS "ownerPrincipalId",
|
|
18203
|
+
latest_version AS "latestVersion", created_at AS "createdAt", updated_at AS "updatedAt"
|
|
18204
|
+
`;
|
|
18205
|
+
var VERSION_COLUMNS = `
|
|
18206
|
+
project_id AS "projectId", artifact_id AS "artifactId", version, title, entry, files, outline,
|
|
18207
|
+
refs, author_principal_type AS "authorPrincipalType", author_principal_id AS "authorPrincipalId",
|
|
18208
|
+
origin_thread_id AS "originThreadId", origin_run_id AS "originRunId", created_at AS "createdAt"
|
|
18209
|
+
`;
|
|
18210
|
+
var FRAME_GRANT_COLUMNS = `
|
|
18211
|
+
project_id AS "projectId", token_hash AS "tokenHash", artifact_id AS "artifactId", version,
|
|
18212
|
+
targets, created_at AS "createdAt", expires_at AS "expiresAt"
|
|
18213
|
+
`;
|
|
18214
|
+
|
|
18215
|
+
class PgArtifactStorage {
|
|
18216
|
+
sql;
|
|
18217
|
+
constructor(sql) {
|
|
18218
|
+
this.sql = sql;
|
|
18219
|
+
}
|
|
18220
|
+
async create(input) {
|
|
18221
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
18222
|
+
const [artifact] = await tx`
|
|
18223
|
+
INSERT INTO artifacts (
|
|
18224
|
+
project_id, id, kind, title, owner_principal_type, owner_principal_id,
|
|
18225
|
+
latest_version, next_version, created_at, updated_at
|
|
18226
|
+
) VALUES (
|
|
18227
|
+
${input.projectId}, ${input.id}, ${input.kind}, ${input.version.title},
|
|
18228
|
+
${input.owner.type}, ${input.owner.id}, 1, 2,
|
|
18229
|
+
${input.version.createdAt}, ${input.version.createdAt}
|
|
18230
|
+
)
|
|
18231
|
+
ON CONFLICT (project_id, id) DO NOTHING
|
|
18232
|
+
RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
|
|
18233
|
+
`;
|
|
18234
|
+
if (!artifact)
|
|
18235
|
+
throw artifactStorageErrors.exists(input.id);
|
|
18236
|
+
return {
|
|
18237
|
+
artifact: parseArtifactRow(artifact),
|
|
18238
|
+
version: await insertVersion(tx, input.projectId, input.id, 1, input.version)
|
|
18239
|
+
};
|
|
18240
|
+
});
|
|
18241
|
+
}
|
|
18242
|
+
async publish(input) {
|
|
18243
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
18244
|
+
const [current] = await tx`
|
|
18245
|
+
SELECT kind, next_version FROM artifacts
|
|
18246
|
+
WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
18247
|
+
FOR UPDATE
|
|
18248
|
+
`;
|
|
18249
|
+
if (!current)
|
|
18250
|
+
throw artifactStorageErrors.notFound(input.artifactId);
|
|
18251
|
+
const kind = current.kind;
|
|
18252
|
+
if (kind !== input.kind) {
|
|
18253
|
+
throw artifactStorageErrors.kindMismatch(input.artifactId, kind, input.kind);
|
|
18254
|
+
}
|
|
18255
|
+
const version = await insertVersion(tx, input.projectId, input.artifactId, Number(current.next_version), input.version);
|
|
18256
|
+
const [artifact] = await tx`
|
|
18257
|
+
UPDATE artifacts
|
|
18258
|
+
SET title = ${input.version.title}, latest_version = ${version.version},
|
|
18259
|
+
next_version = ${version.version + 1}, updated_at = ${input.version.createdAt}
|
|
18260
|
+
WHERE project_id = ${input.projectId} AND id = ${input.artifactId}
|
|
18261
|
+
RETURNING ${tx.unsafe(ARTIFACT_COLUMNS)}
|
|
18262
|
+
`;
|
|
18263
|
+
return { artifact: parseArtifactRow(artifact), version };
|
|
18264
|
+
});
|
|
18265
|
+
}
|
|
18266
|
+
async get(input) {
|
|
18267
|
+
const [row] = await this.sql`
|
|
18268
|
+
SELECT ${this.sql.unsafe(ARTIFACT_COLUMNS)} FROM artifacts
|
|
18269
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
18270
|
+
`;
|
|
18271
|
+
return row ? parseArtifactRow(row) : null;
|
|
18272
|
+
}
|
|
18273
|
+
async list(input) {
|
|
18274
|
+
const where = ["a.project_id = $1"];
|
|
18275
|
+
const params = [input.projectId];
|
|
18276
|
+
if (input.owner) {
|
|
18277
|
+
where.push(`a.owner_principal_type = $${params.length + 1}`);
|
|
18278
|
+
params.push(input.owner.type);
|
|
18279
|
+
where.push(`a.owner_principal_id = $${params.length + 1}`);
|
|
18280
|
+
params.push(input.owner.id);
|
|
18281
|
+
}
|
|
18282
|
+
if (input.originThreadId !== undefined) {
|
|
18283
|
+
where.push(`EXISTS (
|
|
18284
|
+
SELECT 1 FROM artifact_versions v
|
|
18285
|
+
WHERE v.project_id = a.project_id AND v.artifact_id = a.id
|
|
18286
|
+
AND v.origin_thread_id = $${params.length + 1}
|
|
18287
|
+
)`);
|
|
18288
|
+
params.push(input.originThreadId);
|
|
18289
|
+
}
|
|
18290
|
+
if (input.kind !== undefined) {
|
|
18291
|
+
where.push(`a.kind = $${params.length + 1}`);
|
|
18292
|
+
params.push(input.kind);
|
|
18293
|
+
}
|
|
18294
|
+
const clause = where.join(" AND ");
|
|
18295
|
+
const [counted] = await this.sql.unsafe(`SELECT COUNT(*)::int AS total FROM artifacts a WHERE ${clause}`, params);
|
|
18296
|
+
const offset = input.offset ?? 0;
|
|
18297
|
+
const rows = await this.sql.unsafe(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
|
|
18298
|
+
ORDER BY a.updated_at DESC, a.id ASC
|
|
18299
|
+
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, input.limit ?? null, offset]);
|
|
18300
|
+
const total = counted?.total ?? 0;
|
|
18301
|
+
return { artifacts: rows.map(parseArtifactRow), hasMore: offset + rows.length < total, total };
|
|
18302
|
+
}
|
|
18303
|
+
async getVersion(input) {
|
|
18304
|
+
const [row] = await this.sql`
|
|
18305
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
|
|
18306
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
18307
|
+
AND version = ${input.version}
|
|
18308
|
+
`;
|
|
18309
|
+
return row ? parseArtifactVersionRow(row) : null;
|
|
18310
|
+
}
|
|
18311
|
+
async listVersions(input) {
|
|
18312
|
+
const [counted] = await this.sql`
|
|
18313
|
+
SELECT COUNT(*)::int AS total FROM artifact_versions
|
|
18314
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
18315
|
+
`;
|
|
18316
|
+
const offset = input.offset ?? 0;
|
|
18317
|
+
const rows = await this.sql`
|
|
18318
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions
|
|
18319
|
+
WHERE project_id = ${input.projectId} AND artifact_id = ${input.artifactId}
|
|
18320
|
+
ORDER BY version DESC
|
|
18321
|
+
LIMIT ${input.limit ?? null} OFFSET ${offset}
|
|
18322
|
+
`;
|
|
18323
|
+
const total = counted?.total ?? 0;
|
|
18324
|
+
return {
|
|
18325
|
+
versions: rows.map(parseArtifactVersionRow),
|
|
18326
|
+
hasMore: offset + rows.length < total,
|
|
18327
|
+
total
|
|
18328
|
+
};
|
|
18329
|
+
}
|
|
18330
|
+
async listReferencingVersions(input) {
|
|
18331
|
+
const prefix = `artifact:${input.artifactId}`;
|
|
18332
|
+
const rows = await this.sql`
|
|
18333
|
+
SELECT ${this.sql.unsafe(VERSION_COLUMNS)} FROM artifact_versions v
|
|
18334
|
+
WHERE project_id = ${input.projectId} AND (artifact_id, version) IN (
|
|
18335
|
+
SELECT id, latest_version FROM artifacts WHERE project_id = ${input.projectId}
|
|
18336
|
+
) AND EXISTS (
|
|
18337
|
+
SELECT 1 FROM jsonb_array_elements(v.refs) r
|
|
18338
|
+
WHERE left(r ->> 'address', ${prefix.length}::int) = ${prefix}
|
|
18339
|
+
AND substr(r ->> 'address', ${prefix.length + 1}::int, 1) IN ('', '@', '/', '#')
|
|
18340
|
+
)
|
|
18341
|
+
ORDER BY artifact_id
|
|
18342
|
+
`;
|
|
18343
|
+
return rows.map(parseArtifactVersionRow);
|
|
18344
|
+
}
|
|
18345
|
+
async createFrameGrant(record) {
|
|
18346
|
+
const rows = await this.sql`
|
|
18347
|
+
INSERT INTO artifact_frame_grants
|
|
18348
|
+
(project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
|
|
18349
|
+
SELECT ${record.projectId}, ${record.tokenHash}, ${record.artifactId}, ${record.version},
|
|
18350
|
+
${JSON.stringify(record.targets)}::text::jsonb, ${record.createdAt},
|
|
18351
|
+
${record.expiresAt}
|
|
18352
|
+
WHERE EXISTS (
|
|
18353
|
+
SELECT 1 FROM artifacts WHERE project_id = ${record.projectId} AND id = ${record.artifactId}
|
|
18354
|
+
)
|
|
18355
|
+
RETURNING token_hash
|
|
18356
|
+
`;
|
|
18357
|
+
if (rows.length === 0)
|
|
18358
|
+
throw artifactStorageErrors.notFound(record.artifactId);
|
|
18359
|
+
}
|
|
18360
|
+
async findFrameGrant(input) {
|
|
18361
|
+
const [row] = await this.sql`
|
|
18362
|
+
SELECT ${this.sql.unsafe(FRAME_GRANT_COLUMNS)} FROM artifact_frame_grants
|
|
18363
|
+
WHERE project_id = ${input.projectId} AND token_hash = ${input.tokenHash}
|
|
18364
|
+
`;
|
|
18365
|
+
return row ? parseArtifactFrameGrantRow(row) : null;
|
|
18366
|
+
}
|
|
18367
|
+
async deleteExpiredFrameGrants(input) {
|
|
18368
|
+
const rows = await this.sql`
|
|
18369
|
+
DELETE FROM artifact_frame_grants
|
|
18370
|
+
WHERE (project_id, token_hash) IN (
|
|
18371
|
+
SELECT project_id, token_hash FROM artifact_frame_grants
|
|
18372
|
+
WHERE project_id = ${input.projectId} AND expires_at < ${input.expiredBefore}
|
|
18373
|
+
ORDER BY expires_at LIMIT ${input.limit}
|
|
18374
|
+
)
|
|
18375
|
+
RETURNING token_hash
|
|
18376
|
+
`;
|
|
18377
|
+
return rows.length;
|
|
18378
|
+
}
|
|
18379
|
+
async delete(input) {
|
|
18380
|
+
const rows = await this.sql`
|
|
18381
|
+
DELETE FROM artifacts WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
18382
|
+
RETURNING id
|
|
18383
|
+
`;
|
|
18384
|
+
return rows.length > 0;
|
|
18385
|
+
}
|
|
18386
|
+
async deleteVersionsByOrigin(input) {
|
|
18387
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
18388
|
+
const locked = await tx`
|
|
18389
|
+
SELECT id FROM artifacts
|
|
18390
|
+
WHERE project_id = ${input.projectId} AND id IN (
|
|
18391
|
+
SELECT artifact_id FROM artifact_versions
|
|
18392
|
+
WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
|
|
18393
|
+
)
|
|
18394
|
+
ORDER BY id
|
|
18395
|
+
FOR UPDATE
|
|
18396
|
+
`;
|
|
18397
|
+
const affected = locked.map((row) => row.id);
|
|
18398
|
+
if (affected.length === 0)
|
|
18399
|
+
return 0;
|
|
18400
|
+
const deleted = await tx`
|
|
18401
|
+
DELETE FROM artifact_versions
|
|
18402
|
+
WHERE project_id = ${input.projectId} AND origin_run_id = ${input.runId}
|
|
18403
|
+
AND artifact_id IN ${tx(affected)}
|
|
18404
|
+
RETURNING version
|
|
18405
|
+
`;
|
|
18406
|
+
await tx`
|
|
18407
|
+
DELETE FROM artifacts a
|
|
18408
|
+
WHERE a.project_id = ${input.projectId} AND a.id IN ${tx(affected)}
|
|
18409
|
+
AND NOT EXISTS (
|
|
18410
|
+
SELECT 1 FROM artifact_versions v
|
|
18411
|
+
WHERE v.project_id = a.project_id AND v.artifact_id = a.id
|
|
18412
|
+
)
|
|
18413
|
+
`;
|
|
18414
|
+
await tx`
|
|
18415
|
+
UPDATE artifacts a
|
|
18416
|
+
SET latest_version = latest.version, title = latest.title, updated_at = latest.created_at
|
|
18417
|
+
FROM (
|
|
18418
|
+
SELECT DISTINCT ON (artifact_id) artifact_id, version, title, created_at
|
|
18419
|
+
FROM artifact_versions
|
|
18420
|
+
WHERE project_id = ${input.projectId} AND artifact_id IN ${tx(affected)}
|
|
18421
|
+
ORDER BY artifact_id, version DESC
|
|
18422
|
+
) latest
|
|
18423
|
+
WHERE a.project_id = ${input.projectId} AND a.id = latest.artifact_id
|
|
18424
|
+
`;
|
|
18425
|
+
return deleted.length;
|
|
18426
|
+
});
|
|
18427
|
+
}
|
|
18428
|
+
}
|
|
18429
|
+
async function insertVersion(sql, projectId, artifactId, version, input) {
|
|
18430
|
+
const [row] = await sql`
|
|
18431
|
+
INSERT INTO artifact_versions (
|
|
18432
|
+
project_id, artifact_id, version, title, entry, files, outline, refs,
|
|
18433
|
+
author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
|
|
18434
|
+
) VALUES (
|
|
18435
|
+
${projectId}, ${artifactId}, ${version}, ${input.title}, ${input.entry},
|
|
18436
|
+
${JSON.stringify(input.files)}::text::jsonb,
|
|
18437
|
+
${JSON.stringify(input.outline)}::text::jsonb,
|
|
18438
|
+
${JSON.stringify(input.references)}::text::jsonb,
|
|
18439
|
+
${input.author.type}, ${input.author.id},
|
|
18440
|
+
${input.origin?.threadId ?? null}, ${input.origin?.runId ?? null}, ${input.createdAt}
|
|
18441
|
+
)
|
|
18442
|
+
RETURNING ${sql.unsafe(VERSION_COLUMNS)}
|
|
18443
|
+
`;
|
|
18444
|
+
return parseArtifactVersionRow(row);
|
|
18445
|
+
}
|
|
18446
|
+
|
|
17583
18447
|
// src/pg-client.ts
|
|
17584
18448
|
import { isIP } from "net";
|
|
17585
18449
|
import { connect as connectTls } from "tls";
|
|
@@ -20004,6 +20868,23 @@ class PgWorkflowRunStorage {
|
|
|
20004
20868
|
`;
|
|
20005
20869
|
return row ? rowToWorkflowRunRecord(row) : null;
|
|
20006
20870
|
}
|
|
20871
|
+
async getByExecutionIds(params) {
|
|
20872
|
+
const executionIds = [...new Set(params.executionIds)];
|
|
20873
|
+
if (executionIds.length === 0) {
|
|
20874
|
+
return [];
|
|
20875
|
+
}
|
|
20876
|
+
const rows = await this.sql`
|
|
20877
|
+
SELECT *
|
|
20878
|
+
FROM workflow_runs
|
|
20879
|
+
WHERE project_id = ${params.projectId}
|
|
20880
|
+
AND execution_id = ANY(${this.sql.array(executionIds)}::text[])
|
|
20881
|
+
`;
|
|
20882
|
+
const byExecution = new Map(rows.map((row) => [row.execution_id, row]));
|
|
20883
|
+
return executionIds.flatMap((executionId) => {
|
|
20884
|
+
const row = byExecution.get(executionId);
|
|
20885
|
+
return row ? [rowToWorkflowRunRecord(row)] : [];
|
|
20886
|
+
});
|
|
20887
|
+
}
|
|
20007
20888
|
async list(input) {
|
|
20008
20889
|
if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
|
|
20009
20890
|
return {
|
|
@@ -20607,6 +21488,23 @@ class PgShareGrantStorage {
|
|
|
20607
21488
|
`;
|
|
20608
21489
|
return row ? recordFromRow(row) : null;
|
|
20609
21490
|
}
|
|
21491
|
+
async getByIds(input) {
|
|
21492
|
+
const ids = [...new Set(input.ids)];
|
|
21493
|
+
if (ids.length === 0) {
|
|
21494
|
+
return [];
|
|
21495
|
+
}
|
|
21496
|
+
const rows = await this.sql`
|
|
21497
|
+
SELECT *
|
|
21498
|
+
FROM share_grants
|
|
21499
|
+
WHERE project_id = ${input.projectId}
|
|
21500
|
+
AND id = ANY(${this.sql.array(ids)}::text[])
|
|
21501
|
+
`;
|
|
21502
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
21503
|
+
return ids.flatMap((id) => {
|
|
21504
|
+
const row = byId.get(id);
|
|
21505
|
+
return row ? [recordFromRow(row)] : [];
|
|
21506
|
+
});
|
|
21507
|
+
}
|
|
20610
21508
|
async list(input) {
|
|
20611
21509
|
const normalized = normalizeListShareGrantsInput(input);
|
|
20612
21510
|
const where = ["project_id = $1"];
|
|
@@ -20826,6 +21724,7 @@ class PostgresStorage {
|
|
|
20826
21724
|
auth;
|
|
20827
21725
|
executions;
|
|
20828
21726
|
agents;
|
|
21727
|
+
artifacts;
|
|
20829
21728
|
aiUsage;
|
|
20830
21729
|
aiCosts;
|
|
20831
21730
|
aiLimits;
|
|
@@ -20843,6 +21742,7 @@ class PostgresStorage {
|
|
|
20843
21742
|
shareSessions;
|
|
20844
21743
|
fileUploadSessions;
|
|
20845
21744
|
fileDownloadGrants;
|
|
21745
|
+
activity;
|
|
20846
21746
|
migrators;
|
|
20847
21747
|
sql;
|
|
20848
21748
|
schemaName;
|
|
@@ -20884,6 +21784,7 @@ class PostgresStorage {
|
|
|
20884
21784
|
this.auth = createAuthOperationScope(stores.auth, scope);
|
|
20885
21785
|
this.executions = createOperationScopedFacade(stores.executions, scope);
|
|
20886
21786
|
this.agents = createAgentOperationScope(stores.agents, scope);
|
|
21787
|
+
this.artifacts = createOperationScopedFacade(stores.artifacts, scope);
|
|
20887
21788
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
|
|
20888
21789
|
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
|
|
20889
21790
|
this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope);
|
|
@@ -20901,6 +21802,7 @@ class PostgresStorage {
|
|
|
20901
21802
|
this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope);
|
|
20902
21803
|
this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope);
|
|
20903
21804
|
this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope);
|
|
21805
|
+
this.activity = createOperationScopedFacade(stores.activity, scope);
|
|
20904
21806
|
registerPostgresStorageTestingAdapter(this, (durationMs) => stores.connectorConnections.advanceTimeForTesting(durationMs));
|
|
20905
21807
|
}
|
|
20906
21808
|
async ping() {
|
|
@@ -20993,6 +21895,7 @@ function createPostgresStores(sql, options) {
|
|
|
20993
21895
|
auth,
|
|
20994
21896
|
executions,
|
|
20995
21897
|
agents: new PgAgentStorage({ sql, executions }),
|
|
21898
|
+
artifacts: new PgArtifactStorage(sql),
|
|
20996
21899
|
aiUsage: new PgAiUsageStorage(sql),
|
|
20997
21900
|
aiCosts: new PgAiCostStorage(sql),
|
|
20998
21901
|
aiLimits: new PgAiLimitStorage(sql),
|
|
@@ -21009,7 +21912,8 @@ function createPostgresStores(sql, options) {
|
|
|
21009
21912
|
shareGrants,
|
|
21010
21913
|
shareSessions,
|
|
21011
21914
|
fileUploadSessions: new PgFileUploadSessionStorage(sql),
|
|
21012
|
-
fileDownloadGrants: new PgFileDownloadGrantStorage(sql)
|
|
21915
|
+
fileDownloadGrants: new PgFileDownloadGrantStorage(sql),
|
|
21916
|
+
activity: new PgActivityStorage(sql)
|
|
21013
21917
|
};
|
|
21014
21918
|
}
|
|
21015
21919
|
function resolveTimeoutMillis(value, label) {
|
|
@@ -21026,4 +21930,4 @@ export {
|
|
|
21026
21930
|
migratePostgresStorage
|
|
21027
21931
|
};
|
|
21028
21932
|
|
|
21029
|
-
//# debugId=
|
|
21933
|
+
//# debugId=E2B2D974990D4A3064756E2164756E21
|