@palbase/backend 23.0.0 → 24.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 +695 -61
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -5
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-FSGSB42K.js → chunk-7Z6MGMXQ.js} +64 -4
- package/dist/chunk-7Z6MGMXQ.js.map +1 -0
- package/dist/{chunk-OMRTHM4X.js → chunk-H3JAISUY.js} +136 -1
- package/dist/chunk-H3JAISUY.js.map +1 -0
- package/dist/{chunk-REZU6UKT.js → chunk-NXDH6VQJ.js} +549 -42
- package/dist/chunk-NXDH6VQJ.js.map +1 -0
- package/dist/{chunk-W5ODXPY3.js → chunk-P2Q27SGP.js} +32 -3
- package/dist/chunk-P2Q27SGP.js.map +1 -0
- package/dist/{chunk-ZC6Q2BRD.js → chunk-T5IOSOE5.js} +7 -2
- package/dist/chunk-T5IOSOE5.js.map +1 -0
- package/dist/{chunk-HAF67F2H.js → chunk-ZUGY7RGS.js} +86 -3
- package/dist/chunk-ZUGY7RGS.js.map +1 -0
- package/dist/db/index.cjs +115 -3
- 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-0_DGBajf.d.ts} +168 -9
- package/dist/{endpoint-i8TTCohk.d.cts → endpoint-CcQ1a36a.d.cts} +168 -9
- package/dist/engine/index.cjs +684 -48
- 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-B3jmmItD.d.ts → index-CJiJU9ux.d.ts} +209 -36
- package/dist/{index-Bmvx1EvJ.d.cts → index-D-4-PNuQ.d.cts} +209 -36
- package/dist/{index-B7YBEG5w.d.ts → index-D17r-MKb.d.ts} +177 -7
- package/dist/{index-E7OscPJT.d.cts → index-DRFxf07H.d.cts} +177 -7
- package/dist/index.cjs +269 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +54 -11
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +45 -5
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +9 -4
- package/dist/openapi/index.d.ts +9 -4
- package/dist/openapi/index.js +35 -11
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-C3H2uPeZ.d.cts → registry-1X-skBNu.d.cts} +101 -7
- package/dist/{registry-DY3d9l1k.d.ts → registry-CEod_5sz.d.ts} +101 -7
- package/dist/test/index.cjs +509 -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 +507 -8
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +4 -4
- package/docs/database.md +115 -11
- package/docs/getting-started.md +5 -4
- package/docs/llms-full.txt +385 -89
- package/docs/migrations.md +81 -59
- package/docs/schema.md +82 -2
- package/docs/services.md +98 -9
- package/package.json +3 -2
- package/stager/return_types.js +23 -0
- package/template/AGENTS.md +121 -41
- package/template/controllers/notes.controller.ts +64 -0
- package/template/package.json +1 -1
- package/template/services/note.service.ts +74 -0
- package/template/tsconfig.json +11 -1
- 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 +0 -90
- package/dist/chunk-Y5HXVUMP.js.map +0 -1
- package/dist/chunk-ZC6Q2BRD.js.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__requestALS,
|
|
3
|
+
__runStartHooks,
|
|
3
4
|
__runWithRuntime
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7Z6MGMXQ.js";
|
|
5
6
|
import {
|
|
7
|
+
UniqueViolation,
|
|
8
|
+
assertZeroArgConstructor,
|
|
6
9
|
getRoutes,
|
|
7
|
-
isHttpError
|
|
8
|
-
|
|
10
|
+
isHttpError,
|
|
11
|
+
resolveEffectiveAuth
|
|
12
|
+
} from "./chunk-H3JAISUY.js";
|
|
9
13
|
|
|
10
14
|
// src/engine/config.ts
|
|
11
15
|
var BootRefused = class extends Error {
|
|
@@ -323,6 +327,14 @@ function makeMemoryCache(opts = {}) {
|
|
|
323
327
|
function quoteIdent(name) {
|
|
324
328
|
return `"${name.replace(/"/g, '""')}"`;
|
|
325
329
|
}
|
|
330
|
+
function encodePgArray(value) {
|
|
331
|
+
const element = (x) => {
|
|
332
|
+
if (x === null || x === void 0) return "NULL";
|
|
333
|
+
if (Array.isArray(x)) return encodePgArray(x);
|
|
334
|
+
return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
|
|
335
|
+
};
|
|
336
|
+
return `{${value.map(element).join(",")}}`;
|
|
337
|
+
}
|
|
326
338
|
var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
|
|
327
339
|
function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
328
340
|
const { lockTimeout } = options;
|
|
@@ -405,18 +417,48 @@ function reviveVectors(row, vectorCols) {
|
|
|
405
417
|
}
|
|
406
418
|
return row;
|
|
407
419
|
}
|
|
420
|
+
function transformsOf(schema, table) {
|
|
421
|
+
const out = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
423
|
+
if ((def.name ?? key) !== table) continue;
|
|
424
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
425
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
426
|
+
const t = d?.transform;
|
|
427
|
+
if (t !== null && typeof t === "object") {
|
|
428
|
+
out.set(col, t);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function applyFromDb(row, transforms) {
|
|
435
|
+
if (transforms.size === 0) return row;
|
|
436
|
+
for (const [col, t] of transforms) {
|
|
437
|
+
if (t.fromDb === void 0) continue;
|
|
438
|
+
const v = row[col];
|
|
439
|
+
if (v === null || v === void 0) continue;
|
|
440
|
+
row[col] = t.fromDb(v);
|
|
441
|
+
}
|
|
442
|
+
return row;
|
|
443
|
+
}
|
|
408
444
|
function asTableRow(table, row) {
|
|
409
|
-
|
|
445
|
+
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
446
|
+
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
410
447
|
}
|
|
411
448
|
function asTableRows(table, rows) {
|
|
412
449
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
413
|
-
|
|
450
|
+
const transforms = transformsOf(currentSchema, table);
|
|
451
|
+
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
414
452
|
}
|
|
415
453
|
function asBindParams(table, cols, data) {
|
|
416
454
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
455
|
+
const transforms = transformsOf(currentSchema, table);
|
|
417
456
|
return cols.map((c) => {
|
|
418
457
|
const v = data[c];
|
|
419
|
-
|
|
458
|
+
if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
|
|
459
|
+
const t = transforms.get(c);
|
|
460
|
+
if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
|
|
461
|
+
return v;
|
|
420
462
|
});
|
|
421
463
|
}
|
|
422
464
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
@@ -543,7 +585,9 @@ function expandSynonyms(query, map) {
|
|
|
543
585
|
}).join("");
|
|
544
586
|
}).join("");
|
|
545
587
|
}
|
|
546
|
-
var searchLogger =
|
|
588
|
+
var searchLogger = (evt, fields) => {
|
|
589
|
+
console.log(evt, JSON.stringify(fields));
|
|
590
|
+
};
|
|
547
591
|
function logNoResults(table, query, mode, n) {
|
|
548
592
|
if (n === 0) {
|
|
549
593
|
searchLogger?.("palbase.search.no_results", {
|
|
@@ -584,19 +628,55 @@ function validitySql(v, add) {
|
|
|
584
628
|
function excludeSql(pk, ids, add) {
|
|
585
629
|
if (ids.length === 0) return "";
|
|
586
630
|
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(", ")}])`;
|
|
631
|
+
return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
632
|
+
}
|
|
633
|
+
function schemaColumns(table) {
|
|
634
|
+
const t = Object.values(currentSchema.tables ?? {}).find((x) => x?.name === table);
|
|
635
|
+
const cols = t?.columns ? Object.keys(t.columns) : [];
|
|
636
|
+
return cols.length > 0 ? new Set(cols) : null;
|
|
637
|
+
}
|
|
638
|
+
function orderClause(table, known, orderBy) {
|
|
639
|
+
if (!orderBy) return "";
|
|
640
|
+
const col = orderBy.column;
|
|
641
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
642
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
643
|
+
throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
|
|
644
|
+
}
|
|
645
|
+
const dir = orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
646
|
+
return ` ORDER BY ${quoteIdent(col)} ${dir}`;
|
|
647
|
+
}
|
|
648
|
+
function limitClause(limit) {
|
|
649
|
+
if (limit === void 0) return "";
|
|
650
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
651
|
+
throw new Error(`findMany: limit bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(limit)})`);
|
|
652
|
+
}
|
|
653
|
+
return ` LIMIT ${limit}`;
|
|
588
654
|
}
|
|
589
|
-
function
|
|
655
|
+
function offsetClause(offset, limit) {
|
|
656
|
+
if (offset === void 0) return "";
|
|
657
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (limit === void 0) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"findMany: offset yaln\u0131z limit ile birlikte verilir \u2014 limitsiz offset bir sayfa de\u011Fil, s\u0131n\u0131rs\u0131z bir kuyru\u011Fun kayd\u0131r\u0131lm\u0131\u015F\u0131d\u0131r"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return ` OFFSET ${offset}`;
|
|
668
|
+
}
|
|
669
|
+
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
590
670
|
const parts = [];
|
|
591
671
|
for (const [col, cond] of Object.entries(where)) {
|
|
592
|
-
if (!colSet.has(col)) {
|
|
593
|
-
throw new Error(
|
|
672
|
+
if (colSet !== null && !colSet.has(col)) {
|
|
673
|
+
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
594
674
|
}
|
|
595
675
|
const q = `t.${quoteIdent(col)}`;
|
|
596
676
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
597
677
|
for (const [op, v] of Object.entries(cond)) {
|
|
598
678
|
if (op === "in") {
|
|
599
|
-
if (!Array.isArray(v)) throw new Error(
|
|
679
|
+
if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
600
680
|
if (v.length === 0) {
|
|
601
681
|
parts.push("false");
|
|
602
682
|
continue;
|
|
@@ -605,7 +685,7 @@ function compileWhere(table, colSet, where, add) {
|
|
|
605
685
|
} else if (op in WHERE_OPS) {
|
|
606
686
|
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
607
687
|
} else {
|
|
608
|
-
throw new Error(
|
|
688
|
+
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
609
689
|
}
|
|
610
690
|
}
|
|
611
691
|
} else {
|
|
@@ -614,6 +694,13 @@ function compileWhere(table, colSet, where, add) {
|
|
|
614
694
|
}
|
|
615
695
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
616
696
|
}
|
|
697
|
+
function onConflictTail(cols, conflict, action) {
|
|
698
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
699
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
700
|
+
const conflictSet = new Set(conflict);
|
|
701
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
702
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
703
|
+
}
|
|
617
704
|
var cachedVectorSchema = null;
|
|
618
705
|
var cachedTrgmSchema = null;
|
|
619
706
|
async function trgmSchemaOf(live) {
|
|
@@ -685,7 +772,8 @@ function createOps(tx) {
|
|
|
685
772
|
const at = () => resolveTx(tx);
|
|
686
773
|
const ops = {
|
|
687
774
|
async query(sql, params = []) {
|
|
688
|
-
|
|
775
|
+
const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
|
|
776
|
+
return asWireRows(await (await at()).unsafe(sql, bound));
|
|
689
777
|
},
|
|
690
778
|
async insert(table, data) {
|
|
691
779
|
const cols = Object.keys(data);
|
|
@@ -701,6 +789,74 @@ function createOps(tx) {
|
|
|
701
789
|
}
|
|
702
790
|
return asTableRow(table, inserted);
|
|
703
791
|
},
|
|
792
|
+
/**
|
|
793
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
794
|
+
*
|
|
795
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
796
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
797
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
798
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
799
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
800
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
801
|
+
* every table with a unique row.
|
|
802
|
+
*
|
|
803
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
804
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
805
|
+
*/
|
|
806
|
+
async upsert(table, data, opts) {
|
|
807
|
+
const conflict = opts.onConflict;
|
|
808
|
+
if (conflict.length === 0) {
|
|
809
|
+
throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
|
|
810
|
+
}
|
|
811
|
+
const cols = Object.keys(data);
|
|
812
|
+
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
813
|
+
if (searchConfigFor(table)?.validity) {
|
|
814
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
815
|
+
const insertSql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
|
|
816
|
+
const live = await at();
|
|
817
|
+
const bindData = asBindParams(table, cols, data);
|
|
818
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
819
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
820
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
821
|
+
const condBind = conflict.map((c) => data[c]);
|
|
822
|
+
const cur = await live.unsafe(
|
|
823
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
824
|
+
condBind
|
|
825
|
+
);
|
|
826
|
+
const old = cur[0];
|
|
827
|
+
if (old === void 0) {
|
|
828
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
829
|
+
}
|
|
830
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
831
|
+
await live.unsafe(
|
|
832
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
833
|
+
[old[pk]]
|
|
834
|
+
);
|
|
835
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
836
|
+
if (!second[0]) {
|
|
837
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 yeni sat\u0131r a\xE7\u0131lamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
838
|
+
}
|
|
839
|
+
const fresh = second[0];
|
|
840
|
+
await live.unsafe(
|
|
841
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
842
|
+
[old[pk], fresh[pk]]
|
|
843
|
+
);
|
|
844
|
+
return asTableRow(table, second[0]);
|
|
845
|
+
}
|
|
846
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
847
|
+
const conflictSet = new Set(conflict);
|
|
848
|
+
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
849
|
+
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
850
|
+
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
851
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
852
|
+
const row = rows[0];
|
|
853
|
+
if (!row) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
return asTableRow(table, row);
|
|
859
|
+
},
|
|
704
860
|
async update(table, id, data) {
|
|
705
861
|
const cols = Object.keys(data);
|
|
706
862
|
if (cols.length === 0) return ops.findById(table, id);
|
|
@@ -712,6 +868,86 @@ function createOps(tx) {
|
|
|
712
868
|
async delete(table, id) {
|
|
713
869
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
714
870
|
},
|
|
871
|
+
/**
|
|
872
|
+
* Update every row the filter matches, in ONE statement.
|
|
873
|
+
*
|
|
874
|
+
* The capability was already here and only reachable from inside a
|
|
875
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
876
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
877
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
878
|
+
* where the typed surface and RLS both stop helping.
|
|
879
|
+
*
|
|
880
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
881
|
+
* one filter language, or the two spellings drift.
|
|
882
|
+
*
|
|
883
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
884
|
+
* and the shape that produces it by accident — a filter object built from
|
|
885
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
886
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
887
|
+
* predicate that is true for every row.
|
|
888
|
+
*/
|
|
889
|
+
async updateMany(table, where, set) {
|
|
890
|
+
const cols = Object.keys(set);
|
|
891
|
+
if (cols.length === 0) return [];
|
|
892
|
+
if (Object.keys(where).length === 0) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`updateMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 g\xFCncellerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
const params = [];
|
|
898
|
+
const add = (v) => {
|
|
899
|
+
params.push(v);
|
|
900
|
+
return `$${params.length}`;
|
|
901
|
+
};
|
|
902
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
903
|
+
const known = schemaColumns(table);
|
|
904
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
905
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
906
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
907
|
+
},
|
|
908
|
+
/**
|
|
909
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
910
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
911
|
+
* {@link updateMany} — and here the accident is worse.
|
|
912
|
+
*/
|
|
913
|
+
async deleteMany(table, where) {
|
|
914
|
+
if (Object.keys(where).length === 0) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`deleteMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 silerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const params = [];
|
|
920
|
+
const add = (v) => {
|
|
921
|
+
params.push(v);
|
|
922
|
+
return `$${params.length}`;
|
|
923
|
+
};
|
|
924
|
+
const known = schemaColumns(table);
|
|
925
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
926
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
927
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
928
|
+
return rows.length;
|
|
929
|
+
},
|
|
930
|
+
/**
|
|
931
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
932
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
933
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
934
|
+
* remove.
|
|
935
|
+
*
|
|
936
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
937
|
+
* reads do not destroy anything.
|
|
938
|
+
*/
|
|
939
|
+
async count(table, where = {}) {
|
|
940
|
+
const params = [];
|
|
941
|
+
const add = (v) => {
|
|
942
|
+
params.push(v);
|
|
943
|
+
return `$${params.length}`;
|
|
944
|
+
};
|
|
945
|
+
const known = schemaColumns(table);
|
|
946
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
947
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
948
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
949
|
+
return Number(rows[0]?.n ?? 0);
|
|
950
|
+
},
|
|
715
951
|
async findById(table, id) {
|
|
716
952
|
const rows = await (await at()).unsafe(
|
|
717
953
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -719,13 +955,19 @@ function createOps(tx) {
|
|
|
719
955
|
);
|
|
720
956
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
721
957
|
},
|
|
722
|
-
async findMany(table, query = {}) {
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
)
|
|
958
|
+
async findMany(table, query = {}, opts = {}) {
|
|
959
|
+
const params = [];
|
|
960
|
+
const add = (v) => {
|
|
961
|
+
params.push(v);
|
|
962
|
+
return `$${params.length}`;
|
|
963
|
+
};
|
|
964
|
+
const known = schemaColumns(table);
|
|
965
|
+
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
966
|
+
const order = orderClause(table, known, opts.orderBy);
|
|
967
|
+
const limit = limitClause(opts.limit);
|
|
968
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
969
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
970
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
729
971
|
},
|
|
730
972
|
/**
|
|
731
973
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
@@ -807,7 +1049,9 @@ function createOps(tx) {
|
|
|
807
1049
|
bindC.push(v);
|
|
808
1050
|
return `$${bindC.length}`;
|
|
809
1051
|
};
|
|
810
|
-
const
|
|
1052
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1053
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1054
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
811
1055
|
const liveC = await at();
|
|
812
1056
|
const K2 = 50;
|
|
813
1057
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -816,8 +1060,19 @@ function createOps(tx) {
|
|
|
816
1060
|
if (qvC !== null) {
|
|
817
1061
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
818
1062
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1063
|
+
let exactOrderC = "";
|
|
1064
|
+
if (userWhereC !== "") {
|
|
1065
|
+
const probeRows = await liveC.unsafe(
|
|
1066
|
+
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1067
|
+
bindC.slice()
|
|
1068
|
+
);
|
|
1069
|
+
const n = probeRows?.[0]?.n;
|
|
1070
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1071
|
+
exactOrderC = " + 0.0";
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
819
1074
|
const vp = addC(toVectorLiteral(qvC));
|
|
820
|
-
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c
|
|
1075
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrderC}) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
821
1076
|
}
|
|
822
1077
|
let qpC = "";
|
|
823
1078
|
let qpCIdx = -1;
|
|
@@ -825,7 +1080,7 @@ function createOps(tx) {
|
|
|
825
1080
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
826
1081
|
qpC = addC(qTextC);
|
|
827
1082
|
qpCIdx = bindC.length - 1;
|
|
828
|
-
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1083
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
829
1084
|
}
|
|
830
1085
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
831
1086
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -840,13 +1095,13 @@ function createOps(tx) {
|
|
|
840
1095
|
} else {
|
|
841
1096
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
842
1097
|
}
|
|
843
|
-
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid
|
|
1098
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
|
|
844
1099
|
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
845
1100
|
};
|
|
846
1101
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
847
1102
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
848
1103
|
const tsch = await trgmSchemaOf(liveC);
|
|
849
|
-
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1104
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) > 0.3 ORDER BY r LIMIT ${cpool}`;
|
|
850
1105
|
const retryBindC = bindC.slice();
|
|
851
1106
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
852
1107
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -942,7 +1197,7 @@ function createOps(tx) {
|
|
|
942
1197
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
943
1198
|
const tsch = await trgmSchemaOf(live);
|
|
944
1199
|
const expr = ftsExprOf(cfg.ftsCols);
|
|
945
|
-
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${
|
|
1200
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) > 0.3${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
946
1201
|
const retryBind = bind.slice();
|
|
947
1202
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
948
1203
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -965,6 +1220,25 @@ function createOps(tx) {
|
|
|
965
1220
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
966
1221
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
967
1222
|
*/
|
|
1223
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1224
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1225
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1226
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1227
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1228
|
+
async facets(table, params) {
|
|
1229
|
+
const cfg = searchConfigFor(table);
|
|
1230
|
+
if (!cfg) {
|
|
1231
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1232
|
+
}
|
|
1233
|
+
for (const col of params.facets) {
|
|
1234
|
+
if (!cfg.colSet.has(col)) {
|
|
1235
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const live = await at();
|
|
1239
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1240
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1241
|
+
},
|
|
968
1242
|
async similar(table, id, opts = {}) {
|
|
969
1243
|
const cfg = searchConfigFor(table);
|
|
970
1244
|
if (!cfg) {
|
|
@@ -1116,6 +1390,27 @@ function createOps(tx) {
|
|
|
1116
1390
|
const live = await at();
|
|
1117
1391
|
return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
|
|
1118
1392
|
},
|
|
1393
|
+
/**
|
|
1394
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
1395
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
1396
|
+
*
|
|
1397
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
1398
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
1399
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
1400
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
1401
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
1402
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
1403
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
1404
|
+
* you can see in the code that crosses it.
|
|
1405
|
+
*
|
|
1406
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
1407
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
1408
|
+
* the surrounding transaction.
|
|
1409
|
+
*/
|
|
1410
|
+
async attempt(fn) {
|
|
1411
|
+
const live = await at();
|
|
1412
|
+
return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
|
|
1413
|
+
},
|
|
1119
1414
|
/**
|
|
1120
1415
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
1121
1416
|
*
|
|
@@ -1166,22 +1461,68 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1166
1461
|
update: (id, data) => ops.update(name, id, data),
|
|
1167
1462
|
delete: (id) => ops.delete(name, id),
|
|
1168
1463
|
findById: (id) => ops.findById(name, id),
|
|
1169
|
-
findMany: (query) => ops.findMany(name, query ?? {})
|
|
1464
|
+
findMany: (query, opts) => ops.findMany(name, query ?? {}, opts),
|
|
1465
|
+
upsert: (data, opts) => ops.upsert(name, data, opts)
|
|
1170
1466
|
};
|
|
1171
1467
|
}
|
|
1172
1468
|
const base = /* @__PURE__ */ Object.create(null);
|
|
1173
1469
|
return Object.assign(base, ops, { tables });
|
|
1174
1470
|
}
|
|
1175
1471
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1472
|
+
function isAbortedTransaction(e) {
|
|
1473
|
+
const message = String(e?.message ?? "");
|
|
1474
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
1475
|
+
}
|
|
1476
|
+
function sqlstateOf(e) {
|
|
1477
|
+
const err = e;
|
|
1478
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
1479
|
+
if (isState(err?.code)) return err.code;
|
|
1480
|
+
if (isState(err?.errno)) return err.errno;
|
|
1481
|
+
if (typeof err?.errno === "number") {
|
|
1482
|
+
const asText = String(err.errno);
|
|
1483
|
+
if (isState(asText)) return asText;
|
|
1484
|
+
}
|
|
1485
|
+
return void 0;
|
|
1486
|
+
}
|
|
1487
|
+
function isForeignKeyViolation(e) {
|
|
1488
|
+
return sqlstateOf(e) === "23503";
|
|
1489
|
+
}
|
|
1490
|
+
function isUniqueViolation(e) {
|
|
1491
|
+
return sqlstateOf(e) === "23505";
|
|
1492
|
+
}
|
|
1493
|
+
function constraintOf(e) {
|
|
1494
|
+
const named = e?.constraint;
|
|
1495
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
1496
|
+
const message = String(e?.message ?? "");
|
|
1497
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1498
|
+
}
|
|
1176
1499
|
function isLockTimeout(e) {
|
|
1177
|
-
const code = e?.code;
|
|
1178
1500
|
const message = String(e?.message ?? "");
|
|
1179
|
-
return
|
|
1501
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1180
1502
|
}
|
|
1181
|
-
function diagnosingDriver(sql) {
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1503
|
+
function diagnosingDriver(sql, surface = "service") {
|
|
1504
|
+
const original = (e) => String(e?.message ?? e);
|
|
1505
|
+
const explain = (e) => {
|
|
1506
|
+
if (isAbortedTransaction(e)) {
|
|
1507
|
+
return new Error(
|
|
1508
|
+
`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)})`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
if (isUniqueViolation(e)) {
|
|
1512
|
+
return new UniqueViolation(constraintOf(e));
|
|
1513
|
+
}
|
|
1514
|
+
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1515
|
+
return new Error(
|
|
1516
|
+
`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)})`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
if (isLockTimeout(e)) {
|
|
1520
|
+
return new Error(
|
|
1521
|
+
`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)})`
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return e;
|
|
1525
|
+
};
|
|
1185
1526
|
const wrapTx = (tx) => ({
|
|
1186
1527
|
async unsafe(text, params) {
|
|
1187
1528
|
try {
|
|
@@ -1200,13 +1541,13 @@ function diagnosingDriver(sql) {
|
|
|
1200
1541
|
};
|
|
1201
1542
|
}
|
|
1202
1543
|
function createRequestDatabase(sql, identity) {
|
|
1203
|
-
const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
|
|
1544
|
+
const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
|
|
1204
1545
|
let serviceTx = null;
|
|
1205
1546
|
let serviceClient = null;
|
|
1206
1547
|
const asService = () => {
|
|
1207
1548
|
if (serviceClient === null) {
|
|
1208
1549
|
serviceTx = createLazyTransaction(
|
|
1209
|
-
diagnosingDriver(sql),
|
|
1550
|
+
diagnosingDriver(sql, "service"),
|
|
1210
1551
|
identity.serviceRole,
|
|
1211
1552
|
identity.claimsJson,
|
|
1212
1553
|
{ lockTimeout: SERVICE_LOCK_TIMEOUT }
|
|
@@ -1287,6 +1628,20 @@ async function runPlanOp(sp, op, results) {
|
|
|
1287
1628
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1288
1629
|
break;
|
|
1289
1630
|
}
|
|
1631
|
+
case "upsert": {
|
|
1632
|
+
const cols = Object.keys(op.values ?? {});
|
|
1633
|
+
const conflict = op.onConflict ?? [];
|
|
1634
|
+
if (cols.length === 0 || conflict.length === 0) {
|
|
1635
|
+
throw Object.assign(new Error(`upsert on ${op.table} needs columns and onConflict`), {
|
|
1636
|
+
error_code: "tx_bad_upsert"
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
const rendered = cols.map(
|
|
1640
|
+
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
1641
|
+
);
|
|
1642
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1290
1645
|
case "insertMany": {
|
|
1291
1646
|
const rows = op.rows ?? [];
|
|
1292
1647
|
if (rows.length === 0 || !rows[0]) return [];
|
|
@@ -1294,7 +1649,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
1294
1649
|
const tuples = rows.map(
|
|
1295
1650
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
1296
1651
|
);
|
|
1297
|
-
|
|
1652
|
+
const conflictCols = op.onConflict ?? [];
|
|
1653
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
1654
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
1298
1655
|
break;
|
|
1299
1656
|
}
|
|
1300
1657
|
case "update": {
|
|
@@ -1351,6 +1708,7 @@ function buildRouteTable(controllers) {
|
|
|
1351
1708
|
`controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
|
|
1352
1709
|
);
|
|
1353
1710
|
}
|
|
1711
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
1354
1712
|
const instance = new ctor();
|
|
1355
1713
|
for (const r of routes) {
|
|
1356
1714
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -1360,7 +1718,10 @@ function buildRouteTable(controllers) {
|
|
|
1360
1718
|
meta: r,
|
|
1361
1719
|
instance,
|
|
1362
1720
|
id: `${r.method} ${full}`,
|
|
1363
|
-
|
|
1721
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
1722
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
1723
|
+
// the route level back on top per request.
|
|
1724
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
1364
1725
|
});
|
|
1365
1726
|
}
|
|
1366
1727
|
}
|
|
@@ -1447,6 +1808,41 @@ function verifySignature(presented, expected) {
|
|
|
1447
1808
|
return diff === 0;
|
|
1448
1809
|
}
|
|
1449
1810
|
|
|
1811
|
+
// src/engine/sse.ts
|
|
1812
|
+
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
1813
|
+
function encodeFrame(value) {
|
|
1814
|
+
return `data: ${JSON.stringify(value)}
|
|
1815
|
+
|
|
1816
|
+
`;
|
|
1817
|
+
}
|
|
1818
|
+
function encodeErrorFrame(requestId) {
|
|
1819
|
+
return `event: error
|
|
1820
|
+
data: ${JSON.stringify({ requestId })}
|
|
1821
|
+
|
|
1822
|
+
`;
|
|
1823
|
+
}
|
|
1824
|
+
function makeSseWriter(opts) {
|
|
1825
|
+
let begun = false;
|
|
1826
|
+
let chain = Promise.resolve();
|
|
1827
|
+
return {
|
|
1828
|
+
write(value) {
|
|
1829
|
+
const frame = encodeFrame(value);
|
|
1830
|
+
if (!begun) {
|
|
1831
|
+
begun = true;
|
|
1832
|
+
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
chain = chain.then(() => opts.enqueue(frame));
|
|
1836
|
+
},
|
|
1837
|
+
started() {
|
|
1838
|
+
return begun;
|
|
1839
|
+
},
|
|
1840
|
+
drained() {
|
|
1841
|
+
return chain;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1450
1846
|
// src/engine/fence.ts
|
|
1451
1847
|
var SECRET_ENV = [
|
|
1452
1848
|
"DATABASE_URL",
|
|
@@ -1682,6 +2078,13 @@ async function createApp(opts) {
|
|
|
1682
2078
|
const args = [];
|
|
1683
2079
|
let parsedBody;
|
|
1684
2080
|
let bodyRead = false;
|
|
2081
|
+
let sseEnqueue = null;
|
|
2082
|
+
let sseSettle = async () => {
|
|
2083
|
+
};
|
|
2084
|
+
const sseWriter = makeSseWriter({
|
|
2085
|
+
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
2086
|
+
onFirstWrite: () => sseSettle()
|
|
2087
|
+
});
|
|
1685
2088
|
for (const p of meta.params ?? []) {
|
|
1686
2089
|
switch (p.kind) {
|
|
1687
2090
|
case "body": {
|
|
@@ -1785,6 +2188,21 @@ async function createApp(opts) {
|
|
|
1785
2188
|
case "req":
|
|
1786
2189
|
args[p.index] = req;
|
|
1787
2190
|
break;
|
|
2191
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
2192
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
2193
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
2194
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
2195
|
+
// request-scoped data and no signal.
|
|
2196
|
+
case "signal":
|
|
2197
|
+
args[p.index] = req.signal;
|
|
2198
|
+
break;
|
|
2199
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
2200
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
2201
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
2202
|
+
// route.
|
|
2203
|
+
case "sseOut":
|
|
2204
|
+
args[p.index] = sseWriter;
|
|
2205
|
+
break;
|
|
1788
2206
|
default:
|
|
1789
2207
|
args[p.index] = void 0;
|
|
1790
2208
|
}
|
|
@@ -1796,6 +2214,84 @@ async function createApp(opts) {
|
|
|
1796
2214
|
});
|
|
1797
2215
|
try {
|
|
1798
2216
|
const services = buildServices(db);
|
|
2217
|
+
if (meta.options?.sseConfig !== void 0) {
|
|
2218
|
+
let sseController;
|
|
2219
|
+
const sseEncoder = new TextEncoder();
|
|
2220
|
+
const stream = new ReadableStream({
|
|
2221
|
+
start(c) {
|
|
2222
|
+
sseController = c;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
sseEnqueue = (chunk) => {
|
|
2226
|
+
try {
|
|
2227
|
+
sseController.enqueue(sseEncoder.encode(chunk));
|
|
2228
|
+
} catch {
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
let sawFirstFrame;
|
|
2232
|
+
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
2233
|
+
sseSettle = async () => {
|
|
2234
|
+
await db.commit();
|
|
2235
|
+
sawFirstFrame();
|
|
2236
|
+
};
|
|
2237
|
+
const guarded = {
|
|
2238
|
+
...services,
|
|
2239
|
+
Database: new Proxy(services.Database, {
|
|
2240
|
+
get(target, prop, recv) {
|
|
2241
|
+
if (sseWriter.started()) {
|
|
2242
|
+
throw new Error(
|
|
2243
|
+
`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.`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
return Reflect.get(target, prop, recv);
|
|
2247
|
+
}
|
|
2248
|
+
})
|
|
2249
|
+
};
|
|
2250
|
+
let thrown = null;
|
|
2251
|
+
const ran = Promise.resolve(runWithRuntime(guarded, () => {
|
|
2252
|
+
const box = requestALS.getStore();
|
|
2253
|
+
if (box) {
|
|
2254
|
+
box.userId = userId ?? null;
|
|
2255
|
+
box.requestId = requestId;
|
|
2256
|
+
box.idempotencyKey = req.headers.get("idempotency-key");
|
|
2257
|
+
}
|
|
2258
|
+
const method = hit.entry.instance[meta.fnName];
|
|
2259
|
+
if (typeof method !== "function") {
|
|
2260
|
+
throw new Error(
|
|
2261
|
+
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
return method.apply(hit.entry.instance, args);
|
|
2265
|
+
})).then(
|
|
2266
|
+
() => void 0,
|
|
2267
|
+
(err) => {
|
|
2268
|
+
thrown = err;
|
|
2269
|
+
}
|
|
2270
|
+
);
|
|
2271
|
+
await Promise.race([firstFrame, ran]);
|
|
2272
|
+
if (!sseWriter.started()) {
|
|
2273
|
+
await ran;
|
|
2274
|
+
await db.commit();
|
|
2275
|
+
if (thrown !== null) throw thrown;
|
|
2276
|
+
return new Response(null, { status: 204 });
|
|
2277
|
+
}
|
|
2278
|
+
void (async () => {
|
|
2279
|
+
await ran;
|
|
2280
|
+
await sseWriter.drained();
|
|
2281
|
+
try {
|
|
2282
|
+
if (thrown !== null) {
|
|
2283
|
+
log.error(`[engine] ${hit.entry.id} threw mid-stream`, thrown);
|
|
2284
|
+
sseController.enqueue(sseEncoder.encode(encodeErrorFrame(requestId)));
|
|
2285
|
+
}
|
|
2286
|
+
sseController.close();
|
|
2287
|
+
} catch {
|
|
2288
|
+
}
|
|
2289
|
+
})();
|
|
2290
|
+
return new Response(stream, {
|
|
2291
|
+
status: 200,
|
|
2292
|
+
headers: { "content-type": SSE_CONTENT_TYPE, "cache-control": "no-cache" }
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
1799
2295
|
const result = await runWithRuntime(services, () => {
|
|
1800
2296
|
const box = requestALS.getStore();
|
|
1801
2297
|
if (box) {
|
|
@@ -1854,18 +2350,29 @@ async function createApp(opts) {
|
|
|
1854
2350
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
1855
2351
|
}
|
|
1856
2352
|
}
|
|
2353
|
+
let runShutdownHooks;
|
|
2354
|
+
try {
|
|
2355
|
+
runShutdownHooks = await __runStartHooks();
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
await closeDriver(sql);
|
|
2358
|
+
throw err;
|
|
2359
|
+
}
|
|
1857
2360
|
return {
|
|
1858
2361
|
handle,
|
|
1859
2362
|
routes,
|
|
1860
2363
|
config,
|
|
1861
2364
|
runInServiceScope,
|
|
1862
2365
|
async shutdown() {
|
|
1863
|
-
|
|
1864
|
-
await
|
|
1865
|
-
await closable.end?.();
|
|
2366
|
+
await runShutdownHooks();
|
|
2367
|
+
await closeDriver(sql);
|
|
1866
2368
|
}
|
|
1867
2369
|
};
|
|
1868
2370
|
}
|
|
2371
|
+
async function closeDriver(sql) {
|
|
2372
|
+
const closable = sql;
|
|
2373
|
+
await closable.close?.();
|
|
2374
|
+
await closable.end?.();
|
|
2375
|
+
}
|
|
1869
2376
|
|
|
1870
2377
|
export {
|
|
1871
2378
|
BootRefused,
|
|
@@ -1886,4 +2393,4 @@ export {
|
|
|
1886
2393
|
installEgressFence,
|
|
1887
2394
|
createApp
|
|
1888
2395
|
};
|
|
1889
|
-
//# sourceMappingURL=chunk-
|
|
2396
|
+
//# sourceMappingURL=chunk-NXDH6VQJ.js.map
|