@palbase/backend 23.1.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.
Files changed (73) hide show
  1. package/dist/bin/palbase-backend.cjs +389 -44
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +4 -5
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-OHALWEOG.js → chunk-7Z6MGMXQ.js} +57 -2
  6. package/dist/chunk-7Z6MGMXQ.js.map +1 -0
  7. package/dist/{chunk-RCLNBJCM.js → chunk-H3JAISUY.js} +136 -1
  8. package/dist/chunk-H3JAISUY.js.map +1 -0
  9. package/dist/{chunk-M5MCBWJI.js → chunk-NXDH6VQJ.js} +264 -28
  10. package/dist/chunk-NXDH6VQJ.js.map +1 -0
  11. package/dist/{chunk-NS5V43YQ.js → chunk-P2Q27SGP.js} +19 -3
  12. package/dist/chunk-P2Q27SGP.js.map +1 -0
  13. package/dist/{chunk-R3KN6RHD.js → chunk-T5IOSOE5.js} +7 -2
  14. package/dist/chunk-T5IOSOE5.js.map +1 -0
  15. package/dist/{chunk-PY7YJDCT.js → chunk-ZUGY7RGS.js} +32 -3
  16. package/dist/chunk-ZUGY7RGS.js.map +1 -0
  17. package/dist/db/index.cjs +48 -3
  18. package/dist/db/index.cjs.map +1 -1
  19. package/dist/db/index.d.cts +2 -2
  20. package/dist/db/index.d.ts +2 -2
  21. package/dist/db/index.js +2 -2
  22. package/dist/{endpoint-CVWXh6oG.d.ts → endpoint-0_DGBajf.d.ts} +100 -3
  23. package/dist/{endpoint-c9h5jriX.d.cts → endpoint-CcQ1a36a.d.cts} +100 -3
  24. package/dist/engine/index.cjs +378 -31
  25. package/dist/engine/index.cjs.map +1 -1
  26. package/dist/engine/index.d.cts +4 -4
  27. package/dist/engine/index.d.ts +4 -4
  28. package/dist/engine/index.js +4 -4
  29. package/dist/{index-CwAJ7HEe.d.ts → index-CJiJU9ux.d.ts} +125 -21
  30. package/dist/{index-By8Dle5U.d.cts → index-D-4-PNuQ.d.cts} +125 -21
  31. package/dist/{index-BZrJXnVh.d.ts → index-D17r-MKb.d.ts} +134 -9
  32. package/dist/{index-CxeQSfJP.d.cts → index-DRFxf07H.d.cts} +134 -9
  33. package/dist/index.cjs +166 -5
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +54 -12
  36. package/dist/index.d.ts +54 -12
  37. package/dist/index.js +28 -11
  38. package/dist/index.js.map +1 -1
  39. package/dist/openapi/index.cjs +16 -1
  40. package/dist/openapi/index.cjs.map +1 -1
  41. package/dist/openapi/index.d.cts +6 -4
  42. package/dist/openapi/index.d.ts +6 -4
  43. package/dist/openapi/index.js +6 -7
  44. package/dist/openapi/index.js.map +1 -1
  45. package/dist/{registry-CqPK2Qby.d.cts → registry-1X-skBNu.d.cts} +1 -1
  46. package/dist/{registry-B3niOVYp.d.ts → registry-CEod_5sz.d.ts} +1 -1
  47. package/dist/test/index.cjs +27 -0
  48. package/dist/test/index.cjs.map +1 -1
  49. package/dist/test/index.d.cts +1 -1
  50. package/dist/test/index.d.ts +1 -1
  51. package/dist/test/index.js +27 -0
  52. package/dist/test/index.js.map +1 -1
  53. package/docs/README.md +4 -4
  54. package/docs/database.md +115 -11
  55. package/docs/getting-started.md +5 -4
  56. package/docs/llms-full.txt +385 -89
  57. package/docs/migrations.md +81 -59
  58. package/docs/schema.md +82 -2
  59. package/docs/services.md +98 -9
  60. package/package.json +2 -2
  61. package/template/AGENTS.md +121 -41
  62. package/template/controllers/notes.controller.ts +64 -0
  63. package/template/package.json +1 -1
  64. package/template/services/note.service.ts +74 -0
  65. package/template/tsconfig.json +11 -1
  66. package/dist/chunk-HQRJDARQ.js +0 -90
  67. package/dist/chunk-HQRJDARQ.js.map +0 -1
  68. package/dist/chunk-M5MCBWJI.js.map +0 -1
  69. package/dist/chunk-NS5V43YQ.js.map +0 -1
  70. package/dist/chunk-OHALWEOG.js.map +0 -1
  71. package/dist/chunk-PY7YJDCT.js.map +0 -1
  72. package/dist/chunk-R3KN6RHD.js.map +0 -1
  73. package/dist/chunk-RCLNBJCM.js.map +0 -1
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  __requestALS,
3
+ __runStartHooks,
3
4
  __runWithRuntime
4
- } from "./chunk-OHALWEOG.js";
5
+ } from "./chunk-7Z6MGMXQ.js";
5
6
  import {
7
+ UniqueViolation,
8
+ assertZeroArgConstructor,
6
9
  getRoutes,
7
- isHttpError
8
- } from "./chunk-RCLNBJCM.js";
10
+ isHttpError,
11
+ resolveEffectiveAuth
12
+ } from "./chunk-H3JAISUY.js";
9
13
 
10
14
  // src/engine/config.ts
11
15
  var BootRefused = class extends Error {
@@ -413,18 +417,48 @@ function reviveVectors(row, vectorCols) {
413
417
  }
414
418
  return row;
415
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
+ }
416
444
  function asTableRow(table, row) {
417
- return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
445
+ const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
446
+ return applyFromDb(revived, transformsOf(currentSchema, table));
418
447
  }
419
448
  function asTableRows(table, rows) {
420
449
  const vectorCols = vectorColumnsOf(currentSchema, table);
421
- return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
450
+ const transforms = transformsOf(currentSchema, table);
451
+ return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
422
452
  }
423
453
  function asBindParams(table, cols, data) {
424
454
  const vectorCols = vectorColumnsOf(currentSchema, table);
455
+ const transforms = transformsOf(currentSchema, table);
425
456
  return cols.map((c) => {
426
457
  const v = data[c];
427
- return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
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;
428
462
  });
429
463
  }
430
464
  var SELECTIVITY_EXACT_THRESHOLD = 1e4;
@@ -618,6 +652,20 @@ function limitClause(limit) {
618
652
  }
619
653
  return ` LIMIT ${limit}`;
620
654
  }
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
+ }
621
669
  function compileWhere(table, colSet, where, add, caller = "search") {
622
670
  const parts = [];
623
671
  for (const [col, cond] of Object.entries(where)) {
@@ -646,6 +694,13 @@ function compileWhere(table, colSet, where, add, caller = "search") {
646
694
  }
647
695
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
648
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
+ }
649
704
  var cachedVectorSchema = null;
650
705
  var cachedTrgmSchema = null;
651
706
  async function trgmSchemaOf(live) {
@@ -755,6 +810,39 @@ function createOps(tx) {
755
810
  }
756
811
  const cols = Object.keys(data);
757
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
+ }
758
846
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
759
847
  const conflictSet = new Set(conflict);
760
848
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
@@ -780,6 +868,86 @@ function createOps(tx) {
780
868
  async delete(table, id) {
781
869
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
782
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
+ },
783
951
  async findById(table, id) {
784
952
  const rows = await (await at()).unsafe(
785
953
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
@@ -797,7 +965,8 @@ function createOps(tx) {
797
965
  const whereSql = compileWhere(table, known, query, add, "findMany");
798
966
  const order = orderClause(table, known, opts.orderBy);
799
967
  const limit = limitClause(opts.limit);
800
- const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
968
+ const offset = offsetClause(opts.offset, opts.limit);
969
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
801
970
  return asTableRows(table, await (await at()).unsafe(sql, params));
802
971
  },
803
972
  /**
@@ -880,7 +1049,9 @@ function createOps(tx) {
880
1049
  bindC.push(v);
881
1050
  return `$${bindC.length}`;
882
1051
  };
883
- const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
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 `;
884
1055
  const liveC = await at();
885
1056
  const K2 = 50;
886
1057
  const cpool = Math.max(limit * 9, 90);
@@ -889,8 +1060,19 @@ function createOps(tx) {
889
1060
  if (qvC !== null) {
890
1061
  const sch = await vectorSchemaWithGuc(liveC);
891
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
+ }
892
1074
  const vp = addC(toVectorLiteral(qvC));
893
- 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 WHERE c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
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}`;
894
1076
  }
895
1077
  let qpC = "";
896
1078
  let qpCIdx = -1;
@@ -898,7 +1080,7 @@ function createOps(tx) {
898
1080
  const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
899
1081
  qpC = addC(qTextC);
900
1082
  qpCIdx = bindC.length - 1;
901
- 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 WHERE c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
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}`;
902
1084
  }
903
1085
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
904
1086
  const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
@@ -913,13 +1095,13 @@ function createOps(tx) {
913
1095
  } else {
914
1096
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
915
1097
  }
916
- 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 WHERE true${whereC}`;
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`;
917
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}`;
918
1100
  };
919
1101
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
920
1102
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
921
1103
  const tsch = await trgmSchemaOf(liveC);
922
- 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 WHERE ${qpC} OPERATOR(${quoteIdent(tsch)}.<%) coalesce(c.content,'') ORDER BY r LIMIT ${cpool}`;
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}`;
923
1105
  const retryBindC = bindC.slice();
924
1106
  if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
925
1107
  rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
@@ -1015,7 +1197,7 @@ function createOps(tx) {
1015
1197
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1016
1198
  const tsch = await trgmSchemaOf(live);
1017
1199
  const expr = ftsExprOf(cfg.ftsCols);
1018
- 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 ${qpRef} OPERATOR(${quoteIdent(tsch)}.<%) ${expr}${whereSql} ORDER BY r LIMIT ${pool}`;
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}`;
1019
1201
  const retryBind = bind.slice();
1020
1202
  if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1021
1203
  rows = await live.unsafe(assemble(trgmKw, false), retryBind);
@@ -1038,6 +1220,25 @@ function createOps(tx) {
1038
1220
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1039
1221
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1040
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
+ },
1041
1242
  async similar(table, id, opts = {}) {
1042
1243
  const cfg = searchConfigFor(table);
1043
1244
  if (!cfg) {
@@ -1269,17 +1470,35 @@ function withTables(ops, schema = currentSchema) {
1269
1470
  }
1270
1471
  var SERVICE_LOCK_TIMEOUT = "5s";
1271
1472
  function isAbortedTransaction(e) {
1272
- const code = e?.code;
1273
1473
  const message = String(e?.message ?? "");
1274
- return code === "25P02" || /current transaction is aborted/i.test(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;
1275
1486
  }
1276
1487
  function isForeignKeyViolation(e) {
1277
- return e?.code === "23503";
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] ?? "";
1278
1498
  }
1279
1499
  function isLockTimeout(e) {
1280
- const code = e?.code;
1281
1500
  const message = String(e?.message ?? "");
1282
- return code === "55P03" || /lock timeout/i.test(message);
1501
+ return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
1283
1502
  }
1284
1503
  function diagnosingDriver(sql, surface = "service") {
1285
1504
  const original = (e) => String(e?.message ?? e);
@@ -1289,6 +1508,9 @@ function diagnosingDriver(sql, surface = "service") {
1289
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)})`
1290
1509
  );
1291
1510
  }
1511
+ if (isUniqueViolation(e)) {
1512
+ return new UniqueViolation(constraintOf(e));
1513
+ }
1292
1514
  if (surface === "service" && isForeignKeyViolation(e)) {
1293
1515
  return new Error(
1294
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)})`
@@ -1417,10 +1639,7 @@ async function runPlanOp(sp, op, results) {
1417
1639
  const rendered = cols.map(
1418
1640
  (c) => renderValue(op.values[c], c, args, results, opVectorCols)
1419
1641
  );
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 *`;
1642
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
1424
1643
  break;
1425
1644
  }
1426
1645
  case "insertMany": {
@@ -1430,7 +1649,9 @@ async function runPlanOp(sp, op, results) {
1430
1649
  const tuples = rows.map(
1431
1650
  (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
1432
1651
  );
1433
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
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 *`;
1434
1655
  break;
1435
1656
  }
1436
1657
  case "update": {
@@ -1487,6 +1708,7 @@ function buildRouteTable(controllers) {
1487
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.`
1488
1709
  );
1489
1710
  }
1711
+ assertZeroArgConstructor(Ctrl, "controller");
1490
1712
  const instance = new ctor();
1491
1713
  for (const r of routes) {
1492
1714
  const full = `${basePath}${r.subpath ?? ""}` || "/";
@@ -1496,7 +1718,10 @@ function buildRouteTable(controllers) {
1496
1718
  meta: r,
1497
1719
  instance,
1498
1720
  id: `${r.method} ${full}`,
1499
- controllerAuth: meta?.defaultAuth
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)
1500
1725
  });
1501
1726
  }
1502
1727
  }
@@ -2125,18 +2350,29 @@ async function createApp(opts) {
2125
2350
  return envelope("internal_error", "The request could not be completed", 500, requestId);
2126
2351
  }
2127
2352
  }
2353
+ let runShutdownHooks;
2354
+ try {
2355
+ runShutdownHooks = await __runStartHooks();
2356
+ } catch (err) {
2357
+ await closeDriver(sql);
2358
+ throw err;
2359
+ }
2128
2360
  return {
2129
2361
  handle,
2130
2362
  routes,
2131
2363
  config,
2132
2364
  runInServiceScope,
2133
2365
  async shutdown() {
2134
- const closable = sql;
2135
- await closable.close?.();
2136
- await closable.end?.();
2366
+ await runShutdownHooks();
2367
+ await closeDriver(sql);
2137
2368
  }
2138
2369
  };
2139
2370
  }
2371
+ async function closeDriver(sql) {
2372
+ const closable = sql;
2373
+ await closable.close?.();
2374
+ await closable.end?.();
2375
+ }
2140
2376
 
2141
2377
  export {
2142
2378
  BootRefused,
@@ -2157,4 +2393,4 @@ export {
2157
2393
  installEgressFence,
2158
2394
  createApp
2159
2395
  };
2160
- //# sourceMappingURL=chunk-M5MCBWJI.js.map
2396
+ //# sourceMappingURL=chunk-NXDH6VQJ.js.map