@alma-harness/postgres-execution 0.6.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/LICENSE +202 -0
- package/README.md +133 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +1177 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { equalExecution as equalExecution4, ExecutionConflictError as ExecutionConflictError6, ExecutionStateError as ExecutionStateError6, settlementIdentifier as id6, settlementLimit as settlementLimit2, settlementScope as settlementScope6 } from "@alma-harness/core";
|
|
3
|
+
import { checkOperationCall, checkOperationDeadline as checkOperationDeadline2, normalizeOperationCall as normalizeOperationCall2, normalizeOperationFence, normalizeOperationRoot as normalizeOperationRoot3, operationCallQuery as operationCallQuery4 } from "@alma-harness/execution";
|
|
4
|
+
import { inScope as inScope6, resolveRlsRole as resolveRlsRole6, resolveStatementTimeout as resolveStatementTimeout6 } from "@alma-harness/postgres";
|
|
5
|
+
|
|
6
|
+
// src/schema.ts
|
|
7
|
+
import { functionPathSql as functionPathSql2 } from "@alma-harness/postgres";
|
|
8
|
+
import { assertRoleIdentifier, DEFAULT_RLS_ROLE, executionStoreMigrationSql, rlsPolicySql } from "@alma-harness/postgres";
|
|
9
|
+
|
|
10
|
+
// src/request-schema.ts
|
|
11
|
+
import { functionPathSql } from "@alma-harness/postgres";
|
|
12
|
+
var operationRequestShapeSql = `
|
|
13
|
+
create or replace function alma_operation_request_v1(v jsonb) returns boolean language plpgsql immutable as $request$
|
|
14
|
+
declare field text;
|
|
15
|
+
begin
|
|
16
|
+
if (jsonb_typeof(v)='object' and v ?& array['inputRevision','configRevision','resultContractVersion','resultRetentionMs']
|
|
17
|
+
and (v-array['inputRevision','configRevision','resultContractVersion','resultRetentionMs'])='{}'::jsonb
|
|
18
|
+
and jsonb_typeof(v->'resultRetentionMs')='number' and (v->>'resultRetentionMs')::numeric between 1 and 31536000000
|
|
19
|
+
and trunc((v->>'resultRetentionMs')::numeric)=(v->>'resultRetentionMs')::numeric) is not true then return false; end if;
|
|
20
|
+
foreach field in array array['inputRevision','configRevision','resultContractVersion'] loop
|
|
21
|
+
if (jsonb_typeof(v->field)='string' and v->>field ~ '^[!-~]{1,200}$') is not true then return false; end if;
|
|
22
|
+
end loop; return true;
|
|
23
|
+
exception when others then return false;
|
|
24
|
+
end $request$;
|
|
25
|
+
`;
|
|
26
|
+
var operationRequestRootSql = `
|
|
27
|
+
alter table alma_operation_roots add column if not exists request jsonb;
|
|
28
|
+
do $request_check$ begin
|
|
29
|
+
if not exists(select from pg_constraint where conrelid='alma_operation_roots'::regclass and conname='alma_operation_request_check') then
|
|
30
|
+
alter table alma_operation_roots add constraint alma_operation_request_check check(request is null or alma_operation_request_v1(request) is true);
|
|
31
|
+
end if;
|
|
32
|
+
end $request_check$;
|
|
33
|
+
create or replace function alma_operation_binding_immutable() returns trigger language plpgsql as $binding$
|
|
34
|
+
begin
|
|
35
|
+
if (new.org,new.uid,new.key,new.id,new.session_id,new.policy_version,new.caps,new.max_sensitivity,new.deadline_at,new.max_calls,new.token,new.created_at,new.request)
|
|
36
|
+
is distinct from (old.org,old.uid,old.key,old.id,old.session_id,old.policy_version,old.caps,old.max_sensitivity,old.deadline_at,old.max_calls,old.token,old.created_at,old.request) then
|
|
37
|
+
raise exception 'Immutable operation binding' using errcode='23514';
|
|
38
|
+
end if; return new;
|
|
39
|
+
end $binding$;
|
|
40
|
+
do $binding_trigger$ begin
|
|
41
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_roots'::regclass and tgname='alma_operation_binding_immutable') then
|
|
42
|
+
create trigger alma_operation_binding_immutable before update on alma_operation_roots for each row execute function alma_operation_binding_immutable();
|
|
43
|
+
end if;
|
|
44
|
+
end $binding_trigger$;
|
|
45
|
+
`;
|
|
46
|
+
var operationRequestAdmissionSql = `
|
|
47
|
+
create or replace function alma_admission_input_v2(v jsonb) returns boolean language sql immutable as $shape$
|
|
48
|
+
select (alma_admission_input(v-'request') and (not(v ? 'request') or alma_operation_request_v1(v->'request'))) is true;
|
|
49
|
+
$shape$;
|
|
50
|
+
do $admission_check$ declare legacy record;
|
|
51
|
+
begin
|
|
52
|
+
-- Replace only the original input-shape check, not state, fence or scope controls.
|
|
53
|
+
for legacy in select conname from pg_constraint where conrelid='alma_operation_admissions'::regclass
|
|
54
|
+
and contype='c' and pg_get_constraintdef(oid) like '%alma_admission_input(input)%' loop
|
|
55
|
+
execute format('alter table alma_operation_admissions drop constraint %I',legacy.conname);
|
|
56
|
+
end loop;
|
|
57
|
+
if not exists(select from pg_constraint where conrelid='alma_operation_admissions'::regclass and conname='alma_admission_input_v2_check') then
|
|
58
|
+
alter table alma_operation_admissions add constraint alma_admission_input_v2_check check((alma_admission_input_v2(input)
|
|
59
|
+
and input->'scope'->>'org'=org and input->'scope'->>'uid'=uid and input->>'key'=root_key
|
|
60
|
+
and input->>'id'=root_id and input->>'sessionId'=session_id and (input->>'deadlineAt')::timestamptz=deadline_at) is true);
|
|
61
|
+
end if;
|
|
62
|
+
end $admission_check$;
|
|
63
|
+
${functionPathSql("alma_admission_input_v2(jsonb)")}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
// src/schema.ts
|
|
67
|
+
function operationTreeMigrationSql(role = DEFAULT_RLS_ROLE) {
|
|
68
|
+
assertRoleIdentifier(role);
|
|
69
|
+
return `${executionStoreMigrationSql(role)}${operationRequestShapeSql}
|
|
70
|
+
create or replace function alma_operation_caps(v jsonb) returns boolean language plpgsql immutable as $caps$
|
|
71
|
+
declare item record;
|
|
72
|
+
begin
|
|
73
|
+
if jsonb_typeof(v) <> 'object' then return false; end if;
|
|
74
|
+
for item in select * from jsonb_each(v) loop
|
|
75
|
+
if (item.key in ('perTurnUsd','perSessionUsd','perTenantDayUsd') and jsonb_typeof(item.value)='object'
|
|
76
|
+
and item.value ?& array['usd','onExceeded'] and (item.value-'usd'-'onExceeded')='{}'::jsonb
|
|
77
|
+
and jsonb_typeof(item.value->'usd')='number' and item.value->'usd'>='0'::jsonb
|
|
78
|
+
and item.value->>'onExceeded' in ('warn','block')) is not true then return false; end if;
|
|
79
|
+
end loop; return true;
|
|
80
|
+
end $caps$;
|
|
81
|
+
create table if not exists alma_operation_roots (
|
|
82
|
+
org text not null, uid text not null, key text collate "C" not null, id text not null,
|
|
83
|
+
session_id text not null, policy_version text not null, caps jsonb not null,
|
|
84
|
+
max_sensitivity text not null check(max_sensitivity in ('public','internal','personal','health')),
|
|
85
|
+
deadline_at timestamptz not null, max_calls int not null check(max_calls between 1 and 512),
|
|
86
|
+
status text not null check(status in ('active','closed','reconciliation_required')),
|
|
87
|
+
token text not null, call_count int not null default 0 check(call_count between 0 and max_calls),
|
|
88
|
+
created_at timestamptz not null, updated_at timestamptz not null,
|
|
89
|
+
primary key(org,uid,key), unique(org,uid,id),
|
|
90
|
+
check((${["org", "uid", "key", "id", "session_id", "policy_version", "token"].map((k) => `${k} ~ '^[!-~]{1,200}$'`).join(" and ")}) is true),
|
|
91
|
+
check(alma_operation_caps(caps) is true)
|
|
92
|
+
);
|
|
93
|
+
create index if not exists alma_operation_expired on alma_operation_roots(org,uid,deadline_at,key) where status='active';
|
|
94
|
+
create table if not exists alma_operation_calls (
|
|
95
|
+
org text not null, uid text not null, root_id text not null, slot text collate "C" not null,
|
|
96
|
+
ordinal int not null check(ordinal between 1 and 512), kind text not null check(kind in ('main','direct','delegate','summary')),
|
|
97
|
+
parent_call_id text, call_id text not null, settlement_id text not null, operation_key text not null,
|
|
98
|
+
input jsonb not null, reserved_at timestamptz not null,
|
|
99
|
+
primary key(org,uid,root_id,slot), unique(org,uid,root_id,ordinal), unique(org,uid,root_id,call_id),
|
|
100
|
+
unique(org,uid,call_id), unique(org,uid,settlement_id), unique(org,uid,operation_key),
|
|
101
|
+
foreign key(org,uid,root_id) references alma_operation_roots(org,uid,id),
|
|
102
|
+
foreign key(org,uid,root_id,parent_call_id) references alma_operation_calls(org,uid,root_id,call_id),
|
|
103
|
+
check((kind='main')=(parent_call_id is null)), check(parent_call_id is null or parent_call_id<>call_id),
|
|
104
|
+
check(slot ~ '^[!-~]{1,200}$'),
|
|
105
|
+
constraint alma_operation_call_shape check((alma_execution_shape_v2(jsonb_set(input,'{controls}',(input->'controls')-'temperature'),'input')
|
|
106
|
+
and input ?& array['scope','operationKey','operationId','attemptId','callId','settlementId','sessionId','inputRevision','policyVersion','outputContractVersion','intent','model','requestedTier','priceVersion','prices','controls','occurredAt','deadlineAt','governance']
|
|
107
|
+
and input->'scope'->>'org'=org and input->'scope'->>'uid'=uid and input->>'callId'=call_id
|
|
108
|
+
and input->>'settlementId'=settlement_id and input->>'operationKey'=operation_key) is true),
|
|
109
|
+
constraint alma_operation_temperature_check check((not (input->'controls' ? 'temperature') or
|
|
110
|
+
(jsonb_typeof(input->'controls'->'temperature')='number' and input->'controls'->'temperature'>='0'::jsonb and input->'controls'->'temperature'<='2'::jsonb)) is true)
|
|
111
|
+
);
|
|
112
|
+
-- Replace only the legacy parent-kind CHECK; old migration replay uses CREATE IF NOT EXISTS.
|
|
113
|
+
alter table alma_operation_calls drop constraint if exists alma_operation_calls_check;
|
|
114
|
+
do $parent_kind$ begin
|
|
115
|
+
if not exists(select from pg_constraint where conrelid='alma_operation_calls'::regclass and conname='alma_operation_parent_kind') then
|
|
116
|
+
alter table alma_operation_calls add constraint alma_operation_parent_kind
|
|
117
|
+
check((kind='main' and parent_call_id is null) or kind='summary' or (kind in ('direct','delegate') and parent_call_id is not null));
|
|
118
|
+
end if;
|
|
119
|
+
end $parent_kind$;
|
|
120
|
+
create or replace function alma_operation_reserve() returns trigger language plpgsql as $reserve$
|
|
121
|
+
declare r alma_operation_roots%rowtype;
|
|
122
|
+
begin
|
|
123
|
+
select * into r from alma_operation_roots where org=new.org and uid=new.uid and id=new.root_id for update;
|
|
124
|
+
if (r.status='active' and r.deadline_at>clock_timestamp() and new.ordinal=r.call_count+1 and new.ordinal<=r.max_calls
|
|
125
|
+
and new.input->>'sessionId'=r.session_id and new.input->>'policyVersion'=r.policy_version
|
|
126
|
+
and new.input->'governance'->'caps'=r.caps and (new.input->>'deadlineAt')::timestamptz<=r.deadline_at
|
|
127
|
+
and array_position(array['public','internal','personal','health'],new.input->'intent'->>'sensitivity')<=array_position(array['public','internal','personal','health'],r.max_sensitivity)) is not true then
|
|
128
|
+
raise exception 'Invalid operation reservation' using errcode='23514';
|
|
129
|
+
end if;
|
|
130
|
+
if new.parent_call_id is not null and not exists(select from alma_operation_calls where org=new.org and uid=new.uid and root_id=new.root_id and call_id=new.parent_call_id and ordinal<new.ordinal) then
|
|
131
|
+
raise exception 'Invalid operation parent' using errcode='23514';
|
|
132
|
+
end if;
|
|
133
|
+
update alma_operation_roots set call_count=new.ordinal,updated_at=clock_timestamp() where org=new.org and uid=new.uid and id=new.root_id;
|
|
134
|
+
return new;
|
|
135
|
+
end $reserve$;
|
|
136
|
+
do $trigger$ begin
|
|
137
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_calls'::regclass and tgname='alma_operation_reserve') then
|
|
138
|
+
create trigger alma_operation_reserve before insert on alma_operation_calls for each row execute function alma_operation_reserve();
|
|
139
|
+
end if;
|
|
140
|
+
end $trigger$;
|
|
141
|
+
${rlsPolicySql("alma_operation_roots")}${rlsPolicySql("alma_operation_calls")}
|
|
142
|
+
grant select,insert,update on alma_operation_roots to ${role};
|
|
143
|
+
grant select,insert on alma_operation_calls to ${role};
|
|
144
|
+
${operationRequestRootSql}
|
|
145
|
+
${functionPathSql2("alma_operation_reserve()")}
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
async function migrateOperationTreeStore(pool, opts = {}) {
|
|
149
|
+
await pool.query(operationTreeMigrationSql(opts.role));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/rows.ts
|
|
153
|
+
import { normalizeOperationRoot, normalizeOperationCall } from "@alma-harness/execution";
|
|
154
|
+
var ROOT = "org,uid,key,id,session_id,policy_version,caps,max_sensitivity,deadline_at,max_calls,status,call_count,created_at,updated_at,request";
|
|
155
|
+
function record(r) {
|
|
156
|
+
return { input: normalizeOperationRoot({ ...r.request === null ? {} : { request: r.request }, scope: { org: r.org, uid: r.uid }, key: r.key, id: r.id, sessionId: r.session_id, policyVersion: r.policy_version, caps: r.caps, maxSensitivity: r.max_sensitivity, deadlineAt: r.deadline_at.toISOString(), maxCalls: r.max_calls }), status: r.status, callCount: r.call_count, createdAt: r.created_at.toISOString(), updatedAt: r.updated_at.toISOString() };
|
|
157
|
+
}
|
|
158
|
+
function call(r) {
|
|
159
|
+
return { rootId: r.root_id, ordinal: r.ordinal, input: normalizeOperationCall({ slot: r.slot, kind: r.kind, ...r.parent_call_id !== null ? { parentCallId: r.parent_call_id } : {}, execution: r.input }), reservedAt: r.reserved_at.toISOString() };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/accounting.ts
|
|
163
|
+
import { ExecutionConflictError, ExecutionStateError, settlementIdentifier as id, settlementScope } from "@alma-harness/core";
|
|
164
|
+
import { normalizeGovernedCostReceipt, normalizeRootCallReceipt, operationCallQuery, rootCallReceipt, rootWarnings } from "@alma-harness/execution";
|
|
165
|
+
import { inScope, resolveRlsRole, resolveStatementTimeout } from "@alma-harness/postgres";
|
|
166
|
+
function receipt(r) {
|
|
167
|
+
return normalizeRootCallReceipt({ rootId: r.root_id, callId: r.call_id, settlementId: r.settlement_id, accountingOrdinal: r.accounting_ordinal, reservationOrdinal: r.reservation_ordinal, costUsd: r.cost_usd, previousUsd: r.previous_usd, currentUsd: r.current_usd, decisions: r.decisions, newWarnings: r.new_warnings, recordedAt: r.recorded_at.toISOString() });
|
|
168
|
+
}
|
|
169
|
+
var PostgresOperationAccountingStore = class {
|
|
170
|
+
constructor(pool, opts = {}) {
|
|
171
|
+
this.pool = pool;
|
|
172
|
+
this.#role = resolveRlsRole(opts);
|
|
173
|
+
this.#timeout = resolveStatementTimeout(opts);
|
|
174
|
+
}
|
|
175
|
+
pool;
|
|
176
|
+
#role;
|
|
177
|
+
#timeout;
|
|
178
|
+
async #scope(scope, fn) {
|
|
179
|
+
try {
|
|
180
|
+
return await inScope(this.pool, this.#role, scope, fn, this.#timeout);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (["23505", "23503", "23514"].includes(e.code ?? "")) throw new ExecutionConflictError();
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async #root(c, s, k, write) {
|
|
187
|
+
return (await c.query(`select ${ROOT} from alma_operation_roots where org=$1 and uid=$2 and key=$3 for ${write ? "update" : "share"}`, [s.org, s.uid, k])).rows[0];
|
|
188
|
+
}
|
|
189
|
+
async record(scope, rootKey, callId) {
|
|
190
|
+
const s = settlementScope(scope), k = id(rootKey), identity = id(callId);
|
|
191
|
+
return this.#scope(s, async (c) => {
|
|
192
|
+
const root = await this.#root(c, s, k, true);
|
|
193
|
+
if (!root) throw new ExecutionStateError();
|
|
194
|
+
const args = [s.org, s.uid, root.id];
|
|
195
|
+
const old = (await c.query("select * from alma_operation_financial_calls where org=$1 and uid=$2 and root_id=$3 and call_id=$4", [...args, identity])).rows[0];
|
|
196
|
+
if (old) return { status: "replayed", receipt: receipt(old) };
|
|
197
|
+
const row = (await c.query("select * from alma_operation_calls where org=$1 and uid=$2 and root_id=$3 and call_id=$4", [...args, identity])).rows[0];
|
|
198
|
+
if (!row) throw new ExecutionStateError();
|
|
199
|
+
const member = call(row);
|
|
200
|
+
const cost = (await c.query(`select c.id,c.settlement_payload,c.settlement_session_usd,c.settlement_day_usd,g.request,g.decisions
|
|
201
|
+
from alma_audit_cost c left join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id)
|
|
202
|
+
where c.org=$1 and c.uid=$2 and c.settlement_id=$3 and c.settlement_governed`, [s.org, s.uid, member.input.execution.settlementId])).rows[0];
|
|
203
|
+
if (!cost) return { status: "pending" };
|
|
204
|
+
const source = normalizeGovernedCostReceipt({ request: cost.request, decisions: cost.decisions, receipt: { settlement: cost.settlement_payload, totals: { sessionUsd: cost.settlement_session_usd, tenantDayUsd: cost.settlement_day_usd } } });
|
|
205
|
+
const last = (await c.query("select * from alma_operation_financial_calls where org=$1 and uid=$2 and root_id=$3 order by accounting_ordinal desc limit 1", args)).rows[0];
|
|
206
|
+
const warned = (await c.query("select cap from alma_operation_warnings where org=$1 and uid=$2 and root_id=$3", args)).rows.map((r) => r.cap);
|
|
207
|
+
const now = (await c.query("select clock_timestamp() at")).rows[0].at.toISOString();
|
|
208
|
+
const value = rootCallReceipt(record(root).input, member, source, last ? receipt(last) : void 0, warned, now);
|
|
209
|
+
const saved = (await c.query(`insert into alma_operation_financial_calls(org,uid,root_id,call_id,settlement_id,cost_id,accounting_ordinal,reservation_ordinal,cost_usd,previous_usd,current_usd,decisions,new_warnings,recorded_at)
|
|
210
|
+
values($1,$2,$3,$4,$5,$6::uuid,$7,$8,$9,$10,$11,$12::jsonb,$13::text[],$14::timestamptz) returning *`, [...args, value.callId, value.settlementId, cost.id, value.accountingOrdinal, value.reservationOrdinal, value.costUsd, value.previousUsd, value.currentUsd, JSON.stringify(value.decisions), value.newWarnings, value.recordedAt])).rows[0];
|
|
211
|
+
return { status: "applied", receipt: receipt(saved) };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async get(scope, rootKey) {
|
|
215
|
+
const s = settlementScope(scope), k = id(rootKey);
|
|
216
|
+
return this.#scope(s, async (c) => {
|
|
217
|
+
const root = await this.#root(c, s, k, false);
|
|
218
|
+
if (!root) return null;
|
|
219
|
+
const args = [s.org, s.uid, root.id], last = (await c.query("select * from alma_operation_financial_calls where org=$1 and uid=$2 and root_id=$3 order by accounting_ordinal desc limit 1", args)).rows[0];
|
|
220
|
+
const rows = (await c.query(`select f.*,w.cap from alma_operation_warnings w join alma_operation_financial_calls f on (f.org,f.uid,f.root_id,f.call_id)=(w.org,w.uid,w.root_id,w.call_id)
|
|
221
|
+
where w.org=$1 and w.uid=$2 and w.root_id=$3 order by f.accounting_ordinal,w.cap`, args)).rows;
|
|
222
|
+
const warnings = rows.map((r) => {
|
|
223
|
+
const found = rootWarnings(s, receipt(r)).find((w) => w.cap === r.cap);
|
|
224
|
+
if (!found) throw new ExecutionConflictError();
|
|
225
|
+
return found;
|
|
226
|
+
});
|
|
227
|
+
return { rootId: root.id, costUsd: last ? receipt(last).currentUsd : 0, reservedCalls: root.call_count, settledCalls: last?.accounting_ordinal ?? 0, rootStatus: root.status, warnings };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async list(scope, rootKey, query) {
|
|
231
|
+
const s = settlementScope(scope), k = id(rootKey), q = operationCallQuery(query);
|
|
232
|
+
return this.#scope(s, async (c) => (await c.query(`select f.* from alma_operation_financial_calls f join alma_operation_roots r on (r.org,r.uid,r.id)=(f.org,f.uid,f.root_id)
|
|
233
|
+
where r.org=$1 and r.uid=$2 and r.key=$3 and f.accounting_ordinal>$4 order by f.accounting_ordinal limit $5`, [s.org, s.uid, k, q.afterOrdinal, q.limit])).rows.map(receipt));
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/accounting-schema.ts
|
|
238
|
+
import { functionPathSql as functionPathSql3 } from "@alma-harness/postgres";
|
|
239
|
+
import { assertRoleIdentifier as assertRoleIdentifier2, DEFAULT_RLS_ROLE as DEFAULT_RLS_ROLE2, governedCostSettlementStoreMigrationSql, rlsPolicySql as rlsPolicySql2 } from "@alma-harness/postgres";
|
|
240
|
+
function operationAccountingMigrationSql(role = DEFAULT_RLS_ROLE2) {
|
|
241
|
+
assertRoleIdentifier2(role);
|
|
242
|
+
return `${operationTreeMigrationSql(role)}${governedCostSettlementStoreMigrationSql(role)}
|
|
243
|
+
create table if not exists alma_operation_financial_calls (
|
|
244
|
+
org text not null, uid text not null, root_id text not null, call_id text not null, settlement_id text not null, cost_id uuid not null,
|
|
245
|
+
accounting_ordinal int not null check(accounting_ordinal between 1 and 512), reservation_ordinal int not null check(reservation_ordinal between 1 and 512),
|
|
246
|
+
cost_usd double precision not null, previous_usd double precision not null, current_usd double precision not null,
|
|
247
|
+
decisions jsonb not null, new_warnings text[] not null, recorded_at timestamptz not null,
|
|
248
|
+
primary key(org,uid,root_id,call_id), unique(org,uid,root_id,accounting_ordinal), unique(org,uid,root_id,settlement_id),
|
|
249
|
+
foreign key(org,uid,root_id,call_id) references alma_operation_calls(org,uid,root_id,call_id),
|
|
250
|
+
foreign key(org,uid,cost_id) references alma_audit_cost(org,uid,id),
|
|
251
|
+
check(cost_usd>=0 and cost_usd<'Infinity'::float8 and previous_usd>=0 and previous_usd<'Infinity'::float8 and current_usd=previous_usd+cost_usd and current_usd<'Infinity'::float8),
|
|
252
|
+
check(jsonb_typeof(decisions)='array' and jsonb_array_length(decisions)<=3),
|
|
253
|
+
check(cardinality(new_warnings)<=3 and new_warnings<@array['perTurnUsd','perSessionUsd','perTenantDayUsd']::text[])
|
|
254
|
+
);
|
|
255
|
+
create table if not exists alma_operation_warnings (
|
|
256
|
+
org text not null, uid text not null, root_id text not null, cap text not null check(cap in ('perTurnUsd','perSessionUsd','perTenantDayUsd')), call_id text not null,
|
|
257
|
+
primary key(org,uid,root_id,cap), foreign key(org,uid,root_id,call_id) references alma_operation_financial_calls(org,uid,root_id,call_id)
|
|
258
|
+
);
|
|
259
|
+
create or replace function alma_operation_financial_validate() returns trigger language plpgsql as $financial$
|
|
260
|
+
declare root alma_operation_roots%rowtype; member alma_operation_calls%rowtype; previous alma_operation_financial_calls%rowtype;
|
|
261
|
+
source record; expected jsonb; warnings text[]; field text;
|
|
262
|
+
begin
|
|
263
|
+
select * into root from alma_operation_roots where org=new.org and uid=new.uid and id=new.root_id for update;
|
|
264
|
+
select * into member from alma_operation_calls where org=new.org and uid=new.uid and root_id=new.root_id and call_id=new.call_id;
|
|
265
|
+
select * into previous from alma_operation_financial_calls where org=new.org and uid=new.uid and root_id=new.root_id order by accounting_ordinal desc limit 1;
|
|
266
|
+
select c.settlement_payload payload,g.request,g.decisions into source from alma_audit_cost c join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id)
|
|
267
|
+
where c.org=new.org and c.uid=new.uid and c.id=new.cost_id and c.settlement_governed;
|
|
268
|
+
if (root.id is not null and member.call_id is not null and source.payload is not null
|
|
269
|
+
and new.reservation_ordinal=member.ordinal and new.accounting_ordinal=coalesce(previous.accounting_ordinal,0)+1
|
|
270
|
+
and new.previous_usd=coalesce(previous.current_usd,0) and new.cost_usd=(source.payload->>'costUsd')::float8
|
|
271
|
+
and source.payload->>'callId'=new.call_id and source.payload->>'id'=new.settlement_id
|
|
272
|
+
and source.payload->>'id'=member.input->>'settlementId' and source.payload->'scope'=member.input->'scope'
|
|
273
|
+
and source.payload->'model'=member.input->'model' and source.payload->>'at'=member.input->>'occurredAt'
|
|
274
|
+
and source.payload->'consumers'=member.input->'governance'->'consumers' and source.request->>'policyVersion'=root.policy_version and source.request->'caps'=root.caps
|
|
275
|
+
and (not(source.payload ? 'parentCallId') or source.payload->>'parentCallId'=member.parent_call_id)) is not true then
|
|
276
|
+
raise exception 'Invalid root financial association' using errcode='23514';
|
|
277
|
+
end if;
|
|
278
|
+
foreach field in array array['sessionId','operationId','attemptId','priceVersion'] loop
|
|
279
|
+
if (source.payload->>field=member.input->>field) is not true then raise exception 'Invalid root financial binding' using errcode='23514'; end if;
|
|
280
|
+
end loop;
|
|
281
|
+
select coalesce(jsonb_agg(case when d->>'cap'='perTurnUsd' then d || jsonb_build_object('previousUsd',new.previous_usd,'currentUsd',new.current_usd,'exceeded',new.current_usd>(d->>'thresholdUsd')::float8) else d end order by n),'[]'::jsonb)
|
|
282
|
+
into expected from jsonb_array_elements(source.decisions) with ordinality as x(d,n);
|
|
283
|
+
if new.decisions<>expected then raise exception 'Invalid root financial decisions' using errcode='23514'; end if;
|
|
284
|
+
select coalesce(array_agg(d->>'cap' order by n),array[]::text[]) into warnings from jsonb_array_elements(expected) with ordinality as x(d,n)
|
|
285
|
+
where d->>'onExceeded'='warn' and (d->>'exceeded')::boolean and not exists(select from alma_operation_warnings w where w.org=new.org and w.uid=new.uid and w.root_id=new.root_id and w.cap=d->>'cap');
|
|
286
|
+
if new.new_warnings<>warnings then raise exception 'Invalid root financial warnings' using errcode='23514'; end if;
|
|
287
|
+
return new;
|
|
288
|
+
end $financial$;
|
|
289
|
+
create or replace function alma_operation_financial_warn() returns trigger language plpgsql as $warnings$
|
|
290
|
+
declare cap text;
|
|
291
|
+
begin
|
|
292
|
+
foreach cap in array new.new_warnings loop insert into alma_operation_warnings(org,uid,root_id,cap,call_id) values(new.org,new.uid,new.root_id,cap,new.call_id); end loop;
|
|
293
|
+
return new;
|
|
294
|
+
end $warnings$;
|
|
295
|
+
create or replace function alma_operation_warning_validate() returns trigger language plpgsql as $validate$
|
|
296
|
+
begin
|
|
297
|
+
if not exists(select from alma_operation_financial_calls f where f.org=new.org and f.uid=new.uid and f.root_id=new.root_id and f.call_id=new.call_id and new.cap=any(f.new_warnings)) then
|
|
298
|
+
raise exception 'Invalid root warning association' using errcode='23514';
|
|
299
|
+
end if; return new;
|
|
300
|
+
end $validate$;
|
|
301
|
+
do $triggers$ begin
|
|
302
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_financial_calls'::regclass and tgname='alma_operation_financial_validate') then
|
|
303
|
+
create trigger alma_operation_financial_validate before insert on alma_operation_financial_calls for each row execute function alma_operation_financial_validate();
|
|
304
|
+
create trigger alma_operation_financial_warn after insert on alma_operation_financial_calls for each row execute function alma_operation_financial_warn();
|
|
305
|
+
end if;
|
|
306
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_warnings'::regclass and tgname='alma_operation_warning_validate') then
|
|
307
|
+
create trigger alma_operation_warning_validate before insert on alma_operation_warnings for each row execute function alma_operation_warning_validate();
|
|
308
|
+
end if;
|
|
309
|
+
end $triggers$;
|
|
310
|
+
${rlsPolicySql2("alma_operation_financial_calls")}${rlsPolicySql2("alma_operation_warnings")}
|
|
311
|
+
grant select,insert on alma_operation_financial_calls,alma_operation_warnings to ${role};
|
|
312
|
+
${functionPathSql3("alma_operation_financial_validate()", "alma_operation_financial_warn()", "alma_operation_warning_validate()")}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
async function migrateOperationAccountingStore(pool, opts = {}) {
|
|
316
|
+
await pool.query(operationAccountingMigrationSql(opts.role));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/session.ts
|
|
320
|
+
import { equalExecution, ExecutionConflictError as ExecutionConflictError2, ExecutionStateError as ExecutionStateError2, settlementIdentifier as id2, settlementLimit, settlementScope as settlementScope2 } from "@alma-harness/core";
|
|
321
|
+
import { checkOperationDeadline, normalizeOperationRoot as normalizeOperationRoot2, normalizeOperationSessionFence, operationCallQuery as operationCallQuery2 } from "@alma-harness/execution";
|
|
322
|
+
import { assertRoleIdentifier as assertRoleIdentifier3, DEFAULT_RETENTION_ROLE, inScope as inScope2, resolveRlsRole as resolveRlsRole2, resolveStatementTimeout as resolveStatementTimeout2 } from "@alma-harness/postgres";
|
|
323
|
+
var record2 = (r) => ({ input: normalizeOperationRoot2(r.input), ordinal: Number(r.ordinal), status: r.status, createdAt: r.created_at.toISOString(), updatedAt: r.updated_at.toISOString(), ...r.resolution_id === null ? {} : { resolutionId: r.resolution_id } });
|
|
324
|
+
var PostgresOperationSessionStore = class {
|
|
325
|
+
constructor(pool, opts = {}) {
|
|
326
|
+
this.pool = pool;
|
|
327
|
+
this.#role = resolveRlsRole2(opts);
|
|
328
|
+
this.#timeout = resolveStatementTimeout2(opts);
|
|
329
|
+
this.#operator = opts.operatorRole ?? DEFAULT_RETENTION_ROLE;
|
|
330
|
+
assertRoleIdentifier3(this.#operator);
|
|
331
|
+
if (this.#role === this.#operator) throw new TypeError("Admission operator must have a separate role");
|
|
332
|
+
}
|
|
333
|
+
pool;
|
|
334
|
+
#role;
|
|
335
|
+
#operator;
|
|
336
|
+
#timeout;
|
|
337
|
+
async #scope(s, fn, operator = false) {
|
|
338
|
+
try {
|
|
339
|
+
return await inScope2(this.pool, operator ? this.#operator : this.#role, s, fn, this.#timeout);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
const code = e.code;
|
|
342
|
+
if (code === "23505") throw new ExecutionConflictError2();
|
|
343
|
+
if (code === "23514" || code === "23503") throw new ExecutionStateError2();
|
|
344
|
+
throw e;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async #read(c, s, k, lock = false) {
|
|
348
|
+
return (await c.query(`select * from alma_operation_admissions where org=$1 and uid=$2 and root_key=$3${lock ? " for update" : ""}`, [s.org, s.uid, k])).rows[0];
|
|
349
|
+
}
|
|
350
|
+
async #lock(c, s, sessionId) {
|
|
351
|
+
await c.query("select session_id from alma_operation_sessions where org=$1 and uid=$2 and session_id=$3 for update", [s.org, s.uid, sessionId]);
|
|
352
|
+
}
|
|
353
|
+
async claim(value) {
|
|
354
|
+
const i = normalizeOperationRoot2(value);
|
|
355
|
+
return this.#scope(i.scope, async (c) => {
|
|
356
|
+
await c.query("insert into alma_operation_sessions(org,uid,session_id) values($1,$2,$3) on conflict do nothing", [i.scope.org, i.scope.uid, i.sessionId]);
|
|
357
|
+
await this.#lock(c, i.scope, i.sessionId);
|
|
358
|
+
const old = await this.#read(c, i.scope, i.key);
|
|
359
|
+
if (old) {
|
|
360
|
+
if (!equalExecution(old.input, i)) throw new ExecutionConflictError2();
|
|
361
|
+
return { status: "existing", record: record2(old) };
|
|
362
|
+
}
|
|
363
|
+
if ((await c.query("select 1 from alma_operation_admissions where org=$1 and uid=$2 and root_id=$3", [i.scope.org, i.scope.uid, i.id])).rowCount) throw new ExecutionConflictError2();
|
|
364
|
+
const occupied = (await c.query("select * from alma_operation_admissions where org=$1 and uid=$2 and session_id=$3 and status<>'released'", [i.scope.org, i.scope.uid, i.sessionId])).rows[0];
|
|
365
|
+
if (occupied) return { status: "busy", rootKey: occupied.input.key, rootId: occupied.input.id };
|
|
366
|
+
const at2 = (await c.query("select clock_timestamp() at")).rows[0].at.toISOString();
|
|
367
|
+
checkOperationDeadline(i, at2);
|
|
368
|
+
const token = crypto.randomUUID();
|
|
369
|
+
const row = (await c.query(
|
|
370
|
+
`insert into alma_operation_admissions(org,uid,root_key,root_id,session_id,input,deadline_at,token,ordinal,status,created_at,updated_at)
|
|
371
|
+
values($1,$2,$3,$4,$5,$6::jsonb,$7,$8,(select coalesce(max(ordinal),0)+1 from alma_operation_admissions where org=$1 and uid=$2 and session_id=$5),'active',clock_timestamp(),clock_timestamp()) returning *`,
|
|
372
|
+
[i.scope.org, i.scope.uid, i.key, i.id, i.sessionId, JSON.stringify(i), i.deadlineAt, token]
|
|
373
|
+
)).rows[0];
|
|
374
|
+
return { status: "claimed", record: record2(row), fence: { scope: { ...i.scope }, sessionId: i.sessionId, rootKey: i.key, token } };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async get(scope, rootKey) {
|
|
378
|
+
const s = settlementScope2(scope), k = id2(rootKey);
|
|
379
|
+
return this.#scope(s, async (c) => {
|
|
380
|
+
const r = await this.#read(c, s, k);
|
|
381
|
+
return r ? record2(r) : null;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async list(scope, sessionId, query) {
|
|
385
|
+
const s = settlementScope2(scope), session = id2(sessionId), q = operationCallQuery2(query);
|
|
386
|
+
return this.#scope(s, async (c) => (await c.query("select * from alma_operation_admissions where org=$1 and uid=$2 and session_id=$3 and ordinal>$4 order by ordinal limit $5", [s.org, s.uid, session, q.afterOrdinal, q.limit])).rows.map(record2));
|
|
387
|
+
}
|
|
388
|
+
async #owner(value, finish) {
|
|
389
|
+
const f = normalizeOperationSessionFence(value);
|
|
390
|
+
return this.#scope(f.scope, async (c) => {
|
|
391
|
+
await this.#lock(c, f.scope, f.sessionId);
|
|
392
|
+
const r = await this.#read(c, f.scope, f.rootKey, true);
|
|
393
|
+
if (!r || r.token !== f.token || r.input.sessionId !== f.sessionId) throw new ExecutionStateError2();
|
|
394
|
+
if (finish) {
|
|
395
|
+
if (r.status === "released" && r.resolution_id === null) return record2(r);
|
|
396
|
+
if (r.status !== "active") throw new ExecutionStateError2();
|
|
397
|
+
} else if (r.status !== "active") return record2(r);
|
|
398
|
+
await c.query("select set_config('alma.operation_admission_token',$1,true)", [f.token]);
|
|
399
|
+
return record2((await c.query("update alma_operation_admissions set status=$4 where org=$1 and uid=$2 and root_key=$3 returning *", [f.scope.org, f.scope.uid, f.rootKey, finish ? "released" : "reconciliation_required"])).rows[0]);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
finish(fence) {
|
|
403
|
+
return this.#owner(fence, true);
|
|
404
|
+
}
|
|
405
|
+
markUncertain(fence) {
|
|
406
|
+
return this.#owner(fence, false);
|
|
407
|
+
}
|
|
408
|
+
async reconcileExpired(scope, opts = {}) {
|
|
409
|
+
const s = settlementScope2(scope), limit = settlementLimit(opts.limit);
|
|
410
|
+
return this.#scope(s, async (c) => {
|
|
411
|
+
const sessions = (await c.query(`select session_id from alma_operation_sessions s where org=$1 and uid=$2
|
|
412
|
+
and exists(select from alma_operation_admissions a where (a.org,a.uid,a.session_id)=(s.org,s.uid,s.session_id) and status='active' and deadline_at<=clock_timestamp())
|
|
413
|
+
order by session_id limit $3 for update of s skip locked`, [s.org, s.uid, limit])).rows;
|
|
414
|
+
const records = [];
|
|
415
|
+
for (const row of sessions) records.push(...(await c.query("update alma_operation_admissions set status='reconciliation_required' where org=$1 and uid=$2 and session_id=$3 and status='active' and deadline_at<=clock_timestamp() returning *", [s.org, s.uid, row.session_id])).rows.map(record2));
|
|
416
|
+
return records;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async resolve(scope, rootKey, opts) {
|
|
420
|
+
const s = settlementScope2(scope), k = id2(rootKey), resolutionId = id2(opts.resolutionId);
|
|
421
|
+
return this.#scope(s, async (c) => {
|
|
422
|
+
const meta = await this.#read(c, s, k);
|
|
423
|
+
if (!meta) throw new ExecutionStateError2();
|
|
424
|
+
await this.#lock(c, s, meta.input.sessionId);
|
|
425
|
+
const r = await this.#read(c, s, k, true);
|
|
426
|
+
if (r.status === "released") {
|
|
427
|
+
if (r.resolution_id !== resolutionId) throw new ExecutionConflictError2();
|
|
428
|
+
return record2(r);
|
|
429
|
+
}
|
|
430
|
+
if (r.status !== "reconciliation_required") throw new ExecutionStateError2();
|
|
431
|
+
return record2((await c.query("update alma_operation_admissions set status='released',resolution_id=$4 where org=$1 and uid=$2 and root_key=$3 returning *", [s.org, s.uid, k, resolutionId])).rows[0]);
|
|
432
|
+
}, true);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/session-schema.ts
|
|
437
|
+
import { functionPathSql as functionPathSql4 } from "@alma-harness/postgres";
|
|
438
|
+
import { assertRoleIdentifier as assertRoleIdentifier4, DEFAULT_RLS_ROLE as DEFAULT_RLS_ROLE3, DEFAULT_RETENTION_ROLE as DEFAULT_RETENTION_ROLE2, rlsPolicySql as rlsPolicySql3, roleBootstrapSql } from "@alma-harness/postgres";
|
|
439
|
+
function operationSessionMigrationSql(role = DEFAULT_RLS_ROLE3, operatorRole = DEFAULT_RETENTION_ROLE2) {
|
|
440
|
+
assertRoleIdentifier4(role);
|
|
441
|
+
assertRoleIdentifier4(operatorRole);
|
|
442
|
+
if (role === operatorRole) throw new TypeError("Admission operator must have a separate role");
|
|
443
|
+
return `${operationTreeMigrationSql(role)}${roleBootstrapSql(operatorRole)}
|
|
444
|
+
create table if not exists alma_operation_sessions (
|
|
445
|
+
org text not null, uid text not null, session_id text not null,
|
|
446
|
+
primary key(org,uid,session_id),
|
|
447
|
+
check(org ~ '^[!-~]{1,200}$' and uid ~ '^[!-~]{1,200}$' and session_id ~ '^[!-~]{1,200}$')
|
|
448
|
+
);
|
|
449
|
+
create or replace function alma_admission_input(v jsonb) returns boolean language plpgsql immutable as $shape$
|
|
450
|
+
declare item record;
|
|
451
|
+
begin
|
|
452
|
+
if (jsonb_typeof(v)='object' and v ?& array['scope','key','id','sessionId','policyVersion','caps','maxSensitivity','deadlineAt','maxCalls']
|
|
453
|
+
and (v-array['scope','key','id','sessionId','policyVersion','caps','maxSensitivity','deadlineAt','maxCalls'])='{}'::jsonb
|
|
454
|
+
and jsonb_typeof(v->'scope')='object' and ((v->'scope')-'org'-'uid')='{}'::jsonb
|
|
455
|
+
and jsonb_typeof(v->'maxCalls')='number' and (v->>'maxCalls')::numeric between 1 and 512
|
|
456
|
+
and trunc((v->>'maxCalls')::numeric)=(v->>'maxCalls')::numeric
|
|
457
|
+
and v->>'maxSensitivity' in ('public','internal','personal','health')
|
|
458
|
+
and jsonb_typeof(v->'deadlineAt')='string' and v->>'deadlineAt' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3}Z$'
|
|
459
|
+
and isfinite((v->>'deadlineAt')::timestamptz) and alma_operation_caps(v->'caps')) is not true then return false; end if;
|
|
460
|
+
for item in select value from jsonb_each(v->'scope') union all select value from jsonb_each(v) where key in ('key','id','sessionId','policyVersion') loop
|
|
461
|
+
if (jsonb_typeof(item.value)='string' and (item.value#>>'{}') ~ '^[!-~]{1,200}$') is not true then return false; end if;
|
|
462
|
+
end loop;
|
|
463
|
+
if not (v->'scope' ?& array['org','uid']) then return false; end if;
|
|
464
|
+
for item in select value from jsonb_each(v->'caps') loop
|
|
465
|
+
if (item.value->>'usd')::numeric > 1.7976931348623157e308 then return false; end if;
|
|
466
|
+
end loop;
|
|
467
|
+
return true;
|
|
468
|
+
exception when others then return false;
|
|
469
|
+
end $shape$;
|
|
470
|
+
create table if not exists alma_operation_admissions (
|
|
471
|
+
org text not null, uid text not null, root_key text not null, root_id text not null,
|
|
472
|
+
session_id text not null, input jsonb not null, deadline_at timestamptz not null,
|
|
473
|
+
token text not null check(token ~ '^[!-~]{1,200}$'),
|
|
474
|
+
ordinal bigint not null check(ordinal between 1 and 9007199254740991),
|
|
475
|
+
status text not null check(status in ('active','reconciliation_required','released')),
|
|
476
|
+
created_at timestamptz not null, updated_at timestamptz not null,
|
|
477
|
+
resolution_id text check(resolution_id ~ '^[!-~]{1,200}$'),
|
|
478
|
+
primary key(org,uid,root_key), unique(org,uid,root_id), unique(org,uid,session_id,ordinal),
|
|
479
|
+
foreign key(org,uid,session_id) references alma_operation_sessions(org,uid,session_id),
|
|
480
|
+
check((alma_admission_input(input) and input->'scope'->>'org'=org and input->'scope'->>'uid'=uid
|
|
481
|
+
and input->>'key'=root_key and input->>'id'=root_id and input->>'sessionId'=session_id
|
|
482
|
+
and (input->>'deadlineAt')::timestamptz=deadline_at) is true),
|
|
483
|
+
check(resolution_id is null or status='released')
|
|
484
|
+
);
|
|
485
|
+
create unique index if not exists alma_admission_occupancy on alma_operation_admissions(org,uid,session_id) where status<>'released';
|
|
486
|
+
create or replace function alma_session_lock_immutable() returns trigger language plpgsql as $lock$
|
|
487
|
+
begin
|
|
488
|
+
if new is distinct from old then raise exception 'Immutable session identity' using errcode='23514'; end if;
|
|
489
|
+
return new;
|
|
490
|
+
end $lock$;
|
|
491
|
+
create or replace function alma_admission_transition() returns trigger language plpgsql as $transition$
|
|
492
|
+
declare owner boolean; expected bigint;
|
|
493
|
+
begin
|
|
494
|
+
perform from alma_operation_sessions where org=new.org and uid=new.uid and session_id=new.session_id for update;
|
|
495
|
+
if tg_op='INSERT' then
|
|
496
|
+
select coalesce(max(ordinal),0)+1 into expected from alma_operation_admissions where org=new.org and uid=new.uid and session_id=new.session_id;
|
|
497
|
+
if (new.status='active' and new.resolution_id is null and new.ordinal=expected
|
|
498
|
+
and new.deadline_at>clock_timestamp() and new.deadline_at<=clock_timestamp()+interval '1 hour') is not true then
|
|
499
|
+
raise exception 'Invalid admission' using errcode='23514';
|
|
500
|
+
end if;
|
|
501
|
+
new.created_at=clock_timestamp(); new.updated_at=new.created_at; return new;
|
|
502
|
+
end if;
|
|
503
|
+
if (new.org,new.uid,new.root_key,new.root_id,new.session_id,new.input,new.deadline_at,new.token,new.ordinal,new.created_at)
|
|
504
|
+
is distinct from (old.org,old.uid,old.root_key,old.root_id,old.session_id,old.input,old.deadline_at,old.token,old.ordinal,old.created_at) then
|
|
505
|
+
raise exception 'Immutable admission binding' using errcode='23514';
|
|
506
|
+
end if;
|
|
507
|
+
owner=coalesce(current_setting('alma.operation_admission_token',true)=old.token,false);
|
|
508
|
+
if not (
|
|
509
|
+
(old.status='active' and new.status='released' and new.resolution_id is null and owner and old.deadline_at>clock_timestamp())
|
|
510
|
+
or (old.status='active' and new.status='reconciliation_required' and new.resolution_id is null and (owner or old.deadline_at<=clock_timestamp()))
|
|
511
|
+
or (old.status='reconciliation_required' and new.status='released' and new.resolution_id is not null and pg_has_role(current_user,'${operatorRole}','MEMBER'))
|
|
512
|
+
) then raise exception 'Invalid admission transition' using errcode='23514'; end if;
|
|
513
|
+
new.updated_at=clock_timestamp(); return new;
|
|
514
|
+
end $transition$;
|
|
515
|
+
do $triggers$ begin
|
|
516
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_sessions'::regclass and tgname='alma_session_lock_immutable') then
|
|
517
|
+
create trigger alma_session_lock_immutable before update on alma_operation_sessions for each row execute function alma_session_lock_immutable();
|
|
518
|
+
end if;
|
|
519
|
+
if not exists(select from pg_trigger where tgrelid='alma_operation_admissions'::regclass and tgname='alma_admission_transition') then
|
|
520
|
+
create trigger alma_admission_transition before insert or update on alma_operation_admissions for each row execute function alma_admission_transition();
|
|
521
|
+
end if;
|
|
522
|
+
end $triggers$;
|
|
523
|
+
${rlsPolicySql3("alma_operation_sessions")}${rlsPolicySql3("alma_operation_admissions")}
|
|
524
|
+
grant select,insert,update on alma_operation_sessions to ${role};
|
|
525
|
+
grant select,update on alma_operation_sessions to ${operatorRole};
|
|
526
|
+
grant select,insert,update on alma_operation_admissions to ${role};
|
|
527
|
+
grant select,update on alma_operation_admissions to ${operatorRole};
|
|
528
|
+
${operationRequestAdmissionSql}
|
|
529
|
+
${functionPathSql4("alma_admission_input(jsonb)", "alma_admission_transition()")}
|
|
530
|
+
`;
|
|
531
|
+
}
|
|
532
|
+
async function migrateOperationSessionStore(pool, opts = {}) {
|
|
533
|
+
await pool.query(operationSessionMigrationSql(opts.role, opts.operatorRole));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/batch.ts
|
|
537
|
+
import { equalExecution as equalExecution2, ExecutionConflictError as ExecutionConflictError3, ExecutionStateError as ExecutionStateError3, settlementIdentifier as id3, settlementScope as settlementScope3 } from "@alma-harness/core";
|
|
538
|
+
import { batchQuery, normalizeBatchSummary, bindBatchHandle, checkBatchDeadline, normalizeBatchFence, normalizeBatchHandle, normalizeBatchRecord, normalizeBatchSubmission } from "@alma-harness/execution";
|
|
539
|
+
import { inScope as inScope3, resolveRlsRole as resolveRlsRole3, resolveStatementTimeout as resolveStatementTimeout3 } from "@alma-harness/postgres";
|
|
540
|
+
var record3 = (r) => normalizeBatchRecord({ input: r.input, state: r.state, createdAt: r.created_at.toISOString(), updatedAt: r.updated_at.toISOString(), ...r.dispatched_at ? { dispatchedAt: r.dispatched_at.toISOString() } : {}, ...r.accepted_at ? { acceptedAt: r.accepted_at.toISOString(), handle: r.handle } : {} });
|
|
541
|
+
var PostgresBatchSubmissionStore = class {
|
|
542
|
+
constructor(pool, opts = {}) {
|
|
543
|
+
this.pool = pool;
|
|
544
|
+
this.#role = resolveRlsRole3(opts);
|
|
545
|
+
this.#timeout = resolveStatementTimeout3(opts);
|
|
546
|
+
}
|
|
547
|
+
pool;
|
|
548
|
+
#role;
|
|
549
|
+
#timeout;
|
|
550
|
+
async #scope(s, fn) {
|
|
551
|
+
try {
|
|
552
|
+
return await inScope3(this.pool, this.#role, s, fn, this.#timeout);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
const code = e.code;
|
|
555
|
+
if (code === "23505") throw new ExecutionConflictError3();
|
|
556
|
+
if (code === "23514" || code === "23503") throw new ExecutionStateError3();
|
|
557
|
+
throw e;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async #read(c, s, k, lock = false) {
|
|
561
|
+
return (await c.query(`select * from alma_batch_submissions where org=$1 and uid=$2 and key=$3${lock ? " for update" : ""}`, [s.org, s.uid, k])).rows[0];
|
|
562
|
+
}
|
|
563
|
+
async claim(value) {
|
|
564
|
+
const input = normalizeBatchSubmission(value);
|
|
565
|
+
return this.#scope(input.scope, async (c) => {
|
|
566
|
+
let row = await this.#read(c, input.scope, input.key, true);
|
|
567
|
+
if (!row) {
|
|
568
|
+
const at2 = (await c.query("select clock_timestamp() at")).rows[0].at.toISOString();
|
|
569
|
+
checkBatchDeadline(input, at2);
|
|
570
|
+
row = (await c.query(`insert into alma_batch_submissions(org,uid,key,id,input,state,token,created_at,updated_at) values($1,$2,$3,$4,$5::jsonb,'prepared',$6,clock_timestamp(),clock_timestamp()) on conflict(org,uid,key) do nothing returning *`, [input.scope.org, input.scope.uid, input.key, input.id, JSON.stringify(input), crypto.randomUUID()])).rows[0];
|
|
571
|
+
if (row) return { status: "claimed", record: record3(row), fence: { scope: { ...input.scope }, key: input.key, id: input.id, token: row.token } };
|
|
572
|
+
row = await this.#read(c, input.scope, input.key, true);
|
|
573
|
+
}
|
|
574
|
+
if (!row || !equalExecution2(record3(row).input, input)) throw new ExecutionConflictError3();
|
|
575
|
+
return { status: "existing", record: record3(row) };
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
async #owner(value, fn) {
|
|
579
|
+
const f = normalizeBatchFence(value);
|
|
580
|
+
return this.#scope(f.scope, async (c) => {
|
|
581
|
+
const r = await this.#read(c, f.scope, f.key, true);
|
|
582
|
+
if (!r || r.token !== f.token || r.input.id !== f.id) throw new ExecutionStateError3();
|
|
583
|
+
await c.query("select set_config('alma.batch_token',$1,true)", [f.token]);
|
|
584
|
+
return fn(c, r, f);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
async beginDispatch(fence) {
|
|
588
|
+
return this.#owner(fence, async (c, r, f) => {
|
|
589
|
+
if (r.state !== "prepared") return { dispatch: false, record: record3(r) };
|
|
590
|
+
const row = (await c.query("update alma_batch_submissions set state='dispatching' where org=$1 and uid=$2 and key=$3 returning *", [f.scope.org, f.scope.uid, f.key])).rows[0];
|
|
591
|
+
return { dispatch: true, record: record3(row) };
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async accept(value, raw) {
|
|
595
|
+
const f = normalizeBatchFence(value), handle = normalizeBatchHandle(raw);
|
|
596
|
+
return this.#owner(f, async (c, r) => {
|
|
597
|
+
bindBatchHandle(record3(r).input, handle);
|
|
598
|
+
if (r.handle) {
|
|
599
|
+
if (!equalExecution2(r.handle, handle)) throw new ExecutionConflictError3();
|
|
600
|
+
return record3(r);
|
|
601
|
+
}
|
|
602
|
+
if (!r.dispatched_at) throw new ExecutionStateError3();
|
|
603
|
+
return record3((await c.query(`update alma_batch_submissions set handle=$4::jsonb,state=case when state='dispatching' and (input->>'submitDeadlineAt')::timestamptz>clock_timestamp() then 'submitted' else 'reconciliation_required' end where org=$1 and uid=$2 and key=$3 returning *`, [f.scope.org, f.scope.uid, f.key, JSON.stringify(handle)])).rows[0]);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
async markUncertain(fence) {
|
|
607
|
+
return this.#owner(fence, async (c, r, f) => r.state === "reconciliation_required" ? record3(r) : record3((await c.query("update alma_batch_submissions set state='reconciliation_required' where org=$1 and uid=$2 and key=$3 returning *", [f.scope.org, f.scope.uid, f.key])).rows[0]));
|
|
608
|
+
}
|
|
609
|
+
async get(scope, identity) {
|
|
610
|
+
const s = settlementScope3(scope), k = id3(identity);
|
|
611
|
+
return this.#scope(s, async (c) => {
|
|
612
|
+
const r = await this.#read(c, s, k);
|
|
613
|
+
return r ? record3(r) : null;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async list(scope, query) {
|
|
617
|
+
const s = settlementScope3(scope), q = batchQuery(query);
|
|
618
|
+
return this.#scope(s, async (c) => (await c.query(`select key,id,input->>'sessionId' session_id,input->>'submitDeadlineAt' deadline,jsonb_array_length(input->'items') item_count,state,created_at,updated_at,dispatched_at,accepted_at,handle from alma_batch_submissions where org=$1 and uid=$2 and ($3::text is null or key>$3 collate "C") order by key limit $4`, [s.org, s.uid, q.afterKey ?? null, q.limit])).rows.map((r) => normalizeBatchSummary({ key: r.key, id: r.id, sessionId: r.session_id, submitDeadlineAt: r.deadline, itemCount: r.item_count, state: r.state, createdAt: r.created_at.toISOString(), updatedAt: r.updated_at.toISOString(), ...r.dispatched_at ? { dispatchedAt: r.dispatched_at.toISOString() } : {}, ...r.accepted_at ? { acceptedAt: r.accepted_at.toISOString(), handle: r.handle } : {} })));
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/batch-schema.ts
|
|
623
|
+
import { functionPathSql as functionPathSql5 } from "@alma-harness/postgres";
|
|
624
|
+
import { assertRoleIdentifier as assertRoleIdentifier5, DEFAULT_RLS_ROLE as DEFAULT_RLS_ROLE4, executionStoreMigrationSql as executionStoreMigrationSql2, rlsPolicySql as rlsPolicySql4 } from "@alma-harness/postgres";
|
|
625
|
+
function batchSubmissionMigrationSql(role = DEFAULT_RLS_ROLE4) {
|
|
626
|
+
assertRoleIdentifier5(role);
|
|
627
|
+
return `${executionStoreMigrationSql2(role)}
|
|
628
|
+
create or replace function alma_batch_input_v1(v jsonb) returns boolean language plpgsql immutable as $shape$
|
|
629
|
+
declare item jsonb; e jsonb; first jsonb; field text;
|
|
630
|
+
begin
|
|
631
|
+
if (jsonb_typeof(v)='object' and v ?& array['scope','key','id','sessionId','configRevision','submitDeadlineAt','items']
|
|
632
|
+
and (v-array['scope','key','id','sessionId','configRevision','submitDeadlineAt','items'])='{}'::jsonb
|
|
633
|
+
and jsonb_typeof(v->'scope')='object' and v->'scope' ?& array['org','uid'] and ((v->'scope')-'org'-'uid')='{}'::jsonb
|
|
634
|
+
and jsonb_typeof(v->'items')='array' and jsonb_array_length(v->'items') between 1 and 512
|
|
635
|
+
and octet_length(v::text)<=4194304) is not true then return false; end if;
|
|
636
|
+
foreach field in array array['key','id','sessionId','configRevision'] loop
|
|
637
|
+
if (jsonb_typeof(v->field)='string' and v->>field ~ '^[!-~]{1,200}$') is not true then return false; end if;
|
|
638
|
+
end loop;
|
|
639
|
+
first=v->'items'->0->'execution';
|
|
640
|
+
for item in select value from jsonb_array_elements(v->'items') loop
|
|
641
|
+
e=item->'execution';
|
|
642
|
+
if (jsonb_typeof(item)='object' and item ?& array['id','execution'] and (item-'id'-'execution')='{}'::jsonb
|
|
643
|
+
and jsonb_typeof(item->'id')='string' and item->>'id' ~ '^[!-~]{1,200}$'
|
|
644
|
+
and alma_execution_shape_v2(jsonb_set(e,'{controls}',(e->'controls')-'temperature'),'input')
|
|
645
|
+
and e ?& array['scope','operationKey','operationId','attemptId','callId','settlementId','sessionId','inputRevision','policyVersion','outputContractVersion','intent','model','requestedTier','priceVersion','prices','controls','occurredAt','deadlineAt','governance']
|
|
646
|
+
and (not(e->'controls' ? 'temperature') or (jsonb_typeof(e->'controls'->'temperature')='number' and (e->'controls'->>'temperature')::numeric between 0 and 2))
|
|
647
|
+
and e->'scope'=v->'scope' and e->>'sessionId'=v->>'sessionId' and e->>'requestedTier'='batch'
|
|
648
|
+
and e->>'deadlineAt'=v->>'submitDeadlineAt' and e->'model'=first->'model'
|
|
649
|
+
and e->>'policyVersion'=first->>'policyVersion' and e->'governance'->'caps'=first->'governance'->'caps') is not true then return false; end if;
|
|
650
|
+
end loop; return true;
|
|
651
|
+
exception when others then return false;
|
|
652
|
+
end $shape$;
|
|
653
|
+
create table if not exists alma_batch_submissions (
|
|
654
|
+
org text not null,uid text not null,key text collate "C" not null,id text not null,input jsonb not null,
|
|
655
|
+
state text not null check(state in ('prepared','dispatching','submitted','reconciliation_required')),
|
|
656
|
+
token text not null check(token ~ '^[!-~]{1,200}$'),created_at timestamptz not null,updated_at timestamptz not null,
|
|
657
|
+
dispatched_at timestamptz,accepted_at timestamptz,handle jsonb,
|
|
658
|
+
primary key(org,uid,key),unique(org,uid,id),
|
|
659
|
+
check((alma_batch_input_v1(input) and input->'scope'->>'org'=org and input->'scope'->>'uid'=uid and input->>'key'=key and input->>'id'=id) is true),
|
|
660
|
+
check((handle is null)=(accepted_at is null)),
|
|
661
|
+
check(handle is null or (jsonb_typeof(handle)='object' and handle ?& array['provider','id','model'] and (handle-'provider'-'id'-'model')='{}'::jsonb
|
|
662
|
+
and jsonb_typeof(handle->'id')='string' and handle->>'id' ~ '^[!-~]{1,200}$'
|
|
663
|
+
and handle->'model'=input->'items'->0->'execution'->'model' and handle->>'provider'=handle->'model'->>'provider') is true),
|
|
664
|
+
check(state<>'prepared' or (dispatched_at is null and handle is null)),
|
|
665
|
+
check(state<>'dispatching' or (dispatched_at is not null and handle is null)),
|
|
666
|
+
check(state<>'submitted' or (handle is not null and accepted_at<(input->>'submitDeadlineAt')::timestamptz)),
|
|
667
|
+
check(accepted_at is null or (dispatched_at is not null and accepted_at>=dispatched_at)),
|
|
668
|
+
check(dispatched_at is null or (dispatched_at>=created_at and dispatched_at<(input->>'submitDeadlineAt')::timestamptz)),
|
|
669
|
+
check(updated_at>=created_at and (accepted_at is null or updated_at>=accepted_at) and (dispatched_at is null or updated_at>=dispatched_at))
|
|
670
|
+
);
|
|
671
|
+
create table if not exists alma_batch_items (
|
|
672
|
+
org text not null,uid text not null,batch_key text not null,ordinal int not null check(ordinal between 1 and 512),
|
|
673
|
+
item_id text not null,operation_key text not null,operation_id text not null,call_id text not null,settlement_id text not null,
|
|
674
|
+
primary key(org,uid,batch_key,ordinal),unique(org,uid,batch_key,item_id),
|
|
675
|
+
unique(org,uid,operation_key),unique(org,uid,operation_id),unique(org,uid,call_id),unique(org,uid,settlement_id),
|
|
676
|
+
foreign key(org,uid,batch_key) references alma_batch_submissions(org,uid,key)
|
|
677
|
+
);
|
|
678
|
+
create or replace function alma_batch_transition() returns trigger language plpgsql as $transition$
|
|
679
|
+
declare at timestamptz; deadline timestamptz;
|
|
680
|
+
begin
|
|
681
|
+
at=clock_timestamp();deadline=(new.input->>'submitDeadlineAt')::timestamptz;
|
|
682
|
+
if tg_op='INSERT' then
|
|
683
|
+
if (new.state='prepared' and new.dispatched_at is null and new.accepted_at is null and new.handle is null and deadline>at and deadline<=at+interval '1 hour') is not true then raise exception 'Invalid batch claim' using errcode='23514'; end if;
|
|
684
|
+
new.created_at=at;new.updated_at=at;return new;
|
|
685
|
+
end if;
|
|
686
|
+
if (new.org,new.uid,new.key,new.id,new.input,new.token,new.created_at) is distinct from (old.org,old.uid,old.key,old.id,old.input,old.token,old.created_at)
|
|
687
|
+
or coalesce(current_setting('alma.batch_token',true)=old.token,false) is not true then raise exception 'Invalid batch binding or fence' using errcode='23514'; end if;
|
|
688
|
+
if old.state='prepared' and new.state='dispatching' and deadline>at and new.handle is null and new.accepted_at is null then
|
|
689
|
+
new.dispatched_at=at;
|
|
690
|
+
elsif old.handle is null and new.handle is not null and old.dispatched_at is not null
|
|
691
|
+
and new.dispatched_at=old.dispatched_at and new.state in ('submitted','reconciliation_required') then
|
|
692
|
+
new.state=case when old.state='dispatching' and deadline>at then 'submitted' else 'reconciliation_required' end;new.accepted_at=at;
|
|
693
|
+
elsif old.state<>'reconciliation_required' and new.state='reconciliation_required' and (new.dispatched_at,new.accepted_at,new.handle) is not distinct from (old.dispatched_at,old.accepted_at,old.handle) then null;
|
|
694
|
+
else raise exception 'Invalid batch transition' using errcode='23514';
|
|
695
|
+
end if;
|
|
696
|
+
new.updated_at=at;return new;
|
|
697
|
+
end $transition$;
|
|
698
|
+
create or replace function alma_batch_member() returns trigger language plpgsql as $member$
|
|
699
|
+
declare item jsonb;
|
|
700
|
+
begin
|
|
701
|
+
select input->'items'->(new.ordinal-1) into item from alma_batch_submissions where org=new.org and uid=new.uid and key=new.batch_key;
|
|
702
|
+
if (item->>'id'=new.item_id and item->'execution'->>'operationKey'=new.operation_key and item->'execution'->>'operationId'=new.operation_id
|
|
703
|
+
and item->'execution'->>'callId'=new.call_id and item->'execution'->>'settlementId'=new.settlement_id) is not true then raise exception 'Invalid batch member' using errcode='23514'; end if;return new;
|
|
704
|
+
end $member$;
|
|
705
|
+
create or replace function alma_batch_members() returns trigger language plpgsql as $members$
|
|
706
|
+
begin
|
|
707
|
+
insert into alma_batch_items(org,uid,batch_key,ordinal,item_id,operation_key,operation_id,call_id,settlement_id)
|
|
708
|
+
select new.org,new.uid,new.key,n,value->>'id',value->'execution'->>'operationKey',value->'execution'->>'operationId',value->'execution'->>'callId',value->'execution'->>'settlementId'
|
|
709
|
+
from jsonb_array_elements(new.input->'items') with ordinality as x(value,n);return new;
|
|
710
|
+
end $members$;
|
|
711
|
+
do $triggers$ begin
|
|
712
|
+
if not exists(select from pg_trigger where tgrelid='alma_batch_submissions'::regclass and tgname='alma_batch_transition') then
|
|
713
|
+
create trigger alma_batch_transition before insert or update on alma_batch_submissions for each row execute function alma_batch_transition();
|
|
714
|
+
create trigger alma_batch_members after insert on alma_batch_submissions for each row execute function alma_batch_members();
|
|
715
|
+
end if;
|
|
716
|
+
if not exists(select from pg_trigger where tgrelid='alma_batch_items'::regclass and tgname='alma_batch_member') then
|
|
717
|
+
create trigger alma_batch_member before insert on alma_batch_items for each row execute function alma_batch_member();
|
|
718
|
+
end if;
|
|
719
|
+
end $triggers$;
|
|
720
|
+
${rlsPolicySql4("alma_batch_submissions")}${rlsPolicySql4("alma_batch_items")}
|
|
721
|
+
grant select,insert,update on alma_batch_submissions to ${role};
|
|
722
|
+
grant select,insert on alma_batch_items to ${role};
|
|
723
|
+
${functionPathSql5("alma_batch_input_v1(jsonb)", "alma_batch_member()", "alma_batch_members()")}
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
async function migrateBatchSubmissionStore(pool, opts = {}) {
|
|
727
|
+
await pool.query(batchSubmissionMigrationSql(opts.role));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/batch-usage.ts
|
|
731
|
+
import { equalExecution as equalExecution3, ExecutionConflictError as ExecutionConflictError4, ExecutionStateError as ExecutionStateError4, settlementIdentifier as id4, settlementScope as settlementScope4 } from "@alma-harness/core";
|
|
732
|
+
import { batchUsageMember, batchUsageQuery, batchUsageSettlement, bindBatchUsageReceipt, normalizeBatchUsage, normalizeBatchUsageRecord, normalizeGovernedCostReceipt as normalizeGovernedCostReceipt2 } from "@alma-harness/execution";
|
|
733
|
+
import { inScope as inScope4, resolveRlsRole as resolveRlsRole4, resolveStatementTimeout as resolveStatementTimeout4 } from "@alma-harness/postgres";
|
|
734
|
+
var select = `select u.*,b.input->'items'->(i.ordinal-1)->'execution' execution,c.settlement_payload,c.settlement_session_usd,c.settlement_day_usd,g.request,g.decisions
|
|
735
|
+
from alma_batch_usage u join alma_batch_items i on (i.org,i.uid,i.batch_key,i.item_id)=(u.org,u.uid,u.batch_key,u.item_id)
|
|
736
|
+
join alma_batch_submissions b on (b.org,b.uid,b.key)=(u.org,u.uid,u.batch_key)
|
|
737
|
+
left join alma_audit_cost c on (c.org,c.uid,c.id)=(u.org,u.uid,u.cost_id)
|
|
738
|
+
left join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id)`;
|
|
739
|
+
var receipt2 = (r) => normalizeGovernedCostReceipt2({ request: r.request, decisions: r.decisions, receipt: { settlement: r.settlement_payload, totals: { sessionUsd: r.settlement_session_usd, tenantDayUsd: r.settlement_day_usd } } });
|
|
740
|
+
var record4 = (r) => normalizeBatchUsageRecord({ scope: { org: r.org, uid: r.uid }, batchKey: r.batch_key, input: r.input, execution: r.execution, receivedAt: r.received_at.toISOString(), ...r.cost_id ? { receipt: receipt2(r) } : {} });
|
|
741
|
+
var PostgresBatchUsageStore = class {
|
|
742
|
+
constructor(pool, opts = {}) {
|
|
743
|
+
this.pool = pool;
|
|
744
|
+
this.#role = resolveRlsRole4(opts);
|
|
745
|
+
this.#timeout = resolveStatementTimeout4(opts);
|
|
746
|
+
this.#batches = new PostgresBatchSubmissionStore(pool, opts);
|
|
747
|
+
}
|
|
748
|
+
pool;
|
|
749
|
+
#role;
|
|
750
|
+
#timeout;
|
|
751
|
+
#batches;
|
|
752
|
+
async #scope(s, fn) {
|
|
753
|
+
try {
|
|
754
|
+
return await inScope4(this.pool, this.#role, s, fn, this.#timeout);
|
|
755
|
+
} catch (e) {
|
|
756
|
+
if (["23505", "23514", "23503"].includes(e.code ?? "")) throw new ExecutionConflictError4();
|
|
757
|
+
throw e;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async #read(c, s, k, i, lock = false) {
|
|
761
|
+
const args = [s.org, s.uid, k, i];
|
|
762
|
+
if (lock) await c.query("select id from alma_batch_usage where org=$1 and uid=$2 and batch_key=$3 and id=$4 for update", args);
|
|
763
|
+
return (await c.query(`${select} where u.org=$1 and u.uid=$2 and u.batch_key=$3 and u.id=$4`, args)).rows[0];
|
|
764
|
+
}
|
|
765
|
+
async append(scope, batchKey, value) {
|
|
766
|
+
const s = settlementScope4(scope), k = id4(batchKey), input = normalizeBatchUsage(value);
|
|
767
|
+
batchUsageMember(await this.#batches.get(s, k), input);
|
|
768
|
+
return this.#scope(s, async (c) => {
|
|
769
|
+
await c.query("insert into alma_batch_usage(org,uid,batch_key,id,item_id,input,received_at) values($1,$2,$3,$4,$5,$6::jsonb,clock_timestamp()) on conflict(org,uid,batch_key,id) do nothing", [s.org, s.uid, k, input.id, input.itemId, JSON.stringify(input)]);
|
|
770
|
+
const r = record4(await this.#read(c, s, k, input.id));
|
|
771
|
+
if (!equalExecution3(r.input, input)) throw new ExecutionConflictError4();
|
|
772
|
+
return r;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async get(scope, batchKey, identity) {
|
|
776
|
+
const s = settlementScope4(scope), k = id4(batchKey), i = id4(identity);
|
|
777
|
+
return this.#scope(s, async (c) => {
|
|
778
|
+
const r = await this.#read(c, s, k, i);
|
|
779
|
+
return r ? record4(r) : null;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async list(scope, batchKey, query) {
|
|
783
|
+
const s = settlementScope4(scope), k = id4(batchKey), q = batchUsageQuery(query);
|
|
784
|
+
return this.#scope(s, async (c) => (await c.query(`${select} where u.org=$1 and u.uid=$2 and u.batch_key=$3 and ($4::text is null or u.id>$4 collate "C") and (not $5::boolean or u.cost_id is null) order by u.id limit $6`, [s.org, s.uid, k, q.afterId ?? null, q.pendingOnly, q.limit])).rows.map(record4));
|
|
785
|
+
}
|
|
786
|
+
async recordSettlement(scope, batchKey, identity) {
|
|
787
|
+
const s = settlementScope4(scope), k = id4(batchKey), i = id4(identity);
|
|
788
|
+
return this.#scope(s, async (c) => {
|
|
789
|
+
const row = await this.#read(c, s, k, i, true);
|
|
790
|
+
if (!row) throw new ExecutionStateError4();
|
|
791
|
+
const r = record4(row);
|
|
792
|
+
if (r.receipt) return { status: "replayed", record: r };
|
|
793
|
+
if (!batchUsageSettlement(r)) return { status: "pending" };
|
|
794
|
+
const source = (await c.query(`select c.id,c.settlement_payload,c.settlement_session_usd,c.settlement_day_usd,g.request,g.decisions from alma_audit_cost c join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id) where c.org=$1 and c.uid=$2 and c.settlement_id=$3 and c.settlement_governed`, [s.org, s.uid, r.execution.settlementId])).rows[0];
|
|
795
|
+
if (!source) return { status: "pending" };
|
|
796
|
+
bindBatchUsageReceipt(r, receipt2(source));
|
|
797
|
+
await c.query("update alma_batch_usage set cost_id=$5::uuid where org=$1 and uid=$2 and batch_key=$3 and id=$4", [s.org, s.uid, k, i, source.id]);
|
|
798
|
+
return { status: "applied", record: record4(await this.#read(c, s, k, i)) };
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/batch-usage-schema.ts
|
|
804
|
+
import { functionPathSql as functionPathSql6 } from "@alma-harness/postgres";
|
|
805
|
+
import { assertRoleIdentifier as assertRoleIdentifier6, DEFAULT_RLS_ROLE as DEFAULT_RLS_ROLE5, governedCostSettlementStoreMigrationSql as governedCostSettlementStoreMigrationSql2, rlsPolicySql as rlsPolicySql5 } from "@alma-harness/postgres";
|
|
806
|
+
|
|
807
|
+
// src/batch-pricing-schema.ts
|
|
808
|
+
var batchPricingSql = `
|
|
809
|
+
create or replace function alma_batch_price(e jsonb,u jsonb) returns double precision language plpgsql immutable as $price$
|
|
810
|
+
declare price jsonb; rates jsonb; band jsonb; prompt double precision; best double precision;
|
|
811
|
+
searches double precision; hour_rate double precision; write_rate double precision; amount double precision;
|
|
812
|
+
begin
|
|
813
|
+
select value into price from jsonb_array_elements(e->'prices') where value->'model'=e->'model' and value->>'serviceTier'='batch' limit 1;
|
|
814
|
+
if price is null then return null;end if;
|
|
815
|
+
prompt=(u->>'inputTokens')::float8+coalesce((u->>'cacheReadInputTokens')::float8,0)+coalesce((u->>'cacheWriteInputTokens')::float8,0);
|
|
816
|
+
rates=price;
|
|
817
|
+
for band in select value from jsonb_array_elements(coalesce(price->'bands','[]'::jsonb)) loop
|
|
818
|
+
if prompt>(band->>'aboveInputTokens')::float8 and (best is null or (band->>'aboveInputTokens')::float8>best) then rates=band;best=(band->>'aboveInputTokens')::float8;end if;
|
|
819
|
+
end loop;
|
|
820
|
+
searches=coalesce((u->>'webSearchRequests')::float8,0);
|
|
821
|
+
if searches>0 and not(price ? 'webSearchUsdPerRequest') then return null;end if;
|
|
822
|
+
hour_rate=coalesce((rates->>'cacheWrite1hUsdPerMTok')::float8,(price->>'cacheWrite1hUsdPerMTok')::float8);
|
|
823
|
+
if u->>'cacheWriteTtl'='1h' and coalesce((u->>'cacheWriteInputTokens')::float8,0)>0 then
|
|
824
|
+
if hour_rate is null then return null;end if;write_rate=hour_rate;
|
|
825
|
+
else write_rate=coalesce((rates->>'cacheWriteUsdPerMTok')::float8,(rates->>'inputUsdPerMTok')::float8);end if;
|
|
826
|
+
amount=((u->>'inputTokens')::float8/1000000::float8)*(rates->>'inputUsdPerMTok')::float8
|
|
827
|
+
+((u->>'outputTokens')::float8/1000000::float8)*(rates->>'outputUsdPerMTok')::float8
|
|
828
|
+
+(coalesce((u->>'cacheReadInputTokens')::float8,0)/1000000::float8)*coalesce((rates->>'cacheReadUsdPerMTok')::float8,(rates->>'inputUsdPerMTok')::float8)
|
|
829
|
+
+(coalesce((u->>'cacheWriteInputTokens')::float8,0)/1000000::float8)*write_rate
|
|
830
|
+
+searches*coalesce((price->>'webSearchUsdPerRequest')::float8,0);
|
|
831
|
+
if amount>=0 and amount<'Infinity'::float8 then return amount;end if;return null;
|
|
832
|
+
exception when others then return null;
|
|
833
|
+
end $price$;
|
|
834
|
+
`;
|
|
835
|
+
|
|
836
|
+
// src/batch-usage-schema.ts
|
|
837
|
+
function batchUsageMigrationSql(role = DEFAULT_RLS_ROLE5) {
|
|
838
|
+
assertRoleIdentifier6(role);
|
|
839
|
+
return `${batchSubmissionMigrationSql(role)}${governedCostSettlementStoreMigrationSql2(role)}${batchPricingSql}
|
|
840
|
+
create or replace function alma_batch_usage_shape(v jsonb) returns boolean language plpgsql immutable as $shape$
|
|
841
|
+
declare e jsonb; u jsonb; field text; child jsonb;
|
|
842
|
+
begin
|
|
843
|
+
if (jsonb_typeof(v)='object' and v ?& array['id','itemId','handle','evidence','outcome']
|
|
844
|
+
and (v-array['id','itemId','handle','evidence','outcome','providerRequestId'])='{}'::jsonb
|
|
845
|
+
and v->>'outcome' in ('succeeded','errored','cancelled','expired','unusable')) is not true then return false;end if;
|
|
846
|
+
foreach field in array array['id','itemId','providerRequestId'] loop
|
|
847
|
+
if (v ? field) and (jsonb_typeof(v->field)='string' and v->>field ~ '^[!-~]{1,200}$') is not true then return false;end if;
|
|
848
|
+
end loop;
|
|
849
|
+
e=v->'evidence';u=e->'usage';
|
|
850
|
+
if alma_execution_shape_v2(e,'evidence') is not true then return false;end if;
|
|
851
|
+
if e->>'status'='unknown' then return (e ? 'reason' and (e-'status'-'reason')='{}'::jsonb and e->>'reason' in ('missing','invalid','interrupted')) is true;end if;
|
|
852
|
+
if (e->>'status' in ('known','unpriced') and u ?& array['inputTokens','outputTokens']) is not true then return false;end if;
|
|
853
|
+
if e->>'status'='known' and ((e-'status'-'usage')<>'{}'::jsonb or u->>'serviceTier' is distinct from 'batch') then return false;end if;
|
|
854
|
+
if e->>'status'='unpriced' then
|
|
855
|
+
if (e ? 'issues' and jsonb_array_length(e->'issues') between 1 and 2 and not(e ? 'reason')) is not true then return false;end if;
|
|
856
|
+
for child in select value from jsonb_array_elements(e->'issues') loop if child not in ('"service_tier"'::jsonb,'"cache_ttl"'::jsonb) then return false;end if;end loop;
|
|
857
|
+
if e ? 'reportedServiceTier' and (e->>'reportedServiceTier' ~ '^[!-~]{1,200}$') is not true then return false;end if;
|
|
858
|
+
end if;
|
|
859
|
+
if u ? 'serviceTier' and u->>'serviceTier' not in ('batch','standard','priority','flex') then return false;end if;
|
|
860
|
+
if u ? 'cacheWriteTtl' and u->>'cacheWriteTtl' not in ('5m','1h') then return false;end if;
|
|
861
|
+
foreach field in array array['inputTokens','outputTokens','cacheReadInputTokens','cacheWriteInputTokens','reasoningTokens','webSearchRequests'] loop
|
|
862
|
+
if u ? field and ((u->>field)::numeric between 0 and 9007199254740991 and trunc((u->>field)::numeric)=(u->>field)::numeric) is not true then return false;end if;
|
|
863
|
+
end loop;
|
|
864
|
+
if e ? 'cacheWriteTokensByTtl' then
|
|
865
|
+
if (e->'cacheWriteTokensByTtl' ?& array['5m','1h']) is not true then return false;end if;
|
|
866
|
+
for child in select value from jsonb_each(e->'cacheWriteTokensByTtl') loop if ((child#>>'{}')::numeric between 0 and 9007199254740991 and trunc((child#>>'{}')::numeric)=(child#>>'{}')::numeric) is not true then return false;end if;end loop;
|
|
867
|
+
end if;return true;
|
|
868
|
+
exception when others then return false;
|
|
869
|
+
end $shape$;
|
|
870
|
+
create table if not exists alma_batch_usage (
|
|
871
|
+
org text not null,uid text not null,batch_key text not null,id text collate "C" not null,item_id text not null,
|
|
872
|
+
input jsonb not null,received_at timestamptz not null,cost_id uuid,
|
|
873
|
+
primary key(org,uid,batch_key,id),
|
|
874
|
+
foreign key(org,uid,batch_key,item_id) references alma_batch_items(org,uid,batch_key,item_id),
|
|
875
|
+
foreign key(org,uid,cost_id) references alma_audit_cost(org,uid,id),
|
|
876
|
+
check((alma_batch_usage_shape(input) and input->>'id'=id and input->>'itemId'=item_id) is true)
|
|
877
|
+
);
|
|
878
|
+
create or replace function alma_batch_usage_validate() returns trigger language plpgsql as $validate$
|
|
879
|
+
declare batch alma_batch_submissions%rowtype; e jsonb; source record; field text;
|
|
880
|
+
begin
|
|
881
|
+
select * into batch from alma_batch_submissions where org=new.org and uid=new.uid and key=new.batch_key;
|
|
882
|
+
select batch.input->'items'->(ordinal-1)->'execution' into e from alma_batch_items where org=new.org and uid=new.uid and batch_key=new.batch_key and item_id=new.item_id;
|
|
883
|
+
if (batch.state in ('submitted','reconciliation_required') and batch.handle is not null and batch.handle=new.input->'handle' and e is not null) is not true then raise exception 'Invalid batch observation binding' using errcode='23514';end if;
|
|
884
|
+
if tg_op='INSERT' then
|
|
885
|
+
if new.cost_id is not null then raise exception 'Observe before adopting receipt' using errcode='23514';end if;
|
|
886
|
+
new.received_at=clock_timestamp();return new;
|
|
887
|
+
end if;
|
|
888
|
+
if (new.org,new.uid,new.batch_key,new.id,new.item_id,new.input,new.received_at) is distinct from (old.org,old.uid,old.batch_key,old.id,old.item_id,old.input,old.received_at)
|
|
889
|
+
or old.cost_id is not null or new.cost_id is null then raise exception 'Immutable batch observation' using errcode='23514';end if;
|
|
890
|
+
select c.settlement_payload payload,g.request into source from alma_audit_cost c join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id)
|
|
891
|
+
where c.org=new.org and c.uid=new.uid and c.id=new.cost_id and c.settlement_governed;
|
|
892
|
+
if (source.payload is not null and new.input->'evidence'->>'status'='known' and source.payload->>'id'=e->>'settlementId'
|
|
893
|
+
and source.payload->'scope'=e->'scope' and source.payload->'model'=e->'model' and source.payload->>'at'=e->>'occurredAt'
|
|
894
|
+
and (source.payload->>'costUsd')::float8=alma_batch_price(e,new.input->'evidence'->'usage')
|
|
895
|
+
and source.payload->>'serviceTier'='batch' and source.payload->'usage'=new.input->'evidence'->'usage'
|
|
896
|
+
and source.payload->'consumers'=e->'governance'->'consumers'
|
|
897
|
+
and source.request->>'policyVersion'=e->>'policyVersion' and source.request->'caps'=e->'governance'->'caps'
|
|
898
|
+
and not(source.payload ? 'parentCallId')) is not true then raise exception 'Invalid batch financial association' using errcode='23514';end if;
|
|
899
|
+
if source.payload->'providerRequestId' is distinct from new.input->'providerRequestId' then raise exception 'Invalid provider reference' using errcode='23514';end if;
|
|
900
|
+
foreach field in array array['sessionId','operationId','attemptId','callId','priceVersion'] loop
|
|
901
|
+
if (source.payload->>field=e->>field) is not true then raise exception 'Invalid financial identity' using errcode='23514';end if;
|
|
902
|
+
end loop;return new;
|
|
903
|
+
end $validate$;
|
|
904
|
+
do $trigger$ begin
|
|
905
|
+
if not exists(select from pg_trigger where tgrelid='alma_batch_usage'::regclass and tgname='alma_batch_usage_validate') then
|
|
906
|
+
create trigger alma_batch_usage_validate before insert or update on alma_batch_usage for each row execute function alma_batch_usage_validate();
|
|
907
|
+
end if;
|
|
908
|
+
end $trigger$;
|
|
909
|
+
${rlsPolicySql5("alma_batch_usage")}
|
|
910
|
+
grant select,insert,update on alma_batch_usage to ${role};
|
|
911
|
+
${functionPathSql6("alma_batch_usage_shape(jsonb)", "alma_batch_usage_validate()")}
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
914
|
+
async function migrateBatchUsageStore(pool, opts = {}) {
|
|
915
|
+
await pool.query(batchUsageMigrationSql(opts.role));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/batch-accounting.ts
|
|
919
|
+
import { ExecutionConflictError as ExecutionConflictError5, ExecutionStateError as ExecutionStateError5, settlementIdentifier as id5, settlementScope as settlementScope5 } from "@alma-harness/core";
|
|
920
|
+
import { bindBatchAccounting, batchFinancialReceipt, batchFinancialWarnings, normalizeBatchFinancialReceipt, operationCallQuery as operationCallQuery3 } from "@alma-harness/execution";
|
|
921
|
+
import { inScope as inScope5, resolveRlsRole as resolveRlsRole5, resolveStatementTimeout as resolveStatementTimeout5 } from "@alma-harness/postgres";
|
|
922
|
+
var PostgresBatchAccountingStore = class {
|
|
923
|
+
constructor(pool, opts = {}) {
|
|
924
|
+
this.pool = pool;
|
|
925
|
+
this.#role = resolveRlsRole5(opts);
|
|
926
|
+
this.#timeout = resolveStatementTimeout5(opts);
|
|
927
|
+
this.#batches = new PostgresBatchSubmissionStore(pool, opts);
|
|
928
|
+
this.#usage = new PostgresBatchUsageStore(pool, opts);
|
|
929
|
+
}
|
|
930
|
+
pool;
|
|
931
|
+
#role;
|
|
932
|
+
#timeout;
|
|
933
|
+
#batches;
|
|
934
|
+
#usage;
|
|
935
|
+
async #scope(s, fn) {
|
|
936
|
+
try {
|
|
937
|
+
return await inScope5(this.pool, this.#role, s, fn, this.#timeout);
|
|
938
|
+
} catch (e) {
|
|
939
|
+
if (["23505", "23514", "23503"].includes(e.code ?? "")) throw new ExecutionConflictError5();
|
|
940
|
+
throw e;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async record(scope, batchKey, observationId) {
|
|
944
|
+
const s = settlementScope5(scope), k = id5(batchKey), i = id5(observationId), batch = await this.#batches.get(s, k);
|
|
945
|
+
if (!batch) throw new ExecutionStateError5();
|
|
946
|
+
const value = await this.#usage.get(s, k, i);
|
|
947
|
+
if (!value) return { status: "pending" };
|
|
948
|
+
const source = bindBatchAccounting(batch, value);
|
|
949
|
+
if (!source.receipt) return { status: "pending" };
|
|
950
|
+
return this.#scope(s, async (c) => {
|
|
951
|
+
const args = [s.org, s.uid, k];
|
|
952
|
+
await c.query("select key from alma_batch_submissions where org=$1 and uid=$2 and key=$3 for update", args);
|
|
953
|
+
const old = (await c.query("select receipt from alma_batch_financial_items where org=$1 and uid=$2 and batch_key=$3 and item_id=$4", [...args, source.input.itemId])).rows[0];
|
|
954
|
+
if (old) return { status: "replayed", receipt: normalizeBatchFinancialReceipt(old.receipt) };
|
|
955
|
+
const previous = (await c.query("select receipt from alma_batch_financial_items where org=$1 and uid=$2 and batch_key=$3 order by ordinal desc limit 1", args)).rows[0];
|
|
956
|
+
const warned = (await c.query("select cap from alma_batch_warnings where org=$1 and uid=$2 and batch_key=$3", args)).rows.map((r) => r.cap);
|
|
957
|
+
const now = (await c.query("select clock_timestamp() at")).rows[0].at.toISOString();
|
|
958
|
+
const receipt3 = batchFinancialReceipt(batch, source, previous ? normalizeBatchFinancialReceipt(previous.receipt) : void 0, warned, now);
|
|
959
|
+
await c.query("insert into alma_batch_financial_items(org,uid,batch_key,item_id,observation_id,ordinal,receipt) values($1,$2,$3,$4,$5,$6,$7::jsonb)", [...args, receipt3.itemId, i, receipt3.accountingOrdinal, JSON.stringify(receipt3)]);
|
|
960
|
+
return { status: "applied", receipt: receipt3 };
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
async get(scope, batchKey) {
|
|
964
|
+
const s = settlementScope5(scope), k = id5(batchKey), batch = await this.#batches.get(s, k);
|
|
965
|
+
if (!batch) return null;
|
|
966
|
+
return this.#scope(s, async (c) => {
|
|
967
|
+
const args = [s.org, s.uid, k], state = (await c.query("select state from alma_batch_submissions where org=$1 and uid=$2 and key=$3 for share", args)).rows[0].state;
|
|
968
|
+
const last = (await c.query("select receipt from alma_batch_financial_items where org=$1 and uid=$2 and batch_key=$3 order by ordinal desc limit 1", args)).rows[0];
|
|
969
|
+
const rows = (await c.query("select f.receipt,w.cap from alma_batch_warnings w join alma_batch_financial_items f on (f.org,f.uid,f.batch_key,f.item_id)=(w.org,w.uid,w.batch_key,w.item_id) where w.org=$1 and w.uid=$2 and w.batch_key=$3 order by f.ordinal,w.cap", args)).rows;
|
|
970
|
+
const warnings = rows.map((r) => {
|
|
971
|
+
const w = batchFinancialWarnings(s, r.receipt).find((w2) => w2.cap === r.cap);
|
|
972
|
+
if (!w) throw new ExecutionConflictError5();
|
|
973
|
+
return w;
|
|
974
|
+
});
|
|
975
|
+
const receipt3 = last ? normalizeBatchFinancialReceipt(last.receipt) : void 0;
|
|
976
|
+
return { batchId: batch.input.id, costUsd: receipt3?.currentUsd ?? 0, expectedItems: batch.input.items.length, accountedItems: receipt3?.accountingOrdinal ?? 0, batchState: state, warnings };
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
async list(scope, batchKey, query) {
|
|
980
|
+
const s = settlementScope5(scope), k = id5(batchKey), q = operationCallQuery3(query);
|
|
981
|
+
return this.#scope(s, async (c) => (await c.query("select receipt from alma_batch_financial_items where org=$1 and uid=$2 and batch_key=$3 and ordinal>$4 order by ordinal limit $5", [s.org, s.uid, k, q.afterOrdinal, q.limit])).rows.map((r) => normalizeBatchFinancialReceipt(r.receipt)));
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/batch-accounting-schema.ts
|
|
986
|
+
import { functionPathSql as functionPathSql7 } from "@alma-harness/postgres";
|
|
987
|
+
import { assertRoleIdentifier as assertRoleIdentifier7, DEFAULT_RLS_ROLE as DEFAULT_RLS_ROLE6, rlsPolicySql as rlsPolicySql6 } from "@alma-harness/postgres";
|
|
988
|
+
function batchAccountingMigrationSql(role = DEFAULT_RLS_ROLE6) {
|
|
989
|
+
assertRoleIdentifier7(role);
|
|
990
|
+
return `${batchUsageMigrationSql(role)}
|
|
991
|
+
create table if not exists alma_batch_financial_items (
|
|
992
|
+
org text not null,uid text not null,batch_key text not null,item_id text not null,observation_id text not null,
|
|
993
|
+
ordinal int not null check(ordinal between 1 and 512),receipt jsonb not null,
|
|
994
|
+
primary key(org,uid,batch_key,item_id),unique(org,uid,batch_key,ordinal),
|
|
995
|
+
foreign key(org,uid,batch_key,item_id) references alma_batch_items(org,uid,batch_key,item_id),
|
|
996
|
+
foreign key(org,uid,batch_key,observation_id) references alma_batch_usage(org,uid,batch_key,id)
|
|
997
|
+
);
|
|
998
|
+
create table if not exists alma_batch_warnings (
|
|
999
|
+
org text not null,uid text not null,batch_key text not null,cap text not null check(cap in ('perTurnUsd','perSessionUsd','perTenantDayUsd')),item_id text not null,
|
|
1000
|
+
primary key(org,uid,batch_key,cap),foreign key(org,uid,batch_key,item_id) references alma_batch_financial_items(org,uid,batch_key,item_id)
|
|
1001
|
+
);
|
|
1002
|
+
create or replace function alma_batch_financial_validate() returns trigger language plpgsql as $financial$
|
|
1003
|
+
declare batch alma_batch_submissions%rowtype; member alma_batch_items%rowtype; prior alma_batch_financial_items%rowtype;
|
|
1004
|
+
source record; r jsonb; before_usd double precision; cost double precision; after_usd double precision; expected jsonb; warnings jsonb; field text;
|
|
1005
|
+
begin
|
|
1006
|
+
select * into batch from alma_batch_submissions where org=new.org and uid=new.uid and key=new.batch_key for update;
|
|
1007
|
+
select * into member from alma_batch_items where org=new.org and uid=new.uid and batch_key=new.batch_key and item_id=new.item_id;
|
|
1008
|
+
select * into prior from alma_batch_financial_items where org=new.org and uid=new.uid and batch_key=new.batch_key order by ordinal desc limit 1;
|
|
1009
|
+
select u.item_id,c.settlement_payload payload,g.decisions into source from alma_batch_usage u
|
|
1010
|
+
join alma_audit_cost c on (c.org,c.uid,c.id)=(u.org,u.uid,u.cost_id) join alma_governed_cost g on (g.org,g.uid,g.cost_id)=(c.org,c.uid,c.id)
|
|
1011
|
+
where u.org=new.org and u.uid=new.uid and u.batch_key=new.batch_key and u.id=new.observation_id;
|
|
1012
|
+
r=new.receipt;before_usd=coalesce((prior.receipt->>'currentUsd')::float8,0);cost=(source.payload->>'costUsd')::float8;after_usd=before_usd+cost;
|
|
1013
|
+
foreach field in array array['batchId','itemId','callId','settlementId','recordedAt'] loop
|
|
1014
|
+
if jsonb_typeof(r->field) is distinct from 'string' then raise exception 'Invalid financial string' using errcode='23514';end if;
|
|
1015
|
+
end loop;
|
|
1016
|
+
foreach field in array array['accountingOrdinal','reservationOrdinal','costUsd','previousUsd','currentUsd'] loop
|
|
1017
|
+
if jsonb_typeof(r->field) is distinct from 'number' then raise exception 'Invalid financial amount' using errcode='23514';end if;
|
|
1018
|
+
end loop;
|
|
1019
|
+
if (source.item_id=new.item_id and source.payload is not null and batch.id is not null
|
|
1020
|
+
and jsonb_typeof(r)='object' and r ?& array['batchId','itemId','callId','settlementId','accountingOrdinal','reservationOrdinal','costUsd','previousUsd','currentUsd','decisions','newWarnings','recordedAt']
|
|
1021
|
+
and (r-array['batchId','itemId','callId','settlementId','accountingOrdinal','reservationOrdinal','costUsd','previousUsd','currentUsd','decisions','newWarnings','recordedAt'])='{}'::jsonb
|
|
1022
|
+
and r->>'batchId'=batch.id and r->>'itemId'=new.item_id and r->>'callId'=member.call_id and r->>'settlementId'=member.settlement_id
|
|
1023
|
+
and new.ordinal=coalesce(prior.ordinal,0)+1 and (r->>'accountingOrdinal')::numeric=new.ordinal and (r->>'reservationOrdinal')::numeric=member.ordinal
|
|
1024
|
+
and (r->>'previousUsd')::float8=before_usd and (r->>'costUsd')::float8=cost and (r->>'currentUsd')::float8=after_usd and after_usd<'Infinity'::float8
|
|
1025
|
+
and to_char((r->>'recordedAt')::timestamptz at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')=r->>'recordedAt'
|
|
1026
|
+
and isfinite((r->>'recordedAt')::timestamptz) and r->>'recordedAt' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3}Z$') is not true then raise exception 'Invalid batch financial binding' using errcode='23514';end if;
|
|
1027
|
+
select coalesce(jsonb_agg(case when d->>'cap'='perTurnUsd' then d||jsonb_build_object('previousUsd',before_usd,'currentUsd',after_usd,'exceeded',after_usd>(d->>'thresholdUsd')::float8) else d end order by n),'[]'::jsonb) into expected from jsonb_array_elements(source.decisions) with ordinality as x(d,n);
|
|
1028
|
+
if r->'decisions' is distinct from expected then raise exception 'Invalid batch financial decisions' using errcode='23514';end if;
|
|
1029
|
+
select coalesce(jsonb_agg(d->>'cap' order by n),'[]'::jsonb) into warnings from jsonb_array_elements(expected) with ordinality as x(d,n)
|
|
1030
|
+
where d->>'onExceeded'='warn' and (d->>'exceeded')::boolean and not exists(select from alma_batch_warnings w where w.org=new.org and w.uid=new.uid and w.batch_key=new.batch_key and w.cap=d->>'cap');
|
|
1031
|
+
if r->'newWarnings' is distinct from warnings then raise exception 'Invalid batch warning receipt' using errcode='23514';end if;
|
|
1032
|
+
return new;
|
|
1033
|
+
end $financial$;
|
|
1034
|
+
create or replace function alma_batch_financial_warn() returns trigger language plpgsql as $warn$
|
|
1035
|
+
begin
|
|
1036
|
+
insert into alma_batch_warnings(org,uid,batch_key,cap,item_id) select new.org,new.uid,new.batch_key,value,new.item_id from jsonb_array_elements_text(new.receipt->'newWarnings');return new;
|
|
1037
|
+
end $warn$;
|
|
1038
|
+
create or replace function alma_batch_warning_validate() returns trigger language plpgsql as $warning$
|
|
1039
|
+
begin
|
|
1040
|
+
if not exists(select from alma_batch_financial_items where org=new.org and uid=new.uid and batch_key=new.batch_key and item_id=new.item_id and receipt->'newWarnings' ? new.cap) then raise exception 'Invalid batch warning association' using errcode='23514';end if;return new;
|
|
1041
|
+
end $warning$;
|
|
1042
|
+
do $triggers$ begin
|
|
1043
|
+
if not exists(select from pg_trigger where tgrelid='alma_batch_financial_items'::regclass and tgname='alma_batch_financial_validate') then
|
|
1044
|
+
create trigger alma_batch_financial_validate before insert on alma_batch_financial_items for each row execute function alma_batch_financial_validate();
|
|
1045
|
+
create trigger alma_batch_financial_warn after insert on alma_batch_financial_items for each row execute function alma_batch_financial_warn();
|
|
1046
|
+
end if;
|
|
1047
|
+
if not exists(select from pg_trigger where tgrelid='alma_batch_warnings'::regclass and tgname='alma_batch_warning_validate') then
|
|
1048
|
+
create trigger alma_batch_warning_validate before insert on alma_batch_warnings for each row execute function alma_batch_warning_validate();
|
|
1049
|
+
end if;
|
|
1050
|
+
end $triggers$;
|
|
1051
|
+
${rlsPolicySql6("alma_batch_financial_items")}${rlsPolicySql6("alma_batch_warnings")}
|
|
1052
|
+
grant select,insert on alma_batch_financial_items,alma_batch_warnings to ${role};
|
|
1053
|
+
${functionPathSql7("alma_batch_financial_validate()", "alma_batch_financial_warn()", "alma_batch_warning_validate()")}
|
|
1054
|
+
`;
|
|
1055
|
+
}
|
|
1056
|
+
async function migrateBatchAccountingStore(pool, opts = {}) {
|
|
1057
|
+
await pool.query(batchAccountingMigrationSql(opts.role));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/index.ts
|
|
1061
|
+
var at = async (c) => (await c.query("select clock_timestamp() at")).rows[0].at.toISOString();
|
|
1062
|
+
var PostgresOperationTreeStore = class {
|
|
1063
|
+
constructor(pool, opts = {}) {
|
|
1064
|
+
this.pool = pool;
|
|
1065
|
+
this.#role = resolveRlsRole6(opts);
|
|
1066
|
+
this.#timeout = resolveStatementTimeout6(opts);
|
|
1067
|
+
}
|
|
1068
|
+
pool;
|
|
1069
|
+
#role;
|
|
1070
|
+
#timeout;
|
|
1071
|
+
async #scope(scope, fn) {
|
|
1072
|
+
try {
|
|
1073
|
+
return await inScope6(this.pool, this.#role, scope, fn, this.#timeout);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
const code = e.code;
|
|
1076
|
+
if (code === "23505") throw new ExecutionConflictError6();
|
|
1077
|
+
if (code === "23503" || code === "23514") throw new ExecutionStateError6();
|
|
1078
|
+
throw e;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async #read(c, scope, key, lock = false) {
|
|
1082
|
+
return (await c.query(`select ${ROOT}${lock ? ",token" : ""} from alma_operation_roots where org=$1 and uid=$2 and key=$3${lock ? " for update" : ""}`, [scope.org, scope.uid, key])).rows[0];
|
|
1083
|
+
}
|
|
1084
|
+
async claim(value) {
|
|
1085
|
+
const i = normalizeOperationRoot3(value);
|
|
1086
|
+
return this.#scope(i.scope, async (c) => {
|
|
1087
|
+
let row = await this.#read(c, i.scope, i.key, true);
|
|
1088
|
+
if (!row) {
|
|
1089
|
+
checkOperationDeadline2(i, await at(c));
|
|
1090
|
+
row = (await c.query(`insert into alma_operation_roots(org,uid,key,id,session_id,policy_version,caps,max_sensitivity,deadline_at,max_calls,status,token,created_at,updated_at,request)
|
|
1091
|
+
values($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9::timestamptz,$10,'active',$11,clock_timestamp(),clock_timestamp(),$12::jsonb)
|
|
1092
|
+
on conflict(org,uid,key) do nothing returning ${ROOT},token`, [i.scope.org, i.scope.uid, i.key, i.id, i.sessionId, i.policyVersion, JSON.stringify(i.caps), i.maxSensitivity, i.deadlineAt, i.maxCalls, crypto.randomUUID(), i.request ? JSON.stringify(i.request) : null])).rows[0];
|
|
1093
|
+
if (row) {
|
|
1094
|
+
checkOperationDeadline2(i, await at(c));
|
|
1095
|
+
return { status: "claimed", record: record(row), fence: { scope: { ...i.scope }, key: i.key, id: i.id, token: row.token } };
|
|
1096
|
+
}
|
|
1097
|
+
row = await this.#read(c, i.scope, i.key, true);
|
|
1098
|
+
}
|
|
1099
|
+
if (!row || !equalExecution4(record(row).input, i)) throw new ExecutionConflictError6();
|
|
1100
|
+
return { status: "existing", record: record(row) };
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
async #authorize(c, f, closing = false) {
|
|
1104
|
+
const r = await this.#read(c, f.scope, f.key, true);
|
|
1105
|
+
if (!r || r.id !== f.id || r.token !== f.token) throw new ExecutionStateError6();
|
|
1106
|
+
if (closing && r.status === "closed") return r;
|
|
1107
|
+
if (r.status !== "active" || r.deadline_at.toISOString() <= await at(c)) throw new ExecutionStateError6();
|
|
1108
|
+
return r;
|
|
1109
|
+
}
|
|
1110
|
+
async reserve(value, request) {
|
|
1111
|
+
const f = normalizeOperationFence(value), input = normalizeOperationCall2(request);
|
|
1112
|
+
return this.#scope(f.scope, async (c) => {
|
|
1113
|
+
const r = await this.#authorize(c, f);
|
|
1114
|
+
checkOperationCall(record(r).input, input);
|
|
1115
|
+
const old = (await c.query("select * from alma_operation_calls where org=$1 and uid=$2 and root_id=$3 and slot=$4", [f.scope.org, f.scope.uid, f.id, input.slot])).rows[0];
|
|
1116
|
+
if (old) {
|
|
1117
|
+
if (!equalExecution4(call(old).input, input)) throw new ExecutionConflictError6();
|
|
1118
|
+
return call(old);
|
|
1119
|
+
}
|
|
1120
|
+
if (r.call_count >= r.max_calls) throw new ExecutionStateError6();
|
|
1121
|
+
const e = input.execution;
|
|
1122
|
+
const row = (await c.query(`insert into alma_operation_calls(org,uid,root_id,slot,ordinal,kind,parent_call_id,call_id,settlement_id,operation_key,input,reserved_at)
|
|
1123
|
+
values($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,clock_timestamp()) returning *`, [f.scope.org, f.scope.uid, f.id, input.slot, r.call_count + 1, input.kind, input.parentCallId ?? null, e.callId, e.settlementId, e.operationKey, JSON.stringify(e)])).rows[0];
|
|
1124
|
+
return call(row);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
async close(value) {
|
|
1128
|
+
const f = normalizeOperationFence(value);
|
|
1129
|
+
return this.#scope(f.scope, async (c) => {
|
|
1130
|
+
const row = await this.#authorize(c, f, true);
|
|
1131
|
+
if (row.status === "closed") return record(row);
|
|
1132
|
+
return record((await c.query(`update alma_operation_roots set status='closed',updated_at=clock_timestamp() where org=$1 and uid=$2 and key=$3 returning ${ROOT}`, [f.scope.org, f.scope.uid, f.key])).rows[0]);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
async get(scope, identity) {
|
|
1136
|
+
const s = settlementScope6(scope), k = id6(identity);
|
|
1137
|
+
return this.#scope(s, async (c) => {
|
|
1138
|
+
const row = await this.#read(c, s, k);
|
|
1139
|
+
return row ? record(row) : null;
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
async listCalls(scope, identity, query) {
|
|
1143
|
+
const s = settlementScope6(scope), k = id6(identity), q = operationCallQuery4(query);
|
|
1144
|
+
return this.#scope(s, async (c) => (await c.query(`select c.* from alma_operation_calls c join alma_operation_roots r on (r.org,r.uid,r.id)=(c.org,c.uid,c.root_id)
|
|
1145
|
+
where r.org=$1 and r.uid=$2 and r.key=$3 and c.ordinal>$4 order by c.ordinal limit $5`, [s.org, s.uid, k, q.afterOrdinal, q.limit])).rows.map(call));
|
|
1146
|
+
}
|
|
1147
|
+
async reconcileExpired(scope, opts = {}) {
|
|
1148
|
+
const s = settlementScope6(scope), limit = settlementLimit2(opts.limit);
|
|
1149
|
+
return this.#scope(s, async (c) => {
|
|
1150
|
+
const rows = (await c.query(`select ${ROOT} from alma_operation_roots where org=$1 and uid=$2 and status='active' and deadline_at<=clock_timestamp() order by deadline_at,key limit $3 for update skip locked`, [s.org, s.uid, limit])).rows;
|
|
1151
|
+
const results = [];
|
|
1152
|
+
for (const r of rows) results.push(record((await c.query(`update alma_operation_roots set status='reconciliation_required',updated_at=clock_timestamp() where org=$1 and uid=$2 and key=$3 returning ${ROOT}`, [s.org, s.uid, r.key])).rows[0]));
|
|
1153
|
+
return results;
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
export {
|
|
1158
|
+
PostgresBatchAccountingStore,
|
|
1159
|
+
PostgresBatchSubmissionStore,
|
|
1160
|
+
PostgresBatchUsageStore,
|
|
1161
|
+
PostgresOperationAccountingStore,
|
|
1162
|
+
PostgresOperationSessionStore,
|
|
1163
|
+
PostgresOperationTreeStore,
|
|
1164
|
+
batchAccountingMigrationSql,
|
|
1165
|
+
batchSubmissionMigrationSql,
|
|
1166
|
+
batchUsageMigrationSql,
|
|
1167
|
+
migrateBatchAccountingStore,
|
|
1168
|
+
migrateBatchSubmissionStore,
|
|
1169
|
+
migrateBatchUsageStore,
|
|
1170
|
+
migrateOperationAccountingStore,
|
|
1171
|
+
migrateOperationSessionStore,
|
|
1172
|
+
migrateOperationTreeStore,
|
|
1173
|
+
operationAccountingMigrationSql,
|
|
1174
|
+
operationSessionMigrationSql,
|
|
1175
|
+
operationTreeMigrationSql
|
|
1176
|
+
};
|
|
1177
|
+
//# sourceMappingURL=index.js.map
|