@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
@@ -459,7 +459,11 @@ function makeTablesAccessor(ops) {
459
459
  update: (id, data) => ops().update(name, id, data),
460
460
  delete: (id) => ops().delete(name, id),
461
461
  findById: (id) => ops().findById(name, id),
462
- findMany: (query) => ops().findMany(name, query)
462
+ findMany: (query) => ops().findMany(name, query),
463
+ search: (params) => ops().search(name, params),
464
+ similar: (id, params) => ops().similar(name, id, params),
465
+ recommend: (params) => ops().recommend(name, params),
466
+ supersede: (id, row) => ops().supersede(name, id, row)
463
467
  };
464
468
  }
465
469
  }
@@ -468,16 +472,21 @@ function makeTablesAccessor(ops) {
468
472
  }
469
473
  var rawDatabase = makeServiceProxy("Database");
470
474
  function makeTypedSurface(raw) {
475
+ const reco = raw;
471
476
  const ops = {
472
477
  query: (sql, params) => raw.query(sql, params),
473
478
  insert: (table, data) => raw.insert(table, data),
474
479
  update: (table, id, data) => raw.update(table, id, data),
475
480
  delete: (table, id) => raw.delete(table, id),
476
481
  findById: (table, id) => raw.findById(table, id),
477
- findMany: (table, query) => raw.findMany(table, query)
482
+ findMany: (table, query) => raw.findMany(table, query),
483
+ search: (table, params) => raw.search(table, params),
484
+ similar: (table, id, params) => reco.similar(table, id, params),
485
+ recommend: (table, params) => reco.recommend(table, params),
486
+ supersede: (table, id, row) => raw.supersede(table, id, row)
478
487
  };
479
488
  return Object.assign(ops, {
480
- tables: makeTablesAccessor(() => raw),
489
+ tables: makeTablesAccessor(() => reco),
481
490
  transaction(fn) {
482
491
  const builder = new TxPlanBuilder();
483
492
  return runTxPlan(raw, makeTxTablesAccessor(builder), builder, fn);
@@ -540,7 +549,6 @@ var Cache = makeServiceProxy("Cache");
540
549
  var Secrets = makeServiceProxy("Secrets");
541
550
  var Log = makeServiceProxy("Log");
542
551
  var Notifications = makeServiceProxy("Notifications");
543
- var Purchases = makeServiceProxy("Purchases");
544
552
  var rawFlags = makeServiceProxy("Flags");
545
553
  var Flags = Object.assign(
546
554
  {
@@ -1005,6 +1013,305 @@ function asWireRow(row) {
1005
1013
  function asWireRows(rows) {
1006
1014
  return rows.map((row) => asWireRow(row));
1007
1015
  }
1016
+ function toVectorLiteral(v) {
1017
+ return `[${v.join(",")}]`;
1018
+ }
1019
+ function vectorColumnsOf(schema, table) {
1020
+ const out = /* @__PURE__ */ new Set();
1021
+ for (const [key, def] of Object.entries(schema.tables ?? {})) {
1022
+ if ((def.name ?? key) !== table) continue;
1023
+ for (const [col, c] of Object.entries(def.columns ?? {})) {
1024
+ const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
1025
+ if (d !== null && typeof d === "object" && d.type === "vector") out.add(col);
1026
+ }
1027
+ }
1028
+ return out;
1029
+ }
1030
+ function reviveVectors(row, vectorCols) {
1031
+ if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
1032
+ const out = row;
1033
+ for (const col of vectorCols) {
1034
+ const v = out[col];
1035
+ if (typeof v === "string") out[col] = JSON.parse(v);
1036
+ }
1037
+ return row;
1038
+ }
1039
+ function asTableRow(table, row) {
1040
+ return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1041
+ }
1042
+ function asTableRows(table, rows) {
1043
+ const vectorCols = vectorColumnsOf(currentSchema, table);
1044
+ return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
1045
+ }
1046
+ function asBindParams(table, cols, data) {
1047
+ const vectorCols = vectorColumnsOf(currentSchema, table);
1048
+ return cols.map((c) => {
1049
+ const v = data[c];
1050
+ return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
1051
+ });
1052
+ }
1053
+ var SELECTIVITY_EXACT_THRESHOLD = 1e4;
1054
+ var METRIC_OPERATOR = {
1055
+ cosine: "<=>",
1056
+ euclidean: "<->",
1057
+ inner_product: "<#>"
1058
+ };
1059
+ function searchConfigFor(table) {
1060
+ const t = currentSchema.tables?.[table];
1061
+ if (!t) return null;
1062
+ const columns = t.columns ?? {};
1063
+ const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
1064
+ const cols = Object.keys(columns);
1065
+ const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
1066
+ const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
1067
+ const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
1068
+ const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
1069
+ if (pk === null) {
1070
+ throw new Error(
1071
+ `search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
1072
+ );
1073
+ }
1074
+ const search = t.search;
1075
+ const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? { synonyms: search.synonyms } : {};
1076
+ const validity = search?.validity === true ? { validity: true } : {};
1077
+ if (search?.from !== void 0 && search.model !== void 0) {
1078
+ const m = search.model;
1079
+ const embed = {
1080
+ model: m.model,
1081
+ apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
1082
+ ...m.baseURL !== void 0 ? { baseURL: m.baseURL } : {},
1083
+ ...m.dimensions !== void 0 ? { dimensions: m.dimensions } : {}
1084
+ };
1085
+ const metric = search.metric ?? "cosine";
1086
+ const ftsOn = search.text !== false;
1087
+ const ftsColsNew = ftsOn ? Array.isArray(search.text) && search.text.length > 0 ? search.text : search.from : [];
1088
+ if (vectorCols.length > 0) {
1089
+ return {
1090
+ pk,
1091
+ cols,
1092
+ colSet: new Set(cols),
1093
+ ftsCols: ftsColsNew,
1094
+ legs: [{ column: vectorCols[0], metric, embed }],
1095
+ ...synonyms,
1096
+ ...validity
1097
+ };
1098
+ }
1099
+ return {
1100
+ pk,
1101
+ cols,
1102
+ colSet: new Set(cols),
1103
+ ftsCols: ftsColsNew,
1104
+ legs: [],
1105
+ chunk: { table: `${table}__palbase_chunks`, metric, embed, fts: ftsOn },
1106
+ ...synonyms,
1107
+ ...validity
1108
+ };
1109
+ }
1110
+ const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
1111
+ const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
1112
+ let legs;
1113
+ if (rawLegs.length > 0) {
1114
+ legs = rawLegs.map((leg) => {
1115
+ const l = leg;
1116
+ const column = l.column ?? (vectorCols.length === 1 ? vectorCols[0] : void 0);
1117
+ if (column === void 0) {
1118
+ throw new Error(`search(${table}): birden \xE7ok vector kolonu var \u2014 beyanda 'column' zorunlu (FR-010)`);
1119
+ }
1120
+ const model = l.model;
1121
+ return {
1122
+ column,
1123
+ metric: l.metric ?? "cosine",
1124
+ ...model !== void 0 ? { embed: {
1125
+ model: model.model,
1126
+ apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
1127
+ ...model.baseURL !== void 0 ? { baseURL: model.baseURL } : {},
1128
+ ...model.dimensions !== void 0 ? { dimensions: model.dimensions } : {}
1129
+ } } : {}
1130
+ };
1131
+ });
1132
+ } else {
1133
+ legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
1134
+ }
1135
+ if (ftsCols.length === 0 && legs.length === 0) return null;
1136
+ return { pk, cols, colSet: new Set(cols), ftsCols, legs, ...synonyms, ...validity };
1137
+ }
1138
+ function pickLeg(table, legs, using) {
1139
+ if (legs.length === 0) return null;
1140
+ if (using !== void 0) {
1141
+ const hit = legs.find((l) => l.column === using);
1142
+ if (!hit) {
1143
+ throw new Error(
1144
+ `search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
1145
+ );
1146
+ }
1147
+ return hit;
1148
+ }
1149
+ if (legs.length === 1) return legs[0];
1150
+ 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`);
1151
+ }
1152
+ var HALF_LIFE_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
1153
+ function parseHalfLife(s) {
1154
+ const m = /^(\d+)(s|m|h|d)$/.exec(s);
1155
+ const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
1156
+ if (!Number.isFinite(sec) || sec <= 0) {
1157
+ throw new Error(`recency.halfLife "${s}" \xE7\xF6z\xFCmlenemedi \u2014 beklenen: pozitif <say\u0131>+(s|m|h|d), \xF6rn. "30d" (FR-004)`);
1158
+ }
1159
+ return sec;
1160
+ }
1161
+ var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
1162
+ function ftsExprOf(ftsCols) {
1163
+ return [...ftsCols].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
1164
+ }
1165
+ function expandSynonyms(query, map) {
1166
+ const lower = {};
1167
+ for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
1168
+ return query.split(/("[^"]*")/).map((seg) => {
1169
+ if (seg.startsWith('"')) return seg;
1170
+ return seg.split(/(\s+)/).map((tok) => {
1171
+ if (tok === "" || /^\s+$/.test(tok)) return tok;
1172
+ const alts = lower[tok.toLowerCase()];
1173
+ return alts !== void 0 && alts.length > 0 ? `(${tok} OR ${alts.join(" OR ")})` : tok;
1174
+ }).join("");
1175
+ }).join("");
1176
+ }
1177
+ var searchLogger = null;
1178
+ function logNoResults(table, query, mode, n) {
1179
+ if (n === 0) {
1180
+ searchLogger?.("palbase.search.no_results", {
1181
+ table,
1182
+ query: (query ?? "").slice(0, 200),
1183
+ mode: mode ?? "hybrid"
1184
+ });
1185
+ }
1186
+ }
1187
+ async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
1188
+ const bind = [];
1189
+ const add = (v) => {
1190
+ bind.push(v);
1191
+ return `$${bind.length}`;
1192
+ };
1193
+ const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
1194
+ const parts = facets.map(
1195
+ (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)`
1196
+ );
1197
+ const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
1198
+ const out = {};
1199
+ for (const col of facets) out[col] = [];
1200
+ for (const r of rows) {
1201
+ if (typeof r.f === "string" && out[r.f] !== void 0) {
1202
+ out[r.f].push({ value: r.v ?? null, count: Number(r.n) });
1203
+ }
1204
+ }
1205
+ return out;
1206
+ }
1207
+ function validitySql(v, add) {
1208
+ if (v === "all") return "";
1209
+ if (v !== void 0) {
1210
+ const p = add(v.asOf);
1211
+ return ` AND t."valid_from" <= ${p} AND (t."valid_to" IS NULL OR t."valid_to" > ${p})`;
1212
+ }
1213
+ return ` AND t."valid_to" IS NULL`;
1214
+ }
1215
+ function excludeSql(pk, ids, add) {
1216
+ if (ids.length === 0) return "";
1217
+ if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
1218
+ return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
1219
+ }
1220
+ function compileWhere(table, colSet, where, add) {
1221
+ const parts = [];
1222
+ for (const [col, cond] of Object.entries(where)) {
1223
+ if (!colSet.has(col)) {
1224
+ throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
1225
+ }
1226
+ const q = `t.${quoteIdent(col)}`;
1227
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
1228
+ for (const [op, v] of Object.entries(cond)) {
1229
+ if (op === "in") {
1230
+ if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
1231
+ if (v.length === 0) {
1232
+ parts.push("false");
1233
+ continue;
1234
+ }
1235
+ parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
1236
+ } else if (op in WHERE_OPS) {
1237
+ parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
1238
+ } else {
1239
+ throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
1240
+ }
1241
+ }
1242
+ } else {
1243
+ parts.push(`${q} = ${add(cond)}`);
1244
+ }
1245
+ }
1246
+ return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
1247
+ }
1248
+ var cachedVectorSchema = null;
1249
+ var cachedTrgmSchema = null;
1250
+ async function trgmSchemaOf(live) {
1251
+ if (cachedTrgmSchema !== null) return cachedTrgmSchema;
1252
+ const rows = await live.unsafe(
1253
+ "select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
1254
+ );
1255
+ cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
1256
+ return cachedTrgmSchema;
1257
+ }
1258
+ async function vectorSchemaWithGuc(runner) {
1259
+ if (cachedVectorSchema !== null) {
1260
+ await runner.unsafe(
1261
+ "select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
1262
+ );
1263
+ return cachedVectorSchema;
1264
+ }
1265
+ const rows = await runner.unsafe(
1266
+ "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"
1267
+ );
1268
+ const name = rows?.[0]?.nspname;
1269
+ if (typeof name !== "string" || name === "") {
1270
+ throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
1271
+ }
1272
+ cachedVectorSchema = name;
1273
+ return name;
1274
+ }
1275
+ var secretReader = null;
1276
+ function setSecretReader(fn) {
1277
+ secretReader = fn;
1278
+ }
1279
+ var embedFetch = (url, init) => fetch(url, init);
1280
+ async function embedQuery(embed, text) {
1281
+ if (secretReader === null) {
1282
+ throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
1283
+ }
1284
+ const key = await secretReader(embed.apiKeyName);
1285
+ if (key === null || key === "") {
1286
+ throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
1287
+ }
1288
+ const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
1289
+ const controller = new AbortController();
1290
+ const timer = setTimeout(() => controller.abort(), 1e4);
1291
+ try {
1292
+ const res = await embedFetch(url, {
1293
+ method: "POST",
1294
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
1295
+ body: JSON.stringify({
1296
+ model: embed.model,
1297
+ input: [text],
1298
+ ...embed.dimensions !== void 0 ? { dimensions: embed.dimensions } : {}
1299
+ }),
1300
+ signal: controller.signal
1301
+ });
1302
+ if (!res.ok) {
1303
+ throw new Error(`query embed: sa\u011Flay\u0131c\u0131 ${res.status} d\xF6nd\xFC (${embed.apiKeyName} ile) \u2014 anahtar/model do\u011Fru mu?`);
1304
+ }
1305
+ const data = await res.json();
1306
+ const vec = data.data?.[0]?.embedding;
1307
+ if (!Array.isArray(vec)) {
1308
+ throw new Error("query embed: sa\u011Flay\u0131c\u0131 yan\u0131t\u0131nda data[0].embedding yok (CLAIM-N1 \u015Fekli)");
1309
+ }
1310
+ return vec;
1311
+ } finally {
1312
+ clearTimeout(timer);
1313
+ }
1314
+ }
1008
1315
  function createOps(tx) {
1009
1316
  const at = () => resolveTx(tx);
1010
1317
  const ops = {
@@ -1016,22 +1323,22 @@ function createOps(tx) {
1016
1323
  if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
1017
1324
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1018
1325
  const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
1019
- const rows = await (await at()).unsafe(sql, cols.map((c) => data[c]));
1326
+ const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
1020
1327
  const inserted = rows[0];
1021
1328
  if (!inserted) {
1022
1329
  throw new Error(
1023
1330
  `insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
1024
1331
  );
1025
1332
  }
1026
- return asWireRow(inserted);
1333
+ return asTableRow(table, inserted);
1027
1334
  },
1028
1335
  async update(table, id, data) {
1029
1336
  const cols = Object.keys(data);
1030
1337
  if (cols.length === 0) return ops.findById(table, id);
1031
1338
  const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
1032
1339
  const sql = `UPDATE ${quoteIdent(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
1033
- const rows = await (await at()).unsafe(sql, [...cols.map((c) => data[c]), id]);
1034
- return rows[0] ? asWireRow(rows[0]) : null;
1340
+ const rows = await (await at()).unsafe(sql, [...asBindParams(table, cols, data), id]);
1341
+ return rows[0] ? asTableRow(table, rows[0]) : null;
1035
1342
  },
1036
1343
  async delete(table, id) {
1037
1344
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
@@ -1041,16 +1348,400 @@ function createOps(tx) {
1041
1348
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
1042
1349
  [id]
1043
1350
  );
1044
- return rows[0] ? asWireRow(rows[0]) : null;
1351
+ return rows[0] ? asTableRow(table, rows[0]) : null;
1045
1352
  },
1046
1353
  async findMany(table, query = {}) {
1047
1354
  const cols = Object.keys(query);
1048
1355
  const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
1049
- return asWireRows(await (await at()).unsafe(
1356
+ return asTableRows(table, await (await at()).unsafe(
1050
1357
  `SELECT * FROM ${quoteIdent(table)}${where}`,
1051
1358
  cols.map((c) => query[c])
1052
1359
  ));
1053
1360
  },
1361
+ /**
1362
+ * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
1363
+ * (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
1364
+ * hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
1365
+ * RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
1366
+ * T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
1367
+ */
1368
+ async search(table, params = {}) {
1369
+ const cfg = searchConfigFor(table);
1370
+ if (!cfg) {
1371
+ throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1372
+ }
1373
+ const excl = params.__excludeIds ?? [];
1374
+ if (params.facets !== void 0) {
1375
+ for (const col of params.facets) {
1376
+ if (!cfg.colSet.has(col)) {
1377
+ throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1378
+ }
1379
+ }
1380
+ }
1381
+ if (params.validity !== void 0 && cfg.validity !== true) {
1382
+ throw new Error(
1383
+ `search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
1384
+ );
1385
+ }
1386
+ const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
1387
+ const rawLimit = params.limit ?? 20;
1388
+ if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
1389
+ throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
1390
+ }
1391
+ const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
1392
+ const pool = Math.max(limit * 3, 30);
1393
+ if (params.minScore !== void 0 && (typeof params.minScore !== "number" || !Number.isFinite(params.minScore))) {
1394
+ throw new Error(`search(${table}): minScore sonlu bir say\u0131 olmal\u0131, ${String(params.minScore)} verildi (FR-001)`);
1395
+ }
1396
+ let recencyMul = "";
1397
+ if (params.recency !== void 0) {
1398
+ const { field, halfLife } = params.recency;
1399
+ if (!cfg.colSet.has(field)) {
1400
+ throw new Error(`search(${table}): recency.field "${field}" tabloda yok (FR-004)`);
1401
+ }
1402
+ recencyMul = ` * exp(-ln(2) * extract(epoch from (now() - t.${quoteIdent(field)})) / ${parseHalfLife(halfLife)})`;
1403
+ }
1404
+ let boostMul = "";
1405
+ if (params.boost !== void 0) {
1406
+ const { field, weight } = params.boost;
1407
+ if (!cfg.colSet.has(field)) {
1408
+ throw new Error(`search(${table}): boost.field "${field}" tabloda yok (FR-030)`);
1409
+ }
1410
+ if (typeof weight !== "number" || !Number.isFinite(weight)) {
1411
+ throw new Error(`search(${table}): boost.weight sonlu bir say\u0131 olmal\u0131, ${String(weight)} verildi (FR-030)`);
1412
+ }
1413
+ const g = `greatest(t.${quoteIdent(field)},0)`;
1414
+ boostMul = ` * (1 + ${weight} * ${g}/(1+${g}))`;
1415
+ }
1416
+ const scoreMul = boostMul + recencyMul;
1417
+ if (cfg.chunk !== void 0) {
1418
+ const ck = cfg.chunk;
1419
+ const pidQ = quoteIdent(`parent_${cfg.pk}`);
1420
+ const bpr = Math.min(Math.max(1, Math.trunc(params.blocksPerRow ?? 3)), 10);
1421
+ const wantTextC = params.mode !== "vector" && ck.fts && typeof params.query === "string" && params.query !== "";
1422
+ let qvC = Array.isArray(params.vector) ? params.vector : null;
1423
+ if (qvC === null && ck.embed !== void 0 && params.mode !== "text" && typeof params.query === "string" && params.query !== "") {
1424
+ try {
1425
+ qvC = await embedQuery(ck.embed, params.query);
1426
+ } catch (e) {
1427
+ if (!wantTextC) throw e;
1428
+ qvC = null;
1429
+ }
1430
+ }
1431
+ if (!wantTextC && qvC === null) {
1432
+ throw new Error(
1433
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
1434
+ );
1435
+ }
1436
+ const bindC = [];
1437
+ const addC = (v) => {
1438
+ bindC.push(v);
1439
+ return `$${bindC.length}`;
1440
+ };
1441
+ const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1442
+ const liveC = await at();
1443
+ const K2 = 50;
1444
+ const cpool = Math.max(limit * 9, 90);
1445
+ let semC = "";
1446
+ let kwC = "";
1447
+ if (qvC !== null) {
1448
+ const sch = await vectorSchemaWithGuc(liveC);
1449
+ const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1450
+ const vp = addC(toVectorLiteral(qvC));
1451
+ 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}`;
1452
+ }
1453
+ let qpC = "";
1454
+ let qpCIdx = -1;
1455
+ if (wantTextC) {
1456
+ const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1457
+ qpC = addC(qTextC);
1458
+ qpCIdx = bindC.length - 1;
1459
+ 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}`;
1460
+ }
1461
+ const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1462
+ const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
1463
+ const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
1464
+ const assembleC = (kwIn, withKwn) => {
1465
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
1466
+ let fusedSrc;
1467
+ if (semC !== "" && kwIn !== "") {
1468
+ 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)`;
1469
+ } else if (semC !== "") {
1470
+ fusedSrc = `WITH sem AS (${semC}), fused AS (SELECT sem.pid, sem.chunk_seq, (1.0/(${K2} + sem.r))::float8 AS cscore FROM sem)`;
1471
+ } else {
1472
+ fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1473
+ }
1474
+ 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}`;
1475
+ 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}`;
1476
+ };
1477
+ let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1478
+ if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
1479
+ const tsch = await trgmSchemaOf(liveC);
1480
+ 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}`;
1481
+ const retryBindC = bindC.slice();
1482
+ if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
1483
+ rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
1484
+ }
1485
+ for (const r of rowsC) delete r._kwn;
1486
+ const outC = asTableRows(table, rowsC).map((r) => ({ ...r, blocks: r.blocks ?? [] }));
1487
+ if (params.facets !== void 0 && params.facets.length > 0) {
1488
+ Object.assign(outC, {
1489
+ _facets: await fetchFacets(liveC, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
1490
+ });
1491
+ }
1492
+ logNoResults(table, params.query, params.mode, outC.length);
1493
+ return outC;
1494
+ }
1495
+ const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
1496
+ const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
1497
+ const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
1498
+ const leg = vectorAsked ? pickLeg(table, cfg.legs, params.using) : null;
1499
+ let qv = Array.isArray(params.vector) ? params.vector : null;
1500
+ if (qv === null && leg?.embed !== void 0 && typeof params.query === "string" && params.query !== "") {
1501
+ try {
1502
+ qv = await embedQuery(leg.embed, params.query);
1503
+ } catch (e) {
1504
+ if (!wantText) throw e;
1505
+ qv = null;
1506
+ }
1507
+ }
1508
+ const wantVector = leg !== null && qv !== null;
1509
+ if (!wantText && !wantVector) {
1510
+ throw new Error(
1511
+ `search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
1512
+ );
1513
+ }
1514
+ const bind = [];
1515
+ const add = (v) => {
1516
+ bind.push(v);
1517
+ return `$${bind.length}`;
1518
+ };
1519
+ const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
1520
+ const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
1521
+ const live = await at();
1522
+ const K = 50;
1523
+ let semSql = "";
1524
+ let kwSql = "";
1525
+ if (wantVector && leg) {
1526
+ const sch = await vectorSchemaWithGuc(live);
1527
+ const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
1528
+ let exactOrder = "";
1529
+ if (userWhere !== "") {
1530
+ const probeRows = await live.unsafe(
1531
+ `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`,
1532
+ bind.slice()
1533
+ );
1534
+ const n = probeRows?.[0]?.n;
1535
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1536
+ exactOrder = " + 0.0";
1537
+ }
1538
+ }
1539
+ const vp = add(toVectorLiteral(qv));
1540
+ 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}`;
1541
+ }
1542
+ let qpRef = "";
1543
+ let qpIdx = -1;
1544
+ if (wantText) {
1545
+ const qText = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1546
+ const qp = add(qText);
1547
+ qpIdx = bind.length - 1;
1548
+ qpRef = qp;
1549
+ 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}`;
1550
+ }
1551
+ const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
1552
+ const msP = params.minScore === void 0 ? "" : add(params.minScore);
1553
+ const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
1554
+ const assemble = (kwIn, withKwn) => {
1555
+ const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
1556
+ let inner;
1557
+ let orderScore;
1558
+ if (semSql !== "" && kwIn !== "") {
1559
+ const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
1560
+ 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`;
1561
+ orderScore = scoreMul === "" ? "fused._score" : "_score";
1562
+ } else {
1563
+ const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
1564
+ const alias = semSql !== "" ? "sem" : "kw";
1565
+ const base = `(1.0/(${K} + ${alias}.r))::float8`;
1566
+ const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
1567
+ inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
1568
+ orderScore = "_score";
1569
+ }
1570
+ 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}`;
1571
+ };
1572
+ let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
1573
+ if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1574
+ const tsch = await trgmSchemaOf(live);
1575
+ const expr = ftsExprOf(cfg.ftsCols);
1576
+ 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}`;
1577
+ const retryBind = bind.slice();
1578
+ if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1579
+ rows = await live.unsafe(assemble(trgmKw, false), retryBind);
1580
+ }
1581
+ for (const r of rows) delete r._kwn;
1582
+ const out = asTableRows(table, rows).map((r) => ({ ...r, blocks: [] }));
1583
+ if (params.facets !== void 0 && params.facets.length > 0) {
1584
+ Object.assign(out, {
1585
+ _facets: await fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, validityFor)
1586
+ });
1587
+ }
1588
+ logNoResults(table, params.query, params.mode, out.length);
1589
+ return out;
1590
+ },
1591
+ /**
1592
+ * similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
1593
+ * okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
1594
+ * şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
1595
+ * kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
1596
+ * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1597
+ * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1598
+ */
1599
+ async similar(table, id, opts = {}) {
1600
+ const cfg = searchConfigFor(table);
1601
+ if (!cfg) {
1602
+ throw new Error(`similar(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1603
+ }
1604
+ const live = await at();
1605
+ let v;
1606
+ if (cfg.chunk !== void 0) {
1607
+ const sch = await vectorSchemaWithGuc(live);
1608
+ const rows = await live.unsafe(
1609
+ `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`,
1610
+ [id]
1611
+ );
1612
+ v = rows?.[0]?.v;
1613
+ } else {
1614
+ const leg = pickLeg(table, cfg.legs, opts.using);
1615
+ if (leg === null) {
1616
+ throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
1617
+ }
1618
+ const rows = await live.unsafe(
1619
+ `SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteIdent(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`,
1620
+ [id]
1621
+ );
1622
+ v = rows?.[0]?.v;
1623
+ }
1624
+ if (typeof v !== "string") {
1625
+ throw new Error(`similar(${table}): id "${String(id)}" bulunamad\u0131 ya da embedding'i yok (FR-022)`);
1626
+ }
1627
+ const p = {
1628
+ vector: JSON.parse(v),
1629
+ mode: "vector",
1630
+ where: opts.where,
1631
+ limit: opts.limit,
1632
+ using: opts.using,
1633
+ minScore: opts.minScore,
1634
+ recency: opts.recency,
1635
+ blocksPerRow: opts.blocksPerRow,
1636
+ validity: opts.validity,
1637
+ boost: opts.boost,
1638
+ __excludeIds: [id]
1639
+ };
1640
+ return ops.search(table, p);
1641
+ },
1642
+ /**
1643
+ * recommend (T018, FR-023): positive/negative id kümelerinden öneri.
1644
+ * Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
1645
+ * negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
1646
+ * ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
1647
+ * Vektör matematiği istemciye inmez; bulunamayan positive/negative
1648
+ * adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
1649
+ */
1650
+ async recommend(table, opts) {
1651
+ const positive = opts?.positive;
1652
+ if (!Array.isArray(positive) || positive.length === 0) {
1653
+ throw new Error(`recommend(${table}): positive bo\u015F olamaz \u2014 en az bir kaynak id gerekir (FR-023)`);
1654
+ }
1655
+ const negative = Array.isArray(opts.negative) ? opts.negative : [];
1656
+ const cfg = searchConfigFor(table);
1657
+ if (!cfg) {
1658
+ throw new Error(`recommend(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
1659
+ }
1660
+ let leg = null;
1661
+ if (cfg.chunk === void 0) {
1662
+ leg = pickLeg(table, cfg.legs, opts.using);
1663
+ if (leg === null) {
1664
+ throw new Error(`recommend(${table}): tabloda vekt\xF6r kolonu yok \u2014 \xF6neri semantik vekt\xF6r ister (FR-023)`);
1665
+ }
1666
+ }
1667
+ const live = await at();
1668
+ const sch = await vectorSchemaWithGuc(live);
1669
+ const bind = [];
1670
+ const add = (x) => {
1671
+ bind.push(x);
1672
+ return `$${bind.length}`;
1673
+ };
1674
+ const avgSel = (ids) => {
1675
+ const inList = ids.map((x) => add(x)).join(", ");
1676
+ 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`;
1677
+ };
1678
+ 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`;
1679
+ const rows = await live.unsafe(targetSql, bind);
1680
+ const r0 = rows?.[0];
1681
+ if (r0 === void 0 || r0.pos_missing === true) {
1682
+ throw new Error(`recommend(${table}): positive id'lerin hi\xE7biri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1683
+ }
1684
+ if (negative.length > 0 && r0.neg_missing === true) {
1685
+ throw new Error(`recommend(${table}): negative id'leri bulunamad\u0131 ya da embedding'i yok (FR-023)`);
1686
+ }
1687
+ if (typeof r0.v !== "string") {
1688
+ throw new Error(`recommend(${table}): hedef vekt\xF6r hesaplanamad\u0131 (FR-023)`);
1689
+ }
1690
+ const p = {
1691
+ vector: JSON.parse(r0.v),
1692
+ mode: "vector",
1693
+ where: opts.where,
1694
+ limit: opts.limit,
1695
+ using: opts.using,
1696
+ minScore: opts.minScore,
1697
+ recency: opts.recency,
1698
+ blocksPerRow: opts.blocksPerRow,
1699
+ validity: opts.validity,
1700
+ boost: opts.boost,
1701
+ __excludeIds: [...positive, ...negative]
1702
+ };
1703
+ return ops.search(table, p);
1704
+ },
1705
+ /**
1706
+ * supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
1707
+ * versiyonunu TEK savepoint'te yazar — eski satır kapatılır
1708
+ * (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
1709
+ * yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
1710
+ * randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
1711
+ * kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
1712
+ * ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
1713
+ * kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
1714
+ */
1715
+ async supersede(table, id, row) {
1716
+ const cfg = searchConfigFor(table);
1717
+ if (cfg === null || cfg.validity !== true) {
1718
+ throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
1719
+ }
1720
+ const live = await at();
1721
+ return live.savepoint(async (sp) => {
1722
+ const newId = row[cfg.pk] ?? crypto.randomUUID();
1723
+ const closed = await sp.unsafe(
1724
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`,
1725
+ [newId, id]
1726
+ );
1727
+ if (closed.length === 0) {
1728
+ throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
1729
+ }
1730
+ const data = { ...row, [cfg.pk]: newId };
1731
+ const cols = Object.keys(data);
1732
+ const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1733
+ const rows = await sp.unsafe(
1734
+ `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
1735
+ asBindParams(table, cols, data)
1736
+ );
1737
+ if (!rows[0]) {
1738
+ throw new Error(
1739
+ `supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
1740
+ );
1741
+ }
1742
+ return asTableRow(table, rows[0]);
1743
+ });
1744
+ },
1054
1745
  /** A real SAVEPOINT inside the request's transaction. */
1055
1746
  async transaction(cb) {
1056
1747
  const live = await at();
@@ -1079,7 +1770,7 @@ function createOps(tx) {
1079
1770
  return live.savepoint(async (sp) => {
1080
1771
  const results = [];
1081
1772
  for (const op of plan.ops) {
1082
- const rows = await runPlanOp(sp, op, results);
1773
+ const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
1083
1774
  const result = { rows, rows_affected: rows.length };
1084
1775
  results.push(result);
1085
1776
  assertGuard(op, result);
@@ -1092,6 +1783,8 @@ function createOps(tx) {
1092
1783
  }
1093
1784
  var currentSchema = {};
1094
1785
  function setSchema(schema) {
1786
+ cachedVectorSchema = null;
1787
+ cachedTrgmSchema = null;
1095
1788
  const s = schema;
1096
1789
  currentSchema = (s && "default" in s ? s.default : s) ?? {};
1097
1790
  }
@@ -1178,7 +1871,7 @@ function isRef(v) {
1178
1871
  function isExpr(v) {
1179
1872
  return typeof v === "object" && v !== null && "$expr" in v;
1180
1873
  }
1181
- function renderValue(value, column, args, results) {
1874
+ function renderValue(value, column, args, results, vectorCols) {
1182
1875
  if (isRef(value)) {
1183
1876
  const source = results[value.$ref.op];
1184
1877
  const row = source?.rows[0];
@@ -1187,15 +1880,15 @@ function renderValue(value, column, args, results) {
1187
1880
  error_code: "tx_ref_unresolved"
1188
1881
  });
1189
1882
  }
1190
- return args.bind(row[value.$ref.field]);
1883
+ return bindMaybeVector(args, column, vectorCols, row[value.$ref.field]);
1191
1884
  }
1192
1885
  if (isExpr(value)) {
1193
1886
  const fn = value.$expr;
1194
1887
  if (fn.fn === "now") return "now()";
1195
1888
  const operator = fn.fn === "inc" ? "+" : "-";
1196
- return `${quoteIdent(column)} ${operator} ${args.bind(fn.by)}`;
1889
+ return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
1197
1890
  }
1198
- return args.bind(value);
1891
+ return bindMaybeVector(args, column, vectorCols, value);
1199
1892
  }
1200
1893
  function renderWhere(where, args, results) {
1201
1894
  const cols = Object.keys(where ?? {});
@@ -1207,14 +1900,21 @@ function renderWhere(where, args, results) {
1207
1900
  });
1208
1901
  return ` WHERE ${terms.join(" AND ")}`;
1209
1902
  }
1903
+ function bindMaybeVector(args, column, vectorCols, value) {
1904
+ if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
1905
+ return args.bind(toVectorLiteral(value));
1906
+ }
1907
+ return args.bind(value);
1908
+ }
1210
1909
  async function runPlanOp(sp, op, results) {
1910
+ const opVectorCols = vectorColumnsOf(currentSchema, op.table);
1211
1911
  const args = new Args();
1212
1912
  const table = quoteIdent(op.table);
1213
1913
  let sql;
1214
1914
  switch (op.op) {
1215
1915
  case "insert": {
1216
1916
  const cols = Object.keys(op.values ?? {});
1217
- const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
1917
+ const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols));
1218
1918
  sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
1219
1919
  break;
1220
1920
  }
@@ -1223,7 +1923,7 @@ async function runPlanOp(sp, op, results) {
1223
1923
  if (rows.length === 0 || !rows[0]) return [];
1224
1924
  const cols = Object.keys(rows[0]);
1225
1925
  const tuples = rows.map(
1226
- (r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
1926
+ (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
1227
1927
  );
1228
1928
  sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
1229
1929
  break;
@@ -1232,7 +1932,7 @@ async function runPlanOp(sp, op, results) {
1232
1932
  const cols = Object.keys(op.set ?? {});
1233
1933
  if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
1234
1934
  const assignments = cols.map(
1235
- (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
1935
+ (c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols)}`
1236
1936
  );
1237
1937
  sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
1238
1938
  break;
@@ -1379,7 +2079,8 @@ function grantFor(entry, ctx, bucketLimits) {
1379
2079
  filename: ctx.filename
1380
2080
  }),
1381
2081
  maxBytes: bucketLimits?.maxBytes ?? null,
1382
- mimeTypes: bucketLimits?.mimeTypes ?? null
2082
+ mimeTypes: bucketLimits?.mimeTypes ?? null,
2083
+ ownerUid: ctx.userId ?? null
1383
2084
  };
1384
2085
  }
1385
2086
  var CompletionLedger = class {
@@ -1476,6 +2177,23 @@ function installEgressFence(policy) {
1476
2177
 
1477
2178
  // src/engine/index.ts
1478
2179
  var JSON_HEADERS = { "content-type": "application/json" };
2180
+ function fieldErrors(err) {
2181
+ return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
2182
+ }
2183
+ function headersFor(raw, schema) {
2184
+ const shape = schema.shape;
2185
+ if (typeof shape !== "object" || shape === null) return raw;
2186
+ let aliased = null;
2187
+ for (const declared of Object.keys(shape)) {
2188
+ const lower = declared.toLowerCase();
2189
+ if (lower === declared) continue;
2190
+ const value = raw[lower];
2191
+ if (value === void 0) continue;
2192
+ aliased ??= { ...raw };
2193
+ aliased[declared] = value;
2194
+ }
2195
+ return aliased ?? raw;
2196
+ }
1479
2197
  function envelope(error, description, status, requestId, extra) {
1480
2198
  return new Response(
1481
2199
  JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
@@ -1509,6 +2227,7 @@ async function defaultSqlDriver(config) {
1509
2227
  async function createApp(opts) {
1510
2228
  const { config, controllers } = opts;
1511
2229
  setSchema(opts.schema ?? {});
2230
+ setSecretReader(opts.secretReader ?? null);
1512
2231
  const routes = buildRouteTable(controllers);
1513
2232
  if (routes.length === 0) {
1514
2233
  throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
@@ -1534,7 +2253,6 @@ async function createApp(opts) {
1534
2253
  Notifications: modules.Notifications ?? stubModule("Notifications"),
1535
2254
  Flags: modules.Flags ?? stubModule("Flags"),
1536
2255
  Realtime: modules.Realtime ?? stubModule("Realtime"),
1537
- Purchases: modules.Purchases ?? stubModule("Purchases"),
1538
2256
  // Named, never undefined. A backend started without a secrets client
1539
2257
  // that returned `undefined` here would fail inside the handler as
1540
2258
  // "Cannot read properties of undefined", which says nothing about what
@@ -1621,7 +2339,8 @@ async function createApp(opts) {
1621
2339
  error: "too_many_requests",
1622
2340
  error_description: "Rate limit exceeded for this endpoint",
1623
2341
  status: 429,
1624
- request_id: requestId
2342
+ request_id: requestId,
2343
+ data: { retryAfter }
1625
2344
  }),
1626
2345
  { status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
1627
2346
  );
@@ -1640,7 +2359,7 @@ async function createApp(opts) {
1640
2359
  const r = p.schema.safeParse(parsedBody);
1641
2360
  if (!r.success) {
1642
2361
  return envelope("bad_request", "Request body failed validation", 400, requestId, {
1643
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
2362
+ data: { fields: fieldErrors(r.error) }
1644
2363
  });
1645
2364
  }
1646
2365
  args[p.index] = r.data;
@@ -1650,7 +2369,7 @@ async function createApp(opts) {
1650
2369
  const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
1651
2370
  if (!r.success) {
1652
2371
  return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
1653
- fields: r.error.issues.map((i) => ({ field: i.path.join("."), message: i.message }))
2372
+ data: { fields: fieldErrors(r.error) }
1654
2373
  });
1655
2374
  }
1656
2375
  args[p.index] = r.data;
@@ -1659,9 +2378,21 @@ async function createApp(opts) {
1659
2378
  case "param":
1660
2379
  args[p.index] = hit.params[p.name];
1661
2380
  break;
1662
- case "headers":
1663
- args[p.index] = Object.fromEntries(req.headers);
2381
+ case "headers": {
2382
+ const raw = Object.fromEntries(req.headers);
2383
+ if (!p.schema) {
2384
+ args[p.index] = raw;
2385
+ break;
2386
+ }
2387
+ const r = p.schema.safeParse(headersFor(raw, p.schema));
2388
+ if (!r.success) {
2389
+ return envelope("bad_request", "Request headers failed validation", 400, requestId, {
2390
+ data: { fields: fieldErrors(r.error) }
2391
+ });
2392
+ }
2393
+ args[p.index] = r.data;
1664
2394
  break;
2395
+ }
1665
2396
  case "user":
1666
2397
  case "optionalUser":
1667
2398
  args[p.index] = claims ? {
@@ -1710,6 +2441,14 @@ async function createApp(opts) {
1710
2441
  case "traceId":
1711
2442
  args[p.index] = requestId;
1712
2443
  break;
2444
+ case "client":
2445
+ args[p.index] = {
2446
+ sdkVersion: req.headers.get("x-palbase-sdk-version"),
2447
+ appVersion: req.headers.get("x-palbase-client-version"),
2448
+ platform: req.headers.get("x-platform"),
2449
+ osVersion: req.headers.get("x-os-version")
2450
+ };
2451
+ break;
1713
2452
  case "req":
1714
2453
  args[p.index] = req;
1715
2454
  break;