@palbase/backend 35.0.0 → 36.0.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/dist/bin/palbase-backend.cjs +556 -487
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +6 -6
- package/dist/{chunk-L4R7WXLH.js → chunk-7WAGQ3VR.js} +4 -171
- package/dist/chunk-7WAGQ3VR.js.map +1 -0
- package/dist/{chunk-UMESJJRS.js → chunk-CS6NQ6PO.js} +2 -1
- package/dist/chunk-CS6NQ6PO.js.map +1 -0
- package/dist/{chunk-BOD3DT6D.js → chunk-EHKEMHB4.js} +560 -430
- package/dist/chunk-EHKEMHB4.js.map +1 -0
- package/dist/{chunk-PRH45NZU.js → chunk-FFO36MQQ.js} +2 -45
- package/dist/chunk-FFO36MQQ.js.map +1 -0
- package/dist/{chunk-P7MGSAFT.js → chunk-LFUNRWUE.js} +1 -40
- package/dist/chunk-LFUNRWUE.js.map +1 -0
- package/dist/{chunk-RZLY4BOY.js → chunk-PDD55QWN.js} +2 -2
- package/dist/{chunk-YO5GYM73.js → chunk-X7UR3VXA.js} +5 -6
- package/dist/chunk-X7UR3VXA.js.map +1 -0
- package/dist/{chunk-6FEHCEB7.js → chunk-YANGWAIM.js} +3 -3
- package/dist/db/index.cjs +1 -232
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +3 -46
- package/dist/db/index.d.ts +3 -46
- package/dist/db/index.js +4 -12
- package/dist/engine/index.cjs +556 -487
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +6 -6
- package/dist/{index-DllNjpTl.d.cts → index-CI7S0Wqv.d.cts} +61 -8
- package/dist/{endpoint-BI0yGuEL.d.ts → index-CUy50OLU.d.ts} +6 -82
- package/dist/{endpoint-DaGnjt42.d.cts → index-DRx880KY.d.cts} +6 -82
- package/dist/{index-E8Sd6NFy.d.ts → index-Ogi30dbt.d.ts} +61 -8
- package/dist/index.cjs +5 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -7
- package/dist/index.d.ts +6 -7
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +1 -0
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -3
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +2 -2
- package/dist/{registry-BnKvIyve.d.cts → registry-C7UCkQf0.d.cts} +1 -1
- package/dist/{registry-zDCj0eOU.d.ts → registry-DJcvbomD.d.ts} +1 -1
- package/dist/test/index.cjs +3 -11
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +2 -2
- package/dist/test/index.d.ts +2 -2
- package/dist/test/index.js +2 -9
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/database.md +84 -67
- package/docs/examples/database-dx.ts +12 -17
- package/docs/llms-full.txt +82 -65
- package/package.json +1 -1
- package/stager/inferred_returns.js +153 -0
- package/stager/return_types.js +9 -4
- package/stager/stage.js +3 -1
- package/template/AGENTS.md +5 -5
- package/template/package.json +1 -1
- package/dist/chunk-BOD3DT6D.js.map +0 -1
- package/dist/chunk-L4R7WXLH.js.map +0 -1
- package/dist/chunk-P7MGSAFT.js.map +0 -1
- package/dist/chunk-PRH45NZU.js.map +0 -1
- package/dist/chunk-UMESJJRS.js.map +0 -1
- package/dist/chunk-YO5GYM73.js.map +0 -1
- package/docs/examples/check-query-contracts.ts +0 -5
- package/docs/examples/database-dx.contract.json +0 -65
- /package/dist/{chunk-RZLY4BOY.js.map → chunk-PDD55QWN.js.map} +0 -0
- /package/dist/{chunk-6FEHCEB7.js.map → chunk-YANGWAIM.js.map} +0 -0
|
@@ -663,25 +663,6 @@ function translateRejection(err, builder) {
|
|
|
663
663
|
}
|
|
664
664
|
__name(translateRejection, "translateRejection");
|
|
665
665
|
|
|
666
|
-
// src/db/workload.ts
|
|
667
|
-
function validateWorkload(value) {
|
|
668
|
-
if (typeof value.name !== "string" || !/^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value.name)) throw new Error("workload name must be 1\u201364 letters/digits/dots/dashes");
|
|
669
|
-
for (const name of [
|
|
670
|
-
"concurrency",
|
|
671
|
-
"tenantConcurrency",
|
|
672
|
-
"queueLimit",
|
|
673
|
-
"tenantQueueLimit",
|
|
674
|
-
"queueTimeoutMs"
|
|
675
|
-
]) {
|
|
676
|
-
const n = value[name], min = name.endsWith("Limit") ? 0 : 1;
|
|
677
|
-
const max = name === "queueTimeoutMs" ? 6e4 : name.endsWith("Limit") ? 1e4 : 1e3;
|
|
678
|
-
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`workload ${name} must be an integer between ${min} and ${max}`);
|
|
679
|
-
}
|
|
680
|
-
if (value.tenantConcurrency > value.concurrency || value.tenantQueueLimit > value.queueLimit) throw new Error("workload tenant limits cannot exceed global limits");
|
|
681
|
-
if (typeof value.tenantClaim !== "string" || !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(value.tenantClaim)) throw new Error("workload tenantClaim must be a top-level claim name");
|
|
682
|
-
}
|
|
683
|
-
__name(validateWorkload, "validateWorkload");
|
|
684
|
-
|
|
685
666
|
// src/db/transaction-options.ts
|
|
686
667
|
function retryBudget(value) {
|
|
687
668
|
const n = value ?? 0;
|
|
@@ -713,10 +694,6 @@ function validateAtomicOptions(options) {
|
|
|
713
694
|
throw new Error(`transaction ${name} must be a positive integer in milliseconds`);
|
|
714
695
|
}
|
|
715
696
|
}
|
|
716
|
-
if (options.workload !== void 0) validateWorkload(options.workload);
|
|
717
|
-
if (options.conflictKeys !== void 0 && (!options.workload || !Array.isArray(options.conflictKeys) || options.conflictKeys.length > 64 || options.conflictKeys.some((k) => typeof k !== "string" || k.length < 1 || k.length > 256))) {
|
|
718
|
-
throw new Error("conflictKeys requires a workload and at most 64 nonempty keys of at most 256 characters");
|
|
719
|
-
}
|
|
720
697
|
if (options.signal !== void 0 && (typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function")) {
|
|
721
698
|
throw new Error("transaction signal must be an AbortSignal");
|
|
722
699
|
}
|
|
@@ -876,7 +853,9 @@ function makeTableProxy(ops, prefix) {
|
|
|
876
853
|
// Older than this run, but the run rewrote this proxy for
|
|
877
854
|
// `Database.schema(name).tables.*` and would have carried the gap onto
|
|
878
855
|
// the new surface too.
|
|
879
|
-
updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set
|
|
856
|
+
updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set, q.returning === void 0 ? void 0 : {
|
|
857
|
+
returning: q.returning
|
|
858
|
+
}), "updateMany"),
|
|
880
859
|
deleteMany: /* @__PURE__ */ __name((q) => ops().deleteMany(name, q.where), "deleteMany"),
|
|
881
860
|
count: /* @__PURE__ */ __name((q) => ops().count(name, q?.where), "count"),
|
|
882
861
|
search: /* @__PURE__ */ __name((params) => ops().search(name, params), "search"),
|
|
@@ -896,9 +875,6 @@ function makeTypedSurface(raw) {
|
|
|
896
875
|
const ops = {
|
|
897
876
|
$query: /* @__PURE__ */ __name((sql, params) => raw.query(sql, params), "$query"),
|
|
898
877
|
$diagnostics: /* @__PURE__ */ __name(() => raw.diagnostics(), "$diagnostics"),
|
|
899
|
-
$readQuery: /* @__PURE__ */ __name((query) => raw.readQuery(query), "$readQuery"),
|
|
900
|
-
$compileQuery: /* @__PURE__ */ __name((query) => raw.compileQuery(query), "$compileQuery"),
|
|
901
|
-
$explainQuery: /* @__PURE__ */ __name((query) => raw.explainQuery(query), "$explainQuery"),
|
|
902
878
|
$insert: /* @__PURE__ */ __name((table, data) => raw.insert(table, data), "$insert"),
|
|
903
879
|
$update: /* @__PURE__ */ __name((table, id, data) => raw.update(table, id, data), "$update"),
|
|
904
880
|
$delete: /* @__PURE__ */ __name((table, id) => raw.delete(table, id), "$delete"),
|
|
@@ -1103,6 +1079,7 @@ var Realtime = makeServiceProxy("Realtime");
|
|
|
1103
1079
|
var SQLSTATE = Object.freeze({
|
|
1104
1080
|
serializationFailure: "40001",
|
|
1105
1081
|
deadlockDetected: "40P01",
|
|
1082
|
+
queryCancelled: "57014",
|
|
1106
1083
|
commandGuardFailed: "PBC01"
|
|
1107
1084
|
});
|
|
1108
1085
|
function isTransactionRollbackState(state) {
|
|
@@ -1305,6 +1282,16 @@ ${detail}`);
|
|
|
1305
1282
|
if (!Number.isInteger(poolMax) || poolMax < 1) {
|
|
1306
1283
|
throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
|
|
1307
1284
|
}
|
|
1285
|
+
const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
|
|
1286
|
+
if (env[key2] === void 0) return void 0;
|
|
1287
|
+
const value = Number(env[key2]);
|
|
1288
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
|
|
1289
|
+
return value;
|
|
1290
|
+
}, "capacityNumber");
|
|
1291
|
+
const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
|
|
1292
|
+
const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
|
|
1293
|
+
const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
|
|
1294
|
+
if (tenantClaim !== void 0 && !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(tenantClaim)) throw new BootRefused([], "boot refused: PALBASE_DB_TENANT_CLAIM must be a top-level trusted claim name.");
|
|
1308
1295
|
return {
|
|
1309
1296
|
databaseUrl: env.DATABASE_URL.trim(),
|
|
1310
1297
|
authJwksUrl: env.AUTH_JWKS_URL.trim(),
|
|
@@ -1329,6 +1316,17 @@ ${detail}`);
|
|
|
1329
1316
|
// and the live database agrees (pg_roles.rolbypassrls = true).
|
|
1330
1317
|
dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
|
|
1331
1318
|
poolMax,
|
|
1319
|
+
databaseAdmission: {
|
|
1320
|
+
...queueLimit === void 0 ? {} : {
|
|
1321
|
+
queueLimit
|
|
1322
|
+
},
|
|
1323
|
+
...queueTimeoutMs === void 0 ? {} : {
|
|
1324
|
+
queueTimeoutMs
|
|
1325
|
+
},
|
|
1326
|
+
...tenantClaim === void 0 ? {} : {
|
|
1327
|
+
tenantClaim
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1332
1330
|
...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
|
|
1333
1331
|
databaseDiagnostics: {
|
|
1334
1332
|
onComplete: /* @__PURE__ */ __name((diagnostics) => {
|
|
@@ -1848,46 +1846,6 @@ function validateInsertManyOptions(opts) {
|
|
|
1848
1846
|
}
|
|
1849
1847
|
__name(validateInsertManyOptions, "validateInsertManyOptions");
|
|
1850
1848
|
|
|
1851
|
-
// src/db/query-contract.ts
|
|
1852
|
-
function queryColumnContract(column) {
|
|
1853
|
-
const def = column && typeof column === "object" && "_def" in column ? column._def : column;
|
|
1854
|
-
if (!def?.type) throw new Error("Query contract requires declared column types");
|
|
1855
|
-
if (def.transform !== void 0 && def.codec === void 0) throw new Error("Query contracts require a named codec instead of a free-form column transform");
|
|
1856
|
-
return {
|
|
1857
|
-
type: def.type,
|
|
1858
|
-
nullable: def.nullable === true,
|
|
1859
|
-
...def.codec === void 0 ? {} : {
|
|
1860
|
-
codec: def.codec
|
|
1861
|
-
},
|
|
1862
|
-
...def.enumValues === void 0 ? {} : {
|
|
1863
|
-
enumValues: [
|
|
1864
|
-
...def.enumValues
|
|
1865
|
-
]
|
|
1866
|
-
},
|
|
1867
|
-
...def.dimensions === void 0 ? {} : {
|
|
1868
|
-
dimensions: def.dimensions
|
|
1869
|
-
}
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
__name(queryColumnContract, "queryColumnContract");
|
|
1873
|
-
function stableContract(value) {
|
|
1874
|
-
if (Array.isArray(value)) return `[${value.map(stableContract).join(",")}]`;
|
|
1875
|
-
if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key2) => `${JSON.stringify(key2)}:${stableContract(value[key2])}`).join(",")}}`;
|
|
1876
|
-
return JSON.stringify(value) ?? "null";
|
|
1877
|
-
}
|
|
1878
|
-
__name(stableContract, "stableContract");
|
|
1879
|
-
function assertReadContract(query, columns) {
|
|
1880
|
-
if (!query.options.select.length || new Set(query.options.select).size !== query.options.select.length) {
|
|
1881
|
-
throw new Error(`Query ${query.name}: select must contain distinct, explicit columns`);
|
|
1882
|
-
}
|
|
1883
|
-
for (const name of query.options.select) {
|
|
1884
|
-
if (!columns || !Object.hasOwn(columns, name) || !Object.hasOwn(query.columns, name) || stableContract(queryColumnContract(columns[name])) !== stableContract(query.columns[name])) {
|
|
1885
|
-
throw new Error(`Query ${query.name}: schema drift at ${query.table}.${name}; regenerate and review its response contract`);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
__name(assertReadContract, "assertReadContract");
|
|
1890
|
-
|
|
1891
1849
|
// src/db/unique.ts
|
|
1892
1850
|
function uniqueKeysOf(table) {
|
|
1893
1851
|
const columns = table.columns ?? {};
|
|
@@ -2061,142 +2019,167 @@ function parameterChunks(sql) {
|
|
|
2061
2019
|
__name(parameterChunks, "parameterChunks");
|
|
2062
2020
|
|
|
2063
2021
|
// src/engine/admission.ts
|
|
2064
|
-
var
|
|
2022
|
+
var DatabaseAdmission = class {
|
|
2065
2023
|
static {
|
|
2066
|
-
__name(this, "
|
|
2024
|
+
__name(this, "DatabaseAdmission");
|
|
2067
2025
|
}
|
|
2068
|
-
|
|
2069
|
-
pending = [];
|
|
2026
|
+
options;
|
|
2070
2027
|
active = 0;
|
|
2071
|
-
|
|
2028
|
+
queued = 0;
|
|
2072
2029
|
tenants = /* @__PURE__ */ new Map();
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2030
|
+
// Map insertion order is the round-robin ring; each Set is FIFO. Grant and
|
|
2031
|
+
// cancellation remove nodes directly, without scanning a 10,000-item queue.
|
|
2032
|
+
pending = /* @__PURE__ */ new Map();
|
|
2033
|
+
lastTenant;
|
|
2034
|
+
accepted = 0;
|
|
2035
|
+
rejected = {
|
|
2036
|
+
full: 0,
|
|
2037
|
+
timeout: 0,
|
|
2038
|
+
cancelled: 0
|
|
2039
|
+
};
|
|
2040
|
+
limit;
|
|
2041
|
+
timeout;
|
|
2042
|
+
constructor(options) {
|
|
2043
|
+
this.options = options;
|
|
2044
|
+
this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
|
|
2045
|
+
this.timeout = options.queueTimeoutMs ?? 1e3;
|
|
2046
|
+
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1 || !Number.isSafeInteger(this.limit) || this.limit < 0 || this.limit > 1e4 || !Number.isSafeInteger(this.timeout) || this.timeout < 1 || this.timeout > 6e4) {
|
|
2047
|
+
throw new Error("Invalid database pool or queue deployment settings");
|
|
2048
|
+
}
|
|
2077
2049
|
}
|
|
2078
2050
|
get snapshot() {
|
|
2079
2051
|
return {
|
|
2080
2052
|
active: this.active,
|
|
2081
|
-
queued: this.
|
|
2053
|
+
queued: this.queued,
|
|
2082
2054
|
tenants: this.tenants.size,
|
|
2083
|
-
|
|
2055
|
+
accepted: this.accepted,
|
|
2056
|
+
rejected: {
|
|
2057
|
+
...this.rejected
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
rejection(reason2) {
|
|
2062
|
+
this.rejected[reason2]++;
|
|
2063
|
+
try {
|
|
2064
|
+
this.options.onReject?.({
|
|
2065
|
+
reason: reason2,
|
|
2066
|
+
active: this.active,
|
|
2067
|
+
queued: this.queued
|
|
2068
|
+
});
|
|
2069
|
+
} catch {
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
take(tenant, entered) {
|
|
2073
|
+
this.active++;
|
|
2074
|
+
this.accepted++;
|
|
2075
|
+
this.lastTenant = tenant;
|
|
2076
|
+
this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
|
|
2077
|
+
let released = false;
|
|
2078
|
+
return {
|
|
2079
|
+
queueMs: performance.now() - entered,
|
|
2080
|
+
release: /* @__PURE__ */ __name(() => {
|
|
2081
|
+
if (released) return;
|
|
2082
|
+
released = true;
|
|
2083
|
+
this.active--;
|
|
2084
|
+
const count = this.tenants.get(tenant) - 1;
|
|
2085
|
+
if (count) this.tenants.set(tenant, count);
|
|
2086
|
+
else this.tenants.delete(tenant);
|
|
2087
|
+
this.drain();
|
|
2088
|
+
}, "release")
|
|
2084
2089
|
};
|
|
2085
2090
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2091
|
+
drain() {
|
|
2092
|
+
while (this.active < this.options.concurrency && this.queued) {
|
|
2093
|
+
let tenant = this.pending.keys().next().value;
|
|
2094
|
+
if (this.pending.size > 1 && tenant === this.lastTenant) {
|
|
2095
|
+
const queue2 = this.pending.get(tenant);
|
|
2096
|
+
this.pending.delete(tenant);
|
|
2097
|
+
this.pending.set(tenant, queue2);
|
|
2098
|
+
tenant = this.pending.keys().next().value;
|
|
2099
|
+
}
|
|
2100
|
+
const queue = this.pending.get(tenant), waiter = queue.values().next().value;
|
|
2101
|
+
queue.delete(waiter);
|
|
2102
|
+
this.pending.delete(tenant);
|
|
2103
|
+
if (queue.size) this.pending.set(tenant, queue);
|
|
2104
|
+
this.queued--;
|
|
2105
|
+
waiter.cleanup();
|
|
2106
|
+
waiter.grant(this.take(tenant, waiter.entered));
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async acquire(claimsJson, signal) {
|
|
2110
|
+
if (signal?.aborted) {
|
|
2111
|
+
this.rejection("cancelled");
|
|
2112
|
+
throw signal.reason ?? new Error("Database admission cancelled");
|
|
2113
|
+
}
|
|
2114
|
+
const claims = JSON.parse(claimsJson);
|
|
2115
|
+
const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
|
|
2116
|
+
const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
|
|
2117
|
+
const entered = performance.now();
|
|
2118
|
+
if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
|
|
2119
|
+
if (this.queued >= this.limit) {
|
|
2120
|
+
this.rejection("full");
|
|
2121
|
+
throw new TooManyRequests({
|
|
2098
2122
|
retryAfter: 1
|
|
2099
|
-
},
|
|
2123
|
+
}, "Database queue is full");
|
|
2100
2124
|
}
|
|
2101
2125
|
return new Promise((grant, reject) => {
|
|
2126
|
+
const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
|
|
2127
|
+
if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
|
|
2102
2128
|
const waiter = {
|
|
2103
|
-
tenant,
|
|
2104
|
-
keys: scoped,
|
|
2105
|
-
entered: performance.now(),
|
|
2106
2129
|
grant,
|
|
2107
2130
|
reject,
|
|
2108
|
-
|
|
2109
|
-
|
|
2131
|
+
entered,
|
|
2132
|
+
cleanup() {
|
|
2133
|
+
}
|
|
2110
2134
|
};
|
|
2111
|
-
const remove = /* @__PURE__ */ __name((reason2) => {
|
|
2112
|
-
|
|
2113
|
-
if (
|
|
2114
|
-
this.
|
|
2135
|
+
const remove = /* @__PURE__ */ __name((reason2, error) => {
|
|
2136
|
+
if (!queue.delete(waiter)) return;
|
|
2137
|
+
if (!queue.size) this.pending.delete(tenant);
|
|
2138
|
+
this.queued--;
|
|
2115
2139
|
waiter.cleanup();
|
|
2116
|
-
|
|
2140
|
+
this.rejection(reason2);
|
|
2141
|
+
reject(error);
|
|
2117
2142
|
this.drain();
|
|
2118
2143
|
}, "remove");
|
|
2119
|
-
const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
2120
|
-
const timer = setTimeout(() => remove(new TooManyRequests({
|
|
2144
|
+
const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
2145
|
+
const timer = setTimeout(() => remove("timeout", new TooManyRequests({
|
|
2121
2146
|
retryAfter: 1
|
|
2122
|
-
},
|
|
2147
|
+
}, "Database queue deadline exceeded")), this.timeout);
|
|
2123
2148
|
waiter.cleanup = () => {
|
|
2124
2149
|
clearTimeout(timer);
|
|
2125
2150
|
signal?.removeEventListener("abort", abort);
|
|
2126
2151
|
};
|
|
2152
|
+
queue.add(waiter);
|
|
2153
|
+
this.queued++;
|
|
2127
2154
|
signal?.addEventListener("abort", abort, {
|
|
2128
2155
|
once: true
|
|
2129
2156
|
});
|
|
2130
|
-
this.pending.push(waiter);
|
|
2131
2157
|
if (signal?.aborted) abort();
|
|
2132
2158
|
else this.drain();
|
|
2133
2159
|
});
|
|
2134
2160
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
let released = false;
|
|
2153
|
-
waiter.grant({
|
|
2154
|
-
queueMs: performance.now() - waiter.entered,
|
|
2155
|
-
release: /* @__PURE__ */ __name(() => {
|
|
2156
|
-
if (released) return;
|
|
2157
|
-
released = true;
|
|
2158
|
-
this.active--;
|
|
2159
|
-
const left = this.tenants.get(waiter.tenant) - 1;
|
|
2160
|
-
if (left === 0) this.tenants.delete(waiter.tenant);
|
|
2161
|
-
else this.tenants.set(waiter.tenant, left);
|
|
2162
|
-
for (const key2 of waiter.keys) this.activeKeys.delete(key2);
|
|
2163
|
-
this.drain();
|
|
2164
|
-
}, "release")
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
} finally {
|
|
2168
|
-
this.draining = false;
|
|
2161
|
+
};
|
|
2162
|
+
var databases = /* @__PURE__ */ new WeakMap();
|
|
2163
|
+
function databaseAdmission(owner, options) {
|
|
2164
|
+
let admission = databases.get(owner);
|
|
2165
|
+
if (!admission) {
|
|
2166
|
+
admission = new DatabaseAdmission(options ?? {
|
|
2167
|
+
concurrency: 10
|
|
2168
|
+
});
|
|
2169
|
+
databases.set(owner, admission);
|
|
2170
|
+
} else if (options) {
|
|
2171
|
+
for (const key2 of [
|
|
2172
|
+
"concurrency",
|
|
2173
|
+
"queueLimit",
|
|
2174
|
+
"queueTimeoutMs",
|
|
2175
|
+
"tenantClaim"
|
|
2176
|
+
]) {
|
|
2177
|
+
if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
|
|
2169
2178
|
}
|
|
2170
2179
|
}
|
|
2171
|
-
};
|
|
2172
|
-
var registries = /* @__PURE__ */ new WeakMap();
|
|
2173
|
-
function workloadAdmission(owner, definition) {
|
|
2174
|
-
validateWorkload(definition);
|
|
2175
|
-
let registry2 = registries.get(owner);
|
|
2176
|
-
if (!registry2) {
|
|
2177
|
-
registry2 = /* @__PURE__ */ new Map();
|
|
2178
|
-
registries.set(owner, registry2);
|
|
2179
|
-
}
|
|
2180
|
-
const existing = registry2.get(definition.name);
|
|
2181
|
-
if (existing) {
|
|
2182
|
-
const d = existing.definition;
|
|
2183
|
-
if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
|
|
2184
|
-
return existing;
|
|
2185
|
-
}
|
|
2186
|
-
if (registry2.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
|
|
2187
|
-
const admission = new WorkloadAdmission(Object.freeze({
|
|
2188
|
-
...definition
|
|
2189
|
-
}));
|
|
2190
|
-
registry2.set(definition.name, admission);
|
|
2191
2180
|
return admission;
|
|
2192
2181
|
}
|
|
2193
|
-
__name(
|
|
2194
|
-
function workloadTenant(definition, claimsJson) {
|
|
2195
|
-
const claims = JSON.parse(claimsJson);
|
|
2196
|
-
const value = claims[definition.tenantClaim] ?? claims.sub;
|
|
2197
|
-
return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
|
|
2198
|
-
}
|
|
2199
|
-
__name(workloadTenant, "workloadTenant");
|
|
2182
|
+
__name(databaseAdmission, "databaseAdmission");
|
|
2200
2183
|
|
|
2201
2184
|
// src/engine/transaction-control.ts
|
|
2202
2185
|
var TransactionControl = class {
|
|
@@ -2319,6 +2302,7 @@ var DatabaseTrace = class {
|
|
|
2319
2302
|
sqlMs: 0,
|
|
2320
2303
|
commitMs: 0,
|
|
2321
2304
|
rollbackMs: 0,
|
|
2305
|
+
connectionMs: 0,
|
|
2322
2306
|
commits: 0,
|
|
2323
2307
|
commitFailures: 0,
|
|
2324
2308
|
rollbackRequests: 0,
|
|
@@ -2330,7 +2314,7 @@ var DatabaseTrace = class {
|
|
|
2330
2314
|
if (this.failure) throw this.failure;
|
|
2331
2315
|
}
|
|
2332
2316
|
time(key2, started) {
|
|
2333
|
-
this.state[key2]
|
|
2317
|
+
this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
|
|
2334
2318
|
}
|
|
2335
2319
|
retry() {
|
|
2336
2320
|
this.state.retries++;
|
|
@@ -2654,31 +2638,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
2654
2638
|
fail = rej;
|
|
2655
2639
|
});
|
|
2656
2640
|
void parked.catch(() => void 0);
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
validateAtomicOptions({
|
|
2665
|
-
isolation,
|
|
2666
|
-
readOnly: options.readOnly
|
|
2667
|
-
});
|
|
2668
|
-
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
2641
|
+
settled = (async () => {
|
|
2642
|
+
const queuedAt = performance.now();
|
|
2643
|
+
let lease;
|
|
2644
|
+
try {
|
|
2645
|
+
lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
|
|
2646
|
+
} finally {
|
|
2647
|
+
options.trace?.time("queueMs", queuedAt);
|
|
2669
2648
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
options.control
|
|
2674
|
-
|
|
2649
|
+
const openingAt = performance.now();
|
|
2650
|
+
let connectedAt;
|
|
2651
|
+
try {
|
|
2652
|
+
options.control?.check();
|
|
2653
|
+
if (closed) throw new Error("Database transaction closed while waiting for admission");
|
|
2654
|
+
return await sql.begin(async (tx) => {
|
|
2655
|
+
connectedAt = performance.now();
|
|
2656
|
+
options.trace?.time("poolMs", openingAt);
|
|
2657
|
+
const setupAt = options.trace ? performance.now() : 0;
|
|
2658
|
+
options.control?.check();
|
|
2659
|
+
if (closed) throw new Error("Database transaction closed while waiting for its connection");
|
|
2660
|
+
if (options.isolation !== void 0 || options.readOnly !== void 0) {
|
|
2661
|
+
const isolation = options.isolation ?? "read committed";
|
|
2662
|
+
validateAtomicOptions({
|
|
2663
|
+
isolation,
|
|
2664
|
+
readOnly: options.readOnly
|
|
2665
|
+
});
|
|
2666
|
+
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
2667
|
+
}
|
|
2668
|
+
if (options.control && sql.cancelBackend) {
|
|
2669
|
+
const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
|
|
2670
|
+
SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
|
|
2671
|
+
) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
|
|
2672
|
+
if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
|
|
2673
|
+
options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
|
|
2674
|
+
options.control.check();
|
|
2675
|
+
} else {
|
|
2676
|
+
const binding = tx.unsafe(bindSql, bindParams);
|
|
2677
|
+
await (options.control ? options.control.watch(binding) : binding);
|
|
2678
|
+
}
|
|
2679
|
+
options.trace?.time("setupMs", setupAt);
|
|
2680
|
+
resolveTx2(scoped(tx));
|
|
2681
|
+
await parked;
|
|
2682
|
+
});
|
|
2683
|
+
} finally {
|
|
2684
|
+
if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
|
|
2685
|
+
lease.release();
|
|
2675
2686
|
}
|
|
2676
|
-
|
|
2677
|
-
await (options.control ? options.control.watch(binding) : binding);
|
|
2678
|
-
options.trace?.time("setupMs", setupAt);
|
|
2679
|
-
resolveTx2(scoped(tx));
|
|
2680
|
-
await parked;
|
|
2681
|
-
}).catch((e) => {
|
|
2687
|
+
})().catch((e) => {
|
|
2682
2688
|
rejectTx(e);
|
|
2683
2689
|
throw e;
|
|
2684
2690
|
});
|
|
@@ -2695,10 +2701,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
2695
2701
|
if (pendingSavepoints.size) await Promise.allSettled([
|
|
2696
2702
|
...pendingSavepoints
|
|
2697
2703
|
]);
|
|
2698
|
-
options.control?.check();
|
|
2699
2704
|
options.trace?.check();
|
|
2705
|
+
if (!opening) {
|
|
2706
|
+
closed = true;
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
options.control?.check();
|
|
2700
2710
|
closed = true;
|
|
2701
|
-
if (!opening) return;
|
|
2702
2711
|
const started = options.trace ? performance.now() : 0;
|
|
2703
2712
|
release();
|
|
2704
2713
|
try {
|
|
@@ -2825,10 +2834,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
|
|
|
2825
2834
|
const nested = typeof spec === "object" ? spec.with : void 0;
|
|
2826
2835
|
for (const row of result) {
|
|
2827
2836
|
const value = row[rel.name];
|
|
2828
|
-
|
|
2837
|
+
const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
|
|
2838
|
+
if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
|
|
2829
2839
|
else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
|
|
2830
2840
|
value
|
|
2831
|
-
], nested)[0];
|
|
2841
|
+
], nested, selected)[0];
|
|
2832
2842
|
}
|
|
2833
2843
|
}
|
|
2834
2844
|
for (const row of result) foldCan(table, row, partial);
|
|
@@ -3772,6 +3782,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
|
|
|
3772
3782
|
const i = ++counter.value;
|
|
3773
3783
|
const childAlias = `w${i}`;
|
|
3774
3784
|
const childKnown = schemaColumns(rel.to);
|
|
3785
|
+
if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
|
|
3786
|
+
throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
|
|
3787
|
+
}
|
|
3775
3788
|
const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
|
|
3776
3789
|
...childKnown
|
|
3777
3790
|
] : [];
|
|
@@ -3875,7 +3888,10 @@ function pageSchema(table) {
|
|
|
3875
3888
|
}
|
|
3876
3889
|
__name(pageSchema, "pageSchema");
|
|
3877
3890
|
function createOps(tx, context = {}) {
|
|
3878
|
-
const at = /* @__PURE__ */ __name(() =>
|
|
3891
|
+
const at = /* @__PURE__ */ __name(() => {
|
|
3892
|
+
const target = typeof tx === "function" ? tx() : tx;
|
|
3893
|
+
return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
|
|
3894
|
+
}, "at");
|
|
3879
3895
|
const ops = {
|
|
3880
3896
|
diagnostics() {
|
|
3881
3897
|
return context.trace?.snapshot ?? null;
|
|
@@ -4326,22 +4342,6 @@ function createOps(tx, context = {}) {
|
|
|
4326
4342
|
]);
|
|
4327
4343
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
4328
4344
|
},
|
|
4329
|
-
compileQuery(query) {
|
|
4330
|
-
assertReadContract(query, tableDefOf(query.table)?.columns);
|
|
4331
|
-
return compileFindMany(query.table, query.where, query.options);
|
|
4332
|
-
},
|
|
4333
|
-
async readQuery(query) {
|
|
4334
|
-
const compiled = ops.compileQuery(query);
|
|
4335
|
-
return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
|
|
4336
|
-
name: query.name
|
|
4337
|
-
}), void 0, true);
|
|
4338
|
-
},
|
|
4339
|
-
async explainQuery(query) {
|
|
4340
|
-
const compiled = ops.compileQuery(query);
|
|
4341
|
-
return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
|
|
4342
|
-
name: `${query.name}.explain`
|
|
4343
|
-
});
|
|
4344
|
-
},
|
|
4345
4345
|
async findMany(table, query = {}, opts = {}) {
|
|
4346
4346
|
const compiled = compileFindMany(table, query, opts);
|
|
4347
4347
|
return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
|
|
@@ -4876,7 +4876,7 @@ function createOps(tx, context = {}) {
|
|
|
4876
4876
|
if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
|
|
4877
4877
|
return ops.txPlan(plan);
|
|
4878
4878
|
}
|
|
4879
|
-
if (compiled.steps.length === 0) return {
|
|
4879
|
+
if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
|
|
4880
4880
|
results: []
|
|
4881
4881
|
};
|
|
4882
4882
|
if (compiled.pipeline) {
|
|
@@ -5149,6 +5149,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5149
5149
|
const state = sqlstateOf(e);
|
|
5150
5150
|
if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
|
|
5151
5151
|
if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
|
|
5152
|
+
if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
|
|
5152
5153
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
5153
5154
|
return new Error(`Database.$asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`);
|
|
5154
5155
|
}
|
|
@@ -5175,6 +5176,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5175
5176
|
}
|
|
5176
5177
|
}), "wrapTx");
|
|
5177
5178
|
return {
|
|
5179
|
+
admission: sql.admission ?? databaseAdmission(sql),
|
|
5178
5180
|
unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
|
|
5179
5181
|
cancelBackend: sql.cancelBackend?.bind(sql),
|
|
5180
5182
|
begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
|
|
@@ -5183,9 +5185,12 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5183
5185
|
};
|
|
5184
5186
|
}
|
|
5185
5187
|
__name(diagnosingDriver, "diagnosingDriver");
|
|
5186
|
-
function createRequestDatabase(sql, identity, diagnostics) {
|
|
5188
|
+
function createRequestDatabase(sql, identity, diagnostics, signal) {
|
|
5187
5189
|
const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
|
|
5190
|
+
const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
5188
5191
|
let requestClosed = false;
|
|
5192
|
+
let authorized = false, applicationTouched = false;
|
|
5193
|
+
let authorizationRelease;
|
|
5189
5194
|
const contextFor = /* @__PURE__ */ __name((role) => {
|
|
5190
5195
|
let scope;
|
|
5191
5196
|
return {
|
|
@@ -5204,19 +5209,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5204
5209
|
validateAtomicOptions(options);
|
|
5205
5210
|
if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
|
|
5206
5211
|
if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
|
|
5212
|
+
if (authorized && !applicationTouched && tx.opened) {
|
|
5213
|
+
authorizationRelease ??= (async () => {
|
|
5214
|
+
await tx.commit();
|
|
5215
|
+
tx = requestTransaction();
|
|
5216
|
+
})();
|
|
5217
|
+
}
|
|
5218
|
+
if (authorizationRelease) await authorizationRelease;
|
|
5207
5219
|
const retries = retryBudget(options.retry);
|
|
5208
|
-
const
|
|
5209
|
-
|
|
5220
|
+
const atomicSignal = options.signal && signal ? AbortSignal.any([
|
|
5221
|
+
options.signal,
|
|
5222
|
+
signal
|
|
5223
|
+
]) : options.signal ?? signal;
|
|
5224
|
+
const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
|
|
5210
5225
|
try {
|
|
5211
5226
|
control?.check();
|
|
5212
|
-
if (options.workload) {
|
|
5213
|
-
const queuedAt = trace ? performance.now() : 0;
|
|
5214
|
-
try {
|
|
5215
|
-
lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
|
|
5216
|
-
} finally {
|
|
5217
|
-
trace?.time("queueMs", queuedAt);
|
|
5218
|
-
}
|
|
5219
|
-
}
|
|
5220
5227
|
for (let attempt = 0; ; attempt++) {
|
|
5221
5228
|
control?.check();
|
|
5222
5229
|
const timeouts = [
|
|
@@ -5249,19 +5256,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5249
5256
|
}
|
|
5250
5257
|
} finally {
|
|
5251
5258
|
control?.dispose();
|
|
5252
|
-
lease?.release();
|
|
5253
5259
|
}
|
|
5254
5260
|
}, "atomic");
|
|
5255
|
-
const
|
|
5256
|
-
trace
|
|
5257
|
-
|
|
5261
|
+
const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
|
|
5262
|
+
trace,
|
|
5263
|
+
control: requestControl
|
|
5264
|
+
}), "requestTransaction");
|
|
5265
|
+
let tx = requestTransaction();
|
|
5258
5266
|
let serviceTx = null;
|
|
5267
|
+
let serviceControl;
|
|
5259
5268
|
let serviceClient = null;
|
|
5260
5269
|
const asService = /* @__PURE__ */ __name(() => {
|
|
5261
5270
|
if (serviceClient === null) {
|
|
5271
|
+
serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
5262
5272
|
serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
|
|
5263
5273
|
lockTimeout: SERVICE_LOCK_TIMEOUT,
|
|
5264
|
-
trace
|
|
5274
|
+
trace,
|
|
5275
|
+
control: serviceControl
|
|
5265
5276
|
});
|
|
5266
5277
|
serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
|
|
5267
5278
|
atomic: atomic(identity.serviceRole, "service")
|
|
@@ -5270,16 +5281,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5270
5281
|
return serviceClient;
|
|
5271
5282
|
}, "asService");
|
|
5272
5283
|
return {
|
|
5273
|
-
client: Object.assign(withTables(createOps(
|
|
5284
|
+
client: Object.assign(withTables(createOps(() => {
|
|
5285
|
+
if (authorized) applicationTouched = true;
|
|
5286
|
+
return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
|
|
5287
|
+
}, userContext)), {
|
|
5274
5288
|
asService,
|
|
5275
5289
|
atomic: atomic(identity.role, "user")
|
|
5276
5290
|
}),
|
|
5291
|
+
authorizationComplete() {
|
|
5292
|
+
authorized = true;
|
|
5293
|
+
},
|
|
5277
5294
|
async commit() {
|
|
5278
5295
|
requestClosed = true;
|
|
5279
5296
|
try {
|
|
5280
5297
|
await tx.commit();
|
|
5281
5298
|
await serviceTx?.commit();
|
|
5282
5299
|
trace?.complete("committed");
|
|
5300
|
+
requestControl?.dispose();
|
|
5301
|
+
serviceControl?.dispose();
|
|
5283
5302
|
} catch (error) {
|
|
5284
5303
|
trace?.failed();
|
|
5285
5304
|
throw error;
|
|
@@ -5289,6 +5308,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5289
5308
|
requestClosed = true;
|
|
5290
5309
|
await tx.rollback(reason2);
|
|
5291
5310
|
await serviceTx?.rollback(reason2);
|
|
5311
|
+
requestControl?.dispose();
|
|
5312
|
+
serviceControl?.dispose();
|
|
5292
5313
|
trace?.complete("failed");
|
|
5293
5314
|
}
|
|
5294
5315
|
};
|
|
@@ -5596,7 +5617,7 @@ function compileCommand(plan) {
|
|
|
5596
5617
|
};
|
|
5597
5618
|
});
|
|
5598
5619
|
steps.push({
|
|
5599
|
-
|
|
5620
|
+
sql,
|
|
5600
5621
|
bindings,
|
|
5601
5622
|
guard: op.guard
|
|
5602
5623
|
});
|
|
@@ -5619,11 +5640,15 @@ function compileCommand(plan) {
|
|
|
5619
5640
|
rows_affected: 1
|
|
5620
5641
|
});
|
|
5621
5642
|
}
|
|
5643
|
+
if (!hasRefs) return {
|
|
5644
|
+
steps: [],
|
|
5645
|
+
pipeline
|
|
5646
|
+
};
|
|
5622
5647
|
return {
|
|
5623
|
-
steps,
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
}
|
|
5648
|
+
steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
|
|
5649
|
+
...step,
|
|
5650
|
+
chunks: parameterChunks(sql)
|
|
5651
|
+
})
|
|
5627
5652
|
};
|
|
5628
5653
|
}
|
|
5629
5654
|
__name(compileCommand, "compileCommand");
|
|
@@ -6100,9 +6125,11 @@ function makeSseWriter(opts) {
|
|
|
6100
6125
|
if (!begun) {
|
|
6101
6126
|
begun = true;
|
|
6102
6127
|
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
6128
|
+
void chain.catch(() => void 0);
|
|
6103
6129
|
return;
|
|
6104
6130
|
}
|
|
6105
6131
|
chain = chain.then(() => opts.enqueue(frame));
|
|
6132
|
+
void chain.catch(() => void 0);
|
|
6106
6133
|
},
|
|
6107
6134
|
started() {
|
|
6108
6135
|
return begun;
|
|
@@ -6629,6 +6656,23 @@ async function createApp(opts) {
|
|
|
6629
6656
|
throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
6630
6657
|
}
|
|
6631
6658
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
6659
|
+
const admission = sql.admission ?? databaseAdmission(sql, {
|
|
6660
|
+
concurrency: config.poolMax,
|
|
6661
|
+
...config.databaseAdmission
|
|
6662
|
+
});
|
|
6663
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
6664
|
+
let shuttingDown;
|
|
6665
|
+
let accepting = true;
|
|
6666
|
+
async function serving(run) {
|
|
6667
|
+
const work = run();
|
|
6668
|
+
inFlight.add(work);
|
|
6669
|
+
try {
|
|
6670
|
+
return await work;
|
|
6671
|
+
} finally {
|
|
6672
|
+
inFlight.delete(work);
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
__name(serving, "serving");
|
|
6632
6676
|
await sql.unsafe("select 1");
|
|
6633
6677
|
setSchema(opts.schemas ?? []);
|
|
6634
6678
|
setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
|
|
@@ -6695,6 +6739,19 @@ async function createApp(opts) {
|
|
|
6695
6739
|
}
|
|
6696
6740
|
}
|
|
6697
6741
|
__name(runInServiceScope, "runInServiceScope");
|
|
6742
|
+
function errorResponse(err, endpoint, requestId) {
|
|
6743
|
+
if (isHttpError(err)) {
|
|
6744
|
+
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
6745
|
+
log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
|
|
6746
|
+
}
|
|
6747
|
+
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
6748
|
+
data: err.data
|
|
6749
|
+
} : void 0);
|
|
6750
|
+
}
|
|
6751
|
+
log.error(`[engine] unhandled error in ${endpoint}`, err);
|
|
6752
|
+
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
6753
|
+
}
|
|
6754
|
+
__name(errorResponse, "errorResponse");
|
|
6698
6755
|
async function handleAuthorize(req, requestId) {
|
|
6699
6756
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
6700
6757
|
return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
|
|
@@ -6717,7 +6774,7 @@ async function createApp(opts) {
|
|
|
6717
6774
|
...config.databaseDiagnostics,
|
|
6718
6775
|
endpoint: "upload authorization",
|
|
6719
6776
|
requestId
|
|
6720
|
-
});
|
|
6777
|
+
}, req.signal);
|
|
6721
6778
|
try {
|
|
6722
6779
|
const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
|
|
6723
6780
|
if (refusal) {
|
|
@@ -6727,7 +6784,7 @@ async function createApp(opts) {
|
|
|
6727
6784
|
await uploadDb.commit();
|
|
6728
6785
|
} catch (err) {
|
|
6729
6786
|
await uploadDb.rollback(err);
|
|
6730
|
-
|
|
6787
|
+
return errorResponse(err, "upload authorization", requestId);
|
|
6731
6788
|
}
|
|
6732
6789
|
const grant = grantFor(target?.entry, {
|
|
6733
6790
|
userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
|
|
@@ -6769,256 +6826,257 @@ async function createApp(opts) {
|
|
|
6769
6826
|
...meta.options.databaseBudget,
|
|
6770
6827
|
endpoint: hit.entry.id,
|
|
6771
6828
|
requestId
|
|
6772
|
-
});
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6829
|
+
}, req.signal);
|
|
6830
|
+
try {
|
|
6831
|
+
const rolesOfCaller = makeRoleResolver(db.client);
|
|
6832
|
+
let dispatched = false;
|
|
6833
|
+
const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
|
|
6834
|
+
if (dispatched) return;
|
|
6835
|
+
await db.rollback(new Error("request refused before dispatch"));
|
|
6836
|
+
}, "settleBeforeRefusal");
|
|
6837
|
+
const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
|
|
6838
|
+
if (refusal) {
|
|
6839
|
+
await settleBeforeRefusal();
|
|
6840
|
+
return refusal;
|
|
6841
|
+
}
|
|
6842
|
+
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
6843
|
+
await settleBeforeRefusal();
|
|
6844
|
+
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
6845
|
+
}
|
|
6846
|
+
let parsedBody;
|
|
6847
|
+
let bodyRead = false;
|
|
6848
|
+
let rawBody = null;
|
|
6849
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
6850
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
6851
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
6852
|
+
await settleBeforeRefusal();
|
|
6853
|
+
return new Response(JSON.stringify({
|
|
6854
|
+
error: "payload_too_large",
|
|
6855
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
6856
|
+
status: 413,
|
|
6857
|
+
request_id: requestId
|
|
6858
|
+
}), {
|
|
6859
|
+
status: 413,
|
|
6860
|
+
headers: JSON_HEADERS
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
6864
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
6865
|
+
await settleBeforeRefusal();
|
|
6866
|
+
return new Response(JSON.stringify({
|
|
6867
|
+
error: "payload_too_large",
|
|
6868
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
6869
|
+
status: 413,
|
|
6870
|
+
request_id: requestId
|
|
6871
|
+
}), {
|
|
6872
|
+
status: 413,
|
|
6873
|
+
headers: JSON_HEADERS
|
|
6874
|
+
});
|
|
6875
|
+
}
|
|
6876
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
6877
|
+
}
|
|
6878
|
+
const readParsedBody = /* @__PURE__ */ __name(() => {
|
|
6879
|
+
if (bodyRead) return parsedBody;
|
|
6880
|
+
bodyRead = true;
|
|
6881
|
+
if (!rawBody) {
|
|
6882
|
+
parsedBody = {};
|
|
6883
|
+
return parsedBody;
|
|
6884
|
+
}
|
|
6885
|
+
try {
|
|
6886
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
6887
|
+
} catch {
|
|
6888
|
+
parsedBody = {};
|
|
6889
|
+
}
|
|
6890
|
+
return parsedBody;
|
|
6891
|
+
}, "readParsedBody");
|
|
6892
|
+
const attest = await attestGate(req, rawBody, {
|
|
6893
|
+
userId
|
|
6894
|
+
});
|
|
6895
|
+
if (attest.observation) observations.record(attest.observation);
|
|
6896
|
+
if (attest.outcome === "reject") {
|
|
6897
|
+
const headers = {
|
|
6898
|
+
...JSON_HEADERS
|
|
6899
|
+
};
|
|
6900
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
6794
6901
|
await settleBeforeRefusal();
|
|
6795
6902
|
return new Response(JSON.stringify({
|
|
6796
|
-
error:
|
|
6797
|
-
error_description:
|
|
6798
|
-
status:
|
|
6903
|
+
error: attest.reason,
|
|
6904
|
+
error_description: "This endpoint requires a verified app instance",
|
|
6905
|
+
status: 401,
|
|
6799
6906
|
request_id: requestId
|
|
6800
6907
|
}), {
|
|
6801
|
-
status:
|
|
6802
|
-
headers
|
|
6908
|
+
status: 401,
|
|
6909
|
+
headers
|
|
6803
6910
|
});
|
|
6804
6911
|
}
|
|
6805
|
-
|
|
6806
|
-
|
|
6912
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
6913
|
+
attestChallengeOut = attest.nextChallenge;
|
|
6914
|
+
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
6915
|
+
if (retryAfter !== null) {
|
|
6807
6916
|
await settleBeforeRefusal();
|
|
6808
6917
|
return new Response(JSON.stringify({
|
|
6809
|
-
error: "
|
|
6810
|
-
error_description:
|
|
6811
|
-
status:
|
|
6812
|
-
request_id: requestId
|
|
6918
|
+
error: "too_many_requests",
|
|
6919
|
+
error_description: "Rate limit exceeded for this endpoint",
|
|
6920
|
+
status: 429,
|
|
6921
|
+
request_id: requestId,
|
|
6922
|
+
data: {
|
|
6923
|
+
retryAfter
|
|
6924
|
+
}
|
|
6813
6925
|
}), {
|
|
6814
|
-
status:
|
|
6815
|
-
headers:
|
|
6926
|
+
status: 429,
|
|
6927
|
+
headers: {
|
|
6928
|
+
...JSON_HEADERS,
|
|
6929
|
+
"retry-after": String(retryAfter)
|
|
6930
|
+
}
|
|
6816
6931
|
});
|
|
6817
6932
|
}
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
}
|
|
6827
|
-
try {
|
|
6828
|
-
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
6829
|
-
} catch {
|
|
6830
|
-
parsedBody = {};
|
|
6831
|
-
}
|
|
6832
|
-
return parsedBody;
|
|
6833
|
-
}, "readParsedBody");
|
|
6834
|
-
const attest = await attestGate(req, rawBody, {
|
|
6835
|
-
userId
|
|
6836
|
-
});
|
|
6837
|
-
if (attest.observation) observations.record(attest.observation);
|
|
6838
|
-
if (attest.outcome === "reject") {
|
|
6839
|
-
const headers = {
|
|
6840
|
-
...JSON_HEADERS
|
|
6841
|
-
};
|
|
6842
|
-
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
6843
|
-
await settleBeforeRefusal();
|
|
6844
|
-
return new Response(JSON.stringify({
|
|
6845
|
-
error: attest.reason,
|
|
6846
|
-
error_description: "This endpoint requires a verified app instance",
|
|
6847
|
-
status: 401,
|
|
6848
|
-
request_id: requestId
|
|
6849
|
-
}), {
|
|
6850
|
-
status: 401,
|
|
6851
|
-
headers
|
|
6933
|
+
let completionUploadId = null;
|
|
6934
|
+
const args = [];
|
|
6935
|
+
let sseEnqueue = null;
|
|
6936
|
+
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
6937
|
+
}, "sseSettle");
|
|
6938
|
+
const sseWriter = makeSseWriter({
|
|
6939
|
+
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
6940
|
+
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
6852
6941
|
});
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
}), {
|
|
6868
|
-
status: 429,
|
|
6869
|
-
headers: {
|
|
6870
|
-
...JSON_HEADERS,
|
|
6871
|
-
"retry-after": String(retryAfter)
|
|
6872
|
-
}
|
|
6873
|
-
});
|
|
6874
|
-
}
|
|
6875
|
-
let completionUploadId = null;
|
|
6876
|
-
const args = [];
|
|
6877
|
-
let sseEnqueue = null;
|
|
6878
|
-
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
6879
|
-
}, "sseSettle");
|
|
6880
|
-
const sseWriter = makeSseWriter({
|
|
6881
|
-
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
6882
|
-
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
6883
|
-
});
|
|
6884
|
-
for (const p of meta.params ?? []) {
|
|
6885
|
-
switch (p.kind) {
|
|
6886
|
-
case "body": {
|
|
6887
|
-
readParsedBody();
|
|
6888
|
-
const r = p.schema.safeParse(parsedBody);
|
|
6889
|
-
if (!r.success) {
|
|
6890
|
-
await settleBeforeRefusal();
|
|
6891
|
-
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
6892
|
-
data: {
|
|
6893
|
-
fields: fieldErrors(r.error)
|
|
6894
|
-
}
|
|
6895
|
-
});
|
|
6896
|
-
}
|
|
6897
|
-
args[p.index] = r.data;
|
|
6898
|
-
break;
|
|
6899
|
-
}
|
|
6900
|
-
case "query": {
|
|
6901
|
-
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
6902
|
-
if (!r.success) {
|
|
6903
|
-
await settleBeforeRefusal();
|
|
6904
|
-
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
6905
|
-
data: {
|
|
6906
|
-
fields: fieldErrors(r.error)
|
|
6907
|
-
}
|
|
6908
|
-
});
|
|
6909
|
-
}
|
|
6910
|
-
args[p.index] = r.data;
|
|
6911
|
-
break;
|
|
6912
|
-
}
|
|
6913
|
-
case "param":
|
|
6914
|
-
args[p.index] = hit.params[p.name];
|
|
6915
|
-
break;
|
|
6916
|
-
case "headers": {
|
|
6917
|
-
const raw = Object.fromEntries(req.headers);
|
|
6918
|
-
if (!p.schema) {
|
|
6919
|
-
args[p.index] = raw;
|
|
6942
|
+
for (const p of meta.params ?? []) {
|
|
6943
|
+
switch (p.kind) {
|
|
6944
|
+
case "body": {
|
|
6945
|
+
readParsedBody();
|
|
6946
|
+
const r = p.schema.safeParse(parsedBody);
|
|
6947
|
+
if (!r.success) {
|
|
6948
|
+
await settleBeforeRefusal();
|
|
6949
|
+
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
6950
|
+
data: {
|
|
6951
|
+
fields: fieldErrors(r.error)
|
|
6952
|
+
}
|
|
6953
|
+
});
|
|
6954
|
+
}
|
|
6955
|
+
args[p.index] = r.data;
|
|
6920
6956
|
break;
|
|
6921
6957
|
}
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
case "user":
|
|
6935
|
-
case "optionalUser":
|
|
6936
|
-
args[p.index] = claims ? {
|
|
6937
|
-
id: userId,
|
|
6938
|
-
email: claims.email,
|
|
6939
|
-
role: claims.role,
|
|
6940
|
-
emailVerified: claims.email_verified === true,
|
|
6941
|
-
metadata: claims.metadata ?? {},
|
|
6942
|
-
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
6943
|
-
// same resolution the gate used, so the handler cannot read a
|
|
6944
|
-
// different set than the one it was admitted on. Never from a
|
|
6945
|
-
// claim: a revoked role has to bite on the next request, and a
|
|
6946
|
-
// token says only what was true when it was minted.
|
|
6947
|
-
roles: await rolesOfCaller(),
|
|
6948
|
-
// Server-owned and only ever filled from a verdict palauth
|
|
6949
|
-
// returned. Never from a raw header, never from JWT metadata —
|
|
6950
|
-
// the type has said so since it was written and this is the
|
|
6951
|
-
// first writer.
|
|
6952
|
-
device: attestedDevice
|
|
6953
|
-
} : null;
|
|
6954
|
-
break;
|
|
6955
|
-
case "uploadedObject": {
|
|
6956
|
-
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
6957
|
-
await settleBeforeRefusal();
|
|
6958
|
-
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
6959
|
-
}
|
|
6960
|
-
readParsedBody();
|
|
6961
|
-
const envelopeIn = parsedBody;
|
|
6962
|
-
if (!envelopeIn?.uploadedObject) {
|
|
6963
|
-
await settleBeforeRefusal();
|
|
6964
|
-
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
6958
|
+
case "query": {
|
|
6959
|
+
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
6960
|
+
if (!r.success) {
|
|
6961
|
+
await settleBeforeRefusal();
|
|
6962
|
+
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
6963
|
+
data: {
|
|
6964
|
+
fields: fieldErrors(r.error)
|
|
6965
|
+
}
|
|
6966
|
+
});
|
|
6967
|
+
}
|
|
6968
|
+
args[p.index] = r.data;
|
|
6969
|
+
break;
|
|
6965
6970
|
}
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6971
|
+
case "param":
|
|
6972
|
+
args[p.index] = hit.params[p.name];
|
|
6973
|
+
break;
|
|
6974
|
+
case "headers": {
|
|
6975
|
+
const raw = Object.fromEntries(req.headers);
|
|
6976
|
+
if (!p.schema) {
|
|
6977
|
+
args[p.index] = raw;
|
|
6978
|
+
break;
|
|
6979
|
+
}
|
|
6980
|
+
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
6981
|
+
if (!r.success) {
|
|
6970
6982
|
await settleBeforeRefusal();
|
|
6971
|
-
return
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
} : void 0
|
|
6983
|
+
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
6984
|
+
data: {
|
|
6985
|
+
fields: fieldErrors(r.error)
|
|
6986
|
+
}
|
|
6976
6987
|
});
|
|
6977
6988
|
}
|
|
6978
|
-
|
|
6989
|
+
args[p.index] = r.data;
|
|
6990
|
+
break;
|
|
6979
6991
|
}
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6992
|
+
case "user":
|
|
6993
|
+
case "optionalUser":
|
|
6994
|
+
args[p.index] = claims ? {
|
|
6995
|
+
id: userId,
|
|
6996
|
+
email: claims.email,
|
|
6997
|
+
role: claims.role,
|
|
6998
|
+
emailVerified: claims.email_verified === true,
|
|
6999
|
+
metadata: claims.metadata ?? {},
|
|
7000
|
+
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
7001
|
+
// same resolution the gate used, so the handler cannot read a
|
|
7002
|
+
// different set than the one it was admitted on. Never from a
|
|
7003
|
+
// claim: a revoked role has to bite on the next request, and a
|
|
7004
|
+
// token says only what was true when it was minted.
|
|
7005
|
+
roles: await rolesOfCaller(),
|
|
7006
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
7007
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
7008
|
+
// the type has said so since it was written and this is the
|
|
7009
|
+
// first writer.
|
|
7010
|
+
device: attestedDevice
|
|
7011
|
+
} : null;
|
|
7012
|
+
break;
|
|
7013
|
+
case "uploadedObject": {
|
|
7014
|
+
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
7015
|
+
await settleBeforeRefusal();
|
|
7016
|
+
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
7017
|
+
}
|
|
7018
|
+
readParsedBody();
|
|
7019
|
+
const envelopeIn = parsedBody;
|
|
7020
|
+
if (!envelopeIn?.uploadedObject) {
|
|
7021
|
+
await settleBeforeRefusal();
|
|
7022
|
+
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
7023
|
+
}
|
|
7024
|
+
const uploadId = envelopeIn.uploadedObject.uploadId;
|
|
7025
|
+
if (typeof uploadId === "string" && uploadId !== "") {
|
|
7026
|
+
const already = completions.recall(uploadId);
|
|
7027
|
+
if (already) {
|
|
7028
|
+
await settleBeforeRefusal();
|
|
7029
|
+
return new Response(already.body, {
|
|
7030
|
+
status: already.status,
|
|
7031
|
+
headers: already.contentType ? {
|
|
7032
|
+
"content-type": already.contentType
|
|
7033
|
+
} : void 0
|
|
7034
|
+
});
|
|
7035
|
+
}
|
|
7036
|
+
completionUploadId = uploadId;
|
|
7037
|
+
}
|
|
7038
|
+
args[p.index] = envelopeIn.uploadedObject;
|
|
7039
|
+
parsedBody = envelopeIn.body ?? {};
|
|
7040
|
+
break;
|
|
7041
|
+
}
|
|
7042
|
+
case "requestId":
|
|
7043
|
+
args[p.index] = requestId;
|
|
7044
|
+
break;
|
|
7045
|
+
case "traceId":
|
|
7046
|
+
args[p.index] = requestId;
|
|
7047
|
+
break;
|
|
7048
|
+
case "client":
|
|
7049
|
+
args[p.index] = {
|
|
7050
|
+
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
7051
|
+
appVersion: req.headers.get("x-palbase-client-version"),
|
|
7052
|
+
platform: req.headers.get("x-platform"),
|
|
7053
|
+
osVersion: req.headers.get("x-os-version")
|
|
7054
|
+
};
|
|
7055
|
+
break;
|
|
7056
|
+
case "req":
|
|
7057
|
+
args[p.index] = req;
|
|
7058
|
+
break;
|
|
7059
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
7060
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
7061
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
7062
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
7063
|
+
// request-scoped data and no signal.
|
|
7064
|
+
case "signal":
|
|
7065
|
+
args[p.index] = req.signal;
|
|
7066
|
+
break;
|
|
7067
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
7068
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
7069
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
7070
|
+
// route.
|
|
7071
|
+
case "sseOut":
|
|
7072
|
+
args[p.index] = sseWriter;
|
|
7073
|
+
break;
|
|
7074
|
+
default:
|
|
7075
|
+
args[p.index] = void 0;
|
|
6983
7076
|
}
|
|
6984
|
-
case "requestId":
|
|
6985
|
-
args[p.index] = requestId;
|
|
6986
|
-
break;
|
|
6987
|
-
case "traceId":
|
|
6988
|
-
args[p.index] = requestId;
|
|
6989
|
-
break;
|
|
6990
|
-
case "client":
|
|
6991
|
-
args[p.index] = {
|
|
6992
|
-
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
6993
|
-
appVersion: req.headers.get("x-palbase-client-version"),
|
|
6994
|
-
platform: req.headers.get("x-platform"),
|
|
6995
|
-
osVersion: req.headers.get("x-os-version")
|
|
6996
|
-
};
|
|
6997
|
-
break;
|
|
6998
|
-
case "req":
|
|
6999
|
-
args[p.index] = req;
|
|
7000
|
-
break;
|
|
7001
|
-
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
7002
|
-
// state when the client disconnects. Measured on Bun: an infinite
|
|
7003
|
-
// producer guarded by it stops within a few frames of the client dying.
|
|
7004
|
-
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
7005
|
-
// request-scoped data and no signal.
|
|
7006
|
-
case "signal":
|
|
7007
|
-
args[p.index] = req.signal;
|
|
7008
|
-
break;
|
|
7009
|
-
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
7010
|
-
// on any other route it is an inert writer whose frames go nowhere,
|
|
7011
|
-
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
7012
|
-
// route.
|
|
7013
|
-
case "sseOut":
|
|
7014
|
-
args[p.index] = sseWriter;
|
|
7015
|
-
break;
|
|
7016
|
-
default:
|
|
7017
|
-
args[p.index] = void 0;
|
|
7018
7077
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
try {
|
|
7078
|
+
db.authorizationComplete?.();
|
|
7079
|
+
dispatched = true;
|
|
7022
7080
|
const services = buildServices(db);
|
|
7023
7081
|
if (meta.options?.sseConfig !== void 0) {
|
|
7024
7082
|
let sseController;
|
|
@@ -7034,11 +7092,19 @@ async function createApp(opts) {
|
|
|
7034
7092
|
} catch {
|
|
7035
7093
|
}
|
|
7036
7094
|
}, "sseEnqueue");
|
|
7037
|
-
let sawFirstFrame;
|
|
7038
|
-
const firstFrame = new Promise((
|
|
7095
|
+
let sawFirstFrame, refuseFirstFrame;
|
|
7096
|
+
const firstFrame = new Promise((resolve2, reject) => {
|
|
7097
|
+
sawFirstFrame = resolve2;
|
|
7098
|
+
refuseFirstFrame = reject;
|
|
7099
|
+
});
|
|
7039
7100
|
sseSettle = /* @__PURE__ */ __name(async () => {
|
|
7040
|
-
|
|
7041
|
-
|
|
7101
|
+
try {
|
|
7102
|
+
await db.commit();
|
|
7103
|
+
sawFirstFrame();
|
|
7104
|
+
} catch (error) {
|
|
7105
|
+
refuseFirstFrame(error);
|
|
7106
|
+
throw error;
|
|
7107
|
+
}
|
|
7042
7108
|
}, "sseSettle");
|
|
7043
7109
|
const guarded = {
|
|
7044
7110
|
...services,
|
|
@@ -7079,6 +7145,7 @@ async function createApp(opts) {
|
|
|
7079
7145
|
status: 204
|
|
7080
7146
|
});
|
|
7081
7147
|
}
|
|
7148
|
+
await firstFrame;
|
|
7082
7149
|
void (async () => {
|
|
7083
7150
|
await ran;
|
|
7084
7151
|
await sseWriter.drained();
|
|
@@ -7112,15 +7179,16 @@ async function createApp(opts) {
|
|
|
7112
7179
|
}
|
|
7113
7180
|
return method.apply(hit.entry.instance, args);
|
|
7114
7181
|
});
|
|
7115
|
-
await db.commit();
|
|
7116
7182
|
if (meta.returnSchema) {
|
|
7117
7183
|
const v = meta.returnSchema.safeParse(result);
|
|
7118
7184
|
if (!v.success) {
|
|
7119
7185
|
log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
|
|
7186
|
+
await db.rollback(new Error("output_invalid"));
|
|
7120
7187
|
return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
|
|
7121
7188
|
}
|
|
7122
7189
|
}
|
|
7123
|
-
if (result === void 0 || result === null) {
|
|
7190
|
+
if (result === void 0 || result === null && !meta.returnSchema) {
|
|
7191
|
+
await db.commit();
|
|
7124
7192
|
if (completionUploadId) {
|
|
7125
7193
|
completions.remember(completionUploadId, {
|
|
7126
7194
|
status: 204,
|
|
@@ -7133,6 +7201,7 @@ async function createApp(opts) {
|
|
|
7133
7201
|
});
|
|
7134
7202
|
}
|
|
7135
7203
|
const payload = JSON.stringify(result);
|
|
7204
|
+
await db.commit();
|
|
7136
7205
|
if (completionUploadId) {
|
|
7137
7206
|
completions.remember(completionUploadId, {
|
|
7138
7207
|
status: 200,
|
|
@@ -7149,16 +7218,7 @@ async function createApp(opts) {
|
|
|
7149
7218
|
});
|
|
7150
7219
|
} catch (err) {
|
|
7151
7220
|
await db.rollback(err);
|
|
7152
|
-
|
|
7153
|
-
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
7154
|
-
log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
|
|
7155
|
-
}
|
|
7156
|
-
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
7157
|
-
data: err.data
|
|
7158
|
-
} : void 0);
|
|
7159
|
-
}
|
|
7160
|
-
log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
|
|
7161
|
-
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
7221
|
+
return errorResponse(err, hit.entry.id, requestId);
|
|
7162
7222
|
}
|
|
7163
7223
|
}
|
|
7164
7224
|
__name(handle, "handle");
|
|
@@ -7171,15 +7231,24 @@ async function createApp(opts) {
|
|
|
7171
7231
|
throw err;
|
|
7172
7232
|
}
|
|
7173
7233
|
return {
|
|
7174
|
-
handle,
|
|
7234
|
+
handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
|
|
7235
|
+
get databasePressure() {
|
|
7236
|
+
return admission.snapshot;
|
|
7237
|
+
},
|
|
7175
7238
|
routes,
|
|
7176
7239
|
config,
|
|
7177
7240
|
container: liveContainer,
|
|
7178
|
-
runInServiceScope,
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7241
|
+
runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
|
|
7242
|
+
shutdown() {
|
|
7243
|
+
accepting = false;
|
|
7244
|
+
return shuttingDown ??= (async () => {
|
|
7245
|
+
while (inFlight.size) await Promise.allSettled([
|
|
7246
|
+
...inFlight
|
|
7247
|
+
]);
|
|
7248
|
+
await runShutdownHooks();
|
|
7249
|
+
await closeDriver(sql);
|
|
7250
|
+
released = true;
|
|
7251
|
+
})();
|
|
7183
7252
|
}
|
|
7184
7253
|
};
|
|
7185
7254
|
}
|