@palbase/backend 22.0.1 → 23.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 +764 -25
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +4 -4
  4. package/dist/{chunk-POYAFBLF.js → chunk-FSGSB42K.js} +13 -6
  5. package/dist/chunk-FSGSB42K.js.map +1 -0
  6. package/dist/{chunk-SSGAMC26.js → chunk-HAF67F2H.js} +114 -5
  7. package/dist/chunk-HAF67F2H.js.map +1 -0
  8. package/dist/{chunk-QMVK4X3V.js → chunk-OMRTHM4X.js} +98 -98
  9. package/dist/chunk-OMRTHM4X.js.map +1 -0
  10. package/dist/{chunk-QYOHMVUW.js → chunk-REZU6UKT.js} +755 -24
  11. package/dist/chunk-REZU6UKT.js.map +1 -0
  12. package/dist/{chunk-YL4C5NRY.js → chunk-Y5HXVUMP.js} +2 -2
  13. package/dist/{chunk-N32VDWKH.js → chunk-ZC6Q2BRD.js} +4 -59
  14. package/dist/chunk-ZC6Q2BRD.js.map +1 -0
  15. package/dist/db/env.cjs.map +1 -1
  16. package/dist/db/env.d.cts +3 -21
  17. package/dist/db/env.d.ts +3 -21
  18. package/dist/db/index.cjs +117 -6
  19. package/dist/db/index.cjs.map +1 -1
  20. package/dist/db/index.d.cts +3 -2
  21. package/dist/db/index.d.ts +3 -2
  22. package/dist/db/index.js +7 -3
  23. package/dist/{endpoint-B0LpZixz.d.cts → endpoint-BavvbW4P.d.ts} +116 -14
  24. package/dist/{endpoint-B0LpZixz.d.ts → endpoint-i8TTCohk.d.cts} +116 -14
  25. package/dist/engine/index.cjs +764 -25
  26. package/dist/engine/index.cjs.map +1 -1
  27. package/dist/engine/index.d.cts +6 -5
  28. package/dist/engine/index.d.ts +6 -5
  29. package/dist/engine/index.js +3 -3
  30. package/dist/{index-g-EzitI-.d.ts → index-B3jmmItD.d.ts} +203 -7
  31. package/dist/{index-BCNtlG1w.d.ts → index-B7YBEG5w.d.ts} +117 -93
  32. package/dist/{index-BGSCWlUa.d.cts → index-Bmvx1EvJ.d.cts} +203 -7
  33. package/dist/{index-B4W6d2VJ.d.cts → index-E7OscPJT.d.cts} +117 -93
  34. package/dist/index.cjs +509 -1085
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +203 -1133
  37. package/dist/index.d.ts +203 -1133
  38. package/dist/index.js +288 -900
  39. package/dist/index.js.map +1 -1
  40. package/dist/openapi/index.cjs +3 -57
  41. package/dist/openapi/index.cjs.map +1 -1
  42. package/dist/openapi/index.d.cts +3 -2
  43. package/dist/openapi/index.d.ts +3 -2
  44. package/dist/openapi/index.js +5 -22
  45. package/dist/openapi/index.js.map +1 -1
  46. package/dist/{registry-3BLYv4si.d.ts → registry-C3H2uPeZ.d.cts} +13 -169
  47. package/dist/{registry-Cw0YEYCg.d.cts → registry-DY3d9l1k.d.ts} +13 -169
  48. package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
  49. package/dist/stack.cjs.map +1 -0
  50. package/dist/stack.d.cts +76 -0
  51. package/dist/stack.d.ts +76 -0
  52. package/dist/stack.js +1 -0
  53. package/docs/README.md +7 -6
  54. package/docs/endpoints.md +1 -1
  55. package/docs/errors.md +9 -0
  56. package/docs/llms-full.txt +17 -261
  57. package/docs/llms.txt +0 -2
  58. package/package.json +7 -7
  59. package/template/package.json +1 -1
  60. package/dist/chunk-N32VDWKH.js.map +0 -1
  61. package/dist/chunk-POYAFBLF.js.map +0 -1
  62. package/dist/chunk-QMVK4X3V.js.map +0 -1
  63. package/dist/chunk-QYOHMVUW.js.map +0 -1
  64. package/dist/chunk-SSGAMC26.js.map +0 -1
  65. package/dist/purchases/keys.cjs.map +0 -1
  66. package/dist/purchases/keys.d.cts +0 -42
  67. package/dist/purchases/keys.d.ts +0 -42
  68. package/dist/purchases/keys.js +0 -1
  69. package/docs/config.md +0 -147
  70. package/docs/resources.md +0 -97
  71. package/template/config/secrets.ts +0 -24
  72. /package/dist/{chunk-YL4C5NRY.js.map → chunk-Y5HXVUMP.js.map} +0 -0
  73. /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-POYAFBLF.js";
4
+ } from "./chunk-FSGSB42K.js";
5
5
  import {
6
6
  getRoutes,
7
7
  isHttpError
8
- } from "./chunk-QMVK4X3V.js";
8
+ } from "./chunk-OMRTHM4X.js";
9
9
 
