@palbase/backend 22.1.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/bin/palbase-backend.cjs +750 -40
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +5 -5
  4. package/dist/{chunk-YL4C5NRY.js → chunk-HQRJDARQ.js} +2 -2
  5. package/dist/{chunk-74XDEF5J.js → chunk-M5MCBWJI.js} +723 -37
  6. package/dist/chunk-M5MCBWJI.js.map +1 -0
  7. package/dist/{chunk-W5ODXPY3.js → chunk-NS5V43YQ.js} +14 -1
  8. package/dist/chunk-NS5V43YQ.js.map +1 -0
  9. package/dist/{chunk-SQC5EIWY.js → chunk-OHALWEOG.js} +19 -9
  10. package/dist/chunk-OHALWEOG.js.map +1 -0
  11. package/dist/{chunk-I3ON7MYF.js → chunk-PY7YJDCT.js} +129 -18
  12. package/dist/chunk-PY7YJDCT.js.map +1 -0
  13. package/dist/{chunk-N32VDWKH.js → chunk-R3KN6RHD.js} +4 -59
  14. package/dist/chunk-R3KN6RHD.js.map +1 -0
  15. package/dist/{chunk-QMVK4X3V.js → chunk-RCLNBJCM.js} +98 -98
  16. package/dist/chunk-RCLNBJCM.js.map +1 -0
  17. package/dist/db/env.cjs.map +1 -1
  18. package/dist/db/env.d.cts +3 -21
  19. package/dist/db/env.d.ts +3 -21
  20. package/dist/db/index.cjs +140 -16
  21. package/dist/db/index.cjs.map +1 -1
  22. package/dist/db/index.d.cts +3 -2
  23. package/dist/db/index.d.ts +3 -2
  24. package/dist/db/index.js +2 -2
  25. package/dist/{endpoint-BVT6jcVW.d.cts → endpoint-CVWXh6oG.d.ts} +147 -15
  26. package/dist/{endpoint-BVT6jcVW.d.ts → endpoint-c9h5jriX.d.cts} +147 -15
  27. package/dist/engine/index.cjs +750 -40
  28. package/dist/engine/index.cjs.map +1 -1
  29. package/dist/engine/index.d.cts +6 -5
  30. package/dist/engine/index.d.ts +6 -5
  31. package/dist/engine/index.js +4 -4
  32. package/dist/{index-BS1gW4nV.d.cts → index-BZrJXnVh.d.ts} +142 -72
  33. package/dist/{index-BqCiHao8.d.cts → index-By8Dle5U.d.cts} +196 -21
  34. package/dist/{index-vwHoS0l2.d.ts → index-CwAJ7HEe.d.ts} +196 -21
  35. package/dist/{index-CCZqzych.d.ts → index-CxeQSfJP.d.cts} +142 -72
  36. package/dist/index.cjs +388 -1101
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +89 -1134
  39. package/dist/index.d.ts +89 -1134
  40. package/dist/index.js +133 -902
  41. package/dist/index.js.map +1 -1
  42. package/dist/openapi/index.cjs +32 -61
  43. package/dist/openapi/index.cjs.map +1 -1
  44. package/dist/openapi/index.d.cts +6 -2
  45. package/dist/openapi/index.d.ts +6 -2
  46. package/dist/openapi/index.js +34 -26
  47. package/dist/openapi/index.js.map +1 -1
  48. package/dist/{registry-Bsuf-orT.d.ts → registry-B3niOVYp.d.ts} +108 -170
  49. package/dist/{registry-BWttGlaT.d.cts → registry-CqPK2Qby.d.cts} +108 -170
  50. package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
  51. package/dist/stack.cjs.map +1 -0
  52. package/dist/stack.d.cts +76 -0
  53. package/dist/stack.d.ts +76 -0
  54. package/dist/stack.js +1 -0
  55. package/dist/test/index.cjs +482 -9
  56. package/dist/test/index.cjs.map +1 -1
  57. package/dist/test/index.d.cts +35 -3
  58. package/dist/test/index.d.ts +35 -3
  59. package/dist/test/index.js +480 -8
  60. package/dist/test/index.js.map +1 -1
  61. package/docs/README.md +7 -6
  62. package/docs/llms-full.txt +7 -260
  63. package/docs/llms.txt +0 -2
  64. package/package.json +9 -8
  65. package/stager/return_types.js +23 -0
  66. package/template/package.json +1 -1
  67. package/dist/chunk-74XDEF5J.js.map +0 -1
  68. package/dist/chunk-I3ON7MYF.js.map +0 -1
  69. package/dist/chunk-N32VDWKH.js.map +0 -1
  70. package/dist/chunk-QMVK4X3V.js.map +0 -1
  71. package/dist/chunk-SQC5EIWY.js.map +0 -1
  72. package/dist/chunk-W5ODXPY3.js.map +0 -1
  73. package/dist/purchases/keys.cjs.map +0 -1
  74. package/dist/purchases/keys.d.cts +0 -42
  75. package/dist/purchases/keys.d.ts +0 -42
  76. package/dist/purchases/keys.js +0 -1
  77. package/docs/config.md +0 -147
  78. package/docs/resources.md +0 -97
  79. package/template/config/secrets.ts +0 -24
  80. /package/dist/{chunk-YL4C5NRY.js.map → chunk-HQRJDARQ.js.map} +0 -0
  81. /package/dist/{purchases/keys.js.map → stack.js.map} +0 -0
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  __requestALS,
3
3
  __runWithRuntime
