@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
@@ -425,7 +425,11 @@ function makeTablesAccessor(ops) {
425
425
  update: (id, data) => ops().update(name, id, data),
426
426
  delete: (id) => ops().delete(name, id),
427
427
  findById: (id) => ops().findById(name, id),
428
- findMany: (query) => ops().findMany(name, query)
428
+ findMany: (query) => ops().findMany(name, query),
429
+ search: (params) => ops().search(name, params),
430
+ similar: (id, params) => ops().similar(name, id, params),
431
+ recommend: (params) => ops().recommend(name, params),
432
+ supersede: (id, row) => ops().supersede(name, id, row)
429
433
  };
430
434
  }
431
435
  }
@@ -434,16 +438,21 @@ function makeTablesAccessor(ops) {
434
438
  }
435
439
  var rawDatabase = makeServiceProxy("Database");
436
440
  function makeTypedSurface(raw) {
441
+ const reco = raw;
437
442
  const ops = {
438
443
  query: (sql, params) => raw.query(sql, params),
439
444
  insert: (table, data) => raw.insert(table, data),
440
445
  update: (table, id, data) => raw.update(table, id, data),
441
446
  delete: (table, id) => raw.delete(table, id),
442
447
  findById: (table, id) => raw.findById(table, id),
443
- findMany: (table, query) => raw.findMany(table, query)
448
+ findMany: (table, query) => raw.findMany(table, query),
449
+ search: (table, params) => raw.search(table, params),
450
+ similar: (table, id, params) => reco.similar(table, id, params),
451
+ recommend: (table, params) => reco.recommend(table, params),
452
+ supersede: (table, id, row) => raw.supersede(table, id, row)
444
453
  };
445
454
  return Object.assign(ops, {
446
- tables: makeTablesAccessor(() => raw),
455
+ tables: makeTablesAccessor(() => reco),
447
456
  transaction(fn) {
448
457
  const builder = new TxPlanBuilder();
449
458
  return runTxPlan(raw, makeTxTablesAccessor(builder), builder, fn);
@@ -506,7 +515,6 @@ var Cache = makeServiceProxy("Cache");
506
515
  var Secrets = makeServiceProxy("Secrets");
507
516
  var Log = makeServiceProxy("Log");
508
517
  var Notifications = makeServiceProxy("Notifications");
509
- var Purchases = makeServiceProxy("Purchases");
510
518
  var rawFlags = makeServiceProxy("Flags");
511
519
  var Flags = Object.assign(
512
520
  {
@@ -971,6 +979,305 @@ function asWireRow(row) {
971
979
  function asWireRows(rows) {
972
980
  return rows.map((row) => asWireRow(row));
973
981
  }
982
+ function toVectorLiteral(v) {
983
+ return `[${v.join(",")}]`;
984
+ }
985
+ function vectorColumnsOf(schema, table) {
986
+ const out = /* @__PURE__ */ new Set();
987
+ for (const [key, def] of Object.entries(schema.tables ?? {})) {
988
+ if ((def.name ?? key) !== table) continue;
989
+ for (const [col, c] of Object.entries(def.columns ?? {})) {
990
+ const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
991
+ if (d !== null && typeof d === "object" && d.type === "vector") out.add(col);
992
+ }
993
+ }
994
+ return out;
995
+ }
996
+ function reviveVectors(row, vectorCols) {
997
+ if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
998
+ const out = row;
999
+ for (const col of vectorCols) {
1000
+ const v = out[col];
1001
+ if (typeof v === "string") out[col] = JSON.parse(v);
1002
+ }
1003
+ return row;
1004
+ }
1005
+ function asTableRow(table, row) {
1006
+ return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1007
+ }
1008
+ function asTableRows(table, rows) {
1009
+ const vectorCols = vectorColumnsOf(currentSchema, table);
1010
+ return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
1011
+ }
1012
+ function asBindParams(table, cols, data) {
1013
+ const vectorCols = vectorColumnsOf(currentSchema, table);
1014
+ return cols.map((c) => {
1015
+ const v = data[c];
1016
+ return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
1017
+ });
1018
+ }
1019
+ var SELECTIVITY_EXACT_THRESHOLD = 1e4;
1020
+ var METRIC_OPERATOR = {
1021
+ cosine: "<=>",
1022
+ euclidean: "<->",
1023
+ inner_product: "<#>"
1024
+ };
1025
+ function searchConfigFor(table) {
1026
+ const t = currentSchema.tables?.[table];
1027
+ if (!t) return null;
1028
+ const columns = t.columns ?? {};
1029
+ const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
1030
+ const cols = Object.keys(columns);
1031
+ const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
1032
+ const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
1033
+ const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
1034
+ const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
1035
+ if (pk === null) {
1036
+ throw new Error(
1037
+ `search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
1038
+ );
1039
+ }
1040
+ const search = t.search;
1041
+ const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
1042
+ const validity = search?.validity === true ? { validity: true } : {};
1043
+ if (search?.from !== void 0 && search.model !== void 0) {
1044
+ const m = search.model;
1045
+ const embed = {
1046
+ model: m.model,
1047
+ apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
1048
+ ...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
1049
+ ...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
1050
+ };
1051
+ const metric = search.metric ?? "cosine";
1052
+ const ftsOn = search.text !== false;
1053
+ const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
1054
+ if (vectorCols.length > 0) {
1055
+ return {
1056
+ pk,
1057
+ cols,
1058
+ colSet: new Set(cols),
1059
+ ftsCols: ftsColsNew,
1060
+ legs: [{ column: vectorCols[0], metric, embed }],
1061
+ ...synonyms,
1062
+ ...validity
1063
+ };
1064
+ }
1065
+ return {
1066
+ pk,
1067
+ cols,
1068
+ colSet: new Set(cols),
1069
+ ftsCols: ftsColsNew,
1070
+ legs: [],
1071
+ chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
1072
+ ...synonyms,
1073
+ ...validity
1074
+ };
1075
+ }
1076
+ const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
1077
+ const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
1078
+ let legs;
1079
+ if (rawLegs.length > 0) {
1080
+ legs = rawLegs.map((leg) => {
1081
+ const l = leg;
1082
+ const column = l.column ?? (vectorCols.length === 1 ? vectorCols[0] : void 0);
1083
+ if (column === void 0) {
1084
+ throw new Error(`search(${table}): birden \xE7ok vector kolonu var \u2014 beyanda 'column' zorunlu (FR-010)`);
1085
+ }
1086
+ const model = l.model;
1087
+ return {
1088
+ column,
1089
+ metric: l.metric ?? "cosine",
1090
+ ...model !== void 0 ? { embed: {
1091
+ model: model.model,
1092
+ apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
1093
+ ...model.baseURL !== void 0 ? { baseURL: model.baseURL } : {},
1094
+ ...model.dimensions !== void 0 ? { dimensions: model.dimensions } : {}
1095
+ } } : {}
1096
+ };
1097
+ });
1098
+ } else {
1099
+ legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
1100
+ }
1101
+ if (ftsCols.length === 0 && legs.length === 0) return null;
1102
+ return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
1103
+ }
1104
+ function pickLeg(table, legs, using) {
1105
+ if (legs.length === 0) return null;
1106
+ if (using !== void 0) {
1107
+ const hit = legs.find((l) => l.column === using);
1108
+ if (!hit) {
1109
+ throw new Error(
1110
+ `search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
1111
+ );
1112
+ }
1113
+ return hit;
1114
+ }
1115
+ if (legs.length === 1) return legs[0];
1116
+ 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`);
1117
+ }
1118
+ var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
1119
+ function parseHalfLife(s) {
1120
+ const m = /^(\d+)(s|m|h|d)$/.exec(s);
1121
+ const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
1122
+ if (!Number.isFinite(sec) || sec <= 0) {
1123
+ throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
1124
+ }
1125
+ return sec;
1126
+ }
1127
+ var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
1128
+ function ftsExprOf(ftsCols) {
1129
+ return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
1130
+ }
1131
+ function expandSynonyms(query, map) {
1132
+ const lower = {};
1133
+ for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
1134
+ return query.split(/("[^"]*")/).map((seg) => {
1135
+ if (seg.startsWith('"')) return seg;
1136
+ return seg.split(/(\s+)/).map((tok) => {
1137
+ if (tok === "" || /^\s+$/.test(tok)) return tok;
1138
+ const alts = lower[tok.toLowerCase()];
1139
+ return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
1140
+ }).join("");
1141
+ }).join("");
1142
+ }
1143
+ var searchLogger = null;
1144
+ function logNoResults(table, query, mode, n) {
1145
+ if (n === 0) {
1146
+ searchLogger?.("palbase.search.no_results", {
1147
+ table,
1148
+ query: (query ?? "").slice(0, 200),
1149
+ mode: mode ?? "hybrid"
1150
+ });
1151
+ }
1152
+ }
1153
+ async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
1154
+ const bind = [];
1155
+ const add = (v) => {
1156
+ bind.push(v);
1157
+ return `$${bind.length}`;
1158
+ };
1159
+ const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
1160
+ const parts = facets.map(
1161
+ (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)`
1162
+ );
1163
+ const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
1164
+ const out = {};
1165
+ for (const col of facets) out[col] = [];
1166
+ for (const r of rows) {
1167
+ if (typeof r.f === "string" && out[r.f] !== void 0) {
1168
+ out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
1169
+ }
1170
+ }
1171
+ return out;
1172
+ }
1173
+ function validitySql(v, add) {
1174
+ if (v === "all") return "";
1175
+ if (v !== void 0) {
1176
+ const p = add(v.asOf);
1177
+ return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
1178
+ }
1179
+ return ` AND t."valid_to" IS NULL`;
1180
+ }
1181
+ function excludeSql(pk, ids, add) {
1182
+ if (ids.length === 0) return "";
1183
+ if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
1184
+ return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
1185
+ }
1186
+ function compileWhere(table, colSet, where, add) {
1187
+ const parts = [];
1188
+ for (const [col, cond] of Object.entries(where)) {
1189
+ if (!colSet.has(col)) {
1190
+ throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
1191
+ }
1192
+ const q = `t.${quoteIdent(col)}`;
1193
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
1194
+ for (const [op, v] of Object.entries(cond)) {
1195
+ if (op === "in") {
1196
+ if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
1197
+ if (v.length === 0) {
1198
+ parts.push("false");
1199
+ continue;
1200
+ }
1201
+ parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
1202
+ } else if (op in WHERE_OPS) {
1203
+ parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
1204
+ } else {
1205
+ throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
1206
+ }
1207
+ }
1208
+ } else {
1209
+ parts.push(`${q} = ${add(cond)}`);
1210
+ }
1211
+ }
1212
+ return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
1213
+ }
1214
+ var cachedVectorSchema = null;
1215
+ var cachedTrgmSchema = null;
1216
+ async function trgmSchemaOf(live) {
1217
+ if (cachedTrgmSchema !== null) return cachedTrgmSchema;
1218
+ const rows = await live.unsafe(
1219
+ "select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
1220
+ );
1221
+ cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
1222
+ return cachedTrgmSchema;
1223
+ }
1224
+ async function vectorSchemaWithGuc(runner) {
1225
+ if (cachedVectorSchema !== null) {
1226
+ await runner.unsafe(
1227
+ "select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
1228
+ );
1229
+ return cachedVectorSchema;
1230
+ }
1231
+ const rows = await runner.unsafe(
1232
+ "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"
1233
+ );
1234
+ const name = rows?.[0]?.nspname;
1235
+ if (typeof name !== "string" || name === "") {
1236
+ throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
1237
+ }
1238
+ cachedVectorSchema = name;
1239
+ return name;
1240
+ }
1241
+ var secretReader = null;
1242
+ function setSecretReader(fn) {
1243
+ secretReader = fn;
1244
+ }
1245
+ var embedFetch = (url, init) => fetch(url, init);
1246
+ async function embedQuery(embed, text) {
1247
+ if (secretReader === null) {
1248
+ throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
1249
+ }
1250
+ const key = await secretReader(embed.apiKeyName);
1251
+ if (key === null || key === "") {
1252
+ throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
1253
+ }
1254
+ const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
1255
+ const controller = new AbortController();
1256
+ const timer = setTimeout(() => controller.abort(), 1e4);
1257
+ try {
1258
+ const res = await embedFetch(url, {
1259
+ method: "POST",
1260
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
1261
+ body: JSON.stringify({
1262
+ model: embed.model,
1263
+ input: [text],
1264
+ ...embed.dimensions !== void 0 ? { dimensions: embed.dimensions } : {}
1265
+ }),
1266
+ signal: controller.signal
1267
+ });
1268
+ if (!res.ok) {
1269
+ throw new Error(`query embed: sa\u011Flay\u0131c\u0131 ${res.status} d\xF6nd\xFC (${embed.apiKeyName} ile) \u2014 anahtar/model do\u011Fru mu?`);
1270
+ }
1271
+ const data = await res.json();
1272
+ const vec = data.data?.[0]?.embedding;
1273
+ if (!Array.isArray(vec)) {
1274
+ throw new Error("query embed: sa\u011Flay\u0131c\u0131 yan\u0131t\u0131nda data[0].embedding yok (CLAIM-N1 \u015Fekli)");
1275
+ }
1276
+ return vec;
1277
+ } finally {
1278
+ clearTimeout(timer);
1279
+ }
1280
+ }
974
1281
  function createOps(tx) {
975
1282
  const at = () => resolveTx(tx);
976
1283
  const ops = {
@@ -982,22 +1289,22 @@ function createOps(tx) {
982
1289
  if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
983
1290
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
984
1291
  const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
985
- const rows = await (await at()).unsafe(sql, cols.map((c) => data[c]));
1292
+ const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
986
1293
  const inserted = rows[0];
987
1294
  if (!inserted) {
988
1295
  throw new Error(
989
1296
  `insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
990
1297
  );
991
1298
  }
992
- return asWireRow(inserted);
1299
+ return asTableRow(table, inserted);
993
1300
  },
994
1301
  async update(table, id, data) {
995
1302
  const cols = Object.keys(data);
996
1303
  if (cols.length === 0) return ops.findById(table, id);
997
1304
  const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
998
1305
  const sql = `UPDATE ${quoteIdent(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
999
- const rows = await (await at()).unsafe(sql, [...cols.map((c) => data[c]), id]);
1000
- return rows[0] ? asWireRow(rows[0]) : null;
1306
+ const rows = await (await at()).unsafe(sql, [...asBindParams(table, cols, data), id]);
1307
+ return rows[0] ? asTableRow(table, rows[0]) : null;
1001
1308
  },
1002
1309
  async delete(table, id) {
1003
1310
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
@@ -1007,16 +1314,400 @@ function createOps(tx) {
1007
1314
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
1008
1315
  [id]
1009
1316
  );
1010
- return rows[0] ? asWireRow(rows[0]) : null;
1317
+ return rows[0] ? asTableRow(table, rows[0]) : null;
1011
1318
  },
1012
1319
  async findMany(table, query = {}) {
1013
1320
  const cols = Object.keys(query);
1014
1321
  const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
1015
- return asWireRows(await (await at()).unsafe(
1322
+ return asTableRows(table, await (await at()).unsafe(
1016
1323
  `SELECT * FROM ${quoteIdent(table)}${where}`,
1017
1324
  cols.map((c) => query[c])
1018
1325
  ));
1019
1326
  },
1327
+ /**
1328
+ * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
1329
+ * (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
1330
+ * hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
1331
+ * RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
1332
+ * T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
1333
+ */
1334
+ async search(table, params = {}) {
1335
+ const cfg = searchConfigFor(table);
1336
+ if (!cfg) {
1337
+ throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1338
+ }
1339
+ const excl = params.__excludeIds ?? [];
1340
+ if (params.facets !== void 0) {
1341
+ for (const col of params.facets) {
1342
+ if (!cfg.colSet.has(col)) {
1343
+ throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1344
+ }
1345
+ }
1346
+ }
1347
+ if (params.validity !== void 0 && cfg.validity !== true) {
1348
+ throw new Error(
1349
+ `search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
1350
+ );
1351
+ }
1352
+ const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
1353
+ const rawLimit = params.limit ?? 20;
1354
+ if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
1355
+ throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
1356
+ }
1357
+ const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
1358
+ const pool = Math.max(limit * 3, 30);
1359
+ if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
1360
+ throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
1361
+ }
1362
+ let recencyMul = "";
1363
+ if (params.recency !== void 0) {
1364
+ const { field, halfLife } = params.recency;
1365
+ if (!cfg.colSet.has(field)) {
1366
+ throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
1367
+ }
1368
+ recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
1369
+ }
1370
+ let boostMul = "";
1371
+ if (params.boost !== void 0) {
1372
+ const { field, weight } = params.boost;
1373
+ if (!cfg.colSet.has(field)) {
1374
+ throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
1375
+ }
1376
+ if (typeof weight !== "number" || !Number.isFinite(weight)) {
1377
+ throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
1378
+ }
1379
+ const g = `greatest(t.${quoteIdent(field)},0)`;
1380
+ boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
1381
+ }
1382
+ const scoreMul = boostMul + recencyMul;
1383
+ if (cfg.chunk !== void 0) {
1384
+ const ck = cfg.chunk;
1385
+ const pidQ = quoteIdent(`parent_${cfg.pk}`);
1386
+ const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
1387
+ const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
1388
+ let qvC = Array.isArray(params.vector) ? params.vector : null;
1389
+ if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
1390
+ try {
1391
+ qvC = await embedQuery(ck.embed, params.query);
1392
+ } catch (e) {
1393
+ if (!wantTextC) throw e;
1394
+ qvC = null;
1395
+ }
1396
+ }
1397
+ if (!wantTextC && qvC === null) {
1398
+ throw new Error(
1399
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
1400
+ );
1401
+ }
1402
+ const bindC = [];
1403
+ const addC = (v) => {
1404
+ bindC.push(v);
1405
+ return `$${bindC.length}`;
1406
+ };
1407
+ const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1408
+ const liveC = await at();
1409
+ const K2 = 50;
1410
+ const cpool = Math.max(limit * 9, 90);
1411
+ let semC = "";
1412
+ let kwC = "";
1413
+ if (qvC !== null) {
1414
+ const sch = await vectorSchemaWithGuc(liveC);
1415
+ const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1416
+ const vp = addC(toVectorLiteral(qvC));
1417
+ 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}`;
1418
+ }
1419
+ let qpC = "";
1420
+ let qpCIdx = -1;
1421
+ if (wantTextC) {
1422
+ const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1423
+ qpC = addC(qTextC);
1424
+ qpCIdx = bindC.length - 1;
1425
+ 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}`;
1426
+ }
1427
+ const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1428
+ const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
1429
+ const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
1430
+ const assembleC = (kwIn, withKwn) => {
1431
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
1432
+ let fusedSrc;
1433
+ if (semC !== "" && kwIn !== "") {
1434
+ 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)`;
1435
+ } else if (semC !== "") {
1436
+ fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
1437
+ } else {
1438
+ fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1439
+ }
1440
+ 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}`;
1441
+ 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}`;
1442
+ };
1443
+ let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1444
+ if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
1445
+ const tsch = await trgmSchemaOf(liveC);
1446
+ 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}`;
1447
+ const retryBindC = bindC.slice();
1448
+ if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
1449
+ rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
1450
+ }
1451
+ for (const r of rowsC) delete r._kwn;
1452
+ const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
1453
+ if (params.facets !== void 0 && params.facets.length > 0) {
1454
+ Object.assign(outC, {
1455
+ _facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
1456
+ });
1457
+ }
1458
+ logNoResults(table, params.query, params.mode, outC.length);
1459
+ return outC;
1460
+ }
1461
+ const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
1462
+ const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
1463
+ const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
1464
+ const leg = vectorAsked ? pickLeg(table, cfg.legs, params.using) : null;
1465
+ let qv = Array.isArray(params.vector) ? params.vector : null;
1466
+ if (qv === null && leg?.embed !== void 0 && typeof params.query === "string" && params.query !== "") {
1467
+ try {
1468
+ qv = await embedQuery(leg.embed, params.query);
1469
+ } catch (e) {
1470
+ if (!wantText) throw e;
1471
+ qv = null;
1472
+ }
1473
+ }
1474
+ const wantVector = leg !== null && qv !== null;
1475
+ if (!wantText && !wantVector) {
1476
+ throw new Error(
1477
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
1478
+ );
1479
+ }
1480
+ const bind = [];
1481
+ const add = (v) => {
1482
+ bind.push(v);
1483
+ return `$${bind.length}`;
1484
+ };
1485
+ const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
1486
+ const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
1487
+ const live = await at();
1488
+ const K = 50;
1489
+ let semSql = "";
1490
+ let kwSql = "";
1491
+ if (wantVector && leg) {
1492
+ const sch = await vectorSchemaWithGuc(live);
1493
+ const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
1494
+ let exactOrder = "";
1495
+ if (userWhere !== "") {
1496
+ const probeRows = await live.unsafe(
1497
+ `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`,
1498
+ bind.slice()
1499
+ );
1500
+ const n = probeRows?.[0]?.n;
1501
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1502
+ exactOrder = " + 0.0";
1503
+ }
1504
+ }
1505
+ const vp = add(toVectorLiteral(qv));
1506
+ 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}`;
1507
+ }
1508
+ let qpRef = "";
1509
+ let qpIdx = -1;
1510
+ if (wantText) {
1511
+ const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1512
+ const qp = add(qText);
1513
+ qpIdx = bind.length - 1;
1514
+ qpRef = qp;
1515
+ 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}`;
1516
+ }
1517
+ const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
1518
+ const msP = params.minScore === void 0 ? "" : add(params.minScore);
1519
+ const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
1520
+ const assemble = (kwIn, withKwn) => {
1521
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
1522
+ let inner;
1523
+ let orderScore;
1524
+ if (semSql !== "" && kwIn !== "") {
1525
+ const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
1526
+ 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`;
1527
+ orderScore = scoreMul === "" ? "fused._score" : "_score";
1528
+ } else {
1529
+ const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
1530
+ const alias = semSql !== "" ? "sem" : "kw";
1531
+ const base = `(1.0/(${K} + ${alias}.r))::float8`;
1532
+ const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
1533
+ inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
1534
+ orderScore = "_score";
1535
+ }
1536
+ 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}`;
1537
+ };
1538
+ let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
1539
+ if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1540
+ const tsch = await trgmSchemaOf(live);
1541
+ const expr = ftsExprOf(cfg.ftsCols);
1542
+ 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}`;
1543
+ const retryBind = bind.slice();
1544
+ if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1545
+ rows = await live.unsafe(assemble(trgmKw, false), retryBind);
1546
+ }
1547
+ for (const r of rows) delete r._kwn;
1548
+ const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
1549
+ if (params.facets !== void 0 && params.facets.length > 0) {
1550
+ Object.assign(out, {
1551
+ _facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
1552
+ });
1553
+ }
1554
+ logNoResults(table, params.query, params.mode, out.length);
1555
+ return out;
1556
+ },
1557
+ /**
1558
+ * similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
1559
+ * okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
1560
+ * şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
1561
+ * kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
1562
+ * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1563
+ * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1564
+ */
1565
+ async similar(table, id, opts = {}) {
1566
+ const cfg = searchConfigFor(table);
1567
+ if (!cfg) {
1568
+ throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1569
+ }
1570
+ const live = await at();
1571
+ let v;
1572
+ if (cfg.chunk !== void 0) {
1573
+ const sch = await vectorSchemaWithGuc(live);
1574
+ const rows = await live.unsafe(
1575
+ `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`,
1576
+ [id]
1577
+ );
1578
+ v = rows?.[0]?.v;
1579
+ } else {
1580
+ const leg = pickLeg(table, cfg.legs, opts.using);
1581
+ if (leg === null) {
1582
+ throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
1583
+ }
1584
+ const rows = await live.unsafe(
1585
+ `SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
1586
+ [id]
1587
+ );
1588
+ v = rows?.[0]?.v;
1589
+ }
1590
+ if (typeof v !== "string") {
1591
+ throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
1592
+ }
1593
+ const p = {
1594
+ vector: JSON.parse(v),
1595
+ mode: "vector",
1596
+ where: opts.where,
1597
+ limit: opts.limit,
1598
+ using: opts.using,
1599
+ minScore: opts.minScore,
1600
+ recency: opts.recency,
1601
+ blocksPerRow: opts.blocksPerRow,
1602
+ validity: opts.validity,
1603
+ boost: opts.boost,
1604
+ __excludeIds: [id]
1605
+ };
1606
+ return ops.search(table, p);
1607
+ },
1608
+ /**
1609
+ * recommend (T018, FR-023): positive/negative id kümelerinden öneri.
1610
+ * Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
1611
+ * negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
1612
+ * ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
1613
+ * Vektör matematiği istemciye inmez; bulunamayan positive/negative
1614
+ * adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
1615
+ */
1616
+ async recommend(table, opts) {
1617
+ const positive = opts?.positive;
1618
+ if (!Array.isArray(positive) || positive.length === 0) {
1619
+ throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
1620
+ }
1621
+ const negative = Array.isArray(opts.negative) ? opts.negative : [];
1622
+ const cfg = searchConfigFor(table);
1623
+ if (!cfg) {
1624
+ throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1625
+ }
1626
+ let leg = null;
1627
+ if (cfg.chunk === void 0) {
1628
+ leg = pickLeg(table, cfg.legs, opts.using);
1629
+ if (leg === null) {
1630
+ throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
1631
+ }
1632
+ }
1633
+ const live = await at();
1634
+ const sch = await vectorSchemaWithGuc(live);
1635
+ const bind = [];
1636
+ const add = (x) => {
1637
+ bind.push(x);
1638
+ return `$${bind.length}`;
1639
+ };
1640
+ const avgSel = (ids) => {
1641
+ const inList = ids.map((x) => add(x)).join(", ");
1642
+ 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`;
1643
+ };
1644
+ 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`;
1645
+ const rows = await live.unsafe(targetSql, bind);
1646
+ const r0 = rows?.[0];
1647
+ if (r0 === void 0 || r0.pos_missing === true) {
1648
+ throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1649
+ }
1650
+ if (negative.length > 0 && r0.neg_missing === true) {
1651
+ throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1652
+ }
1653
+ if (typeof r0.v !== "string") {
1654
+ throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
1655
+ }
1656
+ const p = {
1657
+ vector: JSON.parse(r0.v),
1658
+ mode: "vector",
1659
+ where: opts.where,
1660
+ limit: opts.limit,
1661
+ using: opts.using,
1662
+ minScore: opts.minScore,
1663
+ recency: opts.recency,
1664
+ blocksPerRow: opts.blocksPerRow,
1665
+ validity: opts.validity,
1666
+ boost: opts.boost,
1667
+ __excludeIds: [...positive, ...negative]
1668
+ };
1669
+ return ops.search(table, p);
1670
+ },
1671
+ /**
1672
+ * supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
1673
+ * versiyonunu TEK savepoint'te yazar — eski satır kapatılır
1674
+ * (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
1675
+ * yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
1676
+ * randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
1677
+ * kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
1678
+ * ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
1679
+ * kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
1680
+ */
1681
+ async supersede(table, id, row) {
1682
+ const cfg = searchConfigFor(table);
1683
+ if (cfg === null || cfg.validity !== true) {
1684
+ throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
1685
+ }
1686
+ const live = await at();
1687
+ return live.savepoint(async (sp) => {
1688
+ const newId = row[cfg.pk] ?? crypto.randomUUID();
1689
+ const closed = await sp.unsafe(
1690
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
1691
+ [newId, id]
1692
+ );
1693
+ if (closed.length === 0) {
1694
+ throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
1695
+ }
1696
+ const data = { ...row, [cfg.pk]: newId };
1697
+ const cols = Object.keys(data);
1698
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1699
+ const rows = await sp.unsafe(
1700
+ `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
1701
+ asBindParams(table, cols, data)
1702
+ );
1703
+ if (!rows[0]) {
1704
+ throw new Error(
1705
+ `supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
1706
+ );
1707
+ }
1708
+ return asTableRow(table, rows[0]);
1709
+ });
1710
+ },
1020
1711
  /** A real SAVEPOINT inside the request's transaction. */
1021
1712
  async transaction(cb) {
1022
1713
  const live = await at();
@@ -1045,7 +1736,7 @@ function createOps(tx) {
1045
1736
  return live.savepoint(async (sp) => {
1046
1737
  const results = [];
1047
1738
  for (const op of plan.ops) {
1048
- const rows = await runPlanOp(sp, op, results);
1739
+ const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
1049
1740
  const result = { rows, rows_affected: rows.length };
1050
1741
  results.push(result);
1051
1742
  assertGuard(op, result);
@@ -1058,6 +1749,8 @@ function createOps(tx) {
1058
1749
  }
1059
1750
  var currentSchema = {};
1060
1751
  function setSchema(schema) {
1752
+ cachedVectorSchema = null;
1753
+ cachedTrgmSchema = null;
1061
1754
  const s = schema;
1062
1755
  currentSchema = (s && "default" in s ? s.default : s) ?? {};
1063
1756
  }
@@ -1144,7 +1837,7 @@ function isRef(v) {
1144
1837
  function isExpr(v) {
1145
1838
  return typeof v === "object" && v !== null && "$expr" in v;
1146
1839
  }
1147
- function renderValue(value, column, args, results) {
1840
+ function renderValue(value, column, args, results, vectorCols) {
1148
1841
  if (isRef(value)) {
1149
1842
  const source = results[value.$ref.op];
1150
1843
  const row = source?.rows[0];
@@ -1153,15 +1846,15 @@ function renderValue(value, column, args, results) {
1153
1846
  error_code: "tx_ref_unresolved"
1154
1847
  });
1155
1848
  }
1156
- return args.bind(row[value.$ref.field]);
1849
+ return bindMaybeVector(args, column, vectorCols, row[value.$ref.field]);
1157
1850
  }
1158
1851
  if (isExpr(value)) {
1159
1852
  const fn = value.$expr;
1160
1853
  if (fn.fn === "now") return "now()";
1161
1854
  const operator = fn.fn === "inc" ? "+" : "-";
1162
- return `${quoteIdent(column)} ${operator} ${args.bind(fn.by)}`;
1855
+ return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
1163
1856
  }
1164
- return args.bind(value);
1857
+ return bindMaybeVector(args, column, vectorCols, value);
1165
1858
  }
1166
1859
  function renderWhere(where, args, results) {
1167
1860
  const cols = Object.keys(where ?? {});
@@ -1173,14 +1866,21 @@ function renderWhere(where, args, results) {
1173
1866
  });
1174
1867
  return ` WHERE ${terms.join(" AND ")}`;
1175
1868
  }
1869
+ function bindMaybeVector(args, column, vectorCols, value) {
1870
+ if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
1871
+ return args.bind(toVectorLiteral(value));
1872
+ }
1873
+ return args.bind(value);
1874
+ }
1176
1875
  async function runPlanOp(sp, op, results) {
1876
+ const opVectorCols = vectorColumnsOf(currentSchema, op.table);
1177
1877
  const args = new Args();
1178
1878
  const table = quoteIdent(op.table);
1179
1879
  let sql;
1180
1880
  switch (op.op) {
1181
1881
  case "insert": {
1182
1882
  const cols = Object.keys(op.values ?? {});
1183
- const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
1883
+ const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols));
1184
1884
  sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
1185
1885
  break;
1186
1886
  }
@@ -1189,7 +1889,7 @@ async function runPlanOp(sp, op, results) {
1189
1889
  if (rows.length === 0 || !rows[0]) return [];
1190
1890
  const cols = Object.keys(rows[0]);
1191
1891
  const tuples = rows.map(
1192
- (r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
1892
+ (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
1193
1893
  );
1194
1894
  sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
1195
1895
  break;
@@ -1198,7 +1898,7 @@ async function runPlanOp(sp, op, results) {
1198
1898
  const cols = Object.keys(op.set ?? {});
1199
1899
  if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
1200
1900
  const assignments = cols.map(
1201
- (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
1901
+ (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols)}`
1202
1902
  );
1203
1903
  sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
1204
1904
  break;
@@ -1345,7 +2045,8 @@ function grantFor(entry, ctx, bucketLimits) {
1345
2045
  filename: ctx.filename
1346
2046
  }),
1347
2047
  maxBytes: bucketLimits?.maxBytes ?? null,
1348
- mimeTypes: bucketLimits?.mimeTypes ?? null
2048
+ mimeTypes: bucketLimits?.mimeTypes ?? null,
2049
+ ownerUid: ctx.userId ?? null
1349
2050
  };
1350
2051
  }
1351
2052
  var CompletionLedger = class {
@@ -1381,6 +2082,23 @@ function verifySignature(presented, expected) {
1381
2082
 
1382
2083
  // src/engine/index.ts
1383
2084
  var JSON_HEADERS = { "content-type": "application/json" };
2085
+ function fieldErrors(err) {
2086
+ return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
2087
+ }
2088
+ function headersFor(raw, schema) {
2089
+ const shape = schema.shape;
2090
+ if (typeof shape !== "object" || shape === null) return raw;
2091
+ let aliased = null;
2092
+ for (const declared of Object.keys(shape)) {
2093
+ const lower = declared.toLowerCase();
2094
+ if (lower === declared) continue;
2095
+ const value = raw[lower];
2096
+ if (value === void 0) continue;
2097
+ aliased ??= { ...raw };
2098
+ aliased[declared] = value;
2099
+ }
2100
+ return aliased ?? raw;
2101
+ }
1384
2102
  function envelope(error, description, status, requestId, extra) {
1385
2103
  return new Response(
1386
2104
  JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
@@ -1414,6 +2132,7 @@ async function defaultSqlDriver(config) {
1414
2132
  async function createApp(opts) {
1415
2133
  const { config, controllers } = opts;
1416
2134
  setSchema(opts.schema ?? {});
2135
+ setSecretReader(opts.secretReader ?? null);
1417
2136
  const routes = buildRouteTable(controllers);
1418
2137
  if (routes.length === 0) {
1419
2138
  throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
@@ -1439,7 +2158,6 @@ async function createApp(opts) {
1439
2158
  Notifications: modules.Notifications ?? stubModule("Notifications"),
1440
2159
  Flags: modules.Flags ?? stubModule("Flags"),
1441
2160
  Realtime: modules.Realtime ?? stubModule("Realtime"),
1442
- Purchases: modules.Purchases ?? stubModule("Purchases"),
1443
2161
  // Named, never undefined. A backend started without a secrets client
1444
2162
  // that returned `undefined` here would fail inside the handler as
1445
2163
  // "Cannot read properties of undefined", which says nothing about what
@@ -1526,7 +2244,8 @@ async function createApp(opts) {
1526
2244
  error: "too_many_requests",
1527
2245
  error_description: "Rate limit exceeded for this endpoint",
1528
2246
  status: 429,
1529
- request_id: requestId
2247
+ request_id: requestId,
2248
+ data: { retryAfter }
1530
2249
  }),
1531
2250
  { status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
1532
2251
  );
@@ -1545,7 +2264,7 @@ async function createApp(opts) {
1545
2264
  const r = p.schema.safeParse(parsedBody);
1546
2265
  if (!r.success) {
1547
2266
  return envelope("bad_request", "Request body failed validation", 400, requestId, {
1548
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
2267
+ data: { fields: fieldErrors(r.error) }
1549
2268
  });
1550
2269
  }
1551
2270
  args[p.index] = r.data;
@@ -1555,7 +2274,7 @@ async function createApp(opts) {
1555
2274
  const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
1556
2275
  if (!r.success) {
1557
2276
  return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
1558
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
2277
+ data: { fields: fieldErrors(r.error) }
1559
2278
  });
1560
2279
  }
1561
2280
  args[p.index] = r.data;
@@ -1564,9 +2283,21 @@ async function createApp(opts) {
1564
2283
  case "param":
1565
2284
  args[p.index] = hit.params[p.name];
1566
2285
  break;
1567
- case "headers":
1568
- args[p.index] = Object.fromEntries(req.headers);
2286
+ case "headers": {
2287
+ const raw = Object.fromEntries(req.headers);
2288
+ if (!p.schema) {
2289
+ args[p.index] = raw;
2290
+ break;
2291
+ }
2292
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
2293
+ if (!r.success) {
2294
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
2295
+ data: { fields: fieldErrors(r.error) }
2296
+ });
2297
+ }
2298
+ args[p.index] = r.data;
1569
2299
  break;
2300
+ }
1570
2301
  case "user":
1571
2302
  case "optionalUser":
1572
2303
  args[p.index] = claims ? {
@@ -1615,6 +2346,14 @@ async function createApp(opts) {
1615
2346
  case "traceId":
1616
2347
  args[p.index] = requestId;
1617
2348
  break;
2349
+ case "client":
2350
+ args[p.index] = {
2351
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
2352
+ appVersion: req.headers.get("x-palbase-client-version"),
2353
+ platform: req.headers.get("x-platform"),
2354
+ osVersion: req.headers.get("x-os-version")
2355
+ };
2356
+ break;
1618
2357
  case "req":
1619
2358
  args[p.index] = req;
1620
2359
  break;