@alma-harness/postgres 0.1.0 → 0.2.0
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/README.md +30 -3
- package/dist/index.d.ts +127 -14
- package/dist/index.js +275 -75
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// src/session-store.ts
|
|
2
2
|
import { assertWellFormed, scopePath as scopePath2 } from "@alma-harness/core";
|
|
3
3
|
|
|
4
|
-
// src/scoped.ts
|
|
5
|
-
import { scopePath } from "@alma-harness/core";
|
|
6
|
-
|
|
7
4
|
// src/schema.ts
|
|
8
5
|
var DEFAULT_RLS_ROLE = "alma_app";
|
|
6
|
+
var DEFAULT_RETENTION_ROLE = "alma_retention";
|
|
9
7
|
var IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
|
10
8
|
function assertRoleIdentifier(role) {
|
|
11
9
|
if (!IDENTIFIER.test(role)) {
|
|
@@ -30,6 +28,29 @@ create policy ${name} on ${table}
|
|
|
30
28
|
);
|
|
31
29
|
`;
|
|
32
30
|
}
|
|
31
|
+
function retentionPolicySql(table, retentionRole, name) {
|
|
32
|
+
assertRoleIdentifier(table);
|
|
33
|
+
assertRoleIdentifier(retentionRole);
|
|
34
|
+
assertRoleIdentifier(name);
|
|
35
|
+
return `
|
|
36
|
+
drop policy if exists ${name}_read on ${table};
|
|
37
|
+
create policy ${name}_read on ${table}
|
|
38
|
+
for select
|
|
39
|
+
to ${retentionRole}
|
|
40
|
+
using (true);
|
|
41
|
+
|
|
42
|
+
drop policy if exists ${name} on ${table};
|
|
43
|
+
create policy ${name} on ${table}
|
|
44
|
+
for delete
|
|
45
|
+
to ${retentionRole}
|
|
46
|
+
using (true);`;
|
|
47
|
+
}
|
|
48
|
+
function assertIso8601(at, label = "timestamp") {
|
|
49
|
+
if (Number.isNaN(Date.parse(at))) {
|
|
50
|
+
throw new Error(`invalid ISO 8601 ${label}: ${JSON.stringify(at)}`);
|
|
51
|
+
}
|
|
52
|
+
return at;
|
|
53
|
+
}
|
|
33
54
|
function roleBootstrapSql(role) {
|
|
34
55
|
assertRoleIdentifier(role);
|
|
35
56
|
return `
|
|
@@ -38,16 +59,26 @@ begin
|
|
|
38
59
|
if not exists (select from pg_roles where rolname = '${role}') then
|
|
39
60
|
begin
|
|
40
61
|
create role ${role} nologin;
|
|
41
|
-
exception when duplicate_object then
|
|
62
|
+
exception when duplicate_object or unique_violation then
|
|
42
63
|
-- Another instance won the race between the existence check and the
|
|
43
64
|
-- create (pg_roles is cluster-wide and CREATE ROLE has no IF NOT
|
|
44
65
|
-- EXISTS); losing it must not roll back the rest of the migration.
|
|
66
|
+
-- Two creates that overlap surface as unique_violation on
|
|
67
|
+
-- pg_authid, not duplicate_object (review of 056\u2013058).
|
|
45
68
|
null;
|
|
46
69
|
end;
|
|
47
70
|
end if;
|
|
48
71
|
end $$;
|
|
49
72
|
`;
|
|
50
73
|
}
|
|
74
|
+
function grantRoleSql(role, to) {
|
|
75
|
+
assertRoleIdentifier(role);
|
|
76
|
+
assertRoleIdentifier(to);
|
|
77
|
+
return `grant ${role} to ${to};`;
|
|
78
|
+
}
|
|
79
|
+
async function grantRole(pool, opts) {
|
|
80
|
+
await pool.query(grantRoleSql(opts.role ?? DEFAULT_RLS_ROLE, opts.to));
|
|
81
|
+
}
|
|
51
82
|
function sessionStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
|
|
52
83
|
assertRoleIdentifier(role);
|
|
53
84
|
return `
|
|
@@ -84,6 +115,7 @@ async function migrateSessionStore(pool, opts = {}) {
|
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
// src/scoped.ts
|
|
118
|
+
import { scopePath } from "@alma-harness/core";
|
|
87
119
|
var DEFAULT_STATEMENT_TIMEOUT_MS = 3e4;
|
|
88
120
|
function resolveRlsRole(opts) {
|
|
89
121
|
const role = opts.role === void 0 ? DEFAULT_RLS_ROLE : opts.role;
|
|
@@ -97,19 +129,12 @@ function resolveStatementTimeout(opts) {
|
|
|
97
129
|
}
|
|
98
130
|
return ms;
|
|
99
131
|
}
|
|
100
|
-
async function
|
|
101
|
-
scopePath(scope);
|
|
132
|
+
async function inTransaction(pool, role, fn) {
|
|
102
133
|
const client = await pool.connect();
|
|
103
134
|
let rollbackFailed;
|
|
104
135
|
try {
|
|
105
136
|
await client.query("begin");
|
|
106
137
|
if (role !== null) await client.query(`set local role ${role}`);
|
|
107
|
-
await client.query(
|
|
108
|
-
`select set_config('alma.org', $1, true),
|
|
109
|
-
set_config('alma.uid', $2, true),
|
|
110
|
-
set_config('statement_timeout', $3, true)`,
|
|
111
|
-
[scope.org, scope.uid, statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]
|
|
112
|
-
);
|
|
113
138
|
const result = await fn(client);
|
|
114
139
|
await client.query("commit");
|
|
115
140
|
return result;
|
|
@@ -122,6 +147,37 @@ async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATE
|
|
|
122
147
|
client.release(rollbackFailed === void 0 ? void 0 : rollbackFailed);
|
|
123
148
|
}
|
|
124
149
|
}
|
|
150
|
+
async function sweepBefore(pool, opts) {
|
|
151
|
+
assertRoleIdentifier(opts.table);
|
|
152
|
+
assertRoleIdentifier(opts.column);
|
|
153
|
+
const batch = opts.batch ?? 5e3;
|
|
154
|
+
if (!Number.isInteger(batch) || batch < 1) throw new Error(`batch must be a positive integer, got ${batch}`);
|
|
155
|
+
let total = 0;
|
|
156
|
+
for (; ; ) {
|
|
157
|
+
const removed = await inTransaction(pool, opts.role, async (client) => {
|
|
158
|
+
const { rowCount } = await client.query(
|
|
159
|
+
`delete from ${opts.table}
|
|
160
|
+
where ctid = any(array(select ctid from ${opts.table} where ${opts.column} < $1::timestamptz limit $2))`,
|
|
161
|
+
[opts.before, batch]
|
|
162
|
+
);
|
|
163
|
+
return rowCount ?? 0;
|
|
164
|
+
});
|
|
165
|
+
total += removed;
|
|
166
|
+
if (removed < batch) return total;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATEMENT_TIMEOUT_MS) {
|
|
170
|
+
scopePath(scope);
|
|
171
|
+
return inTransaction(pool, role, async (client) => {
|
|
172
|
+
await client.query(
|
|
173
|
+
`select set_config('alma.org', $1, true),
|
|
174
|
+
set_config('alma.uid', $2, true),
|
|
175
|
+
set_config('statement_timeout', $3, true)`,
|
|
176
|
+
[scope.org, scope.uid, statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]
|
|
177
|
+
);
|
|
178
|
+
return fn(client);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
125
181
|
|
|
126
182
|
// src/session-store.ts
|
|
127
183
|
var PostgresSessionStore = class {
|
|
@@ -205,7 +261,7 @@ var PostgresSessionStore = class {
|
|
|
205
261
|
const removals = [];
|
|
206
262
|
for (const row of rows) {
|
|
207
263
|
const survivors = row.msg.blocks.filter(
|
|
208
|
-
(b) => b.type !== "tool_call" && b.type !== "tool_result"
|
|
264
|
+
(b) => b.type !== "tool_call" && b.type !== "tool_result" && b.type !== "reasoning"
|
|
209
265
|
);
|
|
210
266
|
if (survivors.length === row.msg.blocks.length) continue;
|
|
211
267
|
blocks += row.msg.blocks.length - survivors.length;
|
|
@@ -249,11 +305,7 @@ var PostgresSessionStore = class {
|
|
|
249
305
|
return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
|
|
250
306
|
}
|
|
251
307
|
};
|
|
252
|
-
|
|
253
|
-
const ms = Date.parse(at);
|
|
254
|
-
if (Number.isNaN(ms)) throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
|
|
255
|
-
return ms;
|
|
256
|
-
}
|
|
308
|
+
var instant = (at) => Date.parse(assertIso8601(at));
|
|
257
309
|
|
|
258
310
|
// src/memory-stores.ts
|
|
259
311
|
import {
|
|
@@ -1075,7 +1127,6 @@ var AUDIT_ROUTING_TABLE = "alma_audit_routing";
|
|
|
1075
1127
|
var AUDIT_COST_TABLE = "alma_audit_cost";
|
|
1076
1128
|
var AUDIT_RECALL_TABLE = "alma_audit_recall";
|
|
1077
1129
|
var AUDIT_CONTEXT_TABLE = "alma_audit_context";
|
|
1078
|
-
var DEFAULT_RETENTION_ROLE = "alma_retention";
|
|
1079
1130
|
var AUDIT_TABLES = [
|
|
1080
1131
|
AUDIT_ACCESS_TABLE,
|
|
1081
1132
|
AUDIT_ROUTING_TABLE,
|
|
@@ -1225,25 +1276,11 @@ grant select, insert
|
|
|
1225
1276
|
-- 039 review caught: a retention mechanism that silently retains forever, in
|
|
1226
1277
|
-- exactly the deployments careful enough not to connect as a superuser.
|
|
1227
1278
|
--
|
|
1228
|
-
-- TWO policies, not one. A FOR DELETE policy alone
|
|
1229
|
-
-- because a DELETE with a WHERE clause must SCAN the
|
|
1230
|
-
-- the scope-keyed policy is FOR ALL -- which governs
|
|
1231
|
-
-- step surfaced only by running the statement
|
|
1232
|
-
|
|
1233
|
-
${AUDIT_TABLES.map(
|
|
1234
|
-
(t) => `
|
|
1235
|
-
drop policy if exists alma_audit_retention_read on ${t};
|
|
1236
|
-
create policy alma_audit_retention_read on ${t}
|
|
1237
|
-
for select
|
|
1238
|
-
to ${retentionRole}
|
|
1239
|
-
using (true);
|
|
1240
|
-
|
|
1241
|
-
drop policy if exists alma_audit_retention on ${t};
|
|
1242
|
-
create policy alma_audit_retention on ${t}
|
|
1243
|
-
for delete
|
|
1244
|
-
to ${retentionRole}
|
|
1245
|
-
using (true);`
|
|
1246
|
-
).join("")}
|
|
1279
|
+
-- TWO policies, not one (see retentionPolicySql). A FOR DELETE policy alone
|
|
1280
|
+
-- still deleted nothing, because a DELETE with a WHERE clause must SCAN the
|
|
1281
|
+
-- rows to filter them, and the scope-keyed policy is FOR ALL -- which governs
|
|
1282
|
+
-- SELECT too. That second step surfaced only by running the statement.
|
|
1283
|
+
${AUDIT_TABLES.map((t) => retentionPolicySql(t, retentionRole, "alma_audit_retention")).join("")}
|
|
1247
1284
|
|
|
1248
1285
|
grant select, delete
|
|
1249
1286
|
on ${AUDIT_TABLES.join(", ")}
|
|
@@ -1258,32 +1295,15 @@ async function purgeAuditBefore(pool, windows, opts = {}) {
|
|
|
1258
1295
|
assertRoleIdentifier(retentionRole);
|
|
1259
1296
|
for (const table of AUDIT_TABLES) {
|
|
1260
1297
|
const before = windows[table];
|
|
1261
|
-
if (before !== void 0
|
|
1262
|
-
throw new Error(`invalid ISO 8601 timestamp for ${table}: ${JSON.stringify(before)}`);
|
|
1263
|
-
}
|
|
1298
|
+
if (before !== void 0) assertIso8601(before, `timestamp for ${table}`);
|
|
1264
1299
|
}
|
|
1265
1300
|
const purged = {};
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
await
|
|
1270
|
-
for (const table of AUDIT_TABLES) {
|
|
1271
|
-
const before = windows[table];
|
|
1272
|
-
if (before === void 0) continue;
|
|
1273
|
-
const { rowCount } = await client.query(
|
|
1274
|
-
`delete from ${table} where at < $1::timestamptz`,
|
|
1275
|
-
[before]
|
|
1276
|
-
);
|
|
1277
|
-
purged[table] = rowCount ?? 0;
|
|
1278
|
-
}
|
|
1279
|
-
await client.query("commit");
|
|
1280
|
-
return purged;
|
|
1281
|
-
} catch (err) {
|
|
1282
|
-
await client.query("rollback").catch(() => void 0);
|
|
1283
|
-
throw err;
|
|
1284
|
-
} finally {
|
|
1285
|
-
client.release();
|
|
1301
|
+
for (const table of AUDIT_TABLES) {
|
|
1302
|
+
const before = windows[table];
|
|
1303
|
+
if (before === void 0) continue;
|
|
1304
|
+
purged[table] = await sweepBefore(pool, { role: retentionRole, table, column: "at", before, ...opts.batch !== void 0 ? { batch: opts.batch } : {} });
|
|
1286
1305
|
}
|
|
1306
|
+
return purged;
|
|
1287
1307
|
}
|
|
1288
1308
|
|
|
1289
1309
|
// src/audit-store.ts
|
|
@@ -1439,12 +1459,7 @@ var PostgresAuditLog = class {
|
|
|
1439
1459
|
}, this.#timeoutMs);
|
|
1440
1460
|
}
|
|
1441
1461
|
};
|
|
1442
|
-
|
|
1443
|
-
if (Number.isNaN(Date.parse(at))) {
|
|
1444
|
-
throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
|
|
1445
|
-
}
|
|
1446
|
-
return at;
|
|
1447
|
-
}
|
|
1462
|
+
var instant2 = (at) => assertIso8601(at);
|
|
1448
1463
|
|
|
1449
1464
|
// src/turn-store.ts
|
|
1450
1465
|
import {
|
|
@@ -1455,8 +1470,9 @@ import {
|
|
|
1455
1470
|
// src/turn-schema.ts
|
|
1456
1471
|
var TURN_LEASES_TABLE = "alma_turn_leases";
|
|
1457
1472
|
var TURN_CLAIMS_TABLE = "alma_turn_claims";
|
|
1458
|
-
function turnStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
|
|
1473
|
+
function turnStoreMigrationSql(role = DEFAULT_RLS_ROLE, retentionRole = DEFAULT_RETENTION_ROLE) {
|
|
1459
1474
|
assertRoleIdentifier(role);
|
|
1475
|
+
assertRoleIdentifier(retentionRole);
|
|
1460
1476
|
return `
|
|
1461
1477
|
create table if not exists ${TURN_LEASES_TABLE} (
|
|
1462
1478
|
org text not null,
|
|
@@ -1476,15 +1492,30 @@ create table if not exists ${TURN_CLAIMS_TABLE} (
|
|
|
1476
1492
|
created_at timestamptz not null default now(),
|
|
1477
1493
|
primary key (org, uid, session_id, idempotency_key)
|
|
1478
1494
|
);
|
|
1495
|
+
|
|
1496
|
+
create index if not exists alma_turn_claims_created
|
|
1497
|
+
on ${TURN_CLAIMS_TABLE} (created_at);
|
|
1479
1498
|
${rlsPolicySql(TURN_LEASES_TABLE)}${rlsPolicySql(TURN_CLAIMS_TABLE)}
|
|
1480
1499
|
${roleBootstrapSql(role)}
|
|
1500
|
+
${roleBootstrapSql(retentionRole)}
|
|
1481
1501
|
grant select, insert, update, delete
|
|
1482
1502
|
on ${TURN_LEASES_TABLE}, ${TURN_CLAIMS_TABLE}
|
|
1483
1503
|
to ${role};
|
|
1504
|
+
${retentionPolicySql(TURN_CLAIMS_TABLE, retentionRole, "alma_turn_claims_retention")}
|
|
1505
|
+
|
|
1506
|
+
grant select, delete
|
|
1507
|
+
on ${TURN_CLAIMS_TABLE}
|
|
1508
|
+
to ${retentionRole};
|
|
1484
1509
|
`;
|
|
1485
1510
|
}
|
|
1486
1511
|
async function migrateTurnStore(pool, opts = {}) {
|
|
1487
|
-
await pool.query(turnStoreMigrationSql(opts.role));
|
|
1512
|
+
await pool.query(turnStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1513
|
+
}
|
|
1514
|
+
async function purgeTurnClaimsBefore(pool, before, opts = {}) {
|
|
1515
|
+
const role = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
|
|
1516
|
+
assertRoleIdentifier(role);
|
|
1517
|
+
assertIso8601(before);
|
|
1518
|
+
return sweepBefore(pool, { role, table: TURN_CLAIMS_TABLE, column: "created_at", before, ...opts.batch !== void 0 ? { batch: opts.batch } : {} });
|
|
1488
1519
|
}
|
|
1489
1520
|
|
|
1490
1521
|
// src/turn-store.ts
|
|
@@ -1607,12 +1638,10 @@ var PostgresTurnStore = class {
|
|
|
1607
1638
|
await this.#inScope(scope, async (client) => {
|
|
1608
1639
|
const params = sessionId === void 0 ? [scope.org, scope.uid] : [scope.org, scope.uid, sessionId];
|
|
1609
1640
|
const bySession = sessionId === void 0 ? "" : " and session_id = $3";
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
);
|
|
1615
|
-
}
|
|
1641
|
+
await client.query(
|
|
1642
|
+
`delete from ${TURN_CLAIMS_TABLE} where org = $1 and uid = $2${bySession}`,
|
|
1643
|
+
params
|
|
1644
|
+
);
|
|
1616
1645
|
});
|
|
1617
1646
|
}
|
|
1618
1647
|
/** Shared RLS binding — see `scoped.ts`. */
|
|
@@ -1630,6 +1659,165 @@ function assertLeaseOpts(opts) {
|
|
|
1630
1659
|
}
|
|
1631
1660
|
}
|
|
1632
1661
|
}
|
|
1662
|
+
|
|
1663
|
+
// src/routine-run-store.ts
|
|
1664
|
+
import { scopePath as scopePath5 } from "@alma-harness/core";
|
|
1665
|
+
|
|
1666
|
+
// src/routine-schema.ts
|
|
1667
|
+
var ROUTINE_RUNS_TABLE = "alma_routine_runs";
|
|
1668
|
+
function routineRunStoreMigrationSql(role = DEFAULT_RLS_ROLE, retentionRole = DEFAULT_RETENTION_ROLE) {
|
|
1669
|
+
assertRoleIdentifier(role);
|
|
1670
|
+
assertRoleIdentifier(retentionRole);
|
|
1671
|
+
return `
|
|
1672
|
+
create table if not exists ${ROUTINE_RUNS_TABLE} (
|
|
1673
|
+
org text not null,
|
|
1674
|
+
uid text not null,
|
|
1675
|
+
routine_id text not null,
|
|
1676
|
+
run_id text not null,
|
|
1677
|
+
started_at timestamptz not null,
|
|
1678
|
+
finished_at timestamptz,
|
|
1679
|
+
outcome text not null,
|
|
1680
|
+
reason text,
|
|
1681
|
+
cost_usd double precision not null,
|
|
1682
|
+
session_id text,
|
|
1683
|
+
turn_id text,
|
|
1684
|
+
handle jsonb,
|
|
1685
|
+
delivery_hash text,
|
|
1686
|
+
primary key (org, uid, routine_id, run_id),
|
|
1687
|
+
constraint alma_routine_runs_outcome_check
|
|
1688
|
+
check (outcome in ('delivered', 'duplicate', 'submitted', 'waiting', 'refused', 'failed'))
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
create index if not exists alma_routine_runs_recent
|
|
1692
|
+
on ${ROUTINE_RUNS_TABLE} (org, uid, routine_id, started_at desc);
|
|
1693
|
+
${rlsPolicySql(ROUTINE_RUNS_TABLE)}
|
|
1694
|
+
${roleBootstrapSql(role)}
|
|
1695
|
+
${roleBootstrapSql(retentionRole)}
|
|
1696
|
+
grant select, insert, update
|
|
1697
|
+
on ${ROUTINE_RUNS_TABLE}
|
|
1698
|
+
to ${role};
|
|
1699
|
+
${retentionPolicySql(ROUTINE_RUNS_TABLE, retentionRole, "alma_routine_runs_retention")}
|
|
1700
|
+
|
|
1701
|
+
grant select, delete
|
|
1702
|
+
on ${ROUTINE_RUNS_TABLE}
|
|
1703
|
+
to ${retentionRole};
|
|
1704
|
+
`;
|
|
1705
|
+
}
|
|
1706
|
+
async function migrateRoutineRunStore(pool, opts = {}) {
|
|
1707
|
+
await pool.query(routineRunStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1708
|
+
}
|
|
1709
|
+
async function purgeRoutineRunsBefore(pool, before, opts = {}) {
|
|
1710
|
+
const role = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
|
|
1711
|
+
assertRoleIdentifier(role);
|
|
1712
|
+
assertIso8601(before);
|
|
1713
|
+
return sweepBefore(pool, { role, table: ROUTINE_RUNS_TABLE, column: "started_at", before, ...opts.batch !== void 0 ? { batch: opts.batch } : {} });
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/routine-run-store.ts
|
|
1717
|
+
var COLUMNS = "routine_id, run_id, started_at, finished_at, outcome, reason, cost_usd, session_id, turn_id, handle, delivery_hash";
|
|
1718
|
+
var PostgresRoutineRunStore = class {
|
|
1719
|
+
#pool;
|
|
1720
|
+
#role;
|
|
1721
|
+
#timeoutMs;
|
|
1722
|
+
constructor(pool, opts = {}) {
|
|
1723
|
+
this.#pool = pool;
|
|
1724
|
+
this.#role = resolveRlsRole(opts);
|
|
1725
|
+
this.#timeoutMs = resolveStatementTimeout(opts);
|
|
1726
|
+
}
|
|
1727
|
+
async record(run) {
|
|
1728
|
+
scopePath5(run.scope);
|
|
1729
|
+
assertIso8601(run.startedAt, "startedAt");
|
|
1730
|
+
if (run.finishedAt !== void 0) assertIso8601(run.finishedAt, "finishedAt");
|
|
1731
|
+
await this.#inScope(run.scope, async (client) => {
|
|
1732
|
+
await client.query(
|
|
1733
|
+
`insert into ${ROUTINE_RUNS_TABLE}
|
|
1734
|
+
(org, uid, routine_id, run_id, started_at, finished_at, outcome, reason, cost_usd,
|
|
1735
|
+
session_id, turn_id, handle, delivery_hash)
|
|
1736
|
+
values ($1, $2, $3, $4, $5::timestamptz, $6::timestamptz, $7, $8, $9, $10, $11, $12::jsonb, $13)
|
|
1737
|
+
on conflict (org, uid, routine_id, run_id) do update set
|
|
1738
|
+
started_at = excluded.started_at, finished_at = excluded.finished_at,
|
|
1739
|
+
outcome = excluded.outcome, reason = excluded.reason, cost_usd = excluded.cost_usd,
|
|
1740
|
+
session_id = excluded.session_id, turn_id = excluded.turn_id,
|
|
1741
|
+
handle = excluded.handle, delivery_hash = excluded.delivery_hash`,
|
|
1742
|
+
[
|
|
1743
|
+
run.scope.org,
|
|
1744
|
+
run.scope.uid,
|
|
1745
|
+
run.routineId,
|
|
1746
|
+
run.id,
|
|
1747
|
+
run.startedAt,
|
|
1748
|
+
run.finishedAt ?? null,
|
|
1749
|
+
run.outcome,
|
|
1750
|
+
run.reason ?? null,
|
|
1751
|
+
run.costUsd,
|
|
1752
|
+
run.sessionId ?? null,
|
|
1753
|
+
run.turnId ?? null,
|
|
1754
|
+
run.handle === void 0 ? null : JSON.stringify(run.handle),
|
|
1755
|
+
run.deliveryHash ?? null
|
|
1756
|
+
]
|
|
1757
|
+
);
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
async get(scope, routineId, runId) {
|
|
1761
|
+
return this.#inScope(scope, async (client) => {
|
|
1762
|
+
const { rows } = await client.query(
|
|
1763
|
+
`select ${COLUMNS} from ${ROUTINE_RUNS_TABLE}
|
|
1764
|
+
where org = $1 and uid = $2 and routine_id = $3 and run_id = $4`,
|
|
1765
|
+
[scope.org, scope.uid, routineId, runId]
|
|
1766
|
+
);
|
|
1767
|
+
return rows[0] === void 0 ? null : toRun(scope, rows[0]);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
async list(scope, routineId, opts = {}) {
|
|
1771
|
+
scopePath5(scope);
|
|
1772
|
+
if (opts.since !== void 0) assertIso8601(opts.since, "since");
|
|
1773
|
+
if (opts.limit !== void 0 && opts.limit <= 0) return [];
|
|
1774
|
+
return this.#inScope(scope, async (client) => {
|
|
1775
|
+
const params = [scope.org, scope.uid, routineId];
|
|
1776
|
+
const where = ["org = $1", "uid = $2", "routine_id = $3"];
|
|
1777
|
+
if (opts.since !== void 0) {
|
|
1778
|
+
params.push(opts.since);
|
|
1779
|
+
where.push(`started_at >= $${params.length}::timestamptz`);
|
|
1780
|
+
}
|
|
1781
|
+
if (opts.outcome !== void 0) {
|
|
1782
|
+
params.push(opts.outcome);
|
|
1783
|
+
where.push(`outcome = $${params.length}`);
|
|
1784
|
+
}
|
|
1785
|
+
let limit = "";
|
|
1786
|
+
if (opts.limit !== void 0) {
|
|
1787
|
+
params.push(opts.limit);
|
|
1788
|
+
limit = ` limit $${params.length}`;
|
|
1789
|
+
}
|
|
1790
|
+
const { rows } = await client.query(
|
|
1791
|
+
`select ${COLUMNS} from ${ROUTINE_RUNS_TABLE}
|
|
1792
|
+
where ${where.join(" and ")}
|
|
1793
|
+
order by started_at desc, run_id asc${limit}`,
|
|
1794
|
+
params
|
|
1795
|
+
);
|
|
1796
|
+
return rows.map((row) => toRun(scope, row));
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
/** Shared RLS binding — see `scoped.ts`. */
|
|
1800
|
+
async #inScope(scope, fn) {
|
|
1801
|
+
return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
function toRun(scope, row) {
|
|
1805
|
+
const run = {
|
|
1806
|
+
id: row.run_id,
|
|
1807
|
+
routineId: row.routine_id,
|
|
1808
|
+
scope: { org: scope.org, uid: scope.uid },
|
|
1809
|
+
startedAt: row.started_at.toISOString(),
|
|
1810
|
+
outcome: row.outcome,
|
|
1811
|
+
costUsd: row.cost_usd
|
|
1812
|
+
};
|
|
1813
|
+
if (row.finished_at !== null) run.finishedAt = row.finished_at.toISOString();
|
|
1814
|
+
if (row.reason !== null) run.reason = row.reason;
|
|
1815
|
+
if (row.session_id !== null) run.sessionId = row.session_id;
|
|
1816
|
+
if (row.turn_id !== null) run.turnId = row.turn_id;
|
|
1817
|
+
if (row.handle !== null) run.handle = row.handle;
|
|
1818
|
+
if (row.delivery_hash !== null) run.deliveryHash = row.delivery_hash;
|
|
1819
|
+
return run;
|
|
1820
|
+
}
|
|
1633
1821
|
export {
|
|
1634
1822
|
AUDIT_ACCESS_TABLE,
|
|
1635
1823
|
AUDIT_CONTEXT_TABLE,
|
|
@@ -1645,27 +1833,39 @@ export {
|
|
|
1645
1833
|
PostgresEpisodeStore,
|
|
1646
1834
|
PostgresErasureWatermarks,
|
|
1647
1835
|
PostgresProfileStore,
|
|
1836
|
+
PostgresRoutineRunStore,
|
|
1648
1837
|
PostgresSessionStore,
|
|
1649
1838
|
PostgresSpendStore,
|
|
1650
1839
|
PostgresTurnStore,
|
|
1840
|
+
ROUTINE_RUNS_TABLE,
|
|
1651
1841
|
SCOPE_STATE_TABLE,
|
|
1652
1842
|
SPEND_SESSIONS_TABLE,
|
|
1653
1843
|
SPEND_TENANT_DAYS_TABLE,
|
|
1654
1844
|
TURN_CLAIMS_TABLE,
|
|
1655
1845
|
TURN_LEASES_TABLE,
|
|
1846
|
+
assertIso8601,
|
|
1656
1847
|
assertRoleIdentifier,
|
|
1657
1848
|
auditLogMigrationSql,
|
|
1849
|
+
grantRole,
|
|
1850
|
+
grantRoleSql,
|
|
1851
|
+
inTransaction,
|
|
1658
1852
|
memoryStoreMigrationSql,
|
|
1659
1853
|
migrateAuditLog,
|
|
1660
1854
|
migrateMemoryStores,
|
|
1855
|
+
migrateRoutineRunStore,
|
|
1661
1856
|
migrateSessionStore,
|
|
1662
1857
|
migrateSpendStore,
|
|
1663
1858
|
migrateTurnStore,
|
|
1664
1859
|
purgeAuditBefore,
|
|
1860
|
+
purgeRoutineRunsBefore,
|
|
1861
|
+
purgeTurnClaimsBefore,
|
|
1862
|
+
retentionPolicySql,
|
|
1665
1863
|
rlsPolicySql,
|
|
1666
1864
|
roleBootstrapSql,
|
|
1865
|
+
routineRunStoreMigrationSql,
|
|
1667
1866
|
sessionStoreMigrationSql,
|
|
1668
1867
|
spendStoreMigrationSql,
|
|
1868
|
+
sweepBefore,
|
|
1669
1869
|
turnStoreMigrationSql
|
|
1670
1870
|
};
|
|
1671
1871
|
//# sourceMappingURL=index.js.map
|