@alma-harness/postgres 0.1.0 → 0.3.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 +38 -3
- package/dist/index.d.ts +183 -14
- package/dist/index.js +399 -77
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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
|
|
5
|
+
import { assertIso8601 } from "@alma-harness/core";
|
|
8
6
|
var DEFAULT_RLS_ROLE = "alma_app";
|
|
7
|
+
var DEFAULT_RETENTION_ROLE = "alma_retention";
|
|
9
8
|
var IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
|
10
9
|
function assertRoleIdentifier(role) {
|
|
11
10
|
if (!IDENTIFIER.test(role)) {
|
|
@@ -30,6 +29,23 @@ create policy ${name} on ${table}
|
|
|
30
29
|
);
|
|
31
30
|
`;
|
|
32
31
|
}
|
|
32
|
+
function retentionPolicySql(table, retentionRole, name) {
|
|
33
|
+
assertRoleIdentifier(table);
|
|
34
|
+
assertRoleIdentifier(retentionRole);
|
|
35
|
+
assertRoleIdentifier(name);
|
|
36
|
+
return `
|
|
37
|
+
drop policy if exists ${name}_read on ${table};
|
|
38
|
+
create policy ${name}_read on ${table}
|
|
39
|
+
for select
|
|
40
|
+
to ${retentionRole}
|
|
41
|
+
using (true);
|
|
42
|
+
|
|
43
|
+
drop policy if exists ${name} on ${table};
|
|
44
|
+
create policy ${name} on ${table}
|
|
45
|
+
for delete
|
|
46
|
+
to ${retentionRole}
|
|
47
|
+
using (true);`;
|
|
48
|
+
}
|
|
33
49
|
function roleBootstrapSql(role) {
|
|
34
50
|
assertRoleIdentifier(role);
|
|
35
51
|
return `
|
|
@@ -38,16 +54,26 @@ begin
|
|
|
38
54
|
if not exists (select from pg_roles where rolname = '${role}') then
|
|
39
55
|
begin
|
|
40
56
|
create role ${role} nologin;
|
|
41
|
-
exception when duplicate_object then
|
|
57
|
+
exception when duplicate_object or unique_violation then
|
|
42
58
|
-- Another instance won the race between the existence check and the
|
|
43
59
|
-- create (pg_roles is cluster-wide and CREATE ROLE has no IF NOT
|
|
44
60
|
-- EXISTS); losing it must not roll back the rest of the migration.
|
|
61
|
+
-- Two creates that overlap surface as unique_violation on
|
|
62
|
+
-- pg_authid, not duplicate_object (review of 056\u2013058).
|
|
45
63
|
null;
|
|
46
64
|
end;
|
|
47
65
|
end if;
|
|
48
66
|
end $$;
|
|
49
67
|
`;
|
|
50
68
|
}
|
|
69
|
+
function grantRoleSql(role, to) {
|
|
70
|
+
assertRoleIdentifier(role);
|
|
71
|
+
assertRoleIdentifier(to);
|
|
72
|
+
return `grant ${role} to ${to};`;
|
|
73
|
+
}
|
|
74
|
+
async function grantRole(pool, opts) {
|
|
75
|
+
await pool.query(grantRoleSql(opts.role ?? DEFAULT_RLS_ROLE, opts.to));
|
|
76
|
+
}
|
|
51
77
|
function sessionStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
|
|
52
78
|
assertRoleIdentifier(role);
|
|
53
79
|
return `
|
|
@@ -84,6 +110,7 @@ async function migrateSessionStore(pool, opts = {}) {
|
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
// src/scoped.ts
|
|
113
|
+
import { scopePath } from "@alma-harness/core";
|
|
87
114
|
var DEFAULT_STATEMENT_TIMEOUT_MS = 3e4;
|
|
88
115
|
function resolveRlsRole(opts) {
|
|
89
116
|
const role = opts.role === void 0 ? DEFAULT_RLS_ROLE : opts.role;
|
|
@@ -97,19 +124,15 @@ function resolveStatementTimeout(opts) {
|
|
|
97
124
|
}
|
|
98
125
|
return ms;
|
|
99
126
|
}
|
|
100
|
-
async function
|
|
101
|
-
scopePath(scope);
|
|
127
|
+
async function inTransaction(pool, role, fn, statementTimeoutMs) {
|
|
102
128
|
const client = await pool.connect();
|
|
103
129
|
let rollbackFailed;
|
|
104
130
|
try {
|
|
105
131
|
await client.query("begin");
|
|
106
132
|
if (role !== null) await client.query(`set local role ${role}`);
|
|
107
|
-
|
|
108
|
-
`select set_config('
|
|
109
|
-
|
|
110
|
-
set_config('statement_timeout', $3, true)`,
|
|
111
|
-
[scope.org, scope.uid, statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]
|
|
112
|
-
);
|
|
133
|
+
if (statementTimeoutMs !== void 0) {
|
|
134
|
+
await client.query(`select set_config('statement_timeout', $1, true)`, [statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]);
|
|
135
|
+
}
|
|
113
136
|
const result = await fn(client);
|
|
114
137
|
await client.query("commit");
|
|
115
138
|
return result;
|
|
@@ -122,6 +145,43 @@ async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATE
|
|
|
122
145
|
client.release(rollbackFailed === void 0 ? void 0 : rollbackFailed);
|
|
123
146
|
}
|
|
124
147
|
}
|
|
148
|
+
async function sweepBefore(pool, opts) {
|
|
149
|
+
assertRoleIdentifier(opts.table);
|
|
150
|
+
assertRoleIdentifier(opts.column);
|
|
151
|
+
const batch = opts.batch ?? 5e3;
|
|
152
|
+
if (!Number.isInteger(batch) || batch < 1) throw new Error(`batch must be a positive integer, got ${batch}`);
|
|
153
|
+
let total = 0;
|
|
154
|
+
for (; ; ) {
|
|
155
|
+
const removed = await inTransaction(pool, opts.role, async (client) => {
|
|
156
|
+
const { rowCount } = await client.query(
|
|
157
|
+
`delete from ${opts.table}
|
|
158
|
+
where ctid = any(array(select ctid from ${opts.table} where ${opts.column} < $1::timestamptz order by ${opts.column} limit $2))`,
|
|
159
|
+
[opts.before, batch]
|
|
160
|
+
);
|
|
161
|
+
return rowCount ?? 0;
|
|
162
|
+
});
|
|
163
|
+
total += removed;
|
|
164
|
+
if (removed < batch) return total;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function purgeBefore(pool, opts) {
|
|
168
|
+
const role = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
|
|
169
|
+
assertRoleIdentifier(role);
|
|
170
|
+
assertIso8601(opts.before);
|
|
171
|
+
return sweepBefore(pool, { role, table: opts.table, column: opts.column, before: opts.before, ...opts.batch !== void 0 ? { batch: opts.batch } : {} });
|
|
172
|
+
}
|
|
173
|
+
async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATEMENT_TIMEOUT_MS) {
|
|
174
|
+
scopePath(scope);
|
|
175
|
+
return inTransaction(pool, role, async (client) => {
|
|
176
|
+
await client.query(
|
|
177
|
+
`select set_config('alma.org', $1, true),
|
|
178
|
+
set_config('alma.uid', $2, true),
|
|
179
|
+
set_config('statement_timeout', $3, true)`,
|
|
180
|
+
[scope.org, scope.uid, statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]
|
|
181
|
+
);
|
|
182
|
+
return fn(client);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
125
185
|
|
|
126
186
|
// src/session-store.ts
|
|
127
187
|
var PostgresSessionStore = class {
|
|
@@ -205,7 +265,7 @@ var PostgresSessionStore = class {
|
|
|
205
265
|
const removals = [];
|
|
206
266
|
for (const row of rows) {
|
|
207
267
|
const survivors = row.msg.blocks.filter(
|
|
208
|
-
(b) => b.type !== "tool_call" && b.type !== "tool_result"
|
|
268
|
+
(b) => b.type !== "tool_call" && b.type !== "tool_result" && b.type !== "reasoning"
|
|
209
269
|
);
|
|
210
270
|
if (survivors.length === row.msg.blocks.length) continue;
|
|
211
271
|
blocks += row.msg.blocks.length - survivors.length;
|
|
@@ -249,11 +309,7 @@ var PostgresSessionStore = class {
|
|
|
249
309
|
return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
|
|
250
310
|
}
|
|
251
311
|
};
|
|
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
|
-
}
|
|
312
|
+
var instant = (at) => Date.parse(assertIso8601(at));
|
|
257
313
|
|
|
258
314
|
// src/memory-stores.ts
|
|
259
315
|
import {
|
|
@@ -1075,7 +1131,6 @@ var AUDIT_ROUTING_TABLE = "alma_audit_routing";
|
|
|
1075
1131
|
var AUDIT_COST_TABLE = "alma_audit_cost";
|
|
1076
1132
|
var AUDIT_RECALL_TABLE = "alma_audit_recall";
|
|
1077
1133
|
var AUDIT_CONTEXT_TABLE = "alma_audit_context";
|
|
1078
|
-
var DEFAULT_RETENTION_ROLE = "alma_retention";
|
|
1079
1134
|
var AUDIT_TABLES = [
|
|
1080
1135
|
AUDIT_ACCESS_TABLE,
|
|
1081
1136
|
AUDIT_ROUTING_TABLE,
|
|
@@ -1109,6 +1164,9 @@ create index if not exists alma_audit_access_turn
|
|
|
1109
1164
|
create index if not exists alma_audit_access_recent
|
|
1110
1165
|
on ${AUDIT_ACCESS_TABLE} (org, uid, at desc);
|
|
1111
1166
|
|
|
1167
|
+
create index if not exists alma_audit_access_at
|
|
1168
|
+
on ${AUDIT_ACCESS_TABLE} (at);
|
|
1169
|
+
|
|
1112
1170
|
create table if not exists ${AUDIT_ROUTING_TABLE} (
|
|
1113
1171
|
id uuid not null default gen_random_uuid(),
|
|
1114
1172
|
org text not null,
|
|
@@ -1128,6 +1186,9 @@ create table if not exists ${AUDIT_ROUTING_TABLE} (
|
|
|
1128
1186
|
create index if not exists alma_audit_routing_turn
|
|
1129
1187
|
on ${AUDIT_ROUTING_TABLE} (org, uid, turn_id);
|
|
1130
1188
|
|
|
1189
|
+
create index if not exists alma_audit_routing_at
|
|
1190
|
+
on ${AUDIT_ROUTING_TABLE} (at);
|
|
1191
|
+
|
|
1131
1192
|
create table if not exists ${AUDIT_COST_TABLE} (
|
|
1132
1193
|
id uuid not null default gen_random_uuid(),
|
|
1133
1194
|
org text not null,
|
|
@@ -1155,6 +1216,9 @@ create index if not exists alma_audit_cost_turn
|
|
|
1155
1216
|
create index if not exists alma_audit_cost_recent
|
|
1156
1217
|
on ${AUDIT_COST_TABLE} (org, uid, at desc);
|
|
1157
1218
|
|
|
1219
|
+
create index if not exists alma_audit_cost_at
|
|
1220
|
+
on ${AUDIT_COST_TABLE} (at);
|
|
1221
|
+
|
|
1158
1222
|
create table if not exists ${AUDIT_RECALL_TABLE} (
|
|
1159
1223
|
id uuid not null default gen_random_uuid(),
|
|
1160
1224
|
org text not null,
|
|
@@ -1177,6 +1241,9 @@ create table if not exists ${AUDIT_RECALL_TABLE} (
|
|
|
1177
1241
|
create index if not exists alma_audit_recall_turn
|
|
1178
1242
|
on ${AUDIT_RECALL_TABLE} (org, uid, turn_id);
|
|
1179
1243
|
|
|
1244
|
+
create index if not exists alma_audit_recall_at
|
|
1245
|
+
on ${AUDIT_RECALL_TABLE} (at);
|
|
1246
|
+
|
|
1180
1247
|
create table if not exists ${AUDIT_CONTEXT_TABLE} (
|
|
1181
1248
|
id uuid not null default gen_random_uuid(),
|
|
1182
1249
|
org text not null,
|
|
@@ -1206,6 +1273,9 @@ create table if not exists ${AUDIT_CONTEXT_TABLE} (
|
|
|
1206
1273
|
|
|
1207
1274
|
create index if not exists alma_audit_context_turn
|
|
1208
1275
|
on ${AUDIT_CONTEXT_TABLE} (org, uid, turn_id);
|
|
1276
|
+
|
|
1277
|
+
create index if not exists alma_audit_context_at
|
|
1278
|
+
on ${AUDIT_CONTEXT_TABLE} (at);
|
|
1209
1279
|
${AUDIT_TABLES.map((t) => rlsPolicySql(t)).join("")}
|
|
1210
1280
|
${roleBootstrapSql(role)}
|
|
1211
1281
|
${roleBootstrapSql(retentionRole)}
|
|
@@ -1225,25 +1295,11 @@ grant select, insert
|
|
|
1225
1295
|
-- 039 review caught: a retention mechanism that silently retains forever, in
|
|
1226
1296
|
-- exactly the deployments careful enough not to connect as a superuser.
|
|
1227
1297
|
--
|
|
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("")}
|
|
1298
|
+
-- TWO policies, not one (see retentionPolicySql). A FOR DELETE policy alone
|
|
1299
|
+
-- still deleted nothing, because a DELETE with a WHERE clause must SCAN the
|
|
1300
|
+
-- rows to filter them, and the scope-keyed policy is FOR ALL -- which governs
|
|
1301
|
+
-- SELECT too. That second step surfaced only by running the statement.
|
|
1302
|
+
${AUDIT_TABLES.map((t) => retentionPolicySql(t, retentionRole, "alma_audit_retention")).join("")}
|
|
1247
1303
|
|
|
1248
1304
|
grant select, delete
|
|
1249
1305
|
on ${AUDIT_TABLES.join(", ")}
|
|
@@ -1254,36 +1310,17 @@ async function migrateAuditLog(pool, opts = {}) {
|
|
|
1254
1310
|
await pool.query(auditLogMigrationSql(opts.role, opts.retentionRole));
|
|
1255
1311
|
}
|
|
1256
1312
|
async function purgeAuditBefore(pool, windows, opts = {}) {
|
|
1257
|
-
const retentionRole = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
|
|
1258
|
-
assertRoleIdentifier(retentionRole);
|
|
1259
1313
|
for (const table of AUDIT_TABLES) {
|
|
1260
1314
|
const before = windows[table];
|
|
1261
|
-
if (before !== void 0
|
|
1262
|
-
throw new Error(`invalid ISO 8601 timestamp for ${table}: ${JSON.stringify(before)}`);
|
|
1263
|
-
}
|
|
1315
|
+
if (before !== void 0) assertIso8601(before, `timestamp for ${table}`);
|
|
1264
1316
|
}
|
|
1265
1317
|
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();
|
|
1318
|
+
for (const table of AUDIT_TABLES) {
|
|
1319
|
+
const before = windows[table];
|
|
1320
|
+
if (before === void 0) continue;
|
|
1321
|
+
purged[table] = await purgeBefore(pool, { table, column: "at", before, ...opts });
|
|
1286
1322
|
}
|
|
1323
|
+
return purged;
|
|
1287
1324
|
}
|
|
1288
1325
|
|
|
1289
1326
|
// src/audit-store.ts
|
|
@@ -1439,12 +1476,7 @@ var PostgresAuditLog = class {
|
|
|
1439
1476
|
}, this.#timeoutMs);
|
|
1440
1477
|
}
|
|
1441
1478
|
};
|
|
1442
|
-
|
|
1443
|
-
if (Number.isNaN(Date.parse(at))) {
|
|
1444
|
-
throw new Error(`invalid ISO 8601 timestamp: ${JSON.stringify(at)}`);
|
|
1445
|
-
}
|
|
1446
|
-
return at;
|
|
1447
|
-
}
|
|
1479
|
+
var instant2 = (at) => assertIso8601(at);
|
|
1448
1480
|
|
|
1449
1481
|
// src/turn-store.ts
|
|
1450
1482
|
import {
|
|
@@ -1455,8 +1487,9 @@ import {
|
|
|
1455
1487
|
// src/turn-schema.ts
|
|
1456
1488
|
var TURN_LEASES_TABLE = "alma_turn_leases";
|
|
1457
1489
|
var TURN_CLAIMS_TABLE = "alma_turn_claims";
|
|
1458
|
-
function turnStoreMigrationSql(role = DEFAULT_RLS_ROLE) {
|
|
1490
|
+
function turnStoreMigrationSql(role = DEFAULT_RLS_ROLE, retentionRole = DEFAULT_RETENTION_ROLE) {
|
|
1459
1491
|
assertRoleIdentifier(role);
|
|
1492
|
+
assertRoleIdentifier(retentionRole);
|
|
1460
1493
|
return `
|
|
1461
1494
|
create table if not exists ${TURN_LEASES_TABLE} (
|
|
1462
1495
|
org text not null,
|
|
@@ -1476,15 +1509,31 @@ create table if not exists ${TURN_CLAIMS_TABLE} (
|
|
|
1476
1509
|
created_at timestamptz not null default now(),
|
|
1477
1510
|
primary key (org, uid, session_id, idempotency_key)
|
|
1478
1511
|
);
|
|
1512
|
+
|
|
1513
|
+
create index if not exists alma_turn_claims_created
|
|
1514
|
+
on ${TURN_CLAIMS_TABLE} (created_at);
|
|
1479
1515
|
${rlsPolicySql(TURN_LEASES_TABLE)}${rlsPolicySql(TURN_CLAIMS_TABLE)}
|
|
1480
1516
|
${roleBootstrapSql(role)}
|
|
1517
|
+
${roleBootstrapSql(retentionRole)}
|
|
1481
1518
|
grant select, insert, update, delete
|
|
1482
1519
|
on ${TURN_LEASES_TABLE}, ${TURN_CLAIMS_TABLE}
|
|
1483
1520
|
to ${role};
|
|
1521
|
+
${retentionPolicySql(TURN_CLAIMS_TABLE, retentionRole, "alma_turn_claims_retention")}
|
|
1522
|
+
${retentionPolicySql(TURN_LEASES_TABLE, retentionRole, "alma_turn_leases_retention")}
|
|
1523
|
+
|
|
1524
|
+
grant select, delete
|
|
1525
|
+
on ${TURN_CLAIMS_TABLE}, ${TURN_LEASES_TABLE}
|
|
1526
|
+
to ${retentionRole};
|
|
1484
1527
|
`;
|
|
1485
1528
|
}
|
|
1486
1529
|
async function migrateTurnStore(pool, opts = {}) {
|
|
1487
|
-
await pool.query(turnStoreMigrationSql(opts.role));
|
|
1530
|
+
await pool.query(turnStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1531
|
+
}
|
|
1532
|
+
async function purgeTurnClaimsBefore(pool, before, opts = {}) {
|
|
1533
|
+
return purgeBefore(pool, { table: TURN_CLAIMS_TABLE, column: "created_at", before, ...opts });
|
|
1534
|
+
}
|
|
1535
|
+
async function purgeExpiredLeases(pool, before, opts = {}) {
|
|
1536
|
+
return purgeBefore(pool, { table: TURN_LEASES_TABLE, column: "expires_at", before, ...opts });
|
|
1488
1537
|
}
|
|
1489
1538
|
|
|
1490
1539
|
// src/turn-store.ts
|
|
@@ -1607,12 +1656,10 @@ var PostgresTurnStore = class {
|
|
|
1607
1656
|
await this.#inScope(scope, async (client) => {
|
|
1608
1657
|
const params = sessionId === void 0 ? [scope.org, scope.uid] : [scope.org, scope.uid, sessionId];
|
|
1609
1658
|
const bySession = sessionId === void 0 ? "" : " and session_id = $3";
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
);
|
|
1615
|
-
}
|
|
1659
|
+
await client.query(
|
|
1660
|
+
`delete from ${TURN_CLAIMS_TABLE} where org = $1 and uid = $2${bySession}`,
|
|
1661
|
+
params
|
|
1662
|
+
);
|
|
1616
1663
|
});
|
|
1617
1664
|
}
|
|
1618
1665
|
/** Shared RLS binding — see `scoped.ts`. */
|
|
@@ -1630,6 +1677,262 @@ function assertLeaseOpts(opts) {
|
|
|
1630
1677
|
}
|
|
1631
1678
|
}
|
|
1632
1679
|
}
|
|
1680
|
+
|
|
1681
|
+
// src/routine-store-schema.ts
|
|
1682
|
+
var ROUTINES_TABLE = "alma_routines";
|
|
1683
|
+
var DEFAULT_SCHEDULER_ROLE = "alma_scheduler";
|
|
1684
|
+
function routineStoreMigrationSql(role = DEFAULT_RLS_ROLE, schedulerRole = DEFAULT_SCHEDULER_ROLE) {
|
|
1685
|
+
assertRoleIdentifier(role);
|
|
1686
|
+
assertRoleIdentifier(schedulerRole);
|
|
1687
|
+
return `
|
|
1688
|
+
create table if not exists ${ROUTINES_TABLE} (
|
|
1689
|
+
org text not null,
|
|
1690
|
+
uid text not null,
|
|
1691
|
+
routine_id text not null,
|
|
1692
|
+
routine jsonb not null,
|
|
1693
|
+
registered_at timestamptz not null default now(),
|
|
1694
|
+
primary key (org, uid, routine_id)
|
|
1695
|
+
);
|
|
1696
|
+
${rlsPolicySql(ROUTINES_TABLE)}
|
|
1697
|
+
${roleBootstrapSql(role)}
|
|
1698
|
+
${roleBootstrapSql(schedulerRole)}
|
|
1699
|
+
grant select, insert, update, delete
|
|
1700
|
+
on ${ROUTINES_TABLE}
|
|
1701
|
+
to ${role};
|
|
1702
|
+
|
|
1703
|
+
drop policy if exists alma_routines_schedule on ${ROUTINES_TABLE};
|
|
1704
|
+
create policy alma_routines_schedule on ${ROUTINES_TABLE}
|
|
1705
|
+
for select
|
|
1706
|
+
to ${schedulerRole}
|
|
1707
|
+
using (true);
|
|
1708
|
+
|
|
1709
|
+
grant select
|
|
1710
|
+
on ${ROUTINES_TABLE}
|
|
1711
|
+
to ${schedulerRole};
|
|
1712
|
+
`;
|
|
1713
|
+
}
|
|
1714
|
+
async function migrateRoutineStore(pool, opts = {}) {
|
|
1715
|
+
await pool.query(routineStoreMigrationSql(opts.role, opts.schedulerRole));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/routine-store.ts
|
|
1719
|
+
var PostgresRoutineStore = class {
|
|
1720
|
+
#pool;
|
|
1721
|
+
#role;
|
|
1722
|
+
#schedulerRole;
|
|
1723
|
+
#timeoutMs;
|
|
1724
|
+
constructor(pool, opts = {}) {
|
|
1725
|
+
this.#pool = pool;
|
|
1726
|
+
this.#role = resolveRlsRole(opts);
|
|
1727
|
+
this.#schedulerRole = opts.schedulerRole ?? DEFAULT_SCHEDULER_ROLE;
|
|
1728
|
+
assertRoleIdentifier(this.#schedulerRole);
|
|
1729
|
+
this.#timeoutMs = resolveStatementTimeout(opts);
|
|
1730
|
+
}
|
|
1731
|
+
async register(routine) {
|
|
1732
|
+
const { registeredAt, ...plain } = routine;
|
|
1733
|
+
if (registeredAt !== void 0) assertIso8601(registeredAt, "registeredAt");
|
|
1734
|
+
await this.#inScope(routine.scope, async (client) => {
|
|
1735
|
+
await client.query(
|
|
1736
|
+
`insert into ${ROUTINES_TABLE} (org, uid, routine_id, routine, registered_at)
|
|
1737
|
+
values ($1, $2, $3, $4::jsonb, coalesce($5::timestamptz, now()))
|
|
1738
|
+
on conflict (org, uid, routine_id) do update set routine = excluded.routine`,
|
|
1739
|
+
[routine.scope.org, routine.scope.uid, routine.id, JSON.stringify(plain), registeredAt ?? null]
|
|
1740
|
+
);
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async cancel(scope, routineId) {
|
|
1744
|
+
await this.#inScope(scope, async (client) => {
|
|
1745
|
+
await client.query(
|
|
1746
|
+
`delete from ${ROUTINES_TABLE} where org = $1 and uid = $2 and routine_id = $3`,
|
|
1747
|
+
[scope.org, scope.uid, routineId]
|
|
1748
|
+
);
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
async get(scope, routineId) {
|
|
1752
|
+
return this.#inScope(scope, async (client) => {
|
|
1753
|
+
const { rows } = await client.query(
|
|
1754
|
+
`select routine, registered_at from ${ROUTINES_TABLE}
|
|
1755
|
+
where org = $1 and uid = $2 and routine_id = $3`,
|
|
1756
|
+
[scope.org, scope.uid, routineId]
|
|
1757
|
+
);
|
|
1758
|
+
return rows[0] === void 0 ? null : toStored(rows[0]);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
async list() {
|
|
1762
|
+
return inTransaction(this.#pool, this.#schedulerRole, async (client) => {
|
|
1763
|
+
const { rows } = await client.query(
|
|
1764
|
+
`select routine, registered_at from ${ROUTINES_TABLE} order by org, uid, routine_id`
|
|
1765
|
+
);
|
|
1766
|
+
return rows.map(toStored);
|
|
1767
|
+
}, this.#timeoutMs);
|
|
1768
|
+
}
|
|
1769
|
+
/** Shared RLS binding — see `scoped.ts`. */
|
|
1770
|
+
async #inScope(scope, fn) {
|
|
1771
|
+
return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
function toStored(row) {
|
|
1775
|
+
return { ...row.routine, registeredAt: row.registered_at.toISOString() };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/routine-run-store.ts
|
|
1779
|
+
import { scopePath as scopePath5 } from "@alma-harness/core";
|
|
1780
|
+
|
|
1781
|
+
// src/routine-schema.ts
|
|
1782
|
+
var ROUTINE_RUNS_TABLE = "alma_routine_runs";
|
|
1783
|
+
function routineRunStoreMigrationSql(role = DEFAULT_RLS_ROLE, retentionRole = DEFAULT_RETENTION_ROLE) {
|
|
1784
|
+
assertRoleIdentifier(role);
|
|
1785
|
+
assertRoleIdentifier(retentionRole);
|
|
1786
|
+
return `
|
|
1787
|
+
create table if not exists ${ROUTINE_RUNS_TABLE} (
|
|
1788
|
+
org text not null,
|
|
1789
|
+
uid text not null,
|
|
1790
|
+
routine_id text not null,
|
|
1791
|
+
run_id text not null,
|
|
1792
|
+
started_at timestamptz not null,
|
|
1793
|
+
finished_at timestamptz,
|
|
1794
|
+
outcome text not null,
|
|
1795
|
+
reason text,
|
|
1796
|
+
cost_usd double precision not null,
|
|
1797
|
+
session_id text,
|
|
1798
|
+
turn_id text,
|
|
1799
|
+
handle jsonb,
|
|
1800
|
+
delivery_hash text,
|
|
1801
|
+
primary key (org, uid, routine_id, run_id),
|
|
1802
|
+
constraint alma_routine_runs_outcome_check
|
|
1803
|
+
check (outcome in ('delivered', 'duplicate', 'submitted', 'waiting', 'refused', 'failed'))
|
|
1804
|
+
);
|
|
1805
|
+
|
|
1806
|
+
create index if not exists alma_routine_runs_recent
|
|
1807
|
+
on ${ROUTINE_RUNS_TABLE} (org, uid, routine_id, started_at desc);
|
|
1808
|
+
|
|
1809
|
+
create index if not exists alma_routine_runs_started
|
|
1810
|
+
on ${ROUTINE_RUNS_TABLE} (started_at);
|
|
1811
|
+
${rlsPolicySql(ROUTINE_RUNS_TABLE)}
|
|
1812
|
+
${roleBootstrapSql(role)}
|
|
1813
|
+
${roleBootstrapSql(retentionRole)}
|
|
1814
|
+
grant select, insert, update
|
|
1815
|
+
on ${ROUTINE_RUNS_TABLE}
|
|
1816
|
+
to ${role};
|
|
1817
|
+
${retentionPolicySql(ROUTINE_RUNS_TABLE, retentionRole, "alma_routine_runs_retention")}
|
|
1818
|
+
|
|
1819
|
+
grant select, delete
|
|
1820
|
+
on ${ROUTINE_RUNS_TABLE}
|
|
1821
|
+
to ${retentionRole};
|
|
1822
|
+
`;
|
|
1823
|
+
}
|
|
1824
|
+
async function migrateRoutineRunStore(pool, opts = {}) {
|
|
1825
|
+
await pool.query(routineRunStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1826
|
+
}
|
|
1827
|
+
async function purgeRoutineRunsBefore(pool, before, opts = {}) {
|
|
1828
|
+
return purgeBefore(pool, { table: ROUTINE_RUNS_TABLE, column: "started_at", before, ...opts });
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/routine-run-store.ts
|
|
1832
|
+
var COLUMNS = "routine_id, run_id, started_at, finished_at, outcome, reason, cost_usd, session_id, turn_id, handle, delivery_hash";
|
|
1833
|
+
var PostgresRoutineRunStore = class {
|
|
1834
|
+
#pool;
|
|
1835
|
+
#role;
|
|
1836
|
+
#timeoutMs;
|
|
1837
|
+
constructor(pool, opts = {}) {
|
|
1838
|
+
this.#pool = pool;
|
|
1839
|
+
this.#role = resolveRlsRole(opts);
|
|
1840
|
+
this.#timeoutMs = resolveStatementTimeout(opts);
|
|
1841
|
+
}
|
|
1842
|
+
async record(run) {
|
|
1843
|
+
scopePath5(run.scope);
|
|
1844
|
+
assertIso8601(run.startedAt, "startedAt");
|
|
1845
|
+
if (run.finishedAt !== void 0) assertIso8601(run.finishedAt, "finishedAt");
|
|
1846
|
+
await this.#inScope(run.scope, async (client) => {
|
|
1847
|
+
await client.query(
|
|
1848
|
+
`insert into ${ROUTINE_RUNS_TABLE}
|
|
1849
|
+
(org, uid, routine_id, run_id, started_at, finished_at, outcome, reason, cost_usd,
|
|
1850
|
+
session_id, turn_id, handle, delivery_hash)
|
|
1851
|
+
values ($1, $2, $3, $4, $5::timestamptz, $6::timestamptz, $7, $8, $9, $10, $11, $12::jsonb, $13)
|
|
1852
|
+
on conflict (org, uid, routine_id, run_id) do update set
|
|
1853
|
+
started_at = excluded.started_at, finished_at = excluded.finished_at,
|
|
1854
|
+
outcome = excluded.outcome, reason = excluded.reason, cost_usd = excluded.cost_usd,
|
|
1855
|
+
session_id = excluded.session_id, turn_id = excluded.turn_id,
|
|
1856
|
+
handle = excluded.handle, delivery_hash = excluded.delivery_hash`,
|
|
1857
|
+
[
|
|
1858
|
+
run.scope.org,
|
|
1859
|
+
run.scope.uid,
|
|
1860
|
+
run.routineId,
|
|
1861
|
+
run.id,
|
|
1862
|
+
run.startedAt,
|
|
1863
|
+
run.finishedAt ?? null,
|
|
1864
|
+
run.outcome,
|
|
1865
|
+
run.reason ?? null,
|
|
1866
|
+
run.costUsd,
|
|
1867
|
+
run.sessionId ?? null,
|
|
1868
|
+
run.turnId ?? null,
|
|
1869
|
+
run.handle === void 0 ? null : JSON.stringify(run.handle),
|
|
1870
|
+
run.deliveryHash ?? null
|
|
1871
|
+
]
|
|
1872
|
+
);
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
async get(scope, routineId, runId) {
|
|
1876
|
+
return this.#inScope(scope, async (client) => {
|
|
1877
|
+
const { rows } = await client.query(
|
|
1878
|
+
`select ${COLUMNS} from ${ROUTINE_RUNS_TABLE}
|
|
1879
|
+
where org = $1 and uid = $2 and routine_id = $3 and run_id = $4`,
|
|
1880
|
+
[scope.org, scope.uid, routineId, runId]
|
|
1881
|
+
);
|
|
1882
|
+
return rows[0] === void 0 ? null : toRun(scope, rows[0]);
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
async list(scope, routineId, opts = {}) {
|
|
1886
|
+
scopePath5(scope);
|
|
1887
|
+
if (opts.since !== void 0) assertIso8601(opts.since, "since");
|
|
1888
|
+
if (opts.limit !== void 0 && opts.limit <= 0) return [];
|
|
1889
|
+
return this.#inScope(scope, async (client) => {
|
|
1890
|
+
const params = [scope.org, scope.uid, routineId];
|
|
1891
|
+
const where = ["org = $1", "uid = $2", "routine_id = $3"];
|
|
1892
|
+
if (opts.since !== void 0) {
|
|
1893
|
+
params.push(opts.since);
|
|
1894
|
+
where.push(`started_at >= $${params.length}::timestamptz`);
|
|
1895
|
+
}
|
|
1896
|
+
if (opts.outcome !== void 0) {
|
|
1897
|
+
params.push(opts.outcome);
|
|
1898
|
+
where.push(`outcome = $${params.length}`);
|
|
1899
|
+
}
|
|
1900
|
+
let limit = "";
|
|
1901
|
+
if (opts.limit !== void 0) {
|
|
1902
|
+
params.push(opts.limit);
|
|
1903
|
+
limit = ` limit $${params.length}`;
|
|
1904
|
+
}
|
|
1905
|
+
const { rows } = await client.query(
|
|
1906
|
+
`select ${COLUMNS} from ${ROUTINE_RUNS_TABLE}
|
|
1907
|
+
where ${where.join(" and ")}
|
|
1908
|
+
order by started_at desc, run_id asc${limit}`,
|
|
1909
|
+
params
|
|
1910
|
+
);
|
|
1911
|
+
return rows.map((row) => toRun(scope, row));
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
/** Shared RLS binding — see `scoped.ts`. */
|
|
1915
|
+
async #inScope(scope, fn) {
|
|
1916
|
+
return inScope(this.#pool, this.#role, scope, fn, this.#timeoutMs);
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
function toRun(scope, row) {
|
|
1920
|
+
const run = {
|
|
1921
|
+
id: row.run_id,
|
|
1922
|
+
routineId: row.routine_id,
|
|
1923
|
+
scope: { org: scope.org, uid: scope.uid },
|
|
1924
|
+
startedAt: row.started_at.toISOString(),
|
|
1925
|
+
outcome: row.outcome,
|
|
1926
|
+
costUsd: row.cost_usd
|
|
1927
|
+
};
|
|
1928
|
+
if (row.finished_at !== null) run.finishedAt = row.finished_at.toISOString();
|
|
1929
|
+
if (row.reason !== null) run.reason = row.reason;
|
|
1930
|
+
if (row.session_id !== null) run.sessionId = row.session_id;
|
|
1931
|
+
if (row.turn_id !== null) run.turnId = row.turn_id;
|
|
1932
|
+
if (row.handle !== null) run.handle = row.handle;
|
|
1933
|
+
if (row.delivery_hash !== null) run.deliveryHash = row.delivery_hash;
|
|
1934
|
+
return run;
|
|
1935
|
+
}
|
|
1633
1936
|
export {
|
|
1634
1937
|
AUDIT_ACCESS_TABLE,
|
|
1635
1938
|
AUDIT_CONTEXT_TABLE,
|
|
@@ -1639,33 +1942,52 @@ export {
|
|
|
1639
1942
|
AUDIT_TABLES,
|
|
1640
1943
|
DEFAULT_RETENTION_ROLE,
|
|
1641
1944
|
DEFAULT_RLS_ROLE,
|
|
1945
|
+
DEFAULT_SCHEDULER_ROLE,
|
|
1642
1946
|
EPISODES_TABLE,
|
|
1643
1947
|
FACTS_TABLE,
|
|
1644
1948
|
PostgresAuditLog,
|
|
1645
1949
|
PostgresEpisodeStore,
|
|
1646
1950
|
PostgresErasureWatermarks,
|
|
1647
1951
|
PostgresProfileStore,
|
|
1952
|
+
PostgresRoutineRunStore,
|
|
1953
|
+
PostgresRoutineStore,
|
|
1648
1954
|
PostgresSessionStore,
|
|
1649
1955
|
PostgresSpendStore,
|
|
1650
1956
|
PostgresTurnStore,
|
|
1957
|
+
ROUTINES_TABLE,
|
|
1958
|
+
ROUTINE_RUNS_TABLE,
|
|
1651
1959
|
SCOPE_STATE_TABLE,
|
|
1652
1960
|
SPEND_SESSIONS_TABLE,
|
|
1653
1961
|
SPEND_TENANT_DAYS_TABLE,
|
|
1654
1962
|
TURN_CLAIMS_TABLE,
|
|
1655
1963
|
TURN_LEASES_TABLE,
|
|
1964
|
+
assertIso8601,
|
|
1656
1965
|
assertRoleIdentifier,
|
|
1657
1966
|
auditLogMigrationSql,
|
|
1967
|
+
grantRole,
|
|
1968
|
+
grantRoleSql,
|
|
1969
|
+
inTransaction,
|
|
1658
1970
|
memoryStoreMigrationSql,
|
|
1659
1971
|
migrateAuditLog,
|
|
1660
1972
|
migrateMemoryStores,
|
|
1973
|
+
migrateRoutineRunStore,
|
|
1974
|
+
migrateRoutineStore,
|
|
1661
1975
|
migrateSessionStore,
|
|
1662
1976
|
migrateSpendStore,
|
|
1663
1977
|
migrateTurnStore,
|
|
1664
1978
|
purgeAuditBefore,
|
|
1979
|
+
purgeBefore,
|
|
1980
|
+
purgeExpiredLeases,
|
|
1981
|
+
purgeRoutineRunsBefore,
|
|
1982
|
+
purgeTurnClaimsBefore,
|
|
1983
|
+
retentionPolicySql,
|
|
1665
1984
|
rlsPolicySql,
|
|
1666
1985
|
roleBootstrapSql,
|
|
1986
|
+
routineRunStoreMigrationSql,
|
|
1987
|
+
routineStoreMigrationSql,
|
|
1667
1988
|
sessionStoreMigrationSql,
|
|
1668
1989
|
spendStoreMigrationSql,
|
|
1990
|
+
sweepBefore,
|
|
1669
1991
|
turnStoreMigrationSql
|
|
1670
1992
|
};
|
|
1671
1993
|
//# sourceMappingURL=index.js.map
|