@palbase/backend 35.0.1 → 36.0.1
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 +2 -60
- package/docs/examples/database-dx.ts +12 -17
- package/docs/llms-full.txt +3 -61
- 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 +2 -2
- 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
package/dist/engine/index.cjs
CHANGED
|
@@ -703,25 +703,6 @@ function translateRejection(err, builder) {
|
|
|
703
703
|
}
|
|
704
704
|
__name(translateRejection, "translateRejection");
|
|
705
705
|
|
|
706
|
-
// src/db/workload.ts
|
|
707
|
-
function validateWorkload(value) {
|
|
708
|
-
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");
|
|
709
|
-
for (const name of [
|
|
710
|
-
"concurrency",
|
|
711
|
-
"tenantConcurrency",
|
|
712
|
-
"queueLimit",
|
|
713
|
-
"tenantQueueLimit",
|
|
714
|
-
"queueTimeoutMs"
|
|
715
|
-
]) {
|
|
716
|
-
const n = value[name], min = name.endsWith("Limit") ? 0 : 1;
|
|
717
|
-
const max = name === "queueTimeoutMs" ? 6e4 : name.endsWith("Limit") ? 1e4 : 1e3;
|
|
718
|
-
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`workload ${name} must be an integer between ${min} and ${max}`);
|
|
719
|
-
}
|
|
720
|
-
if (value.tenantConcurrency > value.concurrency || value.tenantQueueLimit > value.queueLimit) throw new Error("workload tenant limits cannot exceed global limits");
|
|
721
|
-
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");
|
|
722
|
-
}
|
|
723
|
-
__name(validateWorkload, "validateWorkload");
|
|
724
|
-
|
|
725
706
|
// src/db/transaction-options.ts
|
|
726
707
|
function retryBudget(value) {
|
|
727
708
|
const n = value ?? 0;
|
|
@@ -753,10 +734,6 @@ function validateAtomicOptions(options) {
|
|
|
753
734
|
throw new Error(`transaction ${name} must be a positive integer in milliseconds`);
|
|
754
735
|
}
|
|
755
736
|
}
|
|
756
|
-
if (options.workload !== void 0) validateWorkload(options.workload);
|
|
757
|
-
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))) {
|
|
758
|
-
throw new Error("conflictKeys requires a workload and at most 64 nonempty keys of at most 256 characters");
|
|
759
|
-
}
|
|
760
737
|
if (options.signal !== void 0 && (typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function")) {
|
|
761
738
|
throw new Error("transaction signal must be an AbortSignal");
|
|
762
739
|
}
|
|
@@ -916,7 +893,9 @@ function makeTableProxy(ops, prefix) {
|
|
|
916
893
|
// Older than this run, but the run rewrote this proxy for
|
|
917
894
|
// `Database.schema(name).tables.*` and would have carried the gap onto
|
|
918
895
|
// the new surface too.
|
|
919
|
-
updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set
|
|
896
|
+
updateMany: /* @__PURE__ */ __name((q) => ops().updateMany(name, q.where, q.set, q.returning === void 0 ? void 0 : {
|
|
897
|
+
returning: q.returning
|
|
898
|
+
}), "updateMany"),
|
|
920
899
|
deleteMany: /* @__PURE__ */ __name((q) => ops().deleteMany(name, q.where), "deleteMany"),
|
|
921
900
|
count: /* @__PURE__ */ __name((q) => ops().count(name, q?.where), "count"),
|
|
922
901
|
search: /* @__PURE__ */ __name((params) => ops().search(name, params), "search"),
|
|
@@ -936,9 +915,6 @@ function makeTypedSurface(raw) {
|
|
|
936
915
|
const ops = {
|
|
937
916
|
$query: /* @__PURE__ */ __name((sql, params) => raw.query(sql, params), "$query"),
|
|
938
917
|
$diagnostics: /* @__PURE__ */ __name(() => raw.diagnostics(), "$diagnostics"),
|
|
939
|
-
$readQuery: /* @__PURE__ */ __name((query) => raw.readQuery(query), "$readQuery"),
|
|
940
|
-
$compileQuery: /* @__PURE__ */ __name((query) => raw.compileQuery(query), "$compileQuery"),
|
|
941
|
-
$explainQuery: /* @__PURE__ */ __name((query) => raw.explainQuery(query), "$explainQuery"),
|
|
942
918
|
$insert: /* @__PURE__ */ __name((table, data) => raw.insert(table, data), "$insert"),
|
|
943
919
|
$update: /* @__PURE__ */ __name((table, id, data) => raw.update(table, id, data), "$update"),
|
|
944
920
|
$delete: /* @__PURE__ */ __name((table, id) => raw.delete(table, id), "$delete"),
|
|
@@ -1143,6 +1119,7 @@ var Realtime = makeServiceProxy("Realtime");
|
|
|
1143
1119
|
var SQLSTATE = Object.freeze({
|
|
1144
1120
|
serializationFailure: "40001",
|
|
1145
1121
|
deadlockDetected: "40P01",
|
|
1122
|
+
queryCancelled: "57014",
|
|
1146
1123
|
commandGuardFailed: "PBC01"
|
|
1147
1124
|
});
|
|
1148
1125
|
function isTransactionRollbackState(state) {
|
|
@@ -1345,6 +1322,16 @@ ${detail}`);
|
|
|
1345
1322
|
if (!Number.isInteger(poolMax) || poolMax < 1) {
|
|
1346
1323
|
throw new BootRefused([], `boot refused: DB_POOL_MAX must be a positive integer (got ${env.DB_POOL_MAX}).`);
|
|
1347
1324
|
}
|
|
1325
|
+
const capacityNumber = /* @__PURE__ */ __name((key2, minimum, maximum) => {
|
|
1326
|
+
if (env[key2] === void 0) return void 0;
|
|
1327
|
+
const value = Number(env[key2]);
|
|
1328
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new BootRefused([], `boot refused: ${key2} must be between ${minimum} and ${maximum}.`);
|
|
1329
|
+
return value;
|
|
1330
|
+
}, "capacityNumber");
|
|
1331
|
+
const queueLimit = capacityNumber("PALBASE_DB_QUEUE_LIMIT", 0, 1e4);
|
|
1332
|
+
const queueTimeoutMs = capacityNumber("PALBASE_DB_QUEUE_TIMEOUT_MS", 1, 6e4);
|
|
1333
|
+
const tenantClaim = env.PALBASE_DB_TENANT_CLAIM;
|
|
1334
|
+
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.");
|
|
1348
1335
|
return {
|
|
1349
1336
|
databaseUrl: env.DATABASE_URL.trim(),
|
|
1350
1337
|
authJwksUrl: env.AUTH_JWKS_URL.trim(),
|
|
@@ -1369,6 +1356,17 @@ ${detail}`);
|
|
|
1369
1356
|
// and the live database agrees (pg_roles.rolbypassrls = true).
|
|
1370
1357
|
dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
|
|
1371
1358
|
poolMax,
|
|
1359
|
+
databaseAdmission: {
|
|
1360
|
+
...queueLimit === void 0 ? {} : {
|
|
1361
|
+
queueLimit
|
|
1362
|
+
},
|
|
1363
|
+
...queueTimeoutMs === void 0 ? {} : {
|
|
1364
|
+
queueTimeoutMs
|
|
1365
|
+
},
|
|
1366
|
+
...tenantClaim === void 0 ? {} : {
|
|
1367
|
+
tenantClaim
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1372
1370
|
...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
|
|
1373
1371
|
databaseDiagnostics: {
|
|
1374
1372
|
onComplete: /* @__PURE__ */ __name((diagnostics) => {
|
|
@@ -1892,46 +1890,6 @@ function validateInsertManyOptions(opts) {
|
|
|
1892
1890
|
}
|
|
1893
1891
|
__name(validateInsertManyOptions, "validateInsertManyOptions");
|
|
1894
1892
|
|
|
1895
|
-
// src/db/query-contract.ts
|
|
1896
|
-
function queryColumnContract(column) {
|
|
1897
|
-
const def = column && typeof column === "object" && "_def" in column ? column._def : column;
|
|
1898
|
-
if (!def?.type) throw new Error("Query contract requires declared column types");
|
|
1899
|
-
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");
|
|
1900
|
-
return {
|
|
1901
|
-
type: def.type,
|
|
1902
|
-
nullable: def.nullable === true,
|
|
1903
|
-
...def.codec === void 0 ? {} : {
|
|
1904
|
-
codec: def.codec
|
|
1905
|
-
},
|
|
1906
|
-
...def.enumValues === void 0 ? {} : {
|
|
1907
|
-
enumValues: [
|
|
1908
|
-
...def.enumValues
|
|
1909
|
-
]
|
|
1910
|
-
},
|
|
1911
|
-
...def.dimensions === void 0 ? {} : {
|
|
1912
|
-
dimensions: def.dimensions
|
|
1913
|
-
}
|
|
1914
|
-
};
|
|
1915
|
-
}
|
|
1916
|
-
__name(queryColumnContract, "queryColumnContract");
|
|
1917
|
-
function stableContract(value) {
|
|
1918
|
-
if (Array.isArray(value)) return `[${value.map(stableContract).join(",")}]`;
|
|
1919
|
-
if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key2) => `${JSON.stringify(key2)}:${stableContract(value[key2])}`).join(",")}}`;
|
|
1920
|
-
return JSON.stringify(value) ?? "null";
|
|
1921
|
-
}
|
|
1922
|
-
__name(stableContract, "stableContract");
|
|
1923
|
-
function assertReadContract(query, columns) {
|
|
1924
|
-
if (!query.options.select.length || new Set(query.options.select).size !== query.options.select.length) {
|
|
1925
|
-
throw new Error(`Query ${query.name}: select must contain distinct, explicit columns`);
|
|
1926
|
-
}
|
|
1927
|
-
for (const name of query.options.select) {
|
|
1928
|
-
if (!columns || !Object.hasOwn(columns, name) || !Object.hasOwn(query.columns, name) || stableContract(queryColumnContract(columns[name])) !== stableContract(query.columns[name])) {
|
|
1929
|
-
throw new Error(`Query ${query.name}: schema drift at ${query.table}.${name}; regenerate and review its response contract`);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
__name(assertReadContract, "assertReadContract");
|
|
1934
|
-
|
|
1935
1893
|
// src/db/unique.ts
|
|
1936
1894
|
function uniqueKeysOf(table) {
|
|
1937
1895
|
const columns = table.columns ?? {};
|
|
@@ -2105,142 +2063,167 @@ function parameterChunks(sql) {
|
|
|
2105
2063
|
__name(parameterChunks, "parameterChunks");
|
|
2106
2064
|
|
|
2107
2065
|
// src/engine/admission.ts
|
|
2108
|
-
var
|
|
2066
|
+
var DatabaseAdmission = class {
|
|
2109
2067
|
static {
|
|
2110
|
-
__name(this, "
|
|
2068
|
+
__name(this, "DatabaseAdmission");
|
|
2111
2069
|
}
|
|
2112
|
-
|
|
2113
|
-
pending = [];
|
|
2070
|
+
options;
|
|
2114
2071
|
active = 0;
|
|
2115
|
-
|
|
2072
|
+
queued = 0;
|
|
2116
2073
|
tenants = /* @__PURE__ */ new Map();
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2074
|
+
// Map insertion order is the round-robin ring; each Set is FIFO. Grant and
|
|
2075
|
+
// cancellation remove nodes directly, without scanning a 10,000-item queue.
|
|
2076
|
+
pending = /* @__PURE__ */ new Map();
|
|
2077
|
+
lastTenant;
|
|
2078
|
+
accepted = 0;
|
|
2079
|
+
rejected = {
|
|
2080
|
+
full: 0,
|
|
2081
|
+
timeout: 0,
|
|
2082
|
+
cancelled: 0
|
|
2083
|
+
};
|
|
2084
|
+
limit;
|
|
2085
|
+
timeout;
|
|
2086
|
+
constructor(options) {
|
|
2087
|
+
this.options = options;
|
|
2088
|
+
this.limit = options.queueLimit ?? Math.min(512, Math.max(32, options.concurrency * 8));
|
|
2089
|
+
this.timeout = options.queueTimeoutMs ?? 1e3;
|
|
2090
|
+
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) {
|
|
2091
|
+
throw new Error("Invalid database pool or queue deployment settings");
|
|
2092
|
+
}
|
|
2121
2093
|
}
|
|
2122
2094
|
get snapshot() {
|
|
2123
2095
|
return {
|
|
2124
2096
|
active: this.active,
|
|
2125
|
-
queued: this.
|
|
2097
|
+
queued: this.queued,
|
|
2126
2098
|
tenants: this.tenants.size,
|
|
2127
|
-
|
|
2099
|
+
accepted: this.accepted,
|
|
2100
|
+
rejected: {
|
|
2101
|
+
...this.rejected
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
rejection(reason2) {
|
|
2106
|
+
this.rejected[reason2]++;
|
|
2107
|
+
try {
|
|
2108
|
+
this.options.onReject?.({
|
|
2109
|
+
reason: reason2,
|
|
2110
|
+
active: this.active,
|
|
2111
|
+
queued: this.queued
|
|
2112
|
+
});
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
take(tenant, entered) {
|
|
2117
|
+
this.active++;
|
|
2118
|
+
this.accepted++;
|
|
2119
|
+
this.lastTenant = tenant;
|
|
2120
|
+
this.tenants.set(tenant, (this.tenants.get(tenant) ?? 0) + 1);
|
|
2121
|
+
let released = false;
|
|
2122
|
+
return {
|
|
2123
|
+
queueMs: performance.now() - entered,
|
|
2124
|
+
release: /* @__PURE__ */ __name(() => {
|
|
2125
|
+
if (released) return;
|
|
2126
|
+
released = true;
|
|
2127
|
+
this.active--;
|
|
2128
|
+
const count = this.tenants.get(tenant) - 1;
|
|
2129
|
+
if (count) this.tenants.set(tenant, count);
|
|
2130
|
+
else this.tenants.delete(tenant);
|
|
2131
|
+
this.drain();
|
|
2132
|
+
}, "release")
|
|
2128
2133
|
};
|
|
2129
2134
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2135
|
+
drain() {
|
|
2136
|
+
while (this.active < this.options.concurrency && this.queued) {
|
|
2137
|
+
let tenant = this.pending.keys().next().value;
|
|
2138
|
+
if (this.pending.size > 1 && tenant === this.lastTenant) {
|
|
2139
|
+
const queue2 = this.pending.get(tenant);
|
|
2140
|
+
this.pending.delete(tenant);
|
|
2141
|
+
this.pending.set(tenant, queue2);
|
|
2142
|
+
tenant = this.pending.keys().next().value;
|
|
2143
|
+
}
|
|
2144
|
+
const queue = this.pending.get(tenant), waiter = queue.values().next().value;
|
|
2145
|
+
queue.delete(waiter);
|
|
2146
|
+
this.pending.delete(tenant);
|
|
2147
|
+
if (queue.size) this.pending.set(tenant, queue);
|
|
2148
|
+
this.queued--;
|
|
2149
|
+
waiter.cleanup();
|
|
2150
|
+
waiter.grant(this.take(tenant, waiter.entered));
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
async acquire(claimsJson, signal) {
|
|
2154
|
+
if (signal?.aborted) {
|
|
2155
|
+
this.rejection("cancelled");
|
|
2156
|
+
throw signal.reason ?? new Error("Database admission cancelled");
|
|
2157
|
+
}
|
|
2158
|
+
const claims = JSON.parse(claimsJson);
|
|
2159
|
+
const value = this.options.tenantClaim ? claims[this.options.tenantClaim] : claims.sub;
|
|
2160
|
+
const tenant = typeof value === "string" && value.length ? value : "__anonymous__";
|
|
2161
|
+
const entered = performance.now();
|
|
2162
|
+
if (this.active < this.options.concurrency && !this.queued) return this.take(tenant, entered);
|
|
2163
|
+
if (this.queued >= this.limit) {
|
|
2164
|
+
this.rejection("full");
|
|
2165
|
+
throw new TooManyRequests({
|
|
2142
2166
|
retryAfter: 1
|
|
2143
|
-
},
|
|
2167
|
+
}, "Database queue is full");
|
|
2144
2168
|
}
|
|
2145
2169
|
return new Promise((grant, reject) => {
|
|
2170
|
+
const queue = this.pending.get(tenant) ?? /* @__PURE__ */ new Set();
|
|
2171
|
+
if (!this.pending.has(tenant)) this.pending.set(tenant, queue);
|
|
2146
2172
|
const waiter = {
|
|
2147
|
-
tenant,
|
|
2148
|
-
keys: scoped,
|
|
2149
|
-
entered: performance.now(),
|
|
2150
2173
|
grant,
|
|
2151
2174
|
reject,
|
|
2152
|
-
|
|
2153
|
-
|
|
2175
|
+
entered,
|
|
2176
|
+
cleanup() {
|
|
2177
|
+
}
|
|
2154
2178
|
};
|
|
2155
|
-
const remove = /* @__PURE__ */ __name((reason2) => {
|
|
2156
|
-
|
|
2157
|
-
if (
|
|
2158
|
-
this.
|
|
2179
|
+
const remove = /* @__PURE__ */ __name((reason2, error) => {
|
|
2180
|
+
if (!queue.delete(waiter)) return;
|
|
2181
|
+
if (!queue.size) this.pending.delete(tenant);
|
|
2182
|
+
this.queued--;
|
|
2159
2183
|
waiter.cleanup();
|
|
2160
|
-
|
|
2184
|
+
this.rejection(reason2);
|
|
2185
|
+
reject(error);
|
|
2161
2186
|
this.drain();
|
|
2162
2187
|
}, "remove");
|
|
2163
|
-
const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
2164
|
-
const timer = setTimeout(() => remove(new TooManyRequests({
|
|
2188
|
+
const abort = /* @__PURE__ */ __name(() => remove("cancelled", signal?.reason ?? new Error("Database admission cancelled")), "abort");
|
|
2189
|
+
const timer = setTimeout(() => remove("timeout", new TooManyRequests({
|
|
2165
2190
|
retryAfter: 1
|
|
2166
|
-
},
|
|
2191
|
+
}, "Database queue deadline exceeded")), this.timeout);
|
|
2167
2192
|
waiter.cleanup = () => {
|
|
2168
2193
|
clearTimeout(timer);
|
|
2169
2194
|
signal?.removeEventListener("abort", abort);
|
|
2170
2195
|
};
|
|
2196
|
+
queue.add(waiter);
|
|
2197
|
+
this.queued++;
|
|
2171
2198
|
signal?.addEventListener("abort", abort, {
|
|
2172
2199
|
once: true
|
|
2173
2200
|
});
|
|
2174
|
-
this.pending.push(waiter);
|
|
2175
2201
|
if (signal?.aborted) abort();
|
|
2176
2202
|
else this.drain();
|
|
2177
2203
|
});
|
|
2178
2204
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
let released = false;
|
|
2197
|
-
waiter.grant({
|
|
2198
|
-
queueMs: performance.now() - waiter.entered,
|
|
2199
|
-
release: /* @__PURE__ */ __name(() => {
|
|
2200
|
-
if (released) return;
|
|
2201
|
-
released = true;
|
|
2202
|
-
this.active--;
|
|
2203
|
-
const left = this.tenants.get(waiter.tenant) - 1;
|
|
2204
|
-
if (left === 0) this.tenants.delete(waiter.tenant);
|
|
2205
|
-
else this.tenants.set(waiter.tenant, left);
|
|
2206
|
-
for (const key2 of waiter.keys) this.activeKeys.delete(key2);
|
|
2207
|
-
this.drain();
|
|
2208
|
-
}, "release")
|
|
2209
|
-
});
|
|
2210
|
-
}
|
|
2211
|
-
} finally {
|
|
2212
|
-
this.draining = false;
|
|
2205
|
+
};
|
|
2206
|
+
var databases = /* @__PURE__ */ new WeakMap();
|
|
2207
|
+
function databaseAdmission(owner, options) {
|
|
2208
|
+
let admission = databases.get(owner);
|
|
2209
|
+
if (!admission) {
|
|
2210
|
+
admission = new DatabaseAdmission(options ?? {
|
|
2211
|
+
concurrency: 10
|
|
2212
|
+
});
|
|
2213
|
+
databases.set(owner, admission);
|
|
2214
|
+
} else if (options) {
|
|
2215
|
+
for (const key2 of [
|
|
2216
|
+
"concurrency",
|
|
2217
|
+
"queueLimit",
|
|
2218
|
+
"queueTimeoutMs",
|
|
2219
|
+
"tenantClaim"
|
|
2220
|
+
]) {
|
|
2221
|
+
if (options[key2] !== admission.options[key2]) throw new Error("Database admission cannot be reconfigured while its pool is shared");
|
|
2213
2222
|
}
|
|
2214
2223
|
}
|
|
2215
|
-
};
|
|
2216
|
-
var registries = /* @__PURE__ */ new WeakMap();
|
|
2217
|
-
function workloadAdmission(owner, definition) {
|
|
2218
|
-
validateWorkload(definition);
|
|
2219
|
-
let registry = registries.get(owner);
|
|
2220
|
-
if (!registry) {
|
|
2221
|
-
registry = /* @__PURE__ */ new Map();
|
|
2222
|
-
registries.set(owner, registry);
|
|
2223
|
-
}
|
|
2224
|
-
const existing = registry.get(definition.name);
|
|
2225
|
-
if (existing) {
|
|
2226
|
-
const d = existing.definition;
|
|
2227
|
-
if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
|
|
2228
|
-
return existing;
|
|
2229
|
-
}
|
|
2230
|
-
if (registry.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
|
|
2231
|
-
const admission = new WorkloadAdmission(Object.freeze({
|
|
2232
|
-
...definition
|
|
2233
|
-
}));
|
|
2234
|
-
registry.set(definition.name, admission);
|
|
2235
2224
|
return admission;
|
|
2236
2225
|
}
|
|
2237
|
-
__name(
|
|
2238
|
-
function workloadTenant(definition, claimsJson) {
|
|
2239
|
-
const claims = JSON.parse(claimsJson);
|
|
2240
|
-
const value = claims[definition.tenantClaim] ?? claims.sub;
|
|
2241
|
-
return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
|
|
2242
|
-
}
|
|
2243
|
-
__name(workloadTenant, "workloadTenant");
|
|
2226
|
+
__name(databaseAdmission, "databaseAdmission");
|
|
2244
2227
|
|
|
2245
2228
|
// src/engine/transaction-control.ts
|
|
2246
2229
|
var TransactionControl = class {
|
|
@@ -2363,6 +2346,7 @@ var DatabaseTrace = class {
|
|
|
2363
2346
|
sqlMs: 0,
|
|
2364
2347
|
commitMs: 0,
|
|
2365
2348
|
rollbackMs: 0,
|
|
2349
|
+
connectionMs: 0,
|
|
2366
2350
|
commits: 0,
|
|
2367
2351
|
commitFailures: 0,
|
|
2368
2352
|
rollbackRequests: 0,
|
|
@@ -2374,7 +2358,7 @@ var DatabaseTrace = class {
|
|
|
2374
2358
|
if (this.failure) throw this.failure;
|
|
2375
2359
|
}
|
|
2376
2360
|
time(key2, started) {
|
|
2377
|
-
this.state[key2]
|
|
2361
|
+
this.state[key2] = (this.state[key2] ?? 0) + performance.now() - started;
|
|
2378
2362
|
}
|
|
2379
2363
|
retry() {
|
|
2380
2364
|
this.state.retries++;
|
|
@@ -2698,31 +2682,53 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
2698
2682
|
fail = rej;
|
|
2699
2683
|
});
|
|
2700
2684
|
void parked.catch(() => void 0);
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
validateAtomicOptions({
|
|
2709
|
-
isolation,
|
|
2710
|
-
readOnly: options.readOnly
|
|
2711
|
-
});
|
|
2712
|
-
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
2685
|
+
settled = (async () => {
|
|
2686
|
+
const queuedAt = performance.now();
|
|
2687
|
+
let lease;
|
|
2688
|
+
try {
|
|
2689
|
+
lease = await (sql.admission ?? databaseAdmission(sql)).acquire(claimsJson, options.control?.signal);
|
|
2690
|
+
} finally {
|
|
2691
|
+
options.trace?.time("queueMs", queuedAt);
|
|
2713
2692
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
options.control
|
|
2718
|
-
|
|
2693
|
+
const openingAt = performance.now();
|
|
2694
|
+
let connectedAt;
|
|
2695
|
+
try {
|
|
2696
|
+
options.control?.check();
|
|
2697
|
+
if (closed) throw new Error("Database transaction closed while waiting for admission");
|
|
2698
|
+
return await sql.begin(async (tx) => {
|
|
2699
|
+
connectedAt = performance.now();
|
|
2700
|
+
options.trace?.time("poolMs", openingAt);
|
|
2701
|
+
const setupAt = options.trace ? performance.now() : 0;
|
|
2702
|
+
options.control?.check();
|
|
2703
|
+
if (closed) throw new Error("Database transaction closed while waiting for its connection");
|
|
2704
|
+
if (options.isolation !== void 0 || options.readOnly !== void 0) {
|
|
2705
|
+
const isolation = options.isolation ?? "read committed";
|
|
2706
|
+
validateAtomicOptions({
|
|
2707
|
+
isolation,
|
|
2708
|
+
readOnly: options.readOnly
|
|
2709
|
+
});
|
|
2710
|
+
await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
|
|
2711
|
+
}
|
|
2712
|
+
if (options.control && sql.cancelBackend) {
|
|
2713
|
+
const ticket = await tx.unsafe(`WITH backend AS MATERIALIZED (
|
|
2714
|
+
SELECT pid, backend_start::text AS started FROM pg_catalog.pg_stat_get_activity(pg_catalog.pg_backend_pid())
|
|
2715
|
+
) ${bindSql}, backend.pid, backend.started FROM backend`, bindParams);
|
|
2716
|
+
if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
|
|
2717
|
+
options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
|
|
2718
|
+
options.control.check();
|
|
2719
|
+
} else {
|
|
2720
|
+
const binding = tx.unsafe(bindSql, bindParams);
|
|
2721
|
+
await (options.control ? options.control.watch(binding) : binding);
|
|
2722
|
+
}
|
|
2723
|
+
options.trace?.time("setupMs", setupAt);
|
|
2724
|
+
resolveTx2(scoped(tx));
|
|
2725
|
+
await parked;
|
|
2726
|
+
});
|
|
2727
|
+
} finally {
|
|
2728
|
+
if (connectedAt !== void 0) options.trace?.time("connectionMs", connectedAt);
|
|
2729
|
+
lease.release();
|
|
2719
2730
|
}
|
|
2720
|
-
|
|
2721
|
-
await (options.control ? options.control.watch(binding) : binding);
|
|
2722
|
-
options.trace?.time("setupMs", setupAt);
|
|
2723
|
-
resolveTx2(scoped(tx));
|
|
2724
|
-
await parked;
|
|
2725
|
-
}).catch((e) => {
|
|
2731
|
+
})().catch((e) => {
|
|
2726
2732
|
rejectTx(e);
|
|
2727
2733
|
throw e;
|
|
2728
2734
|
});
|
|
@@ -2739,10 +2745,13 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
2739
2745
|
if (pendingSavepoints.size) await Promise.allSettled([
|
|
2740
2746
|
...pendingSavepoints
|
|
2741
2747
|
]);
|
|
2742
|
-
options.control?.check();
|
|
2743
2748
|
options.trace?.check();
|
|
2749
|
+
if (!opening) {
|
|
2750
|
+
closed = true;
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
options.control?.check();
|
|
2744
2754
|
closed = true;
|
|
2745
|
-
if (!opening) return;
|
|
2746
2755
|
const started = options.trace ? performance.now() : 0;
|
|
2747
2756
|
release();
|
|
2748
2757
|
try {
|
|
@@ -2869,10 +2878,11 @@ function asTableRows(table, rows, withSpec, partial = false) {
|
|
|
2869
2878
|
const nested = typeof spec === "object" ? spec.with : void 0;
|
|
2870
2879
|
for (const row of result) {
|
|
2871
2880
|
const value = row[rel.name];
|
|
2872
|
-
|
|
2881
|
+
const selected = typeof spec === "object" && "select" in spec && spec.select !== void 0;
|
|
2882
|
+
if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested, selected);
|
|
2873
2883
|
else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
|
|
2874
2884
|
value
|
|
2875
|
-
], nested)[0];
|
|
2885
|
+
], nested, selected)[0];
|
|
2876
2886
|
}
|
|
2877
2887
|
}
|
|
2878
2888
|
for (const row of result) foldCan(table, row, partial);
|
|
@@ -3816,6 +3826,9 @@ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, cou
|
|
|
3816
3826
|
const i = ++counter.value;
|
|
3817
3827
|
const childAlias = `w${i}`;
|
|
3818
3828
|
const childKnown = schemaColumns(rel.to);
|
|
3829
|
+
if (o["select"] !== void 0 && (!Array.isArray(o["select"]) || o["select"].length === 0)) {
|
|
3830
|
+
throw new Error(`${caller}(${table}): with.${name}.select must be a non-empty array of columns`);
|
|
3831
|
+
}
|
|
3819
3832
|
const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
|
|
3820
3833
|
...childKnown
|
|
3821
3834
|
] : [];
|
|
@@ -3919,7 +3932,10 @@ function pageSchema(table) {
|
|
|
3919
3932
|
}
|
|
3920
3933
|
__name(pageSchema, "pageSchema");
|
|
3921
3934
|
function createOps(tx, context = {}) {
|
|
3922
|
-
const at = /* @__PURE__ */ __name(() =>
|
|
3935
|
+
const at = /* @__PURE__ */ __name(() => {
|
|
3936
|
+
const target = typeof tx === "function" ? tx() : tx;
|
|
3937
|
+
return target instanceof Promise ? target.then(resolveTx) : resolveTx(target);
|
|
3938
|
+
}, "at");
|
|
3923
3939
|
const ops = {
|
|
3924
3940
|
diagnostics() {
|
|
3925
3941
|
return context.trace?.snapshot ?? null;
|
|
@@ -4370,22 +4386,6 @@ function createOps(tx, context = {}) {
|
|
|
4370
4386
|
]);
|
|
4371
4387
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
4372
4388
|
},
|
|
4373
|
-
compileQuery(query) {
|
|
4374
|
-
assertReadContract(query, tableDefOf(query.table)?.columns);
|
|
4375
|
-
return compileFindMany(query.table, query.where, query.options);
|
|
4376
|
-
},
|
|
4377
|
-
async readQuery(query) {
|
|
4378
|
-
const compiled = ops.compileQuery(query);
|
|
4379
|
-
return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
|
|
4380
|
-
name: query.name
|
|
4381
|
-
}), void 0, true);
|
|
4382
|
-
},
|
|
4383
|
-
async explainQuery(query) {
|
|
4384
|
-
const compiled = ops.compileQuery(query);
|
|
4385
|
-
return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
|
|
4386
|
-
name: `${query.name}.explain`
|
|
4387
|
-
});
|
|
4388
|
-
},
|
|
4389
4389
|
async findMany(table, query = {}, opts = {}) {
|
|
4390
4390
|
const compiled = compileFindMany(table, query, opts);
|
|
4391
4391
|
return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
|
|
@@ -4920,7 +4920,7 @@ function createOps(tx, context = {}) {
|
|
|
4920
4920
|
if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
|
|
4921
4921
|
return ops.txPlan(plan);
|
|
4922
4922
|
}
|
|
4923
|
-
if (compiled.steps.length === 0) return {
|
|
4923
|
+
if (compiled.steps.length === 0 && !compiled.pipeline?.length) return {
|
|
4924
4924
|
results: []
|
|
4925
4925
|
};
|
|
4926
4926
|
if (compiled.pipeline) {
|
|
@@ -5193,6 +5193,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5193
5193
|
const state = sqlstateOf(e);
|
|
5194
5194
|
if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
|
|
5195
5195
|
if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
|
|
5196
|
+
if (state === SQLSTATE.queryCancelled) return markEngineRaised(new HttpError(503, "database_query_cancelled", "Database statement was cancelled"));
|
|
5196
5197
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
5197
5198
|
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)})`);
|
|
5198
5199
|
}
|
|
@@ -5219,6 +5220,7 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5219
5220
|
}
|
|
5220
5221
|
}), "wrapTx");
|
|
5221
5222
|
return {
|
|
5223
|
+
admission: sql.admission ?? databaseAdmission(sql),
|
|
5222
5224
|
unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
|
|
5223
5225
|
cancelBackend: sql.cancelBackend?.bind(sql),
|
|
5224
5226
|
begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
|
|
@@ -5227,9 +5229,12 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
5227
5229
|
};
|
|
5228
5230
|
}
|
|
5229
5231
|
__name(diagnosingDriver, "diagnosingDriver");
|
|
5230
|
-
function createRequestDatabase(sql, identity, diagnostics) {
|
|
5232
|
+
function createRequestDatabase(sql, identity, diagnostics, signal) {
|
|
5231
5233
|
const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
|
|
5234
|
+
const requestControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
5232
5235
|
let requestClosed = false;
|
|
5236
|
+
let authorized = false, applicationTouched = false;
|
|
5237
|
+
let authorizationRelease;
|
|
5233
5238
|
const contextFor = /* @__PURE__ */ __name((role) => {
|
|
5234
5239
|
let scope;
|
|
5235
5240
|
return {
|
|
@@ -5248,19 +5253,21 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5248
5253
|
validateAtomicOptions(options);
|
|
5249
5254
|
if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
|
|
5250
5255
|
if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
|
|
5256
|
+
if (authorized && !applicationTouched && tx.opened) {
|
|
5257
|
+
authorizationRelease ??= (async () => {
|
|
5258
|
+
await tx.commit();
|
|
5259
|
+
tx = requestTransaction();
|
|
5260
|
+
})();
|
|
5261
|
+
}
|
|
5262
|
+
if (authorizationRelease) await authorizationRelease;
|
|
5251
5263
|
const retries = retryBudget(options.retry);
|
|
5252
|
-
const
|
|
5253
|
-
|
|
5264
|
+
const atomicSignal = options.signal && signal ? AbortSignal.any([
|
|
5265
|
+
options.signal,
|
|
5266
|
+
signal
|
|
5267
|
+
]) : options.signal ?? signal;
|
|
5268
|
+
const control = options.timeoutMs !== void 0 || atomicSignal !== void 0 ? new TransactionControl(options.timeoutMs, atomicSignal) : void 0;
|
|
5254
5269
|
try {
|
|
5255
5270
|
control?.check();
|
|
5256
|
-
if (options.workload) {
|
|
5257
|
-
const queuedAt = trace ? performance.now() : 0;
|
|
5258
|
-
try {
|
|
5259
|
-
lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
|
|
5260
|
-
} finally {
|
|
5261
|
-
trace?.time("queueMs", queuedAt);
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5264
5271
|
for (let attempt = 0; ; attempt++) {
|
|
5265
5272
|
control?.check();
|
|
5266
5273
|
const timeouts = [
|
|
@@ -5293,19 +5300,23 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5293
5300
|
}
|
|
5294
5301
|
} finally {
|
|
5295
5302
|
control?.dispose();
|
|
5296
|
-
lease?.release();
|
|
5297
5303
|
}
|
|
5298
5304
|
}, "atomic");
|
|
5299
|
-
const
|
|
5300
|
-
trace
|
|
5301
|
-
|
|
5305
|
+
const requestTransaction = /* @__PURE__ */ __name(() => createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
|
|
5306
|
+
trace,
|
|
5307
|
+
control: requestControl
|
|
5308
|
+
}), "requestTransaction");
|
|
5309
|
+
let tx = requestTransaction();
|
|
5302
5310
|
let serviceTx = null;
|
|
5311
|
+
let serviceControl;
|
|
5303
5312
|
let serviceClient = null;
|
|
5304
5313
|
const asService = /* @__PURE__ */ __name(() => {
|
|
5305
5314
|
if (serviceClient === null) {
|
|
5315
|
+
serviceControl = signal === void 0 ? void 0 : new TransactionControl(void 0, signal);
|
|
5306
5316
|
serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
|
|
5307
5317
|
lockTimeout: SERVICE_LOCK_TIMEOUT,
|
|
5308
|
-
trace
|
|
5318
|
+
trace,
|
|
5319
|
+
control: serviceControl
|
|
5309
5320
|
});
|
|
5310
5321
|
serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
|
|
5311
5322
|
atomic: atomic(identity.serviceRole, "service")
|
|
@@ -5314,16 +5325,24 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5314
5325
|
return serviceClient;
|
|
5315
5326
|
}, "asService");
|
|
5316
5327
|
return {
|
|
5317
|
-
client: Object.assign(withTables(createOps(
|
|
5328
|
+
client: Object.assign(withTables(createOps(() => {
|
|
5329
|
+
if (authorized) applicationTouched = true;
|
|
5330
|
+
return authorizationRelease ? authorizationRelease.then(() => tx) : tx;
|
|
5331
|
+
}, userContext)), {
|
|
5318
5332
|
asService,
|
|
5319
5333
|
atomic: atomic(identity.role, "user")
|
|
5320
5334
|
}),
|
|
5335
|
+
authorizationComplete() {
|
|
5336
|
+
authorized = true;
|
|
5337
|
+
},
|
|
5321
5338
|
async commit() {
|
|
5322
5339
|
requestClosed = true;
|
|
5323
5340
|
try {
|
|
5324
5341
|
await tx.commit();
|
|
5325
5342
|
await serviceTx?.commit();
|
|
5326
5343
|
trace?.complete("committed");
|
|
5344
|
+
requestControl?.dispose();
|
|
5345
|
+
serviceControl?.dispose();
|
|
5327
5346
|
} catch (error) {
|
|
5328
5347
|
trace?.failed();
|
|
5329
5348
|
throw error;
|
|
@@ -5333,6 +5352,8 @@ function createRequestDatabase(sql, identity, diagnostics) {
|
|
|
5333
5352
|
requestClosed = true;
|
|
5334
5353
|
await tx.rollback(reason2);
|
|
5335
5354
|
await serviceTx?.rollback(reason2);
|
|
5355
|
+
requestControl?.dispose();
|
|
5356
|
+
serviceControl?.dispose();
|
|
5336
5357
|
trace?.complete("failed");
|
|
5337
5358
|
}
|
|
5338
5359
|
};
|
|
@@ -5640,7 +5661,7 @@ function compileCommand(plan) {
|
|
|
5640
5661
|
};
|
|
5641
5662
|
});
|
|
5642
5663
|
steps.push({
|
|
5643
|
-
|
|
5664
|
+
sql,
|
|
5644
5665
|
bindings,
|
|
5645
5666
|
guard: op.guard
|
|
5646
5667
|
});
|
|
@@ -5663,11 +5684,15 @@ function compileCommand(plan) {
|
|
|
5663
5684
|
rows_affected: 1
|
|
5664
5685
|
});
|
|
5665
5686
|
}
|
|
5687
|
+
if (!hasRefs) return {
|
|
5688
|
+
steps: [],
|
|
5689
|
+
pipeline
|
|
5690
|
+
};
|
|
5666
5691
|
return {
|
|
5667
|
-
steps,
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
}
|
|
5692
|
+
steps: steps.map(({ sql, ...step }) => sql === void 0 ? step : {
|
|
5693
|
+
...step,
|
|
5694
|
+
chunks: parameterChunks(sql)
|
|
5695
|
+
})
|
|
5671
5696
|
};
|
|
5672
5697
|
}
|
|
5673
5698
|
__name(compileCommand, "compileCommand");
|
|
@@ -6130,9 +6155,11 @@ function makeSseWriter(opts) {
|
|
|
6130
6155
|
if (!begun) {
|
|
6131
6156
|
begun = true;
|
|
6132
6157
|
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
6158
|
+
void chain.catch(() => void 0);
|
|
6133
6159
|
return;
|
|
6134
6160
|
}
|
|
6135
6161
|
chain = chain.then(() => opts.enqueue(frame));
|
|
6162
|
+
void chain.catch(() => void 0);
|
|
6136
6163
|
},
|
|
6137
6164
|
started() {
|
|
6138
6165
|
return begun;
|
|
@@ -6727,6 +6754,23 @@ async function createApp(opts) {
|
|
|
6727
6754
|
throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
6728
6755
|
}
|
|
6729
6756
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
6757
|
+
const admission = sql.admission ?? databaseAdmission(sql, {
|
|
6758
|
+
concurrency: config.poolMax,
|
|
6759
|
+
...config.databaseAdmission
|
|
6760
|
+
});
|
|
6761
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
6762
|
+
let shuttingDown;
|
|
6763
|
+
let accepting = true;
|
|
6764
|
+
async function serving(run) {
|
|
6765
|
+
const work = run();
|
|
6766
|
+
inFlight.add(work);
|
|
6767
|
+
try {
|
|
6768
|
+
return await work;
|
|
6769
|
+
} finally {
|
|
6770
|
+
inFlight.delete(work);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
__name(serving, "serving");
|
|
6730
6774
|
await sql.unsafe("select 1");
|
|
6731
6775
|
setSchema(opts.schemas ?? []);
|
|
6732
6776
|
setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
|
|
@@ -6793,6 +6837,19 @@ async function createApp(opts) {
|
|
|
6793
6837
|
}
|
|
6794
6838
|
}
|
|
6795
6839
|
__name(runInServiceScope, "runInServiceScope");
|
|
6840
|
+
function errorResponse(err, endpoint, requestId) {
|
|
6841
|
+
if (isHttpError(err)) {
|
|
6842
|
+
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
6843
|
+
log.error(`[engine] unhandled ${err.error} in ${endpoint}`, err);
|
|
6844
|
+
}
|
|
6845
|
+
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
6846
|
+
data: err.data
|
|
6847
|
+
} : void 0);
|
|
6848
|
+
}
|
|
6849
|
+
log.error(`[engine] unhandled error in ${endpoint}`, err);
|
|
6850
|
+
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
6851
|
+
}
|
|
6852
|
+
__name(errorResponse, "errorResponse");
|
|
6796
6853
|
async function handleAuthorize(req, requestId) {
|
|
6797
6854
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
6798
6855
|
return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
|
|
@@ -6815,7 +6872,7 @@ async function createApp(opts) {
|
|
|
6815
6872
|
...config.databaseDiagnostics,
|
|
6816
6873
|
endpoint: "upload authorization",
|
|
6817
6874
|
requestId
|
|
6818
|
-
});
|
|
6875
|
+
}, req.signal);
|
|
6819
6876
|
try {
|
|
6820
6877
|
const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
|
|
6821
6878
|
if (refusal) {
|
|
@@ -6825,7 +6882,7 @@ async function createApp(opts) {
|
|
|
6825
6882
|
await uploadDb.commit();
|
|
6826
6883
|
} catch (err) {
|
|
6827
6884
|
await uploadDb.rollback(err);
|
|
6828
|
-
|
|
6885
|
+
return errorResponse(err, "upload authorization", requestId);
|
|
6829
6886
|
}
|
|
6830
6887
|
const grant = grantFor(target?.entry, {
|
|
6831
6888
|
userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
|
|
@@ -6867,256 +6924,257 @@ async function createApp(opts) {
|
|
|
6867
6924
|
...meta.options.databaseBudget,
|
|
6868
6925
|
endpoint: hit.entry.id,
|
|
6869
6926
|
requestId
|
|
6870
|
-
});
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6927
|
+
}, req.signal);
|
|
6928
|
+
try {
|
|
6929
|
+
const rolesOfCaller = makeRoleResolver(db.client);
|
|
6930
|
+
let dispatched = false;
|
|
6931
|
+
const settleBeforeRefusal = /* @__PURE__ */ __name(async () => {
|
|
6932
|
+
if (dispatched) return;
|
|
6933
|
+
await db.rollback(new Error("request refused before dispatch"));
|
|
6934
|
+
}, "settleBeforeRefusal");
|
|
6935
|
+
const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
|
|
6936
|
+
if (refusal) {
|
|
6937
|
+
await settleBeforeRefusal();
|
|
6938
|
+
return refusal;
|
|
6939
|
+
}
|
|
6940
|
+
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
6941
|
+
await settleBeforeRefusal();
|
|
6942
|
+
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
6943
|
+
}
|
|
6944
|
+
let parsedBody;
|
|
6945
|
+
let bodyRead = false;
|
|
6946
|
+
let rawBody = null;
|
|
6947
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
6948
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
6949
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
6950
|
+
await settleBeforeRefusal();
|
|
6951
|
+
return new Response(JSON.stringify({
|
|
6952
|
+
error: "payload_too_large",
|
|
6953
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
6954
|
+
status: 413,
|
|
6955
|
+
request_id: requestId
|
|
6956
|
+
}), {
|
|
6957
|
+
status: 413,
|
|
6958
|
+
headers: JSON_HEADERS
|
|
6959
|
+
});
|
|
6960
|
+
}
|
|
6961
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
6962
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
6963
|
+
await settleBeforeRefusal();
|
|
6964
|
+
return new Response(JSON.stringify({
|
|
6965
|
+
error: "payload_too_large",
|
|
6966
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
6967
|
+
status: 413,
|
|
6968
|
+
request_id: requestId
|
|
6969
|
+
}), {
|
|
6970
|
+
status: 413,
|
|
6971
|
+
headers: JSON_HEADERS
|
|
6972
|
+
});
|
|
6973
|
+
}
|
|
6974
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
6975
|
+
}
|
|
6976
|
+
const readParsedBody = /* @__PURE__ */ __name(() => {
|
|
6977
|
+
if (bodyRead) return parsedBody;
|
|
6978
|
+
bodyRead = true;
|
|
6979
|
+
if (!rawBody) {
|
|
6980
|
+
parsedBody = {};
|
|
6981
|
+
return parsedBody;
|
|
6982
|
+
}
|
|
6983
|
+
try {
|
|
6984
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
6985
|
+
} catch {
|
|
6986
|
+
parsedBody = {};
|
|
6987
|
+
}
|
|
6988
|
+
return parsedBody;
|
|
6989
|
+
}, "readParsedBody");
|
|
6990
|
+
const attest = await attestGate(req, rawBody, {
|
|
6991
|
+
userId
|
|
6992
|
+
});
|
|
6993
|
+
if (attest.observation) observations.record(attest.observation);
|
|
6994
|
+
if (attest.outcome === "reject") {
|
|
6995
|
+
const headers = {
|
|
6996
|
+
...JSON_HEADERS
|
|
6997
|
+
};
|
|
6998
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
6892
6999
|
await settleBeforeRefusal();
|
|
6893
7000
|
return new Response(JSON.stringify({
|
|
6894
|
-
error:
|
|
6895
|
-
error_description:
|
|
6896
|
-
status:
|
|
7001
|
+
error: attest.reason,
|
|
7002
|
+
error_description: "This endpoint requires a verified app instance",
|
|
7003
|
+
status: 401,
|
|
6897
7004
|
request_id: requestId
|
|
6898
7005
|
}), {
|
|
6899
|
-
status:
|
|
6900
|
-
headers
|
|
7006
|
+
status: 401,
|
|
7007
|
+
headers
|
|
6901
7008
|
});
|
|
6902
7009
|
}
|
|
6903
|
-
|
|
6904
|
-
|
|
7010
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
7011
|
+
attestChallengeOut = attest.nextChallenge;
|
|
7012
|
+
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
7013
|
+
if (retryAfter !== null) {
|
|
6905
7014
|
await settleBeforeRefusal();
|
|
6906
7015
|
return new Response(JSON.stringify({
|
|
6907
|
-
error: "
|
|
6908
|
-
error_description:
|
|
6909
|
-
status:
|
|
6910
|
-
request_id: requestId
|
|
7016
|
+
error: "too_many_requests",
|
|
7017
|
+
error_description: "Rate limit exceeded for this endpoint",
|
|
7018
|
+
status: 429,
|
|
7019
|
+
request_id: requestId,
|
|
7020
|
+
data: {
|
|
7021
|
+
retryAfter
|
|
7022
|
+
}
|
|
6911
7023
|
}), {
|
|
6912
|
-
status:
|
|
6913
|
-
headers:
|
|
7024
|
+
status: 429,
|
|
7025
|
+
headers: {
|
|
7026
|
+
...JSON_HEADERS,
|
|
7027
|
+
"retry-after": String(retryAfter)
|
|
7028
|
+
}
|
|
6914
7029
|
});
|
|
6915
7030
|
}
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
}
|
|
6925
|
-
try {
|
|
6926
|
-
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
6927
|
-
} catch {
|
|
6928
|
-
parsedBody = {};
|
|
6929
|
-
}
|
|
6930
|
-
return parsedBody;
|
|
6931
|
-
}, "readParsedBody");
|
|
6932
|
-
const attest = await attestGate(req, rawBody, {
|
|
6933
|
-
userId
|
|
6934
|
-
});
|
|
6935
|
-
if (attest.observation) observations.record(attest.observation);
|
|
6936
|
-
if (attest.outcome === "reject") {
|
|
6937
|
-
const headers = {
|
|
6938
|
-
...JSON_HEADERS
|
|
6939
|
-
};
|
|
6940
|
-
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
6941
|
-
await settleBeforeRefusal();
|
|
6942
|
-
return new Response(JSON.stringify({
|
|
6943
|
-
error: attest.reason,
|
|
6944
|
-
error_description: "This endpoint requires a verified app instance",
|
|
6945
|
-
status: 401,
|
|
6946
|
-
request_id: requestId
|
|
6947
|
-
}), {
|
|
6948
|
-
status: 401,
|
|
6949
|
-
headers
|
|
7031
|
+
let completionUploadId = null;
|
|
7032
|
+
const args = [];
|
|
7033
|
+
let sseEnqueue = null;
|
|
7034
|
+
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
7035
|
+
}, "sseSettle");
|
|
7036
|
+
const sseWriter = makeSseWriter({
|
|
7037
|
+
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
7038
|
+
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
6950
7039
|
});
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
}), {
|
|
6966
|
-
status: 429,
|
|
6967
|
-
headers: {
|
|
6968
|
-
...JSON_HEADERS,
|
|
6969
|
-
"retry-after": String(retryAfter)
|
|
6970
|
-
}
|
|
6971
|
-
});
|
|
6972
|
-
}
|
|
6973
|
-
let completionUploadId = null;
|
|
6974
|
-
const args = [];
|
|
6975
|
-
let sseEnqueue = null;
|
|
6976
|
-
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
6977
|
-
}, "sseSettle");
|
|
6978
|
-
const sseWriter = makeSseWriter({
|
|
6979
|
-
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
6980
|
-
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
6981
|
-
});
|
|
6982
|
-
for (const p of meta.params ?? []) {
|
|
6983
|
-
switch (p.kind) {
|
|
6984
|
-
case "body": {
|
|
6985
|
-
readParsedBody();
|
|
6986
|
-
const r = p.schema.safeParse(parsedBody);
|
|
6987
|
-
if (!r.success) {
|
|
6988
|
-
await settleBeforeRefusal();
|
|
6989
|
-
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
6990
|
-
data: {
|
|
6991
|
-
fields: fieldErrors(r.error)
|
|
6992
|
-
}
|
|
6993
|
-
});
|
|
6994
|
-
}
|
|
6995
|
-
args[p.index] = r.data;
|
|
6996
|
-
break;
|
|
6997
|
-
}
|
|
6998
|
-
case "query": {
|
|
6999
|
-
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
7000
|
-
if (!r.success) {
|
|
7001
|
-
await settleBeforeRefusal();
|
|
7002
|
-
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
7003
|
-
data: {
|
|
7004
|
-
fields: fieldErrors(r.error)
|
|
7005
|
-
}
|
|
7006
|
-
});
|
|
7007
|
-
}
|
|
7008
|
-
args[p.index] = r.data;
|
|
7009
|
-
break;
|
|
7010
|
-
}
|
|
7011
|
-
case "param":
|
|
7012
|
-
args[p.index] = hit.params[p.name];
|
|
7013
|
-
break;
|
|
7014
|
-
case "headers": {
|
|
7015
|
-
const raw = Object.fromEntries(req.headers);
|
|
7016
|
-
if (!p.schema) {
|
|
7017
|
-
args[p.index] = raw;
|
|
7040
|
+
for (const p of meta.params ?? []) {
|
|
7041
|
+
switch (p.kind) {
|
|
7042
|
+
case "body": {
|
|
7043
|
+
readParsedBody();
|
|
7044
|
+
const r = p.schema.safeParse(parsedBody);
|
|
7045
|
+
if (!r.success) {
|
|
7046
|
+
await settleBeforeRefusal();
|
|
7047
|
+
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
7048
|
+
data: {
|
|
7049
|
+
fields: fieldErrors(r.error)
|
|
7050
|
+
}
|
|
7051
|
+
});
|
|
7052
|
+
}
|
|
7053
|
+
args[p.index] = r.data;
|
|
7018
7054
|
break;
|
|
7019
7055
|
}
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
case "user":
|
|
7033
|
-
case "optionalUser":
|
|
7034
|
-
args[p.index] = claims ? {
|
|
7035
|
-
id: userId,
|
|
7036
|
-
email: claims.email,
|
|
7037
|
-
role: claims.role,
|
|
7038
|
-
emailVerified: claims.email_verified === true,
|
|
7039
|
-
metadata: claims.metadata ?? {},
|
|
7040
|
-
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
7041
|
-
// same resolution the gate used, so the handler cannot read a
|
|
7042
|
-
// different set than the one it was admitted on. Never from a
|
|
7043
|
-
// claim: a revoked role has to bite on the next request, and a
|
|
7044
|
-
// token says only what was true when it was minted.
|
|
7045
|
-
roles: await rolesOfCaller(),
|
|
7046
|
-
// Server-owned and only ever filled from a verdict palauth
|
|
7047
|
-
// returned. Never from a raw header, never from JWT metadata —
|
|
7048
|
-
// the type has said so since it was written and this is the
|
|
7049
|
-
// first writer.
|
|
7050
|
-
device: attestedDevice
|
|
7051
|
-
} : null;
|
|
7052
|
-
break;
|
|
7053
|
-
case "uploadedObject": {
|
|
7054
|
-
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
7055
|
-
await settleBeforeRefusal();
|
|
7056
|
-
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
7057
|
-
}
|
|
7058
|
-
readParsedBody();
|
|
7059
|
-
const envelopeIn = parsedBody;
|
|
7060
|
-
if (!envelopeIn?.uploadedObject) {
|
|
7061
|
-
await settleBeforeRefusal();
|
|
7062
|
-
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
7056
|
+
case "query": {
|
|
7057
|
+
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
7058
|
+
if (!r.success) {
|
|
7059
|
+
await settleBeforeRefusal();
|
|
7060
|
+
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
7061
|
+
data: {
|
|
7062
|
+
fields: fieldErrors(r.error)
|
|
7063
|
+
}
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
7066
|
+
args[p.index] = r.data;
|
|
7067
|
+
break;
|
|
7063
7068
|
}
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7069
|
+
case "param":
|
|
7070
|
+
args[p.index] = hit.params[p.name];
|
|
7071
|
+
break;
|
|
7072
|
+
case "headers": {
|
|
7073
|
+
const raw = Object.fromEntries(req.headers);
|
|
7074
|
+
if (!p.schema) {
|
|
7075
|
+
args[p.index] = raw;
|
|
7076
|
+
break;
|
|
7077
|
+
}
|
|
7078
|
+
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
7079
|
+
if (!r.success) {
|
|
7068
7080
|
await settleBeforeRefusal();
|
|
7069
|
-
return
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
} : void 0
|
|
7081
|
+
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
7082
|
+
data: {
|
|
7083
|
+
fields: fieldErrors(r.error)
|
|
7084
|
+
}
|
|
7074
7085
|
});
|
|
7075
7086
|
}
|
|
7076
|
-
|
|
7087
|
+
args[p.index] = r.data;
|
|
7088
|
+
break;
|
|
7077
7089
|
}
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7090
|
+
case "user":
|
|
7091
|
+
case "optionalUser":
|
|
7092
|
+
args[p.index] = claims ? {
|
|
7093
|
+
id: userId,
|
|
7094
|
+
email: claims.email,
|
|
7095
|
+
role: claims.role,
|
|
7096
|
+
emailVerified: claims.email_verified === true,
|
|
7097
|
+
metadata: claims.metadata ?? {},
|
|
7098
|
+
// The caller's APPLICATION roles, from `auth.user_roles` — the
|
|
7099
|
+
// same resolution the gate used, so the handler cannot read a
|
|
7100
|
+
// different set than the one it was admitted on. Never from a
|
|
7101
|
+
// claim: a revoked role has to bite on the next request, and a
|
|
7102
|
+
// token says only what was true when it was minted.
|
|
7103
|
+
roles: await rolesOfCaller(),
|
|
7104
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
7105
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
7106
|
+
// the type has said so since it was written and this is the
|
|
7107
|
+
// first writer.
|
|
7108
|
+
device: attestedDevice
|
|
7109
|
+
} : null;
|
|
7110
|
+
break;
|
|
7111
|
+
case "uploadedObject": {
|
|
7112
|
+
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
7113
|
+
await settleBeforeRefusal();
|
|
7114
|
+
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
7115
|
+
}
|
|
7116
|
+
readParsedBody();
|
|
7117
|
+
const envelopeIn = parsedBody;
|
|
7118
|
+
if (!envelopeIn?.uploadedObject) {
|
|
7119
|
+
await settleBeforeRefusal();
|
|
7120
|
+
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
7121
|
+
}
|
|
7122
|
+
const uploadId = envelopeIn.uploadedObject.uploadId;
|
|
7123
|
+
if (typeof uploadId === "string" && uploadId !== "") {
|
|
7124
|
+
const already = completions.recall(uploadId);
|
|
7125
|
+
if (already) {
|
|
7126
|
+
await settleBeforeRefusal();
|
|
7127
|
+
return new Response(already.body, {
|
|
7128
|
+
status: already.status,
|
|
7129
|
+
headers: already.contentType ? {
|
|
7130
|
+
"content-type": already.contentType
|
|
7131
|
+
} : void 0
|
|
7132
|
+
});
|
|
7133
|
+
}
|
|
7134
|
+
completionUploadId = uploadId;
|
|
7135
|
+
}
|
|
7136
|
+
args[p.index] = envelopeIn.uploadedObject;
|
|
7137
|
+
parsedBody = envelopeIn.body ?? {};
|
|
7138
|
+
break;
|
|
7139
|
+
}
|
|
7140
|
+
case "requestId":
|
|
7141
|
+
args[p.index] = requestId;
|
|
7142
|
+
break;
|
|
7143
|
+
case "traceId":
|
|
7144
|
+
args[p.index] = requestId;
|
|
7145
|
+
break;
|
|
7146
|
+
case "client":
|
|
7147
|
+
args[p.index] = {
|
|
7148
|
+
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
7149
|
+
appVersion: req.headers.get("x-palbase-client-version"),
|
|
7150
|
+
platform: req.headers.get("x-platform"),
|
|
7151
|
+
osVersion: req.headers.get("x-os-version")
|
|
7152
|
+
};
|
|
7153
|
+
break;
|
|
7154
|
+
case "req":
|
|
7155
|
+
args[p.index] = req;
|
|
7156
|
+
break;
|
|
7157
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
7158
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
7159
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
7160
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
7161
|
+
// request-scoped data and no signal.
|
|
7162
|
+
case "signal":
|
|
7163
|
+
args[p.index] = req.signal;
|
|
7164
|
+
break;
|
|
7165
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
7166
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
7167
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
7168
|
+
// route.
|
|
7169
|
+
case "sseOut":
|
|
7170
|
+
args[p.index] = sseWriter;
|
|
7171
|
+
break;
|
|
7172
|
+
default:
|
|
7173
|
+
args[p.index] = void 0;
|
|
7081
7174
|
}
|
|
7082
|
-
case "requestId":
|
|
7083
|
-
args[p.index] = requestId;
|
|
7084
|
-
break;
|
|
7085
|
-
case "traceId":
|
|
7086
|
-
args[p.index] = requestId;
|
|
7087
|
-
break;
|
|
7088
|
-
case "client":
|
|
7089
|
-
args[p.index] = {
|
|
7090
|
-
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
7091
|
-
appVersion: req.headers.get("x-palbase-client-version"),
|
|
7092
|
-
platform: req.headers.get("x-platform"),
|
|
7093
|
-
osVersion: req.headers.get("x-os-version")
|
|
7094
|
-
};
|
|
7095
|
-
break;
|
|
7096
|
-
case "req":
|
|
7097
|
-
args[p.index] = req;
|
|
7098
|
-
break;
|
|
7099
|
-
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
7100
|
-
// state when the client disconnects. Measured on Bun: an infinite
|
|
7101
|
-
// producer guarded by it stops within a few frames of the client dying.
|
|
7102
|
-
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
7103
|
-
// request-scoped data and no signal.
|
|
7104
|
-
case "signal":
|
|
7105
|
-
args[p.index] = req.signal;
|
|
7106
|
-
break;
|
|
7107
|
-
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
7108
|
-
// on any other route it is an inert writer whose frames go nowhere,
|
|
7109
|
-
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
7110
|
-
// route.
|
|
7111
|
-
case "sseOut":
|
|
7112
|
-
args[p.index] = sseWriter;
|
|
7113
|
-
break;
|
|
7114
|
-
default:
|
|
7115
|
-
args[p.index] = void 0;
|
|
7116
7175
|
}
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
try {
|
|
7176
|
+
db.authorizationComplete?.();
|
|
7177
|
+
dispatched = true;
|
|
7120
7178
|
const services = buildServices(db);
|
|
7121
7179
|
if (meta.options?.sseConfig !== void 0) {
|
|
7122
7180
|
let sseController;
|
|
@@ -7132,11 +7190,19 @@ async function createApp(opts) {
|
|
|
7132
7190
|
} catch {
|
|
7133
7191
|
}
|
|
7134
7192
|
}, "sseEnqueue");
|
|
7135
|
-
let sawFirstFrame;
|
|
7136
|
-
const firstFrame = new Promise((
|
|
7193
|
+
let sawFirstFrame, refuseFirstFrame;
|
|
7194
|
+
const firstFrame = new Promise((resolve, reject) => {
|
|
7195
|
+
sawFirstFrame = resolve;
|
|
7196
|
+
refuseFirstFrame = reject;
|
|
7197
|
+
});
|
|
7137
7198
|
sseSettle = /* @__PURE__ */ __name(async () => {
|
|
7138
|
-
|
|
7139
|
-
|
|
7199
|
+
try {
|
|
7200
|
+
await db.commit();
|
|
7201
|
+
sawFirstFrame();
|
|
7202
|
+
} catch (error) {
|
|
7203
|
+
refuseFirstFrame(error);
|
|
7204
|
+
throw error;
|
|
7205
|
+
}
|
|
7140
7206
|
}, "sseSettle");
|
|
7141
7207
|
const guarded = {
|
|
7142
7208
|
...services,
|
|
@@ -7177,6 +7243,7 @@ async function createApp(opts) {
|
|
|
7177
7243
|
status: 204
|
|
7178
7244
|
});
|
|
7179
7245
|
}
|
|
7246
|
+
await firstFrame;
|
|
7180
7247
|
void (async () => {
|
|
7181
7248
|
await ran;
|
|
7182
7249
|
await sseWriter.drained();
|
|
@@ -7210,15 +7277,16 @@ async function createApp(opts) {
|
|
|
7210
7277
|
}
|
|
7211
7278
|
return method.apply(hit.entry.instance, args);
|
|
7212
7279
|
});
|
|
7213
|
-
await db.commit();
|
|
7214
7280
|
if (meta.returnSchema) {
|
|
7215
7281
|
const v = meta.returnSchema.safeParse(result);
|
|
7216
7282
|
if (!v.success) {
|
|
7217
7283
|
log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
|
|
7284
|
+
await db.rollback(new Error("output_invalid"));
|
|
7218
7285
|
return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
|
|
7219
7286
|
}
|
|
7220
7287
|
}
|
|
7221
|
-
if (result === void 0 || result === null) {
|
|
7288
|
+
if (result === void 0 || result === null && !meta.returnSchema) {
|
|
7289
|
+
await db.commit();
|
|
7222
7290
|
if (completionUploadId) {
|
|
7223
7291
|
completions.remember(completionUploadId, {
|
|
7224
7292
|
status: 204,
|
|
@@ -7231,6 +7299,7 @@ async function createApp(opts) {
|
|
|
7231
7299
|
});
|
|
7232
7300
|
}
|
|
7233
7301
|
const payload = JSON.stringify(result);
|
|
7302
|
+
await db.commit();
|
|
7234
7303
|
if (completionUploadId) {
|
|
7235
7304
|
completions.remember(completionUploadId, {
|
|
7236
7305
|
status: 200,
|
|
@@ -7247,16 +7316,7 @@ async function createApp(opts) {
|
|
|
7247
7316
|
});
|
|
7248
7317
|
} catch (err) {
|
|
7249
7318
|
await db.rollback(err);
|
|
7250
|
-
|
|
7251
|
-
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
7252
|
-
log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
|
|
7253
|
-
}
|
|
7254
|
-
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
7255
|
-
data: err.data
|
|
7256
|
-
} : void 0);
|
|
7257
|
-
}
|
|
7258
|
-
log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
|
|
7259
|
-
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
7319
|
+
return errorResponse(err, hit.entry.id, requestId);
|
|
7260
7320
|
}
|
|
7261
7321
|
}
|
|
7262
7322
|
__name(handle, "handle");
|
|
@@ -7269,15 +7329,24 @@ async function createApp(opts) {
|
|
|
7269
7329
|
throw err;
|
|
7270
7330
|
}
|
|
7271
7331
|
return {
|
|
7272
|
-
handle,
|
|
7332
|
+
handle: /* @__PURE__ */ __name((req) => !accepting ? Promise.resolve(envelope("runtime_draining", "This application is shutting down", 503, `req_${crypto.randomUUID()}`)) : serving(() => handle(req)), "handle"),
|
|
7333
|
+
get databasePressure() {
|
|
7334
|
+
return admission.snapshot;
|
|
7335
|
+
},
|
|
7273
7336
|
routes,
|
|
7274
7337
|
config,
|
|
7275
7338
|
container: liveContainer,
|
|
7276
|
-
runInServiceScope,
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7339
|
+
runInServiceScope: /* @__PURE__ */ __name((fn) => !accepting ? Promise.reject(new Error("This application is shutting down; no new database scope can start")) : serving(() => runInServiceScope(fn)), "runInServiceScope"),
|
|
7340
|
+
shutdown() {
|
|
7341
|
+
accepting = false;
|
|
7342
|
+
return shuttingDown ??= (async () => {
|
|
7343
|
+
while (inFlight.size) await Promise.allSettled([
|
|
7344
|
+
...inFlight
|
|
7345
|
+
]);
|
|
7346
|
+
await runShutdownHooks();
|
|
7347
|
+
await closeDriver(sql);
|
|
7348
|
+
released = true;
|
|
7349
|
+
})();
|
|
7281
7350
|
}
|
|
7282
7351
|
};
|
|
7283
7352
|
}
|