4
- } from "./chunk-SQC5EIWY.js";
4
+ } from "./chunk-OHALWEOG.js";
5
5
  import {
6
6
  getRoutes,
7
7
  isHttpError
8
- } from "./chunk-QMVK4X3V.js";
8
+ } from "./chunk-RCLNBJCM.js";
9
9
 
10
10
  // src/engine/config.ts
11
11
  var BootRefused = class extends Error {
@@ -323,6 +323,14 @@ function makeMemoryCache(opts = {}) {
323
323
  function quoteIdent(name) {
324
324
  return `"${name.replace(/"/g, '""')}"`;
325
325
  }
326
+ function encodePgArray(value) {
327
+ const element = (x) => {
328
+ if (x === null || x === void 0) return "NULL";
329
+ if (Array.isArray(x)) return encodePgArray(x);
330
+ return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
331
+ };
332
+ return `{${value.map(element).join(",")}}`;
333
+ }
326
334
  var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
327
335
  function createLazyTransaction(sql, role, claimsJson, options = {}) {
328
336
  const { lockTimeout } = options;
@@ -441,7 +449,42 @@ function searchConfigFor(table) {
441
449
  );
442
450
  }
443
451
  const search = t.search;
444
- const ftsCols = search?.text ?? [];
452
+ const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
453
+ const validity = search?.validity === true ? { validity: true } : {};
454
+ if (search?.from !== void 0 && search.model !== void 0) {
455
+ const m = search.model;
456
+ const embed = {
457
+ model: m.model,
458
+ apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
459
+ ...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
460
+ ...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
461
+ };
462
+ const metric = search.metric ?? "cosine";
463
+ const ftsOn = search.text !== false;
464
+ const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
465
+ if (vectorCols.length > 0) {
466
+ return {
467
+ pk,
468
+ cols,
469
+ colSet: new Set(cols),
470
+ ftsCols: ftsColsNew,
471
+ legs: [{ column: vectorCols[0], metric, embed }],
472
+ ...synonyms,
473
+ ...validity
474
+ };
475
+ }
476
+ return {
477
+ pk,
478
+ cols,
479
+ colSet: new Set(cols),
480
+ ftsCols: ftsColsNew,
481
+ legs: [],
482
+ chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
483
+ ...synonyms,
484
+ ...validity
485
+ };
486
+ }
487
+ const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
445
488
  const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
446
489
  let legs;
447
490
  if (rawLegs.length > 0) {
@@ -467,7 +510,7 @@ function searchConfigFor(table) {
467
510
  legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
468
511
  }
469
512
  if (ftsCols.length === 0 && legs.length === 0) return null;
470
- return { pk, cols, colSet: new Set(cols), ftsCols, legs };
513
+ return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
471
514
  }
472
515
  function pickLeg(table, legs, using) {
473
516
  if (legs.length === 0) return null;
@@ -483,18 +526,109 @@ function pickLeg(table, legs, using) {
483
526
  if (legs.length === 1) return legs[0];
484
527
  throw new Error(`search(${table}): birden \xE7ok vekt\xF6r kolu var \u2014 'using' ile se\xE7in (FR-013) \u2014 salt metin ar\u0131yorsan mode:"text" kullan`);
485
528
  }
529
+ var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
530
+ function parseHalfLife(s) {
531
+ const m = /^(\d+)(s|m|h|d)$/.exec(s);
532
+ const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
533
+ if (!Number.isFinite(sec) || sec <= 0) {
534
+ throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
535
+ }
536
+ return sec;
537
+ }
486
538
  var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
487
- function compileWhere(table, colSet, where, add) {
539
+ function ftsExprOf(ftsCols) {
540
+ return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
541
+ }
542
+ function expandSynonyms(query, map) {
543
+ const lower = {};
544
+ for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
545
+ return query.split(/("[^"]*")/).map((seg) => {
546
+ if (seg.startsWith('"')) return seg;
547
+ return seg.split(/(\s+)/).map((tok) => {
548
+ if (tok === "" || /^\s+$/.test(tok)) return tok;
549
+ const alts = lower[tok.toLowerCase()];
550
+ return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
551
+ }).join("");
552
+ }).join("");
553
+ }
554
+ var searchLogger = (evt, fields) => {
555
+ console.log(evt, JSON.stringify(fields));
556
+ };
557
+ function logNoResults(table, query, mode, n) {
558
+ if (n === 0) {
559
+ searchLogger?.("palbase.search.no_results", {
560
+ table,
561
+ query: (query ?? "").slice(0, 200),
562
+ mode: mode ?? "hybrid"
563
+ });
564
+ }
565
+ }
566
+ async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
567
+ const bind = [];
568
+ const add = (v) => {
569
+ bind.push(v);
570
+ return `$${bind.length}`;
571
+ };
572
+ const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
573
+ const parts = facets.map(
574
+ (col) => `(SELECT '${col.replace(/'/g, "''")}' AS f, t.${quoteIdent(col)}::text AS v, count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql} GROUP BY 2 ORDER BY 3 DESC LIMIT 20)`
575
+ );
576
+ const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
577
+ const out = {};
578
+ for (const col of facets) out[col] = [];
579
+ for (const r of rows) {
580
+ if (typeof r.f === "string" && out[r.f] !== void 0) {
581
+ out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
582
+ }
583
+ }
584
+ return out;
585
+ }
586
+ function validitySql(v, add) {
587
+ if (v === "all") return "";
588
+ if (v !== void 0) {
589
+ const p = add(v.asOf);
590
+ return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
591
+ }
592
+ return ` AND t."valid_to" IS NULL`;
593
+ }
594
+ function excludeSql(pk, ids, add) {
595
+ if (ids.length === 0) return "";
596
+ if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
597
+ return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
598
+ }
599
+ function schemaColumns(table) {
600
+ const t = Object.values(currentSchema.tables ?? {}).find((x) => x?.name === table);
601
+ const cols = t?.columns ? Object.keys(t.columns) : [];
602
+ return cols.length > 0 ? new Set(cols) : null;
603
+ }
604
+ function orderClause(table, known, orderBy) {
605
+ if (!orderBy) return "";
606
+ const col = orderBy.column;
607
+ const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
608
+ if (known !== null ? !known.has(col) : !shapeOK) {
609
+ throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
610
+ }
611
+ const dir = orderBy.direction === "desc" ? "DESC" : "ASC";
612
+ return ` ORDER BY ${quoteIdent(col)} ${dir}`;
613
+ }
614
+ function limitClause(limit) {
615
+ if (limit === void 0) return "";
616
+ if (!Number.isInteger(limit) || limit < 0) {
617
+ throw new Error(`findMany: limit bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(limit)})`);
618
+ }
619
+ return ` LIMIT ${limit}`;
620
+ }
621
+ function compileWhere(table, colSet, where, add, caller = "search") {
488
622
  const parts = [];
489
623
  for (const [col, cond] of Object.entries(where)) {
490
- if (!colSet.has(col)) {
491
- throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
624
+ if (colSet !== null && !colSet.has(col)) {
625
+ throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
492
626
  }
493
627
  const q = `t.${quoteIdent(col)}`;
494
628
  if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
495
629
  for (const [op, v] of Object.entries(cond)) {
496
630
  if (op === "in") {
497
- if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
631
+ if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
498
632
  if (v.length === 0) {
499
633
  parts.push("false");
500
634
  continue;
@@ -503,7 +637,7 @@ function compileWhere(table, colSet, where, add) {
503
637
  } else if (op in WHERE_OPS) {
504
638
  parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
505
639
  } else {
506
- throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
640
+ throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
507
641
  }
508
642
  }
509
643
  } else {
@@ -513,6 +647,15 @@ function compileWhere(table, colSet, where, add) {
513
647
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
514
648
  }
515
649
  var cachedVectorSchema = null;
650
+ var cachedTrgmSchema = null;
651
+ async function trgmSchemaOf(live) {
652
+ if (cachedTrgmSchema !== null) return cachedTrgmSchema;
653
+ const rows = await live.unsafe(
654
+ "select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
655
+ );
656
+ cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
657
+ return cachedTrgmSchema;
658
+ }
516
659
  async function vectorSchemaWithGuc(runner) {
517
660
  if (cachedVectorSchema !== null) {
518
661
  await runner.unsafe(
@@ -574,7 +717,8 @@ function createOps(tx) {
574
717
  const at = () => resolveTx(tx);
575
718
  const ops = {
576
719
  async query(sql, params = []) {
577
- return asWireRows(await (await at()).unsafe(sql, params));
720
+ const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
721
+ return asWireRows(await (await at()).unsafe(sql, bound));
578
722
  },
579
723
  async insert(table, data) {
580
724
  const cols = Object.keys(data);
@@ -590,6 +734,41 @@ function createOps(tx) {
590
734
  }
591
735
  return asTableRow(table, inserted);
592
736
  },
737
+ /**
738
+ * INSERT the row, or UPDATE it when it collides on `onConflict`.
739
+ *
740
+ * WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
741
+ * unique violation, update instead" does not work here: a request runs in ONE
742
+ * Postgres transaction, so the failed insert aborts it and every later
743
+ * statement answers `current transaction is aborted`. A tenant measured that
744
+ * as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
745
+ * trigger create the row instead — a workaround that needs a new trigger for
746
+ * every table with a unique row.
747
+ *
748
+ * The conflict columns are excluded from the SET list: they are what MATCHED,
749
+ * so writing them back is at best a no-op and at worst a surprise.
750
+ */
751
+ async upsert(table, data, opts) {
752
+ const conflict = opts.onConflict;
753
+ if (conflict.length === 0) {
754
+ throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
755
+ }
756
+ const cols = Object.keys(data);
757
+ if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
758
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
759
+ const conflictSet = new Set(conflict);
760
+ const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
761
+ const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
762
+ const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
763
+ const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
764
+ const row = rows[0];
765
+ if (!row) {
766
+ throw new Error(
767
+ `upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
768
+ );
769
+ }
770
+ return asTableRow(table, row);
771
+ },
593
772
  async update(table, id, data) {
594
773
  const cols = Object.keys(data);
595
774
  if (cols.length === 0) return ops.findById(table, id);
@@ -608,13 +787,18 @@ function createOps(tx) {
608
787
  );
609
788
  return rows[0] ? asTableRow(table, rows[0]) : null;
610
789
  },
611
- async findMany(table, query = {}) {
612
- const cols = Object.keys(query);
613
- const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
614
- return asTableRows(table, await (await at()).unsafe(
615
- `SELECT * FROM ${quoteIdent(table)}${where}`,
616
- cols.map((c) => query[c])
617
- ));
790
+ async findMany(table, query = {}, opts = {}) {
791
+ const params = [];
792
+ const add = (v) => {
793
+ params.push(v);
794
+ return `$${params.length}`;
795
+ };
796
+ const known = schemaColumns(table);
797
+ const whereSql = compileWhere(table, known, query, add, "findMany");
798
+ const order = orderClause(table, known, opts.orderBy);
799
+ const limit = limitClause(opts.limit);
800
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
801
+ return asTableRows(table, await (await at()).unsafe(sql, params));
618
802
  },
619
803
  /**
620
804
  * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
@@ -628,12 +812,128 @@ function createOps(tx) {
628
812
  if (!cfg) {
629
813
  throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
630
814
  }
815
+ const excl = params.__excludeIds ?? [];
816
+ if (params.facets !== void 0) {
817
+ for (const col of params.facets) {
818
+ if (!cfg.colSet.has(col)) {
819
+ throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
820
+ }
821
+ }
822
+ }
823
+ if (params.validity !== void 0 && cfg.validity !== true) {
824
+ throw new Error(
825
+ `search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
826
+ );
827
+ }
828
+ const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
631
829
  const rawLimit = params.limit ?? 20;
632
830
  if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
633
831
  throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
634
832
  }
635
833
  const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
636
834
  const pool = Math.max(limit * 3, 30);
835
+ if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
836
+ throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
837
+ }
838
+ let recencyMul = "";
839
+ if (params.recency !== void 0) {
840
+ const { field, halfLife } = params.recency;
841
+ if (!cfg.colSet.has(field)) {
842
+ throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
843
+ }
844
+ recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
845
+ }
846
+ let boostMul = "";
847
+ if (params.boost !== void 0) {
848
+ const { field, weight } = params.boost;
849
+ if (!cfg.colSet.has(field)) {
850
+ throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
851
+ }
852
+ if (typeof weight !== "number" || !Number.isFinite(weight)) {
853
+ throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
854
+ }
855
+ const g = `greatest(t.${quoteIdent(field)},0)`;
856
+ boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
857
+ }
858
+ const scoreMul = boostMul + recencyMul;
859
+ if (cfg.chunk !== void 0) {
860
+ const ck = cfg.chunk;
861
+ const pidQ = quoteIdent(`parent_${cfg.pk}`);
862
+ const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
863
+ const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
864
+ let qvC = Array.isArray(params.vector) ? params.vector : null;
865
+ if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
866
+ try {
867
+ qvC = await embedQuery(ck.embed, params.query);
868
+ } catch (e) {
869
+ if (!wantTextC) throw e;
870
+ qvC = null;
871
+ }
872
+ }
873
+ if (!wantTextC && qvC === null) {
874
+ throw new Error(
875
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
876
+ );
877
+ }
878
+ const bindC = [];
879
+ const addC = (v) => {
880
+ bindC.push(v);
881
+ return `$${bindC.length}`;
882
+ };
883
+ const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
884
+ const liveC = await at();
885
+ const K2 = 50;
886
+ const cpool = Math.max(limit * 9, 90);
887
+ let semC = "";
888
+ let kwC = "";
889
+ if (qvC !== null) {
890
+ const sch = await vectorSchemaWithGuc(liveC);
891
+ const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
892
+ 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}`;
894
+ }
895
+ let qpC = "";
896
+ let qpCIdx = -1;
897
+ if (wantTextC) {
898
+ const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
899
+ qpC = addC(qTextC);
900
+ 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}`;
902
+ }
903
+ const msC = params.minScore === void 0 ? "" : addC(params.minScore);
904
+ const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
905
+ const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
906
+ const assembleC = (kwIn, withKwn) => {
907
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
908
+ let fusedSrc;
909
+ if (semC !== "" && kwIn !== "") {
910
+ fusedSrc = `WITH sem AS (${semC}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.pid, kw.pid) AS pid, COALESCE(sem.chunk_seq, kw.chunk_seq) AS chunk_seq, (COALESCE(1.0/(${K2} + sem.r), 0) + COALESCE(1.0/(${K2} + kw.r), 0))::float8 AS cscore FROM sem FULL OUTER JOIN kw ON sem.pid = kw.pid AND sem.chunk_seq = kw.chunk_seq)`;
911
+ } else if (semC !== "") {
912
+ fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
913
+ } else {
914
+ fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
915
+ }
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}`;
917
+ 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
+ };
919
+ let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
920
+ if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
921
+ 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}`;
923
+ const retryBindC = bindC.slice();
924
+ if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
925
+ rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
926
+ }
927
+ for (const r of rowsC) delete r._kwn;
928
+ const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
929
+ if (params.facets !== void 0 && params.facets.length > 0) {
930
+ Object.assign(outC, {
931
+ _facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
932
+ });
933
+ }
934
+ logNoResults(table, params.query, params.mode, outC.length);
935
+ return outC;
936
+ }
637
937
  const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
638
938
  const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
639
939
  const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
@@ -658,7 +958,8 @@ function createOps(tx) {
658
958
  bind.push(v);
659
959
  return `$${bind.length}`;
660
960
  };
661
- const whereSql = compileWhere(table, cfg.colSet, params.where ?? {}, add);
961
+ const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
962
+ const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
662
963
  const live = await at();
663
964
  const K = 50;
664
965
  let semSql = "";
@@ -667,7 +968,7 @@ function createOps(tx) {
667
968
  const sch = await vectorSchemaWithGuc(live);
668
969
  const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
669
970
  let exactOrder = "";
670
- if (whereSql !== "") {
971
+ if (userWhere !== "") {
671
972
  const probeRows = await live.unsafe(
672
973
  `SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
673
974
  bind.slice()
@@ -680,27 +981,235 @@ function createOps(tx) {
680
981
  const vp = add(toVectorLiteral(qv));
681
982
  semSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY (t.${quoteIdent(leg.column)} OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrder}) AS r FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} ORDER BY r LIMIT ${pool}`;
682
983
  }
984
+ let qpRef = "";
985
+ let qpIdx = -1;
683
986
  if (wantText) {
684
- const qp = add(params.query);
987
+ const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
988
+ const qp = add(qText);
989
+ qpIdx = bind.length - 1;
990
+ qpRef = qp;
685
991
  kwSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(t.palbase_fts, websearch_to_tsquery('simple', ${qp})) DESC) AS r FROM ${quoteIdent(table)} t WHERE t.palbase_fts @@ websearch_to_tsquery('simple', ${qp})${whereSql} ORDER BY r LIMIT ${pool}`;
686
992
  }
687
993
  const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
688
- let sql;
689
- if (semSql !== "" && kwSql !== "") {
690
- sql = `WITH sem AS (${semSql}), kw AS (${kwSql}), fused AS (SELECT COALESCE(sem.id, kw.id) AS id, (COALESCE(1.0/(${K} + sem.r), 0) + COALESCE(1.0/(${K} + kw.r), 0))::float8 AS _score FROM sem FULL OUTER JOIN kw ON sem.id = kw.id) SELECT ${colList}, fused._score AS _score FROM fused JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id ORDER BY fused._score DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
994
+ const msP = params.minScore === void 0 ? "" : add(params.minScore);
995
+ const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
996
+ const assemble = (kwIn, withKwn) => {
997
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
998
+ let inner;
999
+ let orderScore;
1000
+ if (semSql !== "" && kwIn !== "") {
1001
+ const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
1002
+ inner = `WITH sem AS (${semSql}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.id, kw.id) AS id, (COALESCE(1.0/(${K} + sem.r), 0) + COALESCE(1.0/(${K} + kw.r), 0))::float8 AS _score FROM sem FULL OUTER JOIN kw ON sem.id = kw.id) SELECT ${colList}, ${score} AS _score${hlCol}${kwnCol} FROM fused JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id`;
1003
+ orderScore = scoreMul === "" ? "fused._score" : "_score";
1004
+ } else {
1005
+ const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
1006
+ const alias = semSql !== "" ? "sem" : "kw";
1007
+ const base = `(1.0/(${K} + ${alias}.r))::float8`;
1008
+ const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
1009
+ inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
1010
+ orderScore = "_score";
1011
+ }
1012
+ return msP === "" ? `${inner} ORDER BY ${orderScore} DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msP} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
1013
+ };
1014
+ let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
1015
+ if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1016
+ const tsch = await trgmSchemaOf(live);
1017
+ 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}`;
1019
+ const retryBind = bind.slice();
1020
+ if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1021
+ rows = await live.unsafe(assemble(trgmKw, false), retryBind);
1022
+ }
1023
+ for (const r of rows) delete r._kwn;
1024
+ const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
1025
+ if (params.facets !== void 0 && params.facets.length > 0) {
1026
+ Object.assign(out, {
1027
+ _facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
1028
+ });
1029
+ }
1030
+ logNoResults(table, params.query, params.mode, out.length);
1031
+ return out;
1032
+ },
1033
+ /**
1034
+ * similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
1035
+ * okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
1036
+ * şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
1037
+ * kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
1038
+ * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1039
+ * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1040
+ */
1041
+ async similar(table, id, opts = {}) {
1042
+ const cfg = searchConfigFor(table);
1043
+ if (!cfg) {
1044
+ throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1045
+ }
1046
+ const live = await at();
1047
+ let v;
1048
+ if (cfg.chunk !== void 0) {
1049
+ const sch = await vectorSchemaWithGuc(live);
1050
+ const rows = await live.unsafe(
1051
+ `SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} = $1 AND c.embedding IS NOT NULL`,
1052
+ [id]
1053
+ );
1054
+ v = rows?.[0]?.v;
691
1055
  } else {
692
- const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwSql})`;
693
- const alias = semSql !== "" ? "sem" : "kw";
694
- sql = `WITH ${single} SELECT ${colList}, (1.0/(${K} + ${alias}.r))::float8 AS _score FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id ORDER BY _score DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
1056
+ const leg = pickLeg(table, cfg.legs, opts.using);
1057
+ if (leg === null) {
1058
+ throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
1059
+ }
1060
+ const rows = await live.unsafe(
1061
+ `SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
1062
+ [id]
1063
+ );
1064
+ v = rows?.[0]?.v;
695
1065
  }
696
- const rows = await live.unsafe(sql, bind);
697
- return asTableRows(table, rows);
1066
+ if (typeof v !== "string") {
1067
+ throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
1068
+ }
1069
+ const p = {
1070
+ vector: JSON.parse(v),
1071
+ mode: "vector",
1072
+ where: opts.where,
1073
+ limit: opts.limit,
1074
+ using: opts.using,
1075
+ minScore: opts.minScore,
1076
+ recency: opts.recency,
1077
+ blocksPerRow: opts.blocksPerRow,
1078
+ validity: opts.validity,
1079
+ boost: opts.boost,
1080
+ __excludeIds: [id]
1081
+ };
1082
+ return ops.search(table, p);
1083
+ },
1084
+ /**
1085
+ * recommend (T018, FR-023): positive/negative id kümelerinden öneri.
1086
+ * Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
1087
+ * negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
1088
+ * ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
1089
+ * Vektör matematiği istemciye inmez; bulunamayan positive/negative
1090
+ * adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
1091
+ */
1092
+ async recommend(table, opts) {
1093
+ const positive = opts?.positive;
1094
+ if (!Array.isArray(positive) || positive.length === 0) {
1095
+ throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
1096
+ }
1097
+ const negative = Array.isArray(opts.negative) ? opts.negative : [];
1098
+ const cfg = searchConfigFor(table);
1099
+ if (!cfg) {
1100
+ throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1101
+ }
1102
+ let leg = null;
1103
+ if (cfg.chunk === void 0) {
1104
+ leg = pickLeg(table, cfg.legs, opts.using);
1105
+ if (leg === null) {
1106
+ throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
1107
+ }
1108
+ }
1109
+ const live = await at();
1110
+ const sch = await vectorSchemaWithGuc(live);
1111
+ const bind = [];
1112
+ const add = (x) => {
1113
+ bind.push(x);
1114
+ return `$${bind.length}`;
1115
+ };
1116
+ const avgSel = (ids) => {
1117
+ const inList = ids.map((x) => add(x)).join(", ");
1118
+ return cfg.chunk !== void 0 ? `SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteIdent(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} IN (${inList}) AND c.embedding IS NOT NULL` : `SELECT ${quoteIdent(sch)}.avg(t.${quoteIdent(leg.column)}) AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} IN (${inList}) AND t.${quoteIdent(leg.column)} IS NOT NULL`;
1119
+ };
1120
+ const targetSql = negative.length === 0 ? `WITH pos AS (${avgSel(positive)}) SELECT pos.v AS v, (pos.v IS NULL) AS pos_missing FROM pos` : `WITH pos AS (${avgSel(positive)}), neg AS (${avgSel(negative)}) SELECT (pos.v OPERATOR(${quoteIdent(sch)}.+) (pos.v OPERATOR(${quoteIdent(sch)}.-) neg.v)) AS v, (pos.v IS NULL) AS pos_missing, (neg.v IS NULL) AS neg_missing FROM pos, neg`;
1121
+ const rows = await live.unsafe(targetSql, bind);
1122
+ const r0 = rows?.[0];
1123
+ if (r0 === void 0 || r0.pos_missing === true) {
1124
+ throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1125
+ }
1126
+ if (negative.length > 0 && r0.neg_missing === true) {
1127
+ throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1128
+ }
1129
+ if (typeof r0.v !== "string") {
1130
+ throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
1131
+ }
1132
+ const p = {
1133
+ vector: JSON.parse(r0.v),
1134
+ mode: "vector",
1135
+ where: opts.where,
1136
+ limit: opts.limit,
1137
+ using: opts.using,
1138
+ minScore: opts.minScore,
1139
+ recency: opts.recency,
1140
+ blocksPerRow: opts.blocksPerRow,
1141
+ validity: opts.validity,
1142
+ boost: opts.boost,
1143
+ __excludeIds: [...positive, ...negative]
1144
+ };
1145
+ return ops.search(table, p);
1146
+ },
1147
+ /**
1148
+ * supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
1149
+ * versiyonunu TEK savepoint'te yazar — eski satır kapatılır
1150
+ * (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
1151
+ * yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
1152
+ * randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
1153
+ * kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
1154
+ * ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
1155
+ * kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
1156
+ */
1157
+ async supersede(table, id, row) {
1158
+ const cfg = searchConfigFor(table);
1159
+ if (cfg === null || cfg.validity !== true) {
1160
+ throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
1161
+ }
1162
+ const live = await at();
1163
+ return live.savepoint(async (sp) => {
1164
+ const newId = row[cfg.pk] ?? crypto.randomUUID();
1165
+ const closed = await sp.unsafe(
1166
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
1167
+ [newId, id]
1168
+ );
1169
+ if (closed.length === 0) {
1170
+ throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
1171
+ }
1172
+ const data = { ...row, [cfg.pk]: newId };
1173
+ const cols = Object.keys(data);
1174
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1175
+ const rows = await sp.unsafe(
1176
+ `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
1177
+ asBindParams(table, cols, data)
1178
+ );
1179
+ if (!rows[0]) {
1180
+ throw new Error(
1181
+ `supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
1182
+ );
1183
+ }
1184
+ return asTableRow(table, rows[0]);
1185
+ });
698
1186
  },
699
1187
  /** A real SAVEPOINT inside the request's transaction. */
700
1188
  async transaction(cb) {
701
1189
  const live = await at();
702
1190
  return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
703
1191
  },
1192
+ /**
1193
+ * Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
1194
+ * rolls back only what that handle wrote and the request can keep writing.
1195
+ *
1196
+ * WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
1197
+ * ... Database.insert(...) ... })`, with no parameter — would have to point
1198
+ * the ambient `Database` at the savepoint for the duration, and a request is
1199
+ * concurrent with itself: `Promise.all([Database.insert(a),
1200
+ * Database.attempt(...)])` would put `a` inside the savepoint and roll it
1201
+ * back with it. Silent data loss, and the same interleaving this file already
1202
+ * refuses for `asService()`. Passing the handle makes the boundary something
1203
+ * you can see in the code that crosses it.
1204
+ *
1205
+ * Postgres, not us: the savepoint is released on success and rolled back on
1206
+ * failure by the driver, so an aborted statement inside `fn` does not poison
1207
+ * the surrounding transaction.
1208
+ */
1209
+ async attempt(fn) {
1210
+ const live = await at();
1211
+ return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
1212
+ },
704
1213
  /**
705
1214
  * Execute a whole transaction plan — what `Database.transaction(fn)` builds.
706
1215
  *
@@ -738,6 +1247,7 @@ function createOps(tx) {
738
1247
  var currentSchema = {};
739
1248
  function setSchema(schema) {
740
1249
  cachedVectorSchema = null;
1250
+ cachedTrgmSchema = null;
741
1251
  const s = schema;
742
1252
  currentSchema = (s && "default" in s ? s.default : s) ?? {};
743
1253
  }
@@ -750,22 +1260,47 @@ function withTables(ops, schema = currentSchema) {
750
1260
  update: (id, data) => ops.update(name, id, data),
751
1261
  delete: (id) => ops.delete(name, id),
752
1262
  findById: (id) => ops.findById(name, id),
753
- findMany: (query) => ops.findMany(name, query ?? {})
1263
+ findMany: (query, opts) => ops.findMany(name, query ?? {}, opts),
1264
+ upsert: (data, opts) => ops.upsert(name, data, opts)
754
1265
  };
755
1266
  }
756
1267
  const base = /* @__PURE__ */ Object.create(null);
757
1268
  return Object.assign(base, ops, { tables });
758
1269
  }
759
1270
  var SERVICE_LOCK_TIMEOUT = "5s";
1271
+ function isAbortedTransaction(e) {
1272
+ const code = e?.code;
1273
+ const message = String(e?.message ?? "");
1274
+ return code === "25P02" || /current transaction is aborted/i.test(message);
1275
+ }
1276
+ function isForeignKeyViolation(e) {
1277
+ return e?.code === "23503";
1278
+ }
760
1279
  function isLockTimeout(e) {
761
1280
  const code = e?.code;
762
1281
  const message = String(e?.message ?? "");
763
1282
  return code === "55P03" || /lock timeout/i.test(message);
764
1283
  }
765
- function diagnosingDriver(sql) {
766
- const explain = (e) => isLockTimeout(e) ? new Error(
767
- `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. (${String(e?.message ?? e)})`
768
- ) : e;
1284
+ function diagnosingDriver(sql, surface = "service") {
1285
+ const original = (e) => String(e?.message ?? e);
1286
+ const explain = (e) => {
1287
+ if (isAbortedTransaction(e)) {
1288
+ return new Error(
1289
+ `This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
1290
+ );
1291
+ }
1292
+ if (surface === "service" && isForeignKeyViolation(e)) {
1293
+ return new Error(
1294
+ `Database.asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`
1295
+ );
1296
+ }
1297
+ if (isLockTimeout(e)) {
1298
+ return new Error(
1299
+ `Database.asService() waited too long for a row lock. It runs in its OWN transaction, so a row this request already wrote through Database.* is locked against it until the request commits \u2014 a wait that cannot end. Do that row's work on one surface or the other. (${original(e)})`
1300
+ );
1301
+ }
1302
+ return e;
1303
+ };
769
1304
  const wrapTx = (tx) => ({
770
1305
  async unsafe(text, params) {
771
1306
  try {
@@ -784,13 +1319,13 @@ function diagnosingDriver(sql) {
784
1319
  };
785
1320
  }
786
1321
  function createRequestDatabase(sql, identity) {
787
- const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
1322
+ const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
788
1323
  let serviceTx = null;
789
1324
  let serviceClient = null;
790
1325
  const asService = () => {
791
1326
  if (serviceClient === null) {
792
1327
  serviceTx = createLazyTransaction(
793
- diagnosingDriver(sql),
1328
+ diagnosingDriver(sql, "service"),
794
1329
  identity.serviceRole,
795
1330
  identity.claimsJson,
796
1331
  { lockTimeout: SERVICE_LOCK_TIMEOUT }
@@ -871,6 +1406,23 @@ async function runPlanOp(sp, op, results) {
871
1406
  sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
872
1407
  break;
873
1408
  }
1409
+ case "upsert": {
1410
+ const cols = Object.keys(op.values ?? {});
1411
+ const conflict = op.onConflict ?? [];
1412
+ if (cols.length === 0 || conflict.length === 0) {
1413
+ throw Object.assign(new Error(`upsert on ${op.table} needs columns and onConflict`), {
1414
+ error_code: "tx_bad_upsert"
1415
+ });
1416
+ }
1417
+ const rendered = cols.map(
1418
+ (c) => renderValue(op.values[c], c, args, results, opVectorCols)
1419
+ );
1420
+ const conflictSet = new Set(conflict);
1421
+ const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1422
+ const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1423
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
1424
+ break;
1425
+ }
874
1426
  case "insertMany": {
875
1427
  const rows = op.rows ?? [];
876
1428
  if (rows.length === 0 || !rows[0]) return [];
@@ -1031,6 +1583,41 @@ function verifySignature(presented, expected) {
1031
1583
  return diff === 0;
1032
1584
  }
1033
1585
 
1586
+ // src/engine/sse.ts
1587
+ var SSE_CONTENT_TYPE = "text/event-stream";
1588
+ function encodeFrame(value) {
1589
+ return `data: ${JSON.stringify(value)}
1590
+
1591
+ `;
1592
+ }
1593
+ function encodeErrorFrame(requestId) {
1594
+ return `event: error
1595
+ data: ${JSON.stringify({ requestId })}
1596
+
1597
+ `;
1598
+ }
1599
+ function makeSseWriter(opts) {
1600
+ let begun = false;
1601
+ let chain = Promise.resolve();
1602
+ return {
1603
+ write(value) {
1604
+ const frame = encodeFrame(value);
1605
+ if (!begun) {
1606
+ begun = true;
1607
+ chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
1608
+ return;
1609
+ }
1610
+ chain = chain.then(() => opts.enqueue(frame));
1611
+ },
1612
+ started() {
1613
+ return begun;
1614
+ },
1615
+ drained() {
1616
+ return chain;
1617
+ }
1618
+ };
1619
+ }
1620
+
1034
1621
  // src/engine/fence.ts
1035
1622
  var SECRET_ENV = [
1036
1623
  "DATABASE_URL",
@@ -1170,7 +1757,6 @@ async function createApp(opts) {
1170
1757
  Notifications: modules.Notifications ?? stubModule("Notifications"),
1171
1758
  Flags: modules.Flags ?? stubModule("Flags"),
1172
1759
  Realtime: modules.Realtime ?? stubModule("Realtime"),
1173
- Purchases: modules.Purchases ?? stubModule("Purchases"),
1174
1760
  // Named, never undefined. A backend started without a secrets client
1175
1761
  // that returned `undefined` here would fail inside the handler as
1176
1762
  // "Cannot read properties of undefined", which says nothing about what
@@ -1267,6 +1853,13 @@ async function createApp(opts) {
1267
1853
  const args = [];
1268
1854
  let parsedBody;
1269
1855
  let bodyRead = false;
1856
+ let sseEnqueue = null;
1857
+ let sseSettle = async () => {
1858
+ };
1859
+ const sseWriter = makeSseWriter({
1860
+ enqueue: (chunk) => sseEnqueue?.(chunk),
1861
+ onFirstWrite: () => sseSettle()
1862
+ });
1270
1863
  for (const p of meta.params ?? []) {
1271
1864
  switch (p.kind) {
1272
1865
  case "body": {
@@ -1370,6 +1963,21 @@ async function createApp(opts) {
1370
1963
  case "req":
1371
1964
  args[p.index] = req;
1372
1965
  break;
1966
+ // `@Signal()` — the raw request's AbortSignal, which enters the aborted
1967
+ // state when the client disconnects. Measured on Bun: an infinite
1968
+ // producer guarded by it stops within a few frames of the client dying.
1969
+ // It is deliberately NOT reachable through `@Req()`: PBRequest carries
1970
+ // request-scoped data and no signal.
1971
+ case "signal":
1972
+ args[p.index] = req.signal;
1973
+ break;
1974
+ // `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
1975
+ // on any other route it is an inert writer whose frames go nowhere,
1976
+ // which is the same shape `@UploadedObject()` has off an `@Upload`
1977
+ // route.
1978
+ case "sseOut":
1979
+ args[p.index] = sseWriter;
1980
+ break;
1373
1981
  default:
1374
1982
  args[p.index] = void 0;
1375
1983
  }
@@ -1381,6 +1989,84 @@ async function createApp(opts) {
1381
1989
  });
1382
1990
  try {
1383
1991
  const services = buildServices(db);
1992
+ if (meta.options?.sseConfig !== void 0) {
1993
+ let sseController;
1994
+ const sseEncoder = new TextEncoder();
1995
+ const stream = new ReadableStream({
1996
+ start(c) {
1997
+ sseController = c;
1998
+ }
1999
+ });
2000
+ sseEnqueue = (chunk) => {
2001
+ try {
2002
+ sseController.enqueue(sseEncoder.encode(chunk));
2003
+ } catch {
2004
+ }
2005
+ };
2006
+ let sawFirstFrame;
2007
+ const firstFrame = new Promise((r) => sawFirstFrame = r);
2008
+ sseSettle = async () => {
2009
+ await db.commit();
2010
+ sawFirstFrame();
2011
+ };
2012
+ const guarded = {
2013
+ ...services,
2014
+ Database: new Proxy(services.Database, {
2015
+ get(target, prop, recv) {
2016
+ if (sseWriter.started()) {
2017
+ throw new Error(
2018
+ `sse_db_after_write: ${hit.entry.id} touched the database after its first write(). A streaming response settles its transaction with the first frame \u2014 do the database work before the first write.`
2019
+ );
2020
+ }
2021
+ return Reflect.get(target, prop, recv);
2022
+ }
2023
+ })
2024
+ };
2025
+ let thrown = null;
2026
+ const ran = Promise.resolve(runWithRuntime(guarded, () => {
2027
+ const box = requestALS.getStore();
2028
+ if (box) {
2029
+ box.userId = userId ?? null;
2030
+ box.requestId = requestId;
2031
+ box.idempotencyKey = req.headers.get("idempotency-key");
2032
+ }
2033
+ const method = hit.entry.instance[meta.fnName];
2034
+ if (typeof method !== "function") {
2035
+ throw new Error(
2036
+ `route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
2037
+ );
2038
+ }
2039
+ return method.apply(hit.entry.instance, args);
2040
+ })).then(
2041
+ () => void 0,
2042
+ (err) => {
2043
+ thrown = err;
2044
+ }
2045
+ );
2046
+ await Promise.race([firstFrame, ran]);
2047
+ if (!sseWriter.started()) {
2048
+ await ran;
2049
+ await db.commit();
2050
+ if (thrown !== null) throw thrown;
2051
+ return new Response(null, { status: 204 });
2052
+ }
2053
+ void (async () => {
2054
+ await ran;
2055
+ await sseWriter.drained();
2056
+ try {
2057
+ if (thrown !== null) {
2058
+ log.error(`[engine] ${hit.entry.id} threw mid-stream`, thrown);
2059
+ sseController.enqueue(sseEncoder.encode(encodeErrorFrame(requestId)));
2060
+ }
2061
+ sseController.close();
2062
+ } catch {
2063
+ }
2064
+ })();
2065
+ return new Response(stream, {
2066
+ status: 200,
2067
+ headers: { "content-type": SSE_CONTENT_TYPE, "cache-control": "no-cache" }
2068
+ });
2069
+ }
1384
2070
  const result = await runWithRuntime(services, () => {
1385
2071
  const box = requestALS.getStore();
1386
2072
  if (box) {
@@ -1471,4 +2157,4 @@ export {
1471
2157
  installEgressFence,
1472
2158
  createApp
1473
2159
  };
1474
- //# sourceMappingURL=chunk-74XDEF5J.js.map
2160
+ //# sourceMappingURL=chunk-M5MCBWJI.js.map