@alma-harness/postgres 0.2.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 +9 -1
- package/dist/index.d.ts +62 -6
- package/dist/index.js +142 -20
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ with row-level security as defense in depth.
|
|
|
23
23
|
`purgeTurnClaimsBefore` is the claims' time-based retention (spec:
|
|
24
24
|
erasure-reaches-the-claims): it runs as the retention role, like the audit
|
|
25
25
|
sweep, since the app role's delete is scoped and serves erasure only.
|
|
26
|
+
`purgeExpiredLeases` sweeps leases that expired before a cutoff — the
|
|
27
|
+
orphan a crashed holder leaves, since erasure keeps leases (spec:
|
|
28
|
+
close-review-part-two). Every swept column carries an index.
|
|
26
29
|
- `PostgresAuditLog` — the five audit trails (spec 038), written synchronously
|
|
27
30
|
and with no buffering wrapper: a buffered sink that loses its buffer on a
|
|
28
31
|
crash stops writing silently, which is the failure `AuditSinkError` was typed
|
|
@@ -39,7 +42,8 @@ with row-level security as defense in depth.
|
|
|
39
42
|
deployment not connected as a superuser — FORCE ROW LEVEL SECURITY subjects
|
|
40
43
|
even the table owner to the predicate.
|
|
41
44
|
- `migrateSessionStore`, `migrateMemoryStores`, `migrateSpendStore`,
|
|
42
|
-
`migrateTurnStore`, `migrateRoutineRunStore`, `
|
|
45
|
+
`migrateTurnStore`, `migrateRoutineRunStore`, `migrateRoutineStore`,
|
|
46
|
+
`migrateAuditLog` —
|
|
43
47
|
idempotent migrations; running them is the product's choice, typically at
|
|
44
48
|
startup.
|
|
45
49
|
|
|
@@ -48,6 +52,10 @@ with row-level security as defense in depth.
|
|
|
48
52
|
the last delivery's hash for the dedupe, durable across processes. The app
|
|
49
53
|
role gets no delete — a run record is retained like a trail — and
|
|
50
54
|
`purgeRoutineRunsBefore` sweeps by age as the retention role.
|
|
55
|
+
- `PostgresRoutineStore` — the routines themselves (spec: clock-tick), as
|
|
56
|
+
`jsonb` under the shared policy; `list` runs as `alma_scheduler`, the
|
|
57
|
+
role the tick assumes to read every scope, created by the migration.
|
|
58
|
+
Grant it to the login the tick connects as, like the other two roles.
|
|
51
59
|
|
|
52
60
|
Every operation runs inside a transaction that assumes a dedicated
|
|
53
61
|
non-superuser role and binds `alma.org` / `alma.uid` as transaction-local
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SessionStore, Scope, Msg, LoadOpts, ToolTrafficExpiry, EpisodeStore, CopySurface, EpisodeInput, Episode, EpisodeQuery, EpisodeQueryResult, ErasureSelector, TombstoneResult, ErasureWatermarkStore, ProfileStore, ProfileReadOpts, Profile, FactObservation, ObserveResult, InvalidateResult, SpendStore, SpendKey, SpendTotals, AuditLog, AccessEvent, RoutingEvent, CostEvent, RecallEvent, ContextEvent, TurnStore, LeaseOpts, TurnLease, TurnKey, TurnClaim, CompletedTurn, RoutineRunStore, RoutineRun, RoutineRunOutcome } from '@alma-harness/core';
|
|
2
|
-
export { AuditLog, EpisodeStore, ProfileStore, RoutineRunStore, SessionStore, SpendStore, TurnStore } from '@alma-harness/core';
|
|
1
|
+
import { SessionStore, Scope, Msg, LoadOpts, ToolTrafficExpiry, EpisodeStore, CopySurface, EpisodeInput, Episode, EpisodeQuery, EpisodeQueryResult, ErasureSelector, TombstoneResult, ErasureWatermarkStore, ProfileStore, ProfileReadOpts, Profile, FactObservation, ObserveResult, InvalidateResult, SpendStore, SpendKey, SpendTotals, AuditLog, AccessEvent, RoutingEvent, CostEvent, RecallEvent, ContextEvent, TurnStore, LeaseOpts, TurnLease, TurnKey, TurnClaim, CompletedTurn, RoutineStore, Routine, StoredRoutine, RoutineRunStore, RoutineRun, RoutineRunOutcome } from '@alma-harness/core';
|
|
2
|
+
export { AuditLog, EpisodeStore, ProfileStore, RoutineRunStore, RoutineStore, SessionStore, SpendStore, TurnStore, assertIso8601 } from '@alma-harness/core';
|
|
3
3
|
import { Pool, PoolClient } from 'pg';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -35,7 +35,9 @@ interface ScopedStoreOptions {
|
|
|
35
35
|
* inside an aborted transaction; handing it back to the pool would give the
|
|
36
36
|
* next borrower cascading errors, so it is destroyed instead of reused.
|
|
37
37
|
*/
|
|
38
|
-
declare function inTransaction<T>(pool: Pool, role: string | null, fn: (client: PoolClient) => Promise<T
|
|
38
|
+
declare function inTransaction<T>(pool: Pool, role: string | null, fn: (client: PoolClient) => Promise<T>,
|
|
39
|
+
/** A per-transaction `statement_timeout`; the scoped path always sets one, the deployment reads may (spec: close-060-064-findings). */
|
|
40
|
+
statementTimeoutMs?: number | null): Promise<T>;
|
|
39
41
|
/**
|
|
40
42
|
* A retention sweep in BATCHES — spec: close-review-part-two. One unbounded
|
|
41
43
|
* `delete … where stamp < cutoff` on a backlog is one long transaction and a
|
|
@@ -50,6 +52,14 @@ declare function sweepBefore(pool: Pool, opts: {
|
|
|
50
52
|
before: string;
|
|
51
53
|
batch?: number;
|
|
52
54
|
}): Promise<number>;
|
|
55
|
+
/** The three sweeps' one preamble (spec: close-060-064-findings): the role, the cutoff, the batch. */
|
|
56
|
+
declare function purgeBefore(pool: Pool, opts: {
|
|
57
|
+
table: string;
|
|
58
|
+
column: string;
|
|
59
|
+
before: string;
|
|
60
|
+
retentionRole?: string;
|
|
61
|
+
batch?: number;
|
|
62
|
+
}): Promise<number>;
|
|
53
63
|
|
|
54
64
|
type PostgresSessionStoreOptions = ScopedStoreOptions;
|
|
55
65
|
/**
|
|
@@ -154,8 +164,6 @@ declare function rlsPolicySql(table: string, keys?: readonly ("org" | "uid")[]):
|
|
|
154
164
|
* names, so a migration on a populated database drops and recreates its own.
|
|
155
165
|
*/
|
|
156
166
|
declare function retentionPolicySql(table: string, retentionRole: string, name: string): string;
|
|
157
|
-
/** Rejects an unparseable ISO 8601 timestamp with the value in the message, before it reaches SQL. */
|
|
158
|
-
declare function assertIso8601(at: string, label?: string): string;
|
|
159
167
|
/** Creates the RLS role if it does not exist, tolerating a concurrent race. */
|
|
160
168
|
declare function roleBootstrapSql(role: string): string;
|
|
161
169
|
/**
|
|
@@ -422,6 +430,45 @@ declare class PostgresTurnStore implements TurnStore {
|
|
|
422
430
|
erase(scope: Scope, sessionId?: string): Promise<void>;
|
|
423
431
|
}
|
|
424
432
|
|
|
433
|
+
interface PostgresRoutineStoreOptions extends ScopedStoreOptions {
|
|
434
|
+
/** The role `list` assumes — the deployment actor. Default `alma_scheduler`. */
|
|
435
|
+
schedulerRole?: string;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Reference `RoutineStore` adapter — spec: clock-tick. Scoped writes and
|
|
439
|
+
* reads as the app role, like every store here; `list` as the scheduler
|
|
440
|
+
* role, the one cross-scope read, through the same transaction helper the
|
|
441
|
+
* sweeps use.
|
|
442
|
+
*/
|
|
443
|
+
declare class PostgresRoutineStore implements RoutineStore {
|
|
444
|
+
#private;
|
|
445
|
+
constructor(pool: Pool, opts?: PostgresRoutineStoreOptions);
|
|
446
|
+
register(routine: Routine & {
|
|
447
|
+
registeredAt?: string;
|
|
448
|
+
}): Promise<void>;
|
|
449
|
+
cancel(scope: Scope, routineId: string): Promise<void>;
|
|
450
|
+
get(scope: Scope, routineId: string): Promise<StoredRoutine | null>;
|
|
451
|
+
list(): Promise<StoredRoutine[]>;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Schema and migration for the routine store — spec: clock-tick. One
|
|
456
|
+
* scope-stamped table; the routine as `jsonb`, the registration stamp as a
|
|
457
|
+
* column. The app role reads and writes its own scope's rows; the tick
|
|
458
|
+
* lists EVERY scope, so it runs as a role of its own with a policy that
|
|
459
|
+
* admits all rows — the retention role's shape, for the same reason: FORCE
|
|
460
|
+
* ROW LEVEL SECURITY binds the owner too.
|
|
461
|
+
*/
|
|
462
|
+
declare const ROUTINES_TABLE = "alma_routines";
|
|
463
|
+
/** Role the tick assumes to list every scope's routines — spec: clock-tick. */
|
|
464
|
+
declare const DEFAULT_SCHEDULER_ROLE = "alma_scheduler";
|
|
465
|
+
declare function routineStoreMigrationSql(role?: string, schedulerRole?: string): string;
|
|
466
|
+
/** Idempotent; running it is the product's choice (typically at startup). */
|
|
467
|
+
declare function migrateRoutineStore(pool: Pool, opts?: {
|
|
468
|
+
role?: string;
|
|
469
|
+
schedulerRole?: string;
|
|
470
|
+
}): Promise<void>;
|
|
471
|
+
|
|
425
472
|
type PostgresRoutineRunStoreOptions = ScopedStoreOptions;
|
|
426
473
|
/**
|
|
427
474
|
* Reference `RoutineRunStore` adapter — spec: postgres-routine-runs. Same RLS
|
|
@@ -519,6 +566,15 @@ declare function purgeTurnClaimsBefore(pool: Pool, before: string, opts?: {
|
|
|
519
566
|
retentionRole?: string;
|
|
520
567
|
batch?: number;
|
|
521
568
|
}): Promise<number>;
|
|
569
|
+
/**
|
|
570
|
+
* Deletes leases that expired before the cutoff — spec: close-060-064-findings.
|
|
571
|
+
* Erasure leaves leases alone (spec: close-review-part-two), and a holder that
|
|
572
|
+
* crashed never releases; this bounds the orphans, as the retention role.
|
|
573
|
+
*/
|
|
574
|
+
declare function purgeExpiredLeases(pool: Pool, before: string, opts?: {
|
|
575
|
+
retentionRole?: string;
|
|
576
|
+
batch?: number;
|
|
577
|
+
}): Promise<number>;
|
|
522
578
|
|
|
523
579
|
/**
|
|
524
580
|
* Schema and migration for the spend store — spec: spend-store.
|
|
@@ -550,4 +606,4 @@ declare function migrateSpendStore(pool: Pool, opts?: {
|
|
|
550
606
|
role?: string;
|
|
551
607
|
}): Promise<void>;
|
|
552
608
|
|
|
553
|
-
export { AUDIT_ACCESS_TABLE, AUDIT_CONTEXT_TABLE, AUDIT_COST_TABLE, AUDIT_RECALL_TABLE, AUDIT_ROUTING_TABLE, AUDIT_TABLES, type AuditTable, DEFAULT_RETENTION_ROLE, DEFAULT_RLS_ROLE, EPISODES_TABLE, FACTS_TABLE, PostgresAuditLog, type PostgresAuditLogOptions, PostgresEpisodeStore, PostgresErasureWatermarks, type PostgresMemoryStoreOptions, PostgresProfileStore, PostgresRoutineRunStore, type PostgresRoutineRunStoreOptions, PostgresSessionStore, type PostgresSessionStoreOptions, PostgresSpendStore, type PostgresSpendStoreOptions, PostgresTurnStore, type PostgresTurnStoreOptions, ROUTINE_RUNS_TABLE, SCOPE_STATE_TABLE, SPEND_SESSIONS_TABLE, SPEND_TENANT_DAYS_TABLE, type ScopedStoreOptions, TURN_CLAIMS_TABLE, TURN_LEASES_TABLE,
|
|
609
|
+
export { AUDIT_ACCESS_TABLE, AUDIT_CONTEXT_TABLE, AUDIT_COST_TABLE, AUDIT_RECALL_TABLE, AUDIT_ROUTING_TABLE, AUDIT_TABLES, type AuditTable, DEFAULT_RETENTION_ROLE, DEFAULT_RLS_ROLE, DEFAULT_SCHEDULER_ROLE, EPISODES_TABLE, FACTS_TABLE, PostgresAuditLog, type PostgresAuditLogOptions, PostgresEpisodeStore, PostgresErasureWatermarks, type PostgresMemoryStoreOptions, PostgresProfileStore, PostgresRoutineRunStore, type PostgresRoutineRunStoreOptions, PostgresRoutineStore, type PostgresRoutineStoreOptions, PostgresSessionStore, type PostgresSessionStoreOptions, PostgresSpendStore, type PostgresSpendStoreOptions, PostgresTurnStore, type PostgresTurnStoreOptions, ROUTINES_TABLE, ROUTINE_RUNS_TABLE, SCOPE_STATE_TABLE, SPEND_SESSIONS_TABLE, SPEND_TENANT_DAYS_TABLE, type ScopedStoreOptions, TURN_CLAIMS_TABLE, TURN_LEASES_TABLE, assertRoleIdentifier, auditLogMigrationSql, grantRole, grantRoleSql, inTransaction, memoryStoreMigrationSql, migrateAuditLog, migrateMemoryStores, migrateRoutineRunStore, migrateRoutineStore, migrateSessionStore, migrateSpendStore, migrateTurnStore, purgeAuditBefore, purgeBefore, purgeExpiredLeases, purgeRoutineRunsBefore, purgeTurnClaimsBefore, retentionPolicySql, rlsPolicySql, roleBootstrapSql, routineRunStoreMigrationSql, routineStoreMigrationSql, sessionStoreMigrationSql, spendStoreMigrationSql, sweepBefore, turnStoreMigrationSql };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { assertWellFormed, scopePath as scopePath2 } from "@alma-harness/core";
|
|
3
3
|
|
|
4
4
|
// src/schema.ts
|
|
5
|
+
import { assertIso8601 } from "@alma-harness/core";
|
|
5
6
|
var DEFAULT_RLS_ROLE = "alma_app";
|
|
6
7
|
var DEFAULT_RETENTION_ROLE = "alma_retention";
|
|
7
8
|
var IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
|
@@ -45,12 +46,6 @@ create policy ${name} on ${table}
|
|
|
45
46
|
to ${retentionRole}
|
|
46
47
|
using (true);`;
|
|
47
48
|
}
|
|
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
|
-
}
|
|
54
49
|
function roleBootstrapSql(role) {
|
|
55
50
|
assertRoleIdentifier(role);
|
|
56
51
|
return `
|
|
@@ -129,12 +124,15 @@ function resolveStatementTimeout(opts) {
|
|
|
129
124
|
}
|
|
130
125
|
return ms;
|
|
131
126
|
}
|
|
132
|
-
async function inTransaction(pool, role, fn) {
|
|
127
|
+
async function inTransaction(pool, role, fn, statementTimeoutMs) {
|
|
133
128
|
const client = await pool.connect();
|
|
134
129
|
let rollbackFailed;
|
|
135
130
|
try {
|
|
136
131
|
await client.query("begin");
|
|
137
132
|
if (role !== null) await client.query(`set local role ${role}`);
|
|
133
|
+
if (statementTimeoutMs !== void 0) {
|
|
134
|
+
await client.query(`select set_config('statement_timeout', $1, true)`, [statementTimeoutMs === null ? "0" : String(statementTimeoutMs)]);
|
|
135
|
+
}
|
|
138
136
|
const result = await fn(client);
|
|
139
137
|
await client.query("commit");
|
|
140
138
|
return result;
|
|
@@ -157,7 +155,7 @@ async function sweepBefore(pool, opts) {
|
|
|
157
155
|
const removed = await inTransaction(pool, opts.role, async (client) => {
|
|
158
156
|
const { rowCount } = await client.query(
|
|
159
157
|
`delete from ${opts.table}
|
|
160
|
-
where ctid = any(array(select ctid from ${opts.table} where ${opts.column} < $1::timestamptz limit $2))`,
|
|
158
|
+
where ctid = any(array(select ctid from ${opts.table} where ${opts.column} < $1::timestamptz order by ${opts.column} limit $2))`,
|
|
161
159
|
[opts.before, batch]
|
|
162
160
|
);
|
|
163
161
|
return rowCount ?? 0;
|
|
@@ -166,6 +164,12 @@ async function sweepBefore(pool, opts) {
|
|
|
166
164
|
if (removed < batch) return total;
|
|
167
165
|
}
|
|
168
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
|
+
}
|
|
169
173
|
async function inScope(pool, role, scope, fn, statementTimeoutMs = DEFAULT_STATEMENT_TIMEOUT_MS) {
|
|
170
174
|
scopePath(scope);
|
|
171
175
|
return inTransaction(pool, role, async (client) => {
|
|
@@ -1160,6 +1164,9 @@ create index if not exists alma_audit_access_turn
|
|
|
1160
1164
|
create index if not exists alma_audit_access_recent
|
|
1161
1165
|
on ${AUDIT_ACCESS_TABLE} (org, uid, at desc);
|
|
1162
1166
|
|
|
1167
|
+
create index if not exists alma_audit_access_at
|
|
1168
|
+
on ${AUDIT_ACCESS_TABLE} (at);
|
|
1169
|
+
|
|
1163
1170
|
create table if not exists ${AUDIT_ROUTING_TABLE} (
|
|
1164
1171
|
id uuid not null default gen_random_uuid(),
|
|
1165
1172
|
org text not null,
|
|
@@ -1179,6 +1186,9 @@ create table if not exists ${AUDIT_ROUTING_TABLE} (
|
|
|
1179
1186
|
create index if not exists alma_audit_routing_turn
|
|
1180
1187
|
on ${AUDIT_ROUTING_TABLE} (org, uid, turn_id);
|
|
1181
1188
|
|
|
1189
|
+
create index if not exists alma_audit_routing_at
|
|
1190
|
+
on ${AUDIT_ROUTING_TABLE} (at);
|
|
1191
|
+
|
|
1182
1192
|
create table if not exists ${AUDIT_COST_TABLE} (
|
|
1183
1193
|
id uuid not null default gen_random_uuid(),
|
|
1184
1194
|
org text not null,
|
|
@@ -1206,6 +1216,9 @@ create index if not exists alma_audit_cost_turn
|
|
|
1206
1216
|
create index if not exists alma_audit_cost_recent
|
|
1207
1217
|
on ${AUDIT_COST_TABLE} (org, uid, at desc);
|
|
1208
1218
|
|
|
1219
|
+
create index if not exists alma_audit_cost_at
|
|
1220
|
+
on ${AUDIT_COST_TABLE} (at);
|
|
1221
|
+
|
|
1209
1222
|
create table if not exists ${AUDIT_RECALL_TABLE} (
|
|
1210
1223
|
id uuid not null default gen_random_uuid(),
|
|
1211
1224
|
org text not null,
|
|
@@ -1228,6 +1241,9 @@ create table if not exists ${AUDIT_RECALL_TABLE} (
|
|
|
1228
1241
|
create index if not exists alma_audit_recall_turn
|
|
1229
1242
|
on ${AUDIT_RECALL_TABLE} (org, uid, turn_id);
|
|
1230
1243
|
|
|
1244
|
+
create index if not exists alma_audit_recall_at
|
|
1245
|
+
on ${AUDIT_RECALL_TABLE} (at);
|
|
1246
|
+
|
|
1231
1247
|
create table if not exists ${AUDIT_CONTEXT_TABLE} (
|
|
1232
1248
|
id uuid not null default gen_random_uuid(),
|
|
1233
1249
|
org text not null,
|
|
@@ -1257,6 +1273,9 @@ create table if not exists ${AUDIT_CONTEXT_TABLE} (
|
|
|
1257
1273
|
|
|
1258
1274
|
create index if not exists alma_audit_context_turn
|
|
1259
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);
|
|
1260
1279
|
${AUDIT_TABLES.map((t) => rlsPolicySql(t)).join("")}
|
|
1261
1280
|
${roleBootstrapSql(role)}
|
|
1262
1281
|
${roleBootstrapSql(retentionRole)}
|
|
@@ -1291,8 +1310,6 @@ async function migrateAuditLog(pool, opts = {}) {
|
|
|
1291
1310
|
await pool.query(auditLogMigrationSql(opts.role, opts.retentionRole));
|
|
1292
1311
|
}
|
|
1293
1312
|
async function purgeAuditBefore(pool, windows, opts = {}) {
|
|
1294
|
-
const retentionRole = opts.retentionRole ?? DEFAULT_RETENTION_ROLE;
|
|
1295
|
-
assertRoleIdentifier(retentionRole);
|
|
1296
1313
|
for (const table of AUDIT_TABLES) {
|
|
1297
1314
|
const before = windows[table];
|
|
1298
1315
|
if (before !== void 0) assertIso8601(before, `timestamp for ${table}`);
|
|
@@ -1301,7 +1318,7 @@ async function purgeAuditBefore(pool, windows, opts = {}) {
|
|
|
1301
1318
|
for (const table of AUDIT_TABLES) {
|
|
1302
1319
|
const before = windows[table];
|
|
1303
1320
|
if (before === void 0) continue;
|
|
1304
|
-
purged[table] = await
|
|
1321
|
+
purged[table] = await purgeBefore(pool, { table, column: "at", before, ...opts });
|
|
1305
1322
|
}
|
|
1306
1323
|
return purged;
|
|
1307
1324
|
}
|
|
@@ -1502,9 +1519,10 @@ grant select, insert, update, delete
|
|
|
1502
1519
|
on ${TURN_LEASES_TABLE}, ${TURN_CLAIMS_TABLE}
|
|
1503
1520
|
to ${role};
|
|
1504
1521
|
${retentionPolicySql(TURN_CLAIMS_TABLE, retentionRole, "alma_turn_claims_retention")}
|
|
1522
|
+
${retentionPolicySql(TURN_LEASES_TABLE, retentionRole, "alma_turn_leases_retention")}
|
|
1505
1523
|
|
|
1506
1524
|
grant select, delete
|
|
1507
|
-
on ${TURN_CLAIMS_TABLE}
|
|
1525
|
+
on ${TURN_CLAIMS_TABLE}, ${TURN_LEASES_TABLE}
|
|
1508
1526
|
to ${retentionRole};
|
|
1509
1527
|
`;
|
|
1510
1528
|
}
|
|
@@ -1512,10 +1530,10 @@ async function migrateTurnStore(pool, opts = {}) {
|
|
|
1512
1530
|
await pool.query(turnStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1513
1531
|
}
|
|
1514
1532
|
async function purgeTurnClaimsBefore(pool, before, opts = {}) {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
return
|
|
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 });
|
|
1519
1537
|
}
|
|
1520
1538
|
|
|
1521
1539
|
// src/turn-store.ts
|
|
@@ -1660,6 +1678,103 @@ function assertLeaseOpts(opts) {
|
|
|
1660
1678
|
}
|
|
1661
1679
|
}
|
|
1662
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
|
+
|
|
1663
1778
|
// src/routine-run-store.ts
|
|
1664
1779
|
import { scopePath as scopePath5 } from "@alma-harness/core";
|
|
1665
1780
|
|
|
@@ -1690,6 +1805,9 @@ create table if not exists ${ROUTINE_RUNS_TABLE} (
|
|
|
1690
1805
|
|
|
1691
1806
|
create index if not exists alma_routine_runs_recent
|
|
1692
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);
|
|
1693
1811
|
${rlsPolicySql(ROUTINE_RUNS_TABLE)}
|
|
1694
1812
|
${roleBootstrapSql(role)}
|
|
1695
1813
|
${roleBootstrapSql(retentionRole)}
|
|
@@ -1707,10 +1825,7 @@ async function migrateRoutineRunStore(pool, opts = {}) {
|
|
|
1707
1825
|
await pool.query(routineRunStoreMigrationSql(opts.role, opts.retentionRole));
|
|
1708
1826
|
}
|
|
1709
1827
|
async function purgeRoutineRunsBefore(pool, before, opts = {}) {
|
|
1710
|
-
|
|
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 } : {} });
|
|
1828
|
+
return purgeBefore(pool, { table: ROUTINE_RUNS_TABLE, column: "started_at", before, ...opts });
|
|
1714
1829
|
}
|
|
1715
1830
|
|
|
1716
1831
|
// src/routine-run-store.ts
|
|
@@ -1827,6 +1942,7 @@ export {
|
|
|
1827
1942
|
AUDIT_TABLES,
|
|
1828
1943
|
DEFAULT_RETENTION_ROLE,
|
|
1829
1944
|
DEFAULT_RLS_ROLE,
|
|
1945
|
+
DEFAULT_SCHEDULER_ROLE,
|
|
1830
1946
|
EPISODES_TABLE,
|
|
1831
1947
|
FACTS_TABLE,
|
|
1832
1948
|
PostgresAuditLog,
|
|
@@ -1834,9 +1950,11 @@ export {
|
|
|
1834
1950
|
PostgresErasureWatermarks,
|
|
1835
1951
|
PostgresProfileStore,
|
|
1836
1952
|
PostgresRoutineRunStore,
|
|
1953
|
+
PostgresRoutineStore,
|
|
1837
1954
|
PostgresSessionStore,
|
|
1838
1955
|
PostgresSpendStore,
|
|
1839
1956
|
PostgresTurnStore,
|
|
1957
|
+
ROUTINES_TABLE,
|
|
1840
1958
|
ROUTINE_RUNS_TABLE,
|
|
1841
1959
|
SCOPE_STATE_TABLE,
|
|
1842
1960
|
SPEND_SESSIONS_TABLE,
|
|
@@ -1853,16 +1971,20 @@ export {
|
|
|
1853
1971
|
migrateAuditLog,
|
|
1854
1972
|
migrateMemoryStores,
|
|
1855
1973
|
migrateRoutineRunStore,
|
|
1974
|
+
migrateRoutineStore,
|
|
1856
1975
|
migrateSessionStore,
|
|
1857
1976
|
migrateSpendStore,
|
|
1858
1977
|
migrateTurnStore,
|
|
1859
1978
|
purgeAuditBefore,
|
|
1979
|
+
purgeBefore,
|
|
1980
|
+
purgeExpiredLeases,
|
|
1860
1981
|
purgeRoutineRunsBefore,
|
|
1861
1982
|
purgeTurnClaimsBefore,
|
|
1862
1983
|
retentionPolicySql,
|
|
1863
1984
|
rlsPolicySql,
|
|
1864
1985
|
roleBootstrapSql,
|
|
1865
1986
|
routineRunStoreMigrationSql,
|
|
1987
|
+
routineStoreMigrationSql,
|
|
1866
1988
|
sessionStoreMigrationSql,
|
|
1867
1989
|
spendStoreMigrationSql,
|
|
1868
1990
|
sweepBefore,
|