10
10
  // src/engine/config.ts
11
11
  var BootRefused = class extends Error {
@@ -382,6 +382,305 @@ function asWireRow(row) {
382
382
  function asWireRows(rows) {
383
383
  return rows.map((row) => asWireRow(row));
384
384
  }
385
+ function toVectorLiteral(v) {
386
+ return `[${v.join(",")}]`;
387
+ }
388
+ function vectorColumnsOf(schema, table) {
389
+ const out = /* @__PURE__ */ new Set();
390
+ for (const [key, def] of Object.entries(schema.tables ?? {})) {
391
+ if ((def.name ?? key) !== table) continue;
392
+ for (const [col, c] of Object.entries(def.columns ?? {})) {
393
+ const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
394
+ if (d !== null && typeof d === "object" && d.type === "vector") out.add(col);
395
+ }
396
+ }
397
+ return out;
398
+ }
399
+ function reviveVectors(row, vectorCols) {
400
+ if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
401
+ const out = row;
402
+ for (const col of vectorCols) {
403
+ const v = out[col];
404
+ if (typeof v === "string") out[col] = JSON.parse(v);
405
+ }
406
+ return row;
407
+ }
408
+ function asTableRow(table, row) {
409
+ return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
410
+ }
411
+ function asTableRows(table, rows) {
412
+ const vectorCols = vectorColumnsOf(currentSchema, table);
413
+ return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
414
+ }
415
+ function asBindParams(table, cols, data) {
416
+ const vectorCols = vectorColumnsOf(currentSchema, table);
417
+ return cols.map((c) => {
418
+ const v = data[c];
419
+ return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
420
+ });
421
+ }
422
+ var SELECTIVITY_EXACT_THRESHOLD = 1e4;
423
+ var METRIC_OPERATOR = {
424
+ cosine: "<=>",
425
+ euclidean: "<->",
426
+ inner_product: "<#>"
427
+ };
428
+ function searchConfigFor(table) {
429
+ const t = currentSchema.tables?.[table];
430
+ if (!t) return null;
431
+ const columns = t.columns ?? {};
432
+ const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
433
+ const cols = Object.keys(columns);
434
+ const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
435
+ const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
436
+ const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
437
+ const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
438
+ if (pk === null) {
439
+ throw new Error(
440
+ `search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
441
+ );
442
+ }
443
+ const search = t.search;
444
+ const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
445
+ const validity = search?.validity === true ? { validity: true } : {};
446
+ if (search?.from !== void 0 && search.model !== void 0) {
447
+ const m = search.model;
448
+ const embed = {
449
+ model: m.model,
450
+ apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
451
+ ...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
452
+ ...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
453
+ };
454
+ const metric = search.metric ?? "cosine";
455
+ const ftsOn = search.text !== false;
456
+ const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
457
+ if (vectorCols.length > 0) {
458
+ return {
459
+ pk,
460
+ cols,
461
+ colSet: new Set(cols),
462
+ ftsCols: ftsColsNew,
463
+ legs: [{ column: vectorCols[0], metric, embed }],
464
+ ...synonyms,
465
+ ...validity
466
+ };
467
+ }
468
+ return {
469
+ pk,
470
+ cols,
471
+ colSet: new Set(cols),
472
+ ftsCols: ftsColsNew,
473
+ legs: [],
474
+ chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
475
+ ...synonyms,
476
+ ...validity
477
+ };
478
+ }
479
+ const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
480
+ const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
481
+ let legs;
482
+ if (rawLegs.length > 0) {
483
+ legs = rawLegs.map((leg) => {
484
+ const l = leg;
485
+ const column = l.column ?? (vectorCols.length === 1 ? vectorCols[0] : void 0);
486
+ if (column === void 0) {
487
+ throw new Error(`search(${table}): birden \xE7ok vector kolonu var \u2014 beyanda 'column' zorunlu (FR-010)`);
488
+ }
489
+ const model = l.model;
490
+ return {
491
+ column,
492
+ metric: l.metric ?? "cosine",
493
+ ...model !== void 0 ? { embed: {
494
+ model: model.model,
495
+ apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
496
+ ...model.baseURL !== void 0 ? { baseURL: model.baseURL } : {},
497
+ ...model.dimensions !== void 0 ? { dimensions: model.dimensions } : {}
498
+ } } : {}
499
+ };
500
+ });
501
+ } else {
502
+ legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
503
+ }
504
+ if (ftsCols.length === 0 && legs.length === 0) return null;
505
+ return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
506
+ }
507
+ function pickLeg(table, legs, using) {
508
+ if (legs.length === 0) return null;
509
+ if (using !== void 0) {
510
+ const hit = legs.find((l) => l.column === using);
511
+ if (!hit) {
512
+ throw new Error(
513
+ `search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
514
+ );
515
+ }
516
+ return hit;
517
+ }
518
+ if (legs.length === 1) return legs[0];
519
+ 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`);
520
+ }
521
+ var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
522
+ function parseHalfLife(s) {
523
+ const m = /^(\d+)(s|m|h|d)$/.exec(s);
524
+ const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
525
+ if (!Number.isFinite(sec) || sec <= 0) {
526
+ throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
527
+ }
528
+ return sec;
529
+ }
530
+ var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
531
+ function ftsExprOf(ftsCols) {
532
+ return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
533
+ }
534
+ function expandSynonyms(query, map) {
535
+ const lower = {};
536
+ for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
537
+ return query.split(/("[^"]*")/).map((seg) => {
538
+ if (seg.startsWith('"')) return seg;
539
+ return seg.split(/(\s+)/).map((tok) => {
540
+ if (tok === "" || /^\s+$/.test(tok)) return tok;
541
+ const alts = lower[tok.toLowerCase()];
542
+ return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
543
+ }).join("");
544
+ }).join("");
545
+ }
546
+ var searchLogger = null;
547
+ function logNoResults(table, query, mode, n) {
548
+ if (n === 0) {
549
+ searchLogger?.("palbase.search.no_results", {
550
+ table,
551
+ query: (query ?? "").slice(0, 200),
552
+ mode: mode ?? "hybrid"
553
+ });
554
+ }
555
+ }
556
+ async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
557
+ const bind = [];
558
+ const add = (v) => {
559
+ bind.push(v);
560
+ return `$${bind.length}`;
561
+ };
562
+ const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
563
+ const parts = facets.map(
564
+ (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)`
565
+ );
566
+ const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
567
+ const out = {};
568
+ for (const col of facets) out[col] = [];
569
+ for (const r of rows) {
570
+ if (typeof r.f === "string" && out[r.f] !== void 0) {
571
+ out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
572
+ }
573
+ }
574
+ return out;
575
+ }
576
+ function validitySql(v, add) {
577
+ if (v === "all") return "";
578
+ if (v !== void 0) {
579
+ const p = add(v.asOf);
580
+ return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
581
+ }
582
+ return ` AND t."valid_to" IS NULL`;
583
+ }
584
+ function excludeSql(pk, ids, add) {
585
+ if (ids.length === 0) return "";
586
+ if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
587
+ return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
588
+ }
589
+ function compileWhere(table, colSet, where, add) {
590
+ const parts = [];
591
+ for (const [col, cond] of Object.entries(where)) {
592
+ if (!colSet.has(col)) {
593
+ throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
594
+ }
595
+ const q = `t.${quoteIdent(col)}`;
596
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
597
+ for (const [op, v] of Object.entries(cond)) {
598
+ if (op === "in") {
599
+ if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
600
+ if (v.length === 0) {
601
+ parts.push("false");
602
+ continue;
603
+ }
604
+ parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
605
+ } else if (op in WHERE_OPS) {
606
+ parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
607
+ } else {
608
+ throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
609
+ }
610
+ }
611
+ } else {
612
+ parts.push(`${q} = ${add(cond)}`);
613
+ }
614
+ }
615
+ return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
616
+ }
617
+ var cachedVectorSchema = null;
618
+ var cachedTrgmSchema = null;
619
+ async function trgmSchemaOf(live) {
620
+ if (cachedTrgmSchema !== null) return cachedTrgmSchema;
621
+ const rows = await live.unsafe(
622
+ "select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
623
+ );
624
+ cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
625
+ return cachedTrgmSchema;
626
+ }
627
+ async function vectorSchemaWithGuc(runner) {
628
+ if (cachedVectorSchema !== null) {
629
+ await runner.unsafe(
630
+ "select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
631
+ );
632
+ return cachedVectorSchema;
633
+ }
634
+ const rows = await runner.unsafe(
635
+ "select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true), (select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'vector') as nspname"
636
+ );
637
+ const name = rows?.[0]?.nspname;
638
+ if (typeof name !== "string" || name === "") {
639
+ throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
640
+ }
641
+ cachedVectorSchema = name;
642
+ return name;
643
+ }
644
+ var secretReader = null;
645
+ function setSecretReader(fn) {
646
+ secretReader = fn;
647
+ }
648
+ var embedFetch = (url, init) => fetch(url, init);
649
+ async function embedQuery(embed, text) {
650
+ if (secretReader === null) {
651
+ throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
652
+ }
653
+ const key = await secretReader(embed.apiKeyName);
654
+ if (key === null || key === "") {
655
+ throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
656
+ }
657
+ const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
658
+ const controller = new AbortController();
659
+ const timer = setTimeout(() => controller.abort(), 1e4);
660
+ try {
661
+ const res = await embedFetch(url, {
662
+ method: "POST",
663
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
664
+ body: JSON.stringify({
665
+ model: embed.model,
666
+ input: [text],
667
+ ...embed.dimensions !== void 0 ? { dimensions: embed.dimensions } : {}
668
+ }),
669
+ signal: controller.signal
670
+ });
671
+ if (!res.ok) {
672
+ throw new Error(`query embed: sa\u011Flay\u0131c\u0131 ${res.status} d\xF6nd\xFC (${embed.apiKeyName} ile) \u2014 anahtar/model do\u011Fru mu?`);
673
+ }
674
+ const data = await res.json();
675
+ const vec = data.data?.[0]?.embedding;
676
+ if (!Array.isArray(vec)) {
677
+ throw new Error("query embed: sa\u011Flay\u0131c\u0131 yan\u0131t\u0131nda data[0].embedding yok (CLAIM-N1 \u015Fekli)");
678
+ }
679
+ return vec;
680
+ } finally {
681
+ clearTimeout(timer);
682
+ }
683
+ }
385
684
  function createOps(tx) {
386
685
  const at = () => resolveTx(tx);
387
686
  const ops = {
@@ -393,22 +692,22 @@ function createOps(tx) {
393
692
  if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
394
693
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
395
694
  const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
396
- const rows = await (await at()).unsafe(sql, cols.map((c) => data[c]));
695
+ const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
397
696
  const inserted = rows[0];
398
697
  if (!inserted) {
399
698
  throw new Error(
400
699
  `insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
401
700
  );
402
701
  }
403
- return asWireRow(inserted);
702
+ return asTableRow(table, inserted);
404
703
  },
405
704
  async update(table, id, data) {
406
705
  const cols = Object.keys(data);
407
706
  if (cols.length === 0) return ops.findById(table, id);
408
707
  const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
409
708
  const sql = `UPDATE ${quoteIdent(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
410
- const rows = await (await at()).unsafe(sql, [...cols.map((c) => data[c]), id]);
411
- return rows[0] ? asWireRow(rows[0]) : null;
709
+ const rows = await (await at()).unsafe(sql, [...asBindParams(table, cols, data), id]);
710
+ return rows[0] ? asTableRow(table, rows[0]) : null;
412
711
  },
413
712
  async delete(table, id) {
414
713
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
@@ -418,16 +717,400 @@ function createOps(tx) {
418
717
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
419
718
  [id]
420
719
  );
421
- return rows[0] ? asWireRow(rows[0]) : null;
720
+ return rows[0] ? asTableRow(table, rows[0]) : null;
422
721
  },
423
722
  async findMany(table, query = {}) {
424
723
  const cols = Object.keys(query);
425
724
  const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
426
- return asWireRows(await (await at()).unsafe(
725
+ return asTableRows(table, await (await at()).unsafe(
427
726
  `SELECT * FROM ${quoteIdent(table)}${where}`,
428
727
  cols.map((c) => query[c])
429
728
  ));
430
729
  },
730
+ /**
731
+ * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
732
+ * (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
733
+ * hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
734
+ * RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
735
+ * T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
736
+ */
737
+ async search(table, params = {}) {
738
+ const cfg = searchConfigFor(table);
739
+ if (!cfg) {
740
+ throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
741
+ }
742
+ const excl = params.__excludeIds ?? [];
743
+ if (params.facets !== void 0) {
744
+ for (const col of params.facets) {
745
+ if (!cfg.colSet.has(col)) {
746
+ throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
747
+ }
748
+ }
749
+ }
750
+ if (params.validity !== void 0 && cfg.validity !== true) {
751
+ throw new Error(
752
+ `search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
753
+ );
754
+ }
755
+ const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
756
+ const rawLimit = params.limit ?? 20;
757
+ if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
758
+ throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
759
+ }
760
+ const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
761
+ const pool = Math.max(limit * 3, 30);
762
+ if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
763
+ throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
764
+ }
765
+ let recencyMul = "";
766
+ if (params.recency !== void 0) {
767
+ const { field, halfLife } = params.recency;
768
+ if (!cfg.colSet.has(field)) {
769
+ throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
770
+ }
771
+ recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
772
+ }
773
+ let boostMul = "";
774
+ if (params.boost !== void 0) {
775
+ const { field, weight } = params.boost;
776
+ if (!cfg.colSet.has(field)) {
777
+ throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
778
+ }
779
+ if (typeof weight !== "number" || !Number.isFinite(weight)) {
780
+ throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
781
+ }
782
+ const g = `greatest(t.${quoteIdent(field)},0)`;
783
+ boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
784
+ }
785
+ const scoreMul = boostMul + recencyMul;
786
+ if (cfg.chunk !== void 0) {
787
+ const ck = cfg.chunk;
788
+ const pidQ = quoteIdent(`parent_${cfg.pk}`);
789
+ const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
790
+ const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
791
+ let qvC = Array.isArray(params.vector) ? params.vector : null;
792
+ if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
793
+ try {
794
+ qvC = await embedQuery(ck.embed, params.query);
795
+ } catch (e) {
796
+ if (!wantTextC) throw e;
797
+ qvC = null;
798
+ }
799
+ }
800
+ if (!wantTextC && qvC === null) {
801
+ throw new Error(
802
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
803
+ );
804
+ }
805
+ const bindC = [];
806
+ const addC = (v) => {
807
+ bindC.push(v);
808
+ return `$${bindC.length}`;
809
+ };
810
+ const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
811
+ const liveC = await at();
812
+ const K2 = 50;
813
+ const cpool = Math.max(limit * 9, 90);
814
+ let semC = "";
815
+ let kwC = "";
816
+ if (qvC !== null) {
817
+ const sch = await vectorSchemaWithGuc(liveC);
818
+ const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
819
+ const vp = addC(toVectorLiteral(qvC));
820
+ semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c WHERE c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
821
+ }
822
+ let qpC = "";
823
+ let qpCIdx = -1;
824
+ if (wantTextC) {
825
+ const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
826
+ qpC = addC(qTextC);
827
+ qpCIdx = bindC.length - 1;
828
+ kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
829
+ }
830
+ const msC = params.minScore === void 0 ? "" : addC(params.minScore);
831
+ const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
832
+ const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
833
+ const assembleC = (kwIn, withKwn) => {
834
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
835
+ let fusedSrc;
836
+ if (semC !== "" && kwIn !== "") {
837
+ 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)`;
838
+ } else if (semC !== "") {
839
+ fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
840
+ } else {
841
+ fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
842
+ }
843
+ const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid WHERE true${whereC}`;
844
+ return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
845
+ };
846
+ let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
847
+ if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
848
+ const tsch = await trgmSchemaOf(liveC);
849
+ const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE ${qpC} OPERATOR(${quoteIdent(tsch)}.<%) coalesce(c.content,'') ORDER BY r LIMIT ${cpool}`;
850
+ const retryBindC = bindC.slice();
851
+ if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
852
+ rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
853
+ }
854
+ for (const r of rowsC) delete r._kwn;
855
+ const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
856
+ if (params.facets !== void 0 && params.facets.length > 0) {
857
+ Object.assign(outC, {
858
+ _facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
859
+ });
860
+ }
861
+ logNoResults(table, params.query, params.mode, outC.length);
862
+ return outC;
863
+ }
864
+ const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
865
+ const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
866
+ const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
867
+ const leg = vectorAsked ? pickLeg(table, cfg.legs, params.using) : null;
868
+ let qv = Array.isArray(params.vector) ? params.vector : null;
869
+ if (qv === null && leg?.embed !== void 0 && typeof params.query === "string" && params.query !== "") {
870
+ try {
871
+ qv = await embedQuery(leg.embed, params.query);
872
+ } catch (e) {
873
+ if (!wantText) throw e;
874
+ qv = null;
875
+ }
876
+ }
877
+ const wantVector = leg !== null && qv !== null;
878
+ if (!wantText && !wantVector) {
879
+ throw new Error(
880
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
881
+ );
882
+ }
883
+ const bind = [];
884
+ const add = (v) => {
885
+ bind.push(v);
886
+ return `$${bind.length}`;
887
+ };
888
+ const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
889
+ const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
890
+ const live = await at();
891
+ const K = 50;
892
+ let semSql = "";
893
+ let kwSql = "";
894
+ if (wantVector && leg) {
895
+ const sch = await vectorSchemaWithGuc(live);
896
+ const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
897
+ let exactOrder = "";
898
+ if (userWhere !== "") {
899
+ const probeRows = await live.unsafe(
900
+ `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`,
901
+ bind.slice()
902
+ );
903
+ const n = probeRows?.[0]?.n;
904
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
905
+ exactOrder = " + 0.0";
906
+ }
907
+ }
908
+ const vp = add(toVectorLiteral(qv));
909
+ 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}`;
910
+ }
911
+ let qpRef = "";
912
+ let qpIdx = -1;
913
+ if (wantText) {
914
+ const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
915
+ const qp = add(qText);
916
+ qpIdx = bind.length - 1;
917
+ qpRef = qp;
918
+ 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}`;
919
+ }
920
+ const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
921
+ const msP = params.minScore === void 0 ? "" : add(params.minScore);
922
+ const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
923
+ const assemble = (kwIn, withKwn) => {
924
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
925
+ let inner;
926
+ let orderScore;
927
+ if (semSql !== "" && kwIn !== "") {
928
+ const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
929
+ 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`;
930
+ orderScore = scoreMul === "" ? "fused._score" : "_score";
931
+ } else {
932
+ const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
933
+ const alias = semSql !== "" ? "sem" : "kw";
934
+ const base = `(1.0/(${K} + ${alias}.r))::float8`;
935
+ const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
936
+ inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
937
+ orderScore = "_score";
938
+ }
939
+ 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}`;
940
+ };
941
+ let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
942
+ if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
943
+ const tsch = await trgmSchemaOf(live);
944
+ const expr = ftsExprOf(cfg.ftsCols);
945
+ const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${qpRef} OPERATOR(${quoteIdent(tsch)}.<%) ${expr}${whereSql} ORDER BY r LIMIT ${pool}`;
946
+ const retryBind = bind.slice();
947
+ if (qpIdx >= 0) retryBind[qpIdx] = params.query;
948
+ rows = await live.unsafe(assemble(trgmKw, false), retryBind);
949
+ }
950
+ for (const r of rows) delete r._kwn;
951
+ const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
952
+ if (params.facets !== void 0 && params.facets.length > 0) {
953
+ Object.assign(out, {
954
+ _facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
955
+ });
956
+ }
957
+ logNoResults(table, params.query, params.mode, out.length);
958
+ return out;
959
+ },
960
+ /**
961
+ * similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
962
+ * okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
963
+ * şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
964
+ * kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
965
+ * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
966
+ * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
967
+ */
968
+ async similar(table, id, opts = {}) {
969
+ const cfg = searchConfigFor(table);
970
+ if (!cfg) {
971
+ throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
972
+ }
973
+ const live = await at();
974
+ let v;
975
+ if (cfg.chunk !== void 0) {
976
+ const sch = await vectorSchemaWithGuc(live);
977
+ const rows = await live.unsafe(
978
+ `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`,
979
+ [id]
980
+ );
981
+ v = rows?.[0]?.v;
982
+ } else {
983
+ const leg = pickLeg(table, cfg.legs, opts.using);
984
+ if (leg === null) {
985
+ throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
986
+ }
987
+ const rows = await live.unsafe(
988
+ `SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
989
+ [id]
990
+ );
991
+ v = rows?.[0]?.v;
992
+ }
993
+ if (typeof v !== "string") {
994
+ throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
995
+ }
996
+ const p = {
997
+ vector: JSON.parse(v),
998
+ mode: "vector",
999
+ where: opts.where,
1000
+ limit: opts.limit,
1001
+ using: opts.using,
1002
+ minScore: opts.minScore,
1003
+ recency: opts.recency,
1004
+ blocksPerRow: opts.blocksPerRow,
1005
+ validity: opts.validity,
1006
+ boost: opts.boost,
1007
+ __excludeIds: [id]
1008
+ };
1009
+ return ops.search(table, p);
1010
+ },
1011
+ /**
1012
+ * recommend (T018, FR-023): positive/negative id kümelerinden öneri.
1013
+ * Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
1014
+ * negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
1015
+ * ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
1016
+ * Vektör matematiği istemciye inmez; bulunamayan positive/negative
1017
+ * adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
1018
+ */
1019
+ async recommend(table, opts) {
1020
+ const positive = opts?.positive;
1021
+ if (!Array.isArray(positive) || positive.length === 0) {
1022
+ throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
1023
+ }
1024
+ const negative = Array.isArray(opts.negative) ? opts.negative : [];
1025
+ const cfg = searchConfigFor(table);
1026
+ if (!cfg) {
1027
+ throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1028
+ }
1029
+ let leg = null;
1030
+ if (cfg.chunk === void 0) {
1031
+ leg = pickLeg(table, cfg.legs, opts.using);
1032
+ if (leg === null) {
1033
+ throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
1034
+ }
1035
+ }
1036
+ const live = await at();
1037
+ const sch = await vectorSchemaWithGuc(live);
1038
+ const bind = [];
1039
+ const add = (x) => {
1040
+ bind.push(x);
1041
+ return `$${bind.length}`;
1042
+ };
1043
+ const avgSel = (ids) => {
1044
+ const inList = ids.map((x) => add(x)).join(", ");
1045
+ 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`;
1046
+ };
1047
+ 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`;
1048
+ const rows = await live.unsafe(targetSql, bind);
1049
+ const r0 = rows?.[0];
1050
+ if (r0 === void 0 || r0.pos_missing === true) {
1051
+ throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1052
+ }
1053
+ if (negative.length > 0 && r0.neg_missing === true) {
1054
+ throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1055
+ }
1056
+ if (typeof r0.v !== "string") {
1057
+ throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
1058
+ }
1059
+ const p = {
1060
+ vector: JSON.parse(r0.v),
1061
+ mode: "vector",
1062
+ where: opts.where,
1063
+ limit: opts.limit,
1064
+ using: opts.using,
1065
+ minScore: opts.minScore,
1066
+ recency: opts.recency,
1067
+ blocksPerRow: opts.blocksPerRow,
1068
+ validity: opts.validity,
1069
+ boost: opts.boost,
1070
+ __excludeIds: [...positive, ...negative]
1071
+ };
1072
+ return ops.search(table, p);
1073
+ },
1074
+ /**
1075
+ * supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
1076
+ * versiyonunu TEK savepoint'te yazar — eski satır kapatılır
1077
+ * (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
1078
+ * yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
1079
+ * randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
1080
+ * kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
1081
+ * ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
1082
+ * kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
1083
+ */
1084
+ async supersede(table, id, row) {
1085
+ const cfg = searchConfigFor(table);
1086
+ if (cfg === null || cfg.validity !== true) {
1087
+ throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
1088
+ }
1089
+ const live = await at();
1090
+ return live.savepoint(async (sp) => {
1091
+ const newId = row[cfg.pk] ?? crypto.randomUUID();
1092
+ const closed = await sp.unsafe(
1093
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
1094
+ [newId, id]
1095
+ );
1096
+ if (closed.length === 0) {
1097
+ throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
1098
+ }
1099
+ const data = { ...row, [cfg.pk]: newId };
1100
+ const cols = Object.keys(data);
1101
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1102
+ const rows = await sp.unsafe(
1103
+ `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
1104
+ asBindParams(table, cols, data)
1105
+ );
1106
+ if (!rows[0]) {
1107
+ throw new Error(
1108
+ `supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
1109
+ );
1110
+ }
1111
+ return asTableRow(table, rows[0]);
1112
+ });
1113
+ },
431
1114
  /** A real SAVEPOINT inside the request's transaction. */
432
1115
  async transaction(cb) {
433
1116
  const live = await at();
@@ -456,7 +1139,7 @@ function createOps(tx) {
456
1139
  return live.savepoint(async (sp) => {
457
1140
  const results = [];
458
1141
  for (const op of plan.ops) {
459
- const rows = await runPlanOp(sp, op, results);
1142
+ const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
460
1143
  const result = { rows, rows_affected: rows.length };
461
1144
  results.push(result);
462
1145
  assertGuard(op, result);
@@ -469,6 +1152,8 @@ function createOps(tx) {
469
1152
  }
470
1153
  var currentSchema = {};
471
1154
  function setSchema(schema) {
1155
+ cachedVectorSchema = null;
1156
+ cachedTrgmSchema = null;
472
1157
  const s = schema;
473
1158
  currentSchema = (s && "default" in s ? s.default : s) ?? {};
474
1159
  }
@@ -555,7 +1240,7 @@ function isRef(v) {
555
1240
  function isExpr(v) {
556
1241
  return typeof v === "object" && v !== null && "$expr" in v;
557
1242
  }
558
- function renderValue(value, column, args, results) {
1243
+ function renderValue(value, column, args, results, vectorCols) {
559
1244
  if (isRef(value)) {
560
1245
  const source = results[value.$ref.op];
561
1246
  const row = source?.rows[0];
@@ -564,15 +1249,15 @@ function renderValue(value, column, args, results) {
564
1249
  error_code: "tx_ref_unresolved"
565
1250
  });
566
1251
  }
567
- return args.bind(row[value.$ref.field]);
1252
+ return bindMaybeVector(args, column, vectorCols, row[value.$ref.field]);
568
1253
  }
569
1254
  if (isExpr(value)) {
570
1255
  const fn = value.$expr;
571
1256
  if (fn.fn === "now") return "now()";
572
1257
  const operator = fn.fn === "inc" ? "+" : "-";
573
- return `${quoteIdent(column)} ${operator} ${args.bind(fn.by)}`;
1258
+ return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
574
1259
  }
575
- return args.bind(value);
1260
+ return bindMaybeVector(args, column, vectorCols, value);
576
1261
  }
577
1262
  function renderWhere(where, args, results) {
578
1263
  const cols = Object.keys(where ?? {});
@@ -584,14 +1269,21 @@ function renderWhere(where, args, results) {
584
1269
  });
585
1270
  return ` WHERE ${terms.join(" AND ")}`;
586
1271
  }
1272
+ function bindMaybeVector(args, column, vectorCols, value) {
1273
+ if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
1274
+ return args.bind(toVectorLiteral(value));
1275
+ }
1276
+ return args.bind(value);
1277
+ }
587
1278
  async function runPlanOp(sp, op, results) {
1279
+ const opVectorCols = vectorColumnsOf(currentSchema, op.table);
588
1280
  const args = new Args();
589
1281
  const table = quoteIdent(op.table);
590
1282
  let sql;
591
1283
  switch (op.op) {
592
1284
  case "insert": {
593
1285
  const cols = Object.keys(op.values ?? {});
594
- const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
1286
+ const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols));
595
1287
  sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
596
1288
  break;
597
1289
  }
@@ -600,7 +1292,7 @@ async function runPlanOp(sp, op, results) {
600
1292
  if (rows.length === 0 || !rows[0]) return [];
601
1293
  const cols = Object.keys(rows[0]);
602
1294
  const tuples = rows.map(
603
- (r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
1295
+ (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
604
1296
  );
605
1297
  sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
606
1298
  break;
@@ -609,7 +1301,7 @@ async function runPlanOp(sp, op, results) {
609
1301
  const cols = Object.keys(op.set ?? {});
610
1302
  if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
611
1303
  const assignments = cols.map(
612
- (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
1304
+ (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols)}`
613
1305
  );
614
1306
  sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
615
1307
  break;
@@ -720,7 +1412,8 @@ function grantFor(entry, ctx, bucketLimits) {
720
1412
  filename: ctx.filename
721
1413
  }),
722
1414
  maxBytes: bucketLimits?.maxBytes ?? null,
723
- mimeTypes: bucketLimits?.mimeTypes ?? null
1415
+ mimeTypes: bucketLimits?.mimeTypes ?? null,
1416
+ ownerUid: ctx.userId ?? null
724
1417
  };
725
1418
  }
726
1419
  var CompletionLedger = class {
@@ -817,6 +1510,23 @@ function installEgressFence(policy) {
817
1510
 
818
1511
  // src/engine/index.ts
819
1512
  var JSON_HEADERS = { "content-type": "application/json" };
1513
+ function fieldErrors(err) {
1514
+ return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
1515
+ }
1516
+ function headersFor(raw, schema) {
1517
+ const shape = schema.shape;
1518
+ if (typeof shape !== "object" || shape === null) return raw;
1519
+ let aliased = null;
1520
+ for (const declared of Object.keys(shape)) {
1521
+ const lower = declared.toLowerCase();
1522
+ if (lower === declared) continue;
1523
+ const value = raw[lower];
1524
+ if (value === void 0) continue;
1525
+ aliased ??= { ...raw };
1526
+ aliased[declared] = value;
1527
+ }
1528
+ return aliased ?? raw;
1529
+ }
820
1530
  function envelope(error, description, status, requestId, extra) {
821
1531
  return new Response(
822
1532
  JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
@@ -850,6 +1560,7 @@ async function defaultSqlDriver(config) {
850
1560
  async function createApp(opts) {
851
1561
  const { config, controllers } = opts;
852
1562
  setSchema(opts.schema ?? {});
1563
+ setSecretReader(opts.secretReader ?? null);
853
1564
  const routes = buildRouteTable(controllers);
854
1565
  if (routes.length === 0) {
855
1566
  throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
@@ -875,7 +1586,6 @@ async function createApp(opts) {
875
1586
  Notifications: modules.Notifications ?? stubModule("Notifications"),
876
1587
  Flags: modules.Flags ?? stubModule("Flags"),
877
1588
  Realtime: modules.Realtime ?? stubModule("Realtime"),
878
- Purchases: modules.Purchases ?? stubModule("Purchases"),
879
1589
  // Named, never undefined. A backend started without a secrets client
880
1590
  // that returned `undefined` here would fail inside the handler as
881
1591
  // "Cannot read properties of undefined", which says nothing about what
@@ -962,7 +1672,8 @@ async function createApp(opts) {
962
1672
  error: "too_many_requests",
963
1673
  error_description: "Rate limit exceeded for this endpoint",
964
1674
  status: 429,
965
- request_id: requestId
1675
+ request_id: requestId,
1676
+ data: { retryAfter }
966
1677
  }),
967
1678
  { status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
968
1679
  );
@@ -981,7 +1692,7 @@ async function createApp(opts) {
981
1692
  const r = p.schema.safeParse(parsedBody);
982
1693
  if (!r.success) {
983
1694
  return envelope("bad_request", "Request body failed validation", 400, requestId, {
984
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
1695
+ data: { fields: fieldErrors(r.error) }
985
1696
  });
986
1697
  }
987
1698
  args[p.index] = r.data;
@@ -991,7 +1702,7 @@ async function createApp(opts) {
991
1702
  const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
992
1703
  if (!r.success) {
993
1704
  return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
994
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
1705
+ data: { fields: fieldErrors(r.error) }
995
1706
  });
996
1707
  }
997
1708
  args[p.index] = r.data;
@@ -1000,9 +1711,21 @@ async function createApp(opts) {
1000
1711
  case "param":
1001
1712
  args[p.index] = hit.params[p.name];
1002
1713
  break;
1003
- case "headers":
1004
- args[p.index] = Object.fromEntries(req.headers);
1714
+ case "headers": {
1715
+ const raw = Object.fromEntries(req.headers);
1716
+ if (!p.schema) {
1717
+ args[p.index] = raw;
1718
+ break;
1719
+ }
1720
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
1721
+ if (!r.success) {
1722
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
1723
+ data: { fields: fieldErrors(r.error) }
1724
+ });
1725
+ }
1726
+ args[p.index] = r.data;
1005
1727
  break;
1728
+ }
1006
1729
  case "user":
1007
1730
  case "optionalUser":
1008
1731
  args[p.index] = claims ? {
@@ -1051,6 +1774,14 @@ async function createApp(opts) {
1051
1774
  case "traceId":
1052
1775
  args[p.index] = requestId;
1053
1776
  break;
1777
+ case "client":
1778
+ args[p.index] = {
1779
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
1780
+ appVersion: req.headers.get("x-palbase-client-version"),
1781
+ platform: req.headers.get("x-platform"),
1782
+ osVersion: req.headers.get("x-os-version")
1783
+ };
1784
+ break;
1054
1785
  case "req":
1055
1786
  args[p.index] = req;
1056
1787
  break;
@@ -1155,4 +1886,4 @@ export {
1155
1886
  installEgressFence,
1156
1887
  createApp
1157
1888
  };
1158
- //# sourceMappingURL=chunk-QYOHMVUW.js.map
1889
+ //# sourceMappingURL=chunk-REZU6UKT.js.map