@palbase/backend 23.0.0 → 23.1.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 +313 -24
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +5 -5
- package/dist/{chunk-Y5HXVUMP.js → chunk-HQRJDARQ.js} +2 -2
- package/dist/{chunk-REZU6UKT.js → chunk-M5MCBWJI.js} +296 -25
- package/dist/chunk-M5MCBWJI.js.map +1 -0
- package/dist/{chunk-W5ODXPY3.js → chunk-NS5V43YQ.js} +14 -1
- package/dist/chunk-NS5V43YQ.js.map +1 -0
- package/dist/{chunk-FSGSB42K.js → chunk-OHALWEOG.js} +9 -4
- package/dist/chunk-OHALWEOG.js.map +1 -0
- package/dist/{chunk-HAF67F2H.js → chunk-PY7YJDCT.js} +57 -3
- package/dist/chunk-PY7YJDCT.js.map +1 -0
- package/dist/{chunk-ZC6Q2BRD.js → chunk-R3KN6RHD.js} +2 -2
- package/dist/{chunk-OMRTHM4X.js → chunk-RCLNBJCM.js} +1 -1
- package/dist/chunk-RCLNBJCM.js.map +1 -0
- package/dist/db/index.cjs +68 -1
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +2 -2
- package/dist/{endpoint-BavvbW4P.d.ts → endpoint-CVWXh6oG.d.ts} +70 -8
- package/dist/{endpoint-i8TTCohk.d.cts → endpoint-c9h5jriX.d.cts} +70 -8
- package/dist/engine/index.cjs +313 -24
- 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 +4 -4
- package/dist/{index-B7YBEG5w.d.ts → index-BZrJXnVh.d.ts} +49 -4
- package/dist/{index-Bmvx1EvJ.d.cts → index-By8Dle5U.d.cts} +86 -17
- package/dist/{index-B3jmmItD.d.ts → index-CwAJ7HEe.d.ts} +86 -17
- package/dist/{index-E7OscPJT.d.cts → index-CxeQSfJP.d.cts} +49 -4
- package/dist/index.cjs +104 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +32 -6
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +29 -4
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +5 -2
- package/dist/openapi/index.d.ts +5 -2
- package/dist/openapi/index.js +32 -7
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-DY3d9l1k.d.ts → registry-B3niOVYp.d.ts} +101 -7
- package/dist/{registry-C3H2uPeZ.d.cts → registry-CqPK2Qby.d.cts} +101 -7
- package/dist/test/index.cjs +482 -9
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +35 -3
- package/dist/test/index.d.ts +35 -3
- package/dist/test/index.js +480 -8
- package/dist/test/index.js.map +1 -1
- package/package.json +4 -3
- package/stager/return_types.js +23 -0
- package/dist/chunk-FSGSB42K.js.map +0 -1
- package/dist/chunk-HAF67F2H.js.map +0 -1
- package/dist/chunk-OMRTHM4X.js.map +0 -1
- package/dist/chunk-REZU6UKT.js.map +0 -1
- package/dist/chunk-W5ODXPY3.js.map +0 -1
- /package/dist/{chunk-Y5HXVUMP.js.map → chunk-HQRJDARQ.js.map} +0 -0
- /package/dist/{chunk-ZC6Q2BRD.js.map → chunk-R3KN6RHD.js.map} +0 -0
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
BootRefused,
|
|
4
4
|
createApp,
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-M5MCBWJI.js";
|
|
7
|
+
import "../chunk-OHALWEOG.js";
|
|
8
|
+
import "../chunk-NS5V43YQ.js";
|
|
9
9
|
import {
|
|
10
10
|
getRegisteredControllers
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import "../chunk-
|
|
11
|
+
} from "../chunk-HQRJDARQ.js";
|
|
12
|
+
import "../chunk-RCLNBJCM.js";
|
|
13
13
|
import "../chunk-7D4SUZUM.js";
|
|
14
14
|
|
|
15
15
|
// src/bin/palbase-backend.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getRoutes
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RCLNBJCM.js";
|
|
4
4
|
|
|
5
5
|
// src/decorators/controller.ts
|
|
6
6
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
@@ -87,4 +87,4 @@ export {
|
|
|
87
87
|
isController,
|
|
88
88
|
resolveController
|
|
89
89
|
};
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
90
|
+
//# sourceMappingURL=chunk-HQRJDARQ.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__requestALS,
|
|
3
3
|
__runWithRuntime
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OHALWEOG.js";
|
|
5
5
|
import {
|
|
6
6
|
getRoutes,
|
|
7
7
|
isHttpError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RCLNBJCM.js";
|
|
9
9
|
|
|
10
10
|
// src/engine/config.ts
|
|
11
11
|
var BootRefused = class extends Error {
|
|
@@ -323,6 +323,14 @@ function makeMemoryCache(opts = {}) {
|
|
|
323
323
|
function quoteIdent(name) {
|
|
324
324
|
return `"${name.replace(/"/g, '""')}"`;
|
|
325
325
|
}
|
|
326
|
+
function encodePgArray(value) {
|
|
327
|
+
const element = (x) => {
|
|
328
|
+
if (x === null || x === void 0) return "NULL";
|
|
329
|
+
if (Array.isArray(x)) return encodePgArray(x);
|
|
330
|
+
return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
|
|
331
|
+
};
|
|
332
|
+
return `{${value.map(element).join(",")}}`;
|
|
333
|
+
}
|
|
326
334
|
var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
|
|
327
335
|
function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
328
336
|
const { lockTimeout } = options;
|
|
@@ -543,7 +551,9 @@ function expandSynonyms(query, map) {
|
|
|
543
551
|
}).join("");
|
|
544
552
|
}).join("");
|
|
545
553
|
}
|
|
546
|
-
var searchLogger =
|
|
554
|
+
var searchLogger = (evt, fields) => {
|
|
555
|
+
console.log(evt, JSON.stringify(fields));
|
|
556
|
+
};
|
|
547
557
|
function logNoResults(table, query, mode, n) {
|
|
548
558
|
if (n === 0) {
|
|
549
559
|
searchLogger?.("palbase.search.no_results", {
|
|
@@ -584,19 +594,41 @@ function validitySql(v, add) {
|
|
|
584
594
|
function excludeSql(pk, ids, add) {
|
|
585
595
|
if (ids.length === 0) return "";
|
|
586
596
|
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
587
|
-
return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
597
|
+
return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
598
|
+
}
|
|
599
|
+
function schemaColumns(table) {
|
|
600
|
+
const t = Object.values(currentSchema.tables ?? {}).find((x) => x?.name === table);
|
|
601
|
+
const cols = t?.columns ? Object.keys(t.columns) : [];
|
|
602
|
+
return cols.length > 0 ? new Set(cols) : null;
|
|
603
|
+
}
|
|
604
|
+
function orderClause(table, known, orderBy) {
|
|
605
|
+
if (!orderBy) return "";
|
|
606
|
+
const col = orderBy.column;
|
|
607
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
608
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
609
|
+
throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
|
|
610
|
+
}
|
|
611
|
+
const dir = orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
612
|
+
return ` ORDER BY ${quoteIdent(col)} ${dir}`;
|
|
588
613
|
}
|
|
589
|
-
function
|
|
614
|
+
function limitClause(limit) {
|
|
615
|
+
if (limit === void 0) return "";
|
|
616
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
617
|
+
throw new Error(`findMany: limit bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(limit)})`);
|
|
618
|
+
}
|
|
619
|
+
return ` LIMIT ${limit}`;
|
|
620
|
+
}
|
|
621
|
+
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
590
622
|
const parts = [];
|
|
591
623
|
for (const [col, cond] of Object.entries(where)) {
|
|
592
|
-
if (!colSet.has(col)) {
|
|
593
|
-
throw new Error(
|
|
624
|
+
if (colSet !== null && !colSet.has(col)) {
|
|
625
|
+
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
594
626
|
}
|
|
595
627
|
const q = `t.${quoteIdent(col)}`;
|
|
596
628
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
597
629
|
for (const [op, v] of Object.entries(cond)) {
|
|
598
630
|
if (op === "in") {
|
|
599
|
-
if (!Array.isArray(v)) throw new Error(
|
|
631
|
+
if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
600
632
|
if (v.length === 0) {
|
|
601
633
|
parts.push("false");
|
|
602
634
|
continue;
|
|
@@ -605,7 +637,7 @@ function compileWhere(table, colSet, where, add) {
|
|
|
605
637
|
} else if (op in WHERE_OPS) {
|
|
606
638
|
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
607
639
|
} else {
|
|
608
|
-
throw new Error(
|
|
640
|
+
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
609
641
|
}
|
|
610
642
|
}
|
|
611
643
|
} else {
|
|
@@ -685,7 +717,8 @@ function createOps(tx) {
|
|
|
685
717
|
const at = () => resolveTx(tx);
|
|
686
718
|
const ops = {
|
|
687
719
|
async query(sql, params = []) {
|
|
688
|
-
|
|
720
|
+
const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
|
|
721
|
+
return asWireRows(await (await at()).unsafe(sql, bound));
|
|
689
722
|
},
|
|
690
723
|
async insert(table, data) {
|
|
691
724
|
const cols = Object.keys(data);
|
|
@@ -701,6 +734,41 @@ function createOps(tx) {
|
|
|
701
734
|
}
|
|
702
735
|
return asTableRow(table, inserted);
|
|
703
736
|
},
|
|
737
|
+
/**
|
|
738
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
739
|
+
*
|
|
740
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
741
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
742
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
743
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
744
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
745
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
746
|
+
* every table with a unique row.
|
|
747
|
+
*
|
|
748
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
749
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
750
|
+
*/
|
|
751
|
+
async upsert(table, data, opts) {
|
|
752
|
+
const conflict = opts.onConflict;
|
|
753
|
+
if (conflict.length === 0) {
|
|
754
|
+
throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
|
|
755
|
+
}
|
|
756
|
+
const cols = Object.keys(data);
|
|
757
|
+
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
758
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
759
|
+
const conflictSet = new Set(conflict);
|
|
760
|
+
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
761
|
+
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
762
|
+
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
763
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
764
|
+
const row = rows[0];
|
|
765
|
+
if (!row) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
return asTableRow(table, row);
|
|
771
|
+
},
|
|
704
772
|
async update(table, id, data) {
|
|
705
773
|
const cols = Object.keys(data);
|
|
706
774
|
if (cols.length === 0) return ops.findById(table, id);
|
|
@@ -719,13 +787,18 @@ function createOps(tx) {
|
|
|
719
787
|
);
|
|
720
788
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
721
789
|
},
|
|
722
|
-
async findMany(table, query = {}) {
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
)
|
|
790
|
+
async findMany(table, query = {}, opts = {}) {
|
|
791
|
+
const params = [];
|
|
792
|
+
const add = (v) => {
|
|
793
|
+
params.push(v);
|
|
794
|
+
return `$${params.length}`;
|
|
795
|
+
};
|
|
796
|
+
const known = schemaColumns(table);
|
|
797
|
+
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
798
|
+
const order = orderClause(table, known, opts.orderBy);
|
|
799
|
+
const limit = limitClause(opts.limit);
|
|
800
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
|
|
801
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
729
802
|
},
|
|
730
803
|
/**
|
|
731
804
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
@@ -1116,6 +1189,27 @@ function createOps(tx) {
|
|
|
1116
1189
|
const live = await at();
|
|
1117
1190
|
return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
|
|
1118
1191
|
},
|
|
1192
|
+
/**
|
|
1193
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
1194
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
1195
|
+
*
|
|
1196
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
1197
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
1198
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
1199
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
1200
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
1201
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
1202
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
1203
|
+
* you can see in the code that crosses it.
|
|
1204
|
+
*
|
|
1205
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
1206
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
1207
|
+
* the surrounding transaction.
|
|
1208
|
+
*/
|
|
1209
|
+
async attempt(fn) {
|
|
1210
|
+
const live = await at();
|
|
1211
|
+
return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
|
|
1212
|
+
},
|
|
1119
1213
|
/**
|
|
1120
1214
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
1121
1215
|
*
|
|
@@ -1166,22 +1260,47 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1166
1260
|
update: (id, data) => ops.update(name, id, data),
|
|
1167
1261
|
delete: (id) => ops.delete(name, id),
|
|
1168
1262
|
findById: (id) => ops.findById(name, id),
|
|
1169
|
-
findMany: (query) => ops.findMany(name, query ?? {})
|
|
1263
|
+
findMany: (query, opts) => ops.findMany(name, query ?? {}, opts),
|
|
1264
|
+
upsert: (data, opts) => ops.upsert(name, data, opts)
|
|
1170
1265
|
};
|
|
1171
1266
|
}
|
|
1172
1267
|
const base = /* @__PURE__ */ Object.create(null);
|
|
1173
1268
|
return Object.assign(base, ops, { tables });
|
|
1174
1269
|
}
|
|
1175
1270
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1271
|
+
function isAbortedTransaction(e) {
|
|
1272
|
+
const code = e?.code;
|
|
1273
|
+
const message = String(e?.message ?? "");
|
|
1274
|
+
return code === "25P02" || /current transaction is aborted/i.test(message);
|
|
1275
|
+
}
|
|
1276
|
+
function isForeignKeyViolation(e) {
|
|
1277
|
+
return e?.code === "23503";
|
|
1278
|
+
}
|
|
1176
1279
|
function isLockTimeout(e) {
|
|
1177
1280
|
const code = e?.code;
|
|
1178
1281
|
const message = String(e?.message ?? "");
|
|
1179
1282
|
return code === "55P03" || /lock timeout/i.test(message);
|
|
1180
1283
|
}
|
|
1181
|
-
function diagnosingDriver(sql) {
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1284
|
+
function diagnosingDriver(sql, surface = "service") {
|
|
1285
|
+
const original = (e) => String(e?.message ?? e);
|
|
1286
|
+
const explain = (e) => {
|
|
1287
|
+
if (isAbortedTransaction(e)) {
|
|
1288
|
+
return new Error(
|
|
1289
|
+
`This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1293
|
+
return new Error(
|
|
1294
|
+
`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)})`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (isLockTimeout(e)) {
|
|
1298
|
+
return new Error(
|
|
1299
|
+
`Database.asService() waited too long for a row lock. It runs in its OWN transaction, so a row this request already wrote through Database.* is locked against it until the request commits \u2014 a wait that cannot end. Do that row's work on one surface or the other. (${original(e)})`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
return e;
|
|
1303
|
+
};
|
|
1185
1304
|
const wrapTx = (tx) => ({
|
|
1186
1305
|
async unsafe(text, params) {
|
|
1187
1306
|
try {
|
|
@@ -1200,13 +1319,13 @@ function diagnosingDriver(sql) {
|
|
|
1200
1319
|
};
|
|
1201
1320
|
}
|
|
1202
1321
|
function createRequestDatabase(sql, identity) {
|
|
1203
|
-
const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
|
|
1322
|
+
const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
|
|
1204
1323
|
let serviceTx = null;
|
|
1205
1324
|
let serviceClient = null;
|
|
1206
1325
|
const asService = () => {
|
|
1207
1326
|
if (serviceClient === null) {
|
|
1208
1327
|
serviceTx = createLazyTransaction(
|
|
1209
|
-
diagnosingDriver(sql),
|
|
1328
|
+
diagnosingDriver(sql, "service"),
|
|
1210
1329
|
identity.serviceRole,
|
|
1211
1330
|
identity.claimsJson,
|
|
1212
1331
|
{ lockTimeout: SERVICE_LOCK_TIMEOUT }
|
|
@@ -1287,6 +1406,23 @@ async function runPlanOp(sp, op, results) {
|
|
|
1287
1406
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1288
1407
|
break;
|
|
1289
1408
|
}
|
|
1409
|
+
case "upsert": {
|
|
1410
|
+
const cols = Object.keys(op.values ?? {});
|
|
1411
|
+
const conflict = op.onConflict ?? [];
|
|
1412
|
+
if (cols.length === 0 || conflict.length === 0) {
|
|
1413
|
+
throw Object.assign(new Error(`upsert on ${op.table} needs columns and onConflict`), {
|
|
1414
|
+
error_code: "tx_bad_upsert"
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
const rendered = cols.map(
|
|
1418
|
+
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
1419
|
+
);
|
|
1420
|
+
const conflictSet = new Set(conflict);
|
|
1421
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1422
|
+
const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1423
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1290
1426
|
case "insertMany": {
|
|
1291
1427
|
const rows = op.rows ?? [];
|
|
1292
1428
|
if (rows.length === 0 || !rows[0]) return [];
|
|
@@ -1447,6 +1583,41 @@ function verifySignature(presented, expected) {
|
|
|
1447
1583
|
return diff === 0;
|
|
1448
1584
|
}
|
|
1449
1585
|
|
|
1586
|
+
// src/engine/sse.ts
|
|
1587
|
+
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
1588
|
+
function encodeFrame(value) {
|
|
1589
|
+
return `data: ${JSON.stringify(value)}
|
|
1590
|
+
|
|
1591
|
+
`;
|
|
1592
|
+
}
|
|
1593
|
+
function encodeErrorFrame(requestId) {
|
|
1594
|
+
return `event: error
|
|
1595
|
+
data: ${JSON.stringify({ requestId })}
|
|
1596
|
+
|
|
1597
|
+
`;
|
|
1598
|
+
}
|
|
1599
|
+
function makeSseWriter(opts) {
|
|
1600
|
+
let begun = false;
|
|
1601
|
+
let chain = Promise.resolve();
|
|
1602
|
+
return {
|
|
1603
|
+
write(value) {
|
|
1604
|
+
const frame = encodeFrame(value);
|
|
1605
|
+
if (!begun) {
|
|
1606
|
+
begun = true;
|
|
1607
|
+
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
chain = chain.then(() => opts.enqueue(frame));
|
|
1611
|
+
},
|
|
1612
|
+
started() {
|
|
1613
|
+
return begun;
|
|
1614
|
+
},
|
|
1615
|
+
drained() {
|
|
1616
|
+
return chain;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1450
1621
|
// src/engine/fence.ts
|
|
1451
1622
|
var SECRET_ENV = [
|
|
1452
1623
|
"DATABASE_URL",
|
|
@@ -1682,6 +1853,13 @@ async function createApp(opts) {
|
|
|
1682
1853
|
const args = [];
|
|
1683
1854
|
let parsedBody;
|
|
1684
1855
|
let bodyRead = false;
|
|
1856
|
+
let sseEnqueue = null;
|
|
1857
|
+
let sseSettle = async () => {
|
|
1858
|
+
};
|
|
1859
|
+
const sseWriter = makeSseWriter({
|
|
1860
|
+
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
1861
|
+
onFirstWrite: () => sseSettle()
|
|
1862
|
+
});
|
|
1685
1863
|
for (const p of meta.params ?? []) {
|
|
1686
1864
|
switch (p.kind) {
|
|
1687
1865
|
case "body": {
|
|
@@ -1785,6 +1963,21 @@ async function createApp(opts) {
|
|
|
1785
1963
|
case "req":
|
|
1786
1964
|
args[p.index] = req;
|
|
1787
1965
|
break;
|
|
1966
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
1967
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
1968
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
1969
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
1970
|
+
// request-scoped data and no signal.
|
|
1971
|
+
case "signal":
|
|
1972
|
+
args[p.index] = req.signal;
|
|
1973
|
+
break;
|
|
1974
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
1975
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
1976
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
1977
|
+
// route.
|
|
1978
|
+
case "sseOut":
|
|
1979
|
+
args[p.index] = sseWriter;
|
|
1980
|
+
break;
|
|
1788
1981
|
default:
|
|
1789
1982
|
args[p.index] = void 0;
|
|
1790
1983
|
}
|
|
@@ -1796,6 +1989,84 @@ async function createApp(opts) {
|
|
|
1796
1989
|
});
|
|
1797
1990
|
try {
|
|
1798
1991
|
const services = buildServices(db);
|
|
1992
|
+
if (meta.options?.sseConfig !== void 0) {
|
|
1993
|
+
let sseController;
|
|
1994
|
+
const sseEncoder = new TextEncoder();
|
|
1995
|
+
const stream = new ReadableStream({
|
|
1996
|
+
start(c) {
|
|
1997
|
+
sseController = c;
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
sseEnqueue = (chunk) => {
|
|
2001
|
+
try {
|
|
2002
|
+
sseController.enqueue(sseEncoder.encode(chunk));
|
|
2003
|
+
} catch {
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
let sawFirstFrame;
|
|
2007
|
+
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
2008
|
+
sseSettle = async () => {
|
|
2009
|
+
await db.commit();
|
|
2010
|
+
sawFirstFrame();
|
|
2011
|
+
};
|
|
2012
|
+
const guarded = {
|
|
2013
|
+
...services,
|
|
2014
|
+
Database: new Proxy(services.Database, {
|
|
2015
|
+
get(target, prop, recv) {
|
|
2016
|
+
if (sseWriter.started()) {
|
|
2017
|
+
throw new Error(
|
|
2018
|
+
`sse_db_after_write: ${hit.entry.id} touched the database after its first write(). A streaming response settles its transaction with the first frame \u2014 do the database work before the first write.`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return Reflect.get(target, prop, recv);
|
|
2022
|
+
}
|
|
2023
|
+
})
|
|
2024
|
+
};
|
|
2025
|
+
let thrown = null;
|
|
2026
|
+
const ran = Promise.resolve(runWithRuntime(guarded, () => {
|
|
2027
|
+
const box = requestALS.getStore();
|
|
2028
|
+
if (box) {
|
|
2029
|
+
box.userId = userId ?? null;
|
|
2030
|
+
box.requestId = requestId;
|
|
2031
|
+
box.idempotencyKey = req.headers.get("idempotency-key");
|
|
2032
|
+
}
|
|
2033
|
+
const method = hit.entry.instance[meta.fnName];
|
|
2034
|
+
if (typeof method !== "function") {
|
|
2035
|
+
throw new Error(
|
|
2036
|
+
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
return method.apply(hit.entry.instance, args);
|
|
2040
|
+
})).then(
|
|
2041
|
+
() => void 0,
|
|
2042
|
+
(err) => {
|
|
2043
|
+
thrown = err;
|
|
2044
|
+
}
|
|
2045
|
+
);
|
|
2046
|
+
await Promise.race([firstFrame, ran]);
|
|
2047
|
+
if (!sseWriter.started()) {
|
|
2048
|
+
await ran;
|
|
2049
|
+
await db.commit();
|
|
2050
|
+
if (thrown !== null) throw thrown;
|
|
2051
|
+
return new Response(null, { status: 204 });
|
|
2052
|
+
}
|
|
2053
|
+
void (async () => {
|
|
2054
|
+
await ran;
|
|
2055
|
+
await sseWriter.drained();
|
|
2056
|
+
try {
|
|
2057
|
+
if (thrown !== null) {
|
|
2058
|
+
log.error(`[engine] ${hit.entry.id} threw mid-stream`, thrown);
|
|
2059
|
+
sseController.enqueue(sseEncoder.encode(encodeErrorFrame(requestId)));
|
|
2060
|
+
}
|
|
2061
|
+
sseController.close();
|
|
2062
|
+
} catch {
|
|
2063
|
+
}
|
|
2064
|
+
})();
|
|
2065
|
+
return new Response(stream, {
|
|
2066
|
+
status: 200,
|
|
2067
|
+
headers: { "content-type": SSE_CONTENT_TYPE, "cache-control": "no-cache" }
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
1799
2070
|
const result = await runWithRuntime(services, () => {
|
|
1800
2071
|
const box = requestALS.getStore();
|
|
1801
2072
|
if (box) {
|
|
@@ -1886,4 +2157,4 @@ export {
|
|
|
1886
2157
|
installEgressFence,
|
|
1887
2158
|
createApp
|
|
1888
2159
|
};
|
|
1889
|
-
//# sourceMappingURL=chunk-
|
|
2160
|
+
//# sourceMappingURL=chunk-M5MCBWJI.js.map
|