@palbase/backend 23.1.0 → 24.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/bin/palbase-backend.cjs +400 -47
  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-PY7YJDCT.js → chunk-LCL7TUAI.js} +32 -3
  10. package/dist/chunk-LCL7TUAI.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-M5MCBWJI.js → chunk-YSQBC2VL.js} +275 -31
  16. package/dist/chunk-YSQBC2VL.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 +389 -34
  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-BOS_rFBO.d.ts} +125 -21
  30. package/dist/{index-CxeQSfJP.d.cts → index-C84bLgeO.d.cts} +134 -9
  31. package/dist/{index-By8Dle5U.d.cts → index-MoQ31B6M.d.cts} +125 -21
  32. package/dist/{index-BZrJXnVh.d.ts → index-dJNhDZ7j.d.ts} +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,13 +652,33 @@ 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 = [];
671
+ const transforms = transformsOf(currentSchema, table);
672
+ const bindFor = (col) => {
673
+ const t = transforms.get(col);
674
+ return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
675
+ };
623
676
  for (const [col, cond] of Object.entries(where)) {
624
677
  if (colSet !== null && !colSet.has(col)) {
625
678
  throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
626
679
  }
627
680
  const q = `t.${quoteIdent(col)}`;
681
+ const bind = bindFor(col);
628
682
  if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
629
683
  for (const [op, v] of Object.entries(cond)) {
630
684
  if (op === "in") {
@@ -633,19 +687,26 @@ function compileWhere(table, colSet, where, add, caller = "search") {
633
687
  parts.push("false");
634
688
  continue;
635
689
  }
636
- parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
690
+ parts.push(`${q} IN (${v.map((x) => bind(x)).join(", ")})`);
637
691
  } else if (op in WHERE_OPS) {
638
- parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
692
+ parts.push(`${q} ${WHERE_OPS[op]} ${bind(v)}`);
639
693
  } else {
640
694
  throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
641
695
  }
642
696
  }
643
697
  } else {
644
- parts.push(`${q} = ${add(cond)}`);
698
+ parts.push(`${q} = ${bind(cond)}`);
645
699
  }
646
700
  }
647
701
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
648
702
  }
703
+ function onConflictTail(cols, conflict, action) {
704
+ const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
705
+ if (action === "ignore") return `${target} DO NOTHING`;
706
+ const conflictSet = new Set(conflict);
707
+ const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
708
+ return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
709
+ }
649
710
  var cachedVectorSchema = null;
650
711
  var cachedTrgmSchema = null;
