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