@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.
- package/dist/bin/palbase-backend.cjs +764 -25
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -4
- package/dist/{chunk-POYAFBLF.js → chunk-FSGSB42K.js} +13 -6
- package/dist/chunk-FSGSB42K.js.map +1 -0
- package/dist/{chunk-SSGAMC26.js → chunk-HAF67F2H.js} +114 -5
- package/dist/chunk-HAF67F2H.js.map +1 -0
- package/dist/{chunk-QMVK4X3V.js → chunk-OMRTHM4X.js} +98 -98
- package/dist/chunk-OMRTHM4X.js.map +1 -0
- package/dist/{chunk-QYOHMVUW.js → chunk-REZU6UKT.js} +755 -24
- package/dist/chunk-REZU6UKT.js.map +1 -0
- package/dist/{chunk-YL4C5NRY.js → chunk-Y5HXVUMP.js} +2 -2
- package/dist/{chunk-N32VDWKH.js → chunk-ZC6Q2BRD.js} +4 -59
- package/dist/chunk-ZC6Q2BRD.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +3 -21
- package/dist/db/env.d.ts +3 -21
- package/dist/db/index.cjs +117 -6
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +3 -2
- package/dist/db/index.d.ts +3 -2
- package/dist/db/index.js +7 -3
- package/dist/{endpoint-B0LpZixz.d.cts → endpoint-BavvbW4P.d.ts} +116 -14
- package/dist/{endpoint-B0LpZixz.d.ts → endpoint-i8TTCohk.d.cts} +116 -14
- package/dist/engine/index.cjs +764 -25
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -5
- package/dist/engine/index.d.ts +6 -5
- package/dist/engine/index.js +3 -3
- package/dist/{index-g-EzitI-.d.ts → index-B3jmmItD.d.ts} +203 -7
- package/dist/{index-BCNtlG1w.d.ts → index-B7YBEG5w.d.ts} +117 -93
- package/dist/{index-BGSCWlUa.d.cts → index-Bmvx1EvJ.d.cts} +203 -7
- package/dist/{index-B4W6d2VJ.d.cts → index-E7OscPJT.d.cts} +117 -93
- package/dist/index.cjs +509 -1085
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -1133
- package/dist/index.d.ts +203 -1133
- package/dist/index.js +288 -900
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +3 -57
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -2
- package/dist/openapi/index.d.ts +3 -2
- package/dist/openapi/index.js +5 -22
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-3BLYv4si.d.ts → registry-C3H2uPeZ.d.cts} +13 -169
- package/dist/{registry-Cw0YEYCg.d.cts → registry-DY3d9l1k.d.ts} +13 -169
- package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
- package/dist/stack.cjs.map +1 -0
- package/dist/stack.d.cts +76 -0
- package/dist/stack.d.ts +76 -0
- package/dist/stack.js +1 -0
- package/docs/README.md +7 -6
- package/docs/endpoints.md +1 -1
- package/docs/errors.md +9 -0
- package/docs/llms-full.txt +17 -261
- package/docs/llms.txt +0 -2
- package/package.json +7 -7
- package/template/package.json +1 -1
- package/dist/chunk-N32VDWKH.js.map +0 -1
- package/dist/chunk-POYAFBLF.js.map +0 -1
- package/dist/chunk-QMVK4X3V.js.map +0 -1
- package/dist/chunk-QYOHMVUW.js.map +0 -1
- package/dist/chunk-SSGAMC26.js.map +0 -1
- package/dist/purchases/keys.cjs.map +0 -1
- package/dist/purchases/keys.d.cts +0 -42
- package/dist/purchases/keys.d.ts +0 -42
- package/dist/purchases/keys.js +0 -1
- package/docs/config.md +0 -147
- package/docs/resources.md +0 -97
- package/template/config/secrets.ts +0 -24
- /package/dist/{chunk-YL4C5NRY.js.map → chunk-Y5HXVUMP.js.map} +0 -0
- /package/dist/{purchases/keys.js.map → stack.js.map} +0 -0
package/dist/engine/index.cjs
CHANGED
|
@@ -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(() =>
|
|
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,
|
|
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
|
|
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, [...
|
|
1034
|
-
return rows[0] ?
|
|
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] ?
|
|
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
|
|
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
|
|
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
|
|
1889
|
+
return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
|
|
1197
1890
|
}
|
|
1198
|
-
return args
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|