651
712
  async function trgmSchemaOf(live) {
@@ -755,6 +816,39 @@ function createOps(tx) {
755
816
  }
756
817
  const cols = Object.keys(data);
757
818
  if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
819
+ if (searchConfigFor(table)?.validity) {
820
+ const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
821
+ 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 *`;
822
+ const live = await at();
823
+ const bindData = asBindParams(table, cols, data);
824
+ const first = await live.unsafe(insertSql, bindData);
825
+ if (first[0]) return asTableRow(table, first[0]);
826
+ const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
827
+ const condBind = conflict.map((c) => data[c]);
828
+ const cur = await live.unsafe(
829
+ `SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
830
+ condBind
831
+ );
832
+ const old = cur[0];
833
+ if (old === void 0) {
834
+ 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)`);
835
+ }
836
+ const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
837
+ await live.unsafe(
838
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
839
+ [old[pk]]
840
+ );
841
+ const second = await live.unsafe(insertSql, bindData);
842
+ if (!second[0]) {
843
+ 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)`);
844
+ }
845
+ const fresh = second[0];
846
+ await live.unsafe(
847
+ `UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
848
+ [old[pk], fresh[pk]]
849
+ );
850
+ return asTableRow(table, second[0]);
851
+ }
758
852
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
759
853
  const conflictSet = new Set(conflict);
760
854
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
@@ -780,6 +874,82 @@ function createOps(tx) {
780
874
  async delete(table, id) {
781
875
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
782
876
  },
877
+ /**
878
+ * Update every row the filter matches, in ONE statement.
879
+ *
880
+ * The capability was already here and only reachable from inside a
881
+ * transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
882
+ * only door was `update(id, …)`, so "mark every unapproved row in this
883
+ * household" was an N+1 loop or hand-written SQL — and hand-written SQL is
884
+ * where the typed surface and RLS both stop helping.
885
+ *
886
+ * The filter language is `findMany`'s, compiled by the same `compileWhere`:
887
+ * one filter language, or the two spellings drift.
888
+ *
889
+ * AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
890
+ * and the shape that produces it by accident — a filter object built from
891
+ * request input that happened to come back empty — is exactly the shape that
892
+ * should not silently succeed. Callers who mean every row say so with a
893
+ * predicate that is true for every row.
894
+ */
895
+ async updateMany(table, where, set) {
896
+ const cols = Object.keys(set);
897
+ if (cols.length === 0) {
898
+ throw new Error(
899
+ `updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`
900
+ );
901
+ }
902
+ const params = [];
903
+ const add = (v) => {
904
+ params.push(v);
905
+ return `$${params.length}`;
906
+ };
907
+ const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
908
+ const known = schemaColumns(table);
909
+ const whereSql = compileWhere(table, known, where, add, "updateMany");
910
+ assertHasPredicate(whereSql, "updateMany", table);
911
+ const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
912
+ return asTableRows(table, await (await at()).unsafe(sql, params));
913
+ },
914
+ /**
915
+ * Delete every row the filter matches, in ONE statement; resolves to how
916
+ * many went. Same filter language, same empty-filter refusal as
917
+ * {@link updateMany} — and here the accident is worse.
918
+ */
919
+ async deleteMany(table, where) {
920
+ const params = [];
921
+ const add = (v) => {
922
+ params.push(v);
923
+ return `$${params.length}`;
924
+ };
925
+ const known = schemaColumns(table);
926
+ const whereSql = compileWhere(table, known, where, add, "deleteMany");
927
+ assertHasPredicate(whereSql, "deleteMany", table);
928
+ const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
929
+ const rows = await (await at()).unsafe(sql, params);
930
+ return rows.length;
931
+ },
932
+ /**
933
+ * How many rows match — the half of pagination `limit`/`offset` cannot
934
+ * supply. Without it a page count is either a guess or "fetch everything and
935
+ * read .length", and the second one is the scale risk this surface exists to
936
+ * remove.
937
+ *
938
+ * An empty filter is legitimate HERE: counting a whole table is a read, and
939
+ * reads do not destroy anything.
940
+ */
941
+ async count(table, where = {}) {
942
+ const params = [];
943
+ const add = (v) => {
944
+ params.push(v);
945
+ return `$${params.length}`;
946
+ };
947
+ const known = schemaColumns(table);
948
+ const whereSql = compileWhere(table, known, where, add, "count");
949
+ const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
950
+ const rows = await (await at()).unsafe(sql, params);
951
+ return Number(rows[0]?.n ?? 0);
952
+ },
783
953
  async findById(table, id) {
784
954
  const rows = await (await at()).unsafe(
785
955
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
@@ -797,7 +967,8 @@ function createOps(tx) {
797
967
  const whereSql = compileWhere(table, known, query, add, "findMany");
798
968
  const order = orderClause(table, known, opts.orderBy);
799
969
  const limit = limitClause(opts.limit);
800
- const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
970
+ const offset = offsetClause(opts.offset, opts.limit);
971
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
801
972
  return asTableRows(table, await (await at()).unsafe(sql, params));
802
973
  },
803
974
  /**
@@ -880,7 +1051,9 @@ function createOps(tx) {
880
1051
  bindC.push(v);
881
1052
  return `$${bindC.length}`;
882
1053
  };
883
- const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1054
+ const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
1055
+ const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1056
+ const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
884
1057
  const liveC = await at();
885
1058
  const K2 = 50;
886
1059
  const cpool = Math.max(limit * 9, 90);
@@ -889,8 +1062,19 @@ function createOps(tx) {
889
1062
  if (qvC !== null) {
890
1063
  const sch = await vectorSchemaWithGuc(liveC);
891
1064
  const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1065
+ let exactOrderC = "";
1066
+ if (userWhereC !== "") {
1067
+ const probeRows = await liveC.unsafe(
1068
+ `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`,
1069
+ bindC.slice()
1070
+ );
1071
+ const n = probeRows?.[0]?.n;
1072
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1073
+ exactOrderC = " + 0.0";
1074
+ }
1075
+ }
892
1076
  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}`;
1077
+ 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
1078
  }
895
1079
  let qpC = "";
896
1080
  let qpCIdx = -1;
@@ -898,7 +1082,7 @@ function createOps(tx) {
898
1082
  const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
899
1083
  qpC = addC(qTextC);
900
1084
  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}`;
1085
+ 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
1086
  }
903
1087
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
904
1088
  const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
@@ -913,13 +1097,13 @@ function createOps(tx) {
913
1097
  } else {
914
1098
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
915
1099
  }
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}`;
1100
+ 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
1101
  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
1102
  };
919
1103
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
920
1104
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
921
1105
  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}`;
1106
+ 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
1107
  const retryBindC = bindC.slice();
924
1108
  if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
925
1109
  rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
@@ -1015,7 +1199,7 @@ function createOps(tx) {
1015
1199
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1016
1200
  const tsch = await trgmSchemaOf(live);
1017
1201
  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}`;
1202
+ 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
1203
  const retryBind = bind.slice();
1020
1204
  if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1021
1205
  rows = await live.unsafe(assemble(trgmKw, false), retryBind);
@@ -1038,6 +1222,25 @@ function createOps(tx) {
1038
1222
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1039
1223
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1040
1224
  */
1225
+ /** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
1226
+ * `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
1227
+ * yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
1228
+ * search'teki alan geriye-uyum için DURUR. where + validity default'u
1229
+ * sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
1230
+ async facets(table, params) {
1231
+ const cfg = searchConfigFor(table);
1232
+ if (!cfg) {
1233
+ throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
1234
+ }
1235
+ for (const col of params.facets) {
1236
+ if (!cfg.colSet.has(col)) {
1237
+ throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1238
+ }
1239
+ }
1240
+ const live = await at();
1241
+ const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
1242
+ return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
1243
+ },
1041
1244
  async similar(table, id, opts = {}) {
1042
1245
  const cfg = searchConfigFor(table);
1043
1246
  if (!cfg) {
@@ -1269,17 +1472,41 @@ function withTables(ops, schema = currentSchema) {
1269
1472
  }
1270
1473
  var SERVICE_LOCK_TIMEOUT = "5s";
1271
1474
  function isAbortedTransaction(e) {
1272
- const code = e?.code;
1273
1475
  const message = String(e?.message ?? "");
1274
- return code === "25P02" || /current transaction is aborted/i.test(message);
1476
+ return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
1477
+ }
1478
+ function assertHasPredicate(whereSql, op, table) {
1479
+ if (whereSql.trim().length > 0) return;
1480
+ throw new Error(
1481
+ `${op}(${table}): the filter compiled to no condition, so this would have written EVERY row. An empty filter \u2014 or an operator object with no operators in it, like { col: {} } \u2014 is refused. Name a condition.`
1482
+ );
1483
+ }
1484
+ function sqlstateOf(e) {
1485
+ const err = e;
1486
+ const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
1487
+ if (isState(err?.code)) return err.code;
1488
+ if (isState(err?.errno)) return err.errno;
1489
+ if (typeof err?.errno === "number") {
1490
+ const asText = String(err.errno);
1491
+ if (isState(asText)) return asText;
1492
+ }
1493
+ return void 0;
1275
1494
  }
1276
1495
  function isForeignKeyViolation(e) {
1277
- return e?.code === "23503";
1496
+ return sqlstateOf(e) === "23503";
1497
+ }
1498
+ function isUniqueViolation(e) {
1499
+ return sqlstateOf(e) === "23505";
1500
+ }
1501
+ function constraintOf(e) {
1502
+ const named = e?.constraint;
1503
+ if (typeof named === "string" && named.length > 0) return named;
1504
+ const message = String(e?.message ?? "");
1505
+ return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
1278
1506
  }
1279
1507
  function isLockTimeout(e) {
1280
- const code = e?.code;
1281
1508
  const message = String(e?.message ?? "");
1282
- return code === "55P03" || /lock timeout/i.test(message);
1509
+ return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
1283
1510
  }
1284
1511
  function diagnosingDriver(sql, surface = "service") {
1285
1512
  const original = (e) => String(e?.message ?? e);
@@ -1289,6 +1516,9 @@ function diagnosingDriver(sql, surface = "service") {
1289
1516
  `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
1517
  );
1291
1518
  }
1519
+ if (isUniqueViolation(e)) {
1520
+ return new UniqueViolation(constraintOf(e));
1521
+ }
1292
1522
  if (surface === "service" && isForeignKeyViolation(e)) {
1293
1523
  return new Error(
1294
1524
  `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 +1647,7 @@ async function runPlanOp(sp, op, results) {
1417
1647
  const rendered = cols.map(
1418
1648
  (c) => renderValue(op.values[c], c, args, results, opVectorCols)
1419
1649
  );
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 *`;
1650
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
1424
1651
  break;
1425
1652
  }
1426
1653
  case "insertMany": {
@@ -1430,7 +1657,9 @@ async function runPlanOp(sp, op, results) {
1430
1657
  const tuples = rows.map(
1431
1658
  (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
1432
1659
  );
1433
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
1660
+ const conflictCols = op.onConflict ?? [];
1661
+ const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
1662
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
1434
1663
  break;
1435
1664
  }
1436
1665
  case "update": {
@@ -1487,6 +1716,7 @@ function buildRouteTable(controllers) {
1487
1716
  `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
1717
  );
1489
1718
  }
1719
+ assertZeroArgConstructor(Ctrl, "controller");
1490
1720
  const instance = new ctor();
1491
1721
  for (const r of routes) {
1492
1722
  const full = `${basePath}${r.subpath ?? ""}` || "/";
@@ -1496,7 +1726,10 @@ function buildRouteTable(controllers) {
1496
1726
  meta: r,
1497
1727
  instance,
1498
1728
  id: `${r.method} ${full}`,
1499
- controllerAuth: meta?.defaultAuth
1729
+ // The route's OWN spec is deliberately not folded in here: this field
1730
+ // is what applies when the route is silent, and `effectiveAuth` puts
1731
+ // the route level back on top per request.
1732
+ controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
1500
1733
  });
1501
1734
  }
1502
1735
  }
@@ -2125,18 +2358,29 @@ async function createApp(opts) {
2125
2358
  return envelope("internal_error", "The request could not be completed", 500, requestId);
2126
2359
  }
2127
2360
  }
2361
+ let runShutdownHooks;
2362
+ try {
2363
+ runShutdownHooks = await __runStartHooks();
2364
+ } catch (err) {
2365
+ await closeDriver(sql);
2366
+ throw err;
2367
+ }
2128
2368
  return {
2129
2369
  handle,
2130
2370
  routes,
2131
2371
  config,
2132
2372
  runInServiceScope,
2133
2373
  async shutdown() {
2134
- const closable = sql;
2135
- await closable.close?.();
2136
- await closable.end?.();
2374
+ await runShutdownHooks();
2375
+ await closeDriver(sql);
2137
2376
  }
2138
2377
  };
2139
2378
  }
2379
+ async function closeDriver(sql) {
2380
+ const closable = sql;
2381
+ await closable.close?.();
2382
+ await closable.end?.();
2383
+ }
2140
2384
 
2141
2385
  export {
2142
2386
  BootRefused,
@@ -2157,4 +2401,4 @@ export {
2157
2401
  installEgressFence,
2158
2402
  createApp
2159
2403
  };
2160
- //# sourceMappingURL=chunk-M5MCBWJI.js.map
2404
+ //# sourceMappingURL=chunk-YSQBC2VL.js.map