@palbase/backend 22.0.1 → 22.1.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 +340 -22
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +2 -2
- package/dist/{chunk-QYOHMVUW.js → chunk-74XDEF5J.js} +338 -22
- package/dist/chunk-74XDEF5J.js.map +1 -0
- package/dist/{chunk-SSGAMC26.js → chunk-I3ON7MYF.js} +57 -5
- package/dist/chunk-I3ON7MYF.js.map +1 -0
- package/dist/{chunk-POYAFBLF.js → chunk-SQC5EIWY.js} +5 -3
- package/dist/chunk-SQC5EIWY.js.map +1 -0
- package/dist/db/index.cjs +60 -6
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +7 -3
- package/dist/{endpoint-B0LpZixz.d.cts → endpoint-BVT6jcVW.d.cts} +39 -7
- package/dist/{endpoint-B0LpZixz.d.ts → endpoint-BVT6jcVW.d.ts} +39 -7
- package/dist/engine/index.cjs +340 -22
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +2 -2
- package/dist/{index-B4W6d2VJ.d.cts → index-BS1gW4nV.d.cts} +24 -25
- package/dist/{index-BGSCWlUa.d.cts → index-BqCiHao8.d.cts} +97 -7
- package/dist/{index-BCNtlG1w.d.ts → index-CCZqzych.d.ts} +24 -25
- package/dist/{index-g-EzitI-.d.ts → index-vwHoS0l2.d.ts} +97 -7
- package/dist/index.cjs +244 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -9
- package/dist/index.d.ts +124 -9
- package/dist/index.js +186 -3
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.d.cts +2 -2
- package/dist/openapi/index.d.ts +2 -2
- package/dist/{registry-Cw0YEYCg.d.cts → registry-BWttGlaT.d.cts} +1 -1
- package/dist/{registry-3BLYv4si.d.ts → registry-Bsuf-orT.d.ts} +1 -1
- package/docs/endpoints.md +1 -1
- package/docs/errors.md +9 -0
- package/docs/llms-full.txt +10 -1
- package/package.json +2 -2
- package/dist/chunk-POYAFBLF.js.map +0 -1
- package/dist/chunk-QYOHMVUW.js.map +0 -1
- package/dist/chunk-SSGAMC26.js.map +0 -1
package/dist/engine/index.cjs
CHANGED
|
@@ -459,7 +459,8 @@ 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)
|
|
463
464
|
};
|
|
464
465
|
}
|
|
465
466
|
}
|
|
@@ -474,7 +475,8 @@ function makeTypedSurface(raw) {
|
|
|
474
475
|
update: (table, id, data) => raw.update(table, id, data),
|
|
475
476
|
delete: (table, id) => raw.delete(table, id),
|
|
476
477
|
findById: (table, id) => raw.findById(table, id),
|
|
477
|
-
findMany: (table, query) => raw.findMany(table, query)
|
|
478
|
+
findMany: (table, query) => raw.findMany(table, query),
|
|
479
|
+
search: (table, params) => raw.search(table, params)
|
|
478
480
|
};
|
|
479
481
|
return Object.assign(ops, {
|
|
480
482
|
tables: makeTablesAccessor(() => raw),
|
|
@@ -1005,6 +1007,194 @@ function asWireRow(row) {
|
|
|
1005
1007
|
function asWireRows(rows) {
|
|
1006
1008
|
return rows.map((row) => asWireRow(row));
|
|
1007
1009
|
}
|
|
1010
|
+
function toVectorLiteral(v) {
|
|
1011
|
+
return `[${v.join(",")}]`;
|
|
1012
|
+
}
|
|
1013
|
+
function vectorColumnsOf(schema, table) {
|
|
1014
|
+
const out = /* @__PURE__ */ new Set();
|
|
1015
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
1016
|
+
if ((def.name ?? key) !== table) continue;
|
|
1017
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
1018
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
1019
|
+
if (d !== null && typeof d === "object" && d.type === "vector") out.add(col);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
function reviveVectors(row, vectorCols) {
|
|
1025
|
+
if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
|
|
1026
|
+
const out = row;
|
|
1027
|
+
for (const col of vectorCols) {
|
|
1028
|
+
const v = out[col];
|
|
1029
|
+
if (typeof v === "string") out[col] = JSON.parse(v);
|
|
1030
|
+
}
|
|
1031
|
+
return row;
|
|
1032
|
+
}
|
|
1033
|
+
function asTableRow(table, row) {
|
|
1034
|
+
return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1035
|
+
}
|
|
1036
|
+
function asTableRows(table, rows) {
|
|
1037
|
+
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1038
|
+
return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
|
|
1039
|
+
}
|
|
1040
|
+
function asBindParams(table, cols, data) {
|
|
1041
|
+
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1042
|
+
return cols.map((c) => {
|
|
1043
|
+
const v = data[c];
|
|
1044
|
+
return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
1048
|
+
var METRIC_OPERATOR = {
|
|
1049
|
+
cosine: "<=>",
|
|
1050
|
+
euclidean: "<->",
|
|
1051
|
+
inner_product: "<#>"
|
|
1052
|
+
};
|
|
1053
|
+
function searchConfigFor(table) {
|
|
1054
|
+
const t = currentSchema.tables?.[table];
|
|
1055
|
+
if (!t) return null;
|
|
1056
|
+
const columns = t.columns ?? {};
|
|
1057
|
+
const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
1058
|
+
const cols = Object.keys(columns);
|
|
1059
|
+
const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
|
|
1060
|
+
const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
1061
|
+
const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
|
|
1062
|
+
const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
|
|
1063
|
+
if (pk === null) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const search = t.search;
|
|
1069
|
+
const ftsCols = search?.text ?? [];
|
|
1070
|
+
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
|
|
1071
|
+
let legs;
|
|
1072
|
+
if (rawLegs.length > 0) {
|
|
1073
|
+
legs = rawLegs.map((leg) => {
|
|
1074
|
+
const l = leg;
|
|
1075
|
+
const column = l.column ?? (vectorCols.length === 1 ? vectorCols[0] : void 0);
|
|
1076
|
+
if (column === void 0) {
|
|
1077
|
+
throw new Error(`search(${table}): birden \xE7ok vector kolonu var \u2014 beyanda 'column' zorunlu (FR-010)`);
|
|
1078
|
+
}
|
|
1079
|
+
const model = l.model;
|
|
1080
|
+
return {
|
|
1081
|
+
column,
|
|
1082
|
+
metric: l.metric ?? "cosine",
|
|
1083
|
+
...model !== void 0 ? { embed: {
|
|
1084
|
+
model: model.model,
|
|
1085
|
+
apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
|
|
1086
|
+
...model.baseURL !== void 0 ? { baseURL: model.baseURL } : {},
|
|
1087
|
+
...model.dimensions !== void 0 ? { dimensions: model.dimensions } : {}
|
|
1088
|
+
} } : {}
|
|
1089
|
+
};
|
|
1090
|
+
});
|
|
1091
|
+
} else {
|
|
1092
|
+
legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
|
|
1093
|
+
}
|
|
1094
|
+
if (ftsCols.length === 0 && legs.length === 0) return null;
|
|
1095
|
+
return { pk, cols, colSet: new Set(cols), ftsCols, legs };
|
|
1096
|
+
}
|
|
1097
|
+
function pickLeg(table, legs, using) {
|
|
1098
|
+
if (legs.length === 0) return null;
|
|
1099
|
+
if (using !== void 0) {
|
|
1100
|
+
const hit = legs.find((l) => l.column === using);
|
|
1101
|
+
if (!hit) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return hit;
|
|
1107
|
+
}
|
|
1108
|
+
if (legs.length === 1) return legs[0];
|
|
1109
|
+
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`);
|
|
1110
|
+
}
|
|
1111
|
+
var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
|
|
1112
|
+
function compileWhere(table, colSet, where, add) {
|
|
1113
|
+
const parts = [];
|
|
1114
|
+
for (const [col, cond] of Object.entries(where)) {
|
|
1115
|
+
if (!colSet.has(col)) {
|
|
1116
|
+
throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1117
|
+
}
|
|
1118
|
+
const q = `t.${quoteIdent(col)}`;
|
|
1119
|
+
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1120
|
+
for (const [op, v] of Object.entries(cond)) {
|
|
1121
|
+
if (op === "in") {
|
|
1122
|
+
if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
1123
|
+
if (v.length === 0) {
|
|
1124
|
+
parts.push("false");
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
|
|
1128
|
+
} else if (op in WHERE_OPS) {
|
|
1129
|
+
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
1130
|
+
} else {
|
|
1131
|
+
throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
} else {
|
|
1135
|
+
parts.push(`${q} = ${add(cond)}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1139
|
+
}
|
|
1140
|
+
var cachedVectorSchema = null;
|
|
1141
|
+
async function vectorSchemaWithGuc(runner) {
|
|
1142
|
+
if (cachedVectorSchema !== null) {
|
|
1143
|
+
await runner.unsafe(
|
|
1144
|
+
"select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
|
|
1145
|
+
);
|
|
1146
|
+
return cachedVectorSchema;
|
|
1147
|
+
}
|
|
1148
|
+
const rows = await runner.unsafe(
|
|
1149
|
+
"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"
|
|
1150
|
+
);
|
|
1151
|
+
const name = rows?.[0]?.nspname;
|
|
1152
|
+
if (typeof name !== "string" || name === "") {
|
|
1153
|
+
throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
|
|
1154
|
+
}
|
|
1155
|
+
cachedVectorSchema = name;
|
|
1156
|
+
return name;
|
|
1157
|
+
}
|
|
1158
|
+
var secretReader = null;
|
|
1159
|
+
function setSecretReader(fn) {
|
|
1160
|
+
secretReader = fn;
|
|
1161
|
+
}
|
|
1162
|
+
var embedFetch = (url, init) => fetch(url, init);
|
|
1163
|
+
async function embedQuery(embed, text) {
|
|
1164
|
+
if (secretReader === null) {
|
|
1165
|
+
throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
|
|
1166
|
+
}
|
|
1167
|
+
const key = await secretReader(embed.apiKeyName);
|
|
1168
|
+
if (key === null || key === "") {
|
|
1169
|
+
throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
|
|
1170
|
+
}
|
|
1171
|
+
const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
|
|
1172
|
+
const controller = new AbortController();
|
|
1173
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1174
|
+
try {
|
|
1175
|
+
const res = await embedFetch(url, {
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
1178
|
+
body: JSON.stringify({
|
|
1179
|
+
model: embed.model,
|
|
1180
|
+
input: [text],
|
|
1181
|
+
...embed.dimensions !== void 0 ? { dimensions: embed.dimensions } : {}
|
|
1182
|
+
}),
|
|
1183
|
+
signal: controller.signal
|
|
1184
|
+
});
|
|
1185
|
+
if (!res.ok) {
|
|
1186
|
+
throw new Error(`query embed: sa\u011Flay\u0131c\u0131 ${res.status} d\xF6nd\xFC (${embed.apiKeyName} ile) \u2014 anahtar/model do\u011Fru mu?`);
|
|
1187
|
+
}
|
|
1188
|
+
const data = await res.json();
|
|
1189
|
+
const vec = data.data?.[0]?.embedding;
|
|
1190
|
+
if (!Array.isArray(vec)) {
|
|
1191
|
+
throw new Error("query embed: sa\u011Flay\u0131c\u0131 yan\u0131t\u0131nda data[0].embedding yok (CLAIM-N1 \u015Fekli)");
|
|
1192
|
+
}
|
|
1193
|
+
return vec;
|
|
1194
|
+
} finally {
|
|
1195
|
+
clearTimeout(timer);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1008
1198
|
function createOps(tx) {
|
|
1009
1199
|
const at = () => resolveTx(tx);
|
|
1010
1200
|
const ops = {
|
|
@@ -1016,22 +1206,22 @@ function createOps(tx) {
|
|
|
1016
1206
|
if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
|
|
1017
1207
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1018
1208
|
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
|
|
1019
|
-
const rows = await (await at()).unsafe(sql,
|
|
1209
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
1020
1210
|
const inserted = rows[0];
|
|
1021
1211
|
if (!inserted) {
|
|
1022
1212
|
throw new Error(
|
|
1023
1213
|
`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
1024
1214
|
);
|
|
1025
1215
|
}
|
|
1026
|
-
return
|
|
1216
|
+
return asTableRow(table, inserted);
|
|
1027
1217
|
},
|
|
1028
1218
|
async update(table, id, data) {
|
|
1029
1219
|
const cols = Object.keys(data);
|
|
1030
1220
|
if (cols.length === 0) return ops.findById(table, id);
|
|
1031
1221
|
const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
|
|
1032
1222
|
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] ?
|
|
1223
|
+
const rows = await (await at()).unsafe(sql, [...asBindParams(table, cols, data), id]);
|
|
1224
|
+
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1035
1225
|
},
|
|
1036
1226
|
async delete(table, id) {
|
|
1037
1227
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
@@ -1041,16 +1231,96 @@ function createOps(tx) {
|
|
|
1041
1231
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
1042
1232
|
[id]
|
|
1043
1233
|
);
|
|
1044
|
-
return rows[0] ?
|
|
1234
|
+
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1045
1235
|
},
|
|
1046
1236
|
async findMany(table, query = {}) {
|
|
1047
1237
|
const cols = Object.keys(query);
|
|
1048
1238
|
const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
|
|
1049
|
-
return
|
|
1239
|
+
return asTableRows(table, await (await at()).unsafe(
|
|
1050
1240
|
`SELECT * FROM ${quoteIdent(table)}${where}`,
|
|
1051
1241
|
cols.map((c) => query[c])
|
|
1052
1242
|
));
|
|
1053
1243
|
},
|
|
1244
|
+
/**
|
|
1245
|
+
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
1246
|
+
* (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
|
|
1247
|
+
* hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
|
|
1248
|
+
* RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
|
|
1249
|
+
* T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
|
|
1250
|
+
*/
|
|
1251
|
+
async search(table, params = {}) {
|
|
1252
|
+
const cfg = searchConfigFor(table);
|
|
1253
|
+
if (!cfg) {
|
|
1254
|
+
throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
1255
|
+
}
|
|
1256
|
+
const rawLimit = params.limit ?? 20;
|
|
1257
|
+
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
1258
|
+
throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
|
|
1259
|
+
}
|
|
1260
|
+
const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
|
|
1261
|
+
const pool = Math.max(limit * 3, 30);
|
|
1262
|
+
const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
|
|
1263
|
+
const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
|
|
1264
|
+
const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
|
|
1265
|
+
const leg = vectorAsked ? pickLeg(table, cfg.legs, params.using) : null;
|
|
1266
|
+
let qv = Array.isArray(params.vector) ? params.vector : null;
|
|
1267
|
+
if (qv === null && leg?.embed !== void 0 && typeof params.query === "string" && params.query !== "") {
|
|
1268
|
+
try {
|
|
1269
|
+
qv = await embedQuery(leg.embed, params.query);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
if (!wantText) throw e;
|
|
1272
|
+
qv = null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const wantVector = leg !== null && qv !== null;
|
|
1276
|
+
if (!wantText && !wantVector) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const bind = [];
|
|
1282
|
+
const add = (v) => {
|
|
1283
|
+
bind.push(v);
|
|
1284
|
+
return `$${bind.length}`;
|
|
1285
|
+
};
|
|
1286
|
+
const whereSql = compileWhere(table, cfg.colSet, params.where ?? {}, add);
|
|
1287
|
+
const live = await at();
|
|
1288
|
+
const K = 50;
|
|
1289
|
+
let semSql = "";
|
|
1290
|
+
let kwSql = "";
|
|
1291
|
+
if (wantVector && leg) {
|
|
1292
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
1293
|
+
const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
|
|
1294
|
+
let exactOrder = "";
|
|
1295
|
+
if (whereSql !== "") {
|
|
1296
|
+
const probeRows = await live.unsafe(
|
|
1297
|
+
`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`,
|
|
1298
|
+
bind.slice()
|
|
1299
|
+
);
|
|
1300
|
+
const n = probeRows?.[0]?.n;
|
|
1301
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1302
|
+
exactOrder = " + 0.0";
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const vp = add(toVectorLiteral(qv));
|
|
1306
|
+
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}`;
|
|
1307
|
+
}
|
|
1308
|
+
if (wantText) {
|
|
1309
|
+
const qp = add(params.query);
|
|
1310
|
+
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}`;
|
|
1311
|
+
}
|
|
1312
|
+
const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1313
|
+
let sql;
|
|
1314
|
+
if (semSql !== "" && kwSql !== "") {
|
|
1315
|
+
sql = `WITH sem AS (${semSql}), kw AS (${kwSql}), 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}, fused._score AS _score FROM fused JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id ORDER BY fused._score DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1316
|
+
} else {
|
|
1317
|
+
const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwSql})`;
|
|
1318
|
+
const alias = semSql !== "" ? "sem" : "kw";
|
|
1319
|
+
sql = `WITH ${single} SELECT ${colList}, (1.0/(${K} + ${alias}.r))::float8 AS _score FROM ${alias} JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id ORDER BY _score DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1320
|
+
}
|
|
1321
|
+
const rows = await live.unsafe(sql, bind);
|
|
1322
|
+
return asTableRows(table, rows);
|
|
1323
|
+
},
|
|
1054
1324
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
1055
1325
|
async transaction(cb) {
|
|
1056
1326
|
const live = await at();
|
|
@@ -1079,7 +1349,7 @@ function createOps(tx) {
|
|
|
1079
1349
|
return live.savepoint(async (sp) => {
|
|
1080
1350
|
const results = [];
|
|
1081
1351
|
for (const op of plan.ops) {
|
|
1082
|
-
const rows = await runPlanOp(sp, op, results);
|
|
1352
|
+
const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
|
|
1083
1353
|
const result = { rows, rows_affected: rows.length };
|
|
1084
1354
|
results.push(result);
|
|
1085
1355
|
assertGuard(op, result);
|
|
@@ -1092,6 +1362,7 @@ function createOps(tx) {
|
|
|
1092
1362
|
}
|
|
1093
1363
|
var currentSchema = {};
|
|
1094
1364
|
function setSchema(schema) {
|
|
1365
|
+
cachedVectorSchema = null;
|
|
1095
1366
|
const s = schema;
|
|
1096
1367
|
currentSchema = (s && "default" in s ? s.default : s) ?? {};
|
|
1097
1368
|
}
|
|
@@ -1178,7 +1449,7 @@ function isRef(v) {
|
|
|
1178
1449
|
function isExpr(v) {
|
|
1179
1450
|
return typeof v === "object" && v !== null && "$expr" in v;
|
|
1180
1451
|
}
|
|
1181
|
-
function renderValue(value, column, args, results) {
|
|
1452
|
+
function renderValue(value, column, args, results, vectorCols) {
|
|
1182
1453
|
if (isRef(value)) {
|
|
1183
1454
|
const source = results[value.$ref.op];
|
|
1184
1455
|
const row = source?.rows[0];
|
|
@@ -1187,15 +1458,15 @@ function renderValue(value, column, args, results) {
|
|
|
1187
1458
|
error_code: "tx_ref_unresolved"
|
|
1188
1459
|
});
|
|
1189
1460
|
}
|
|
1190
|
-
return args
|
|
1461
|
+
return bindMaybeVector(args, column, vectorCols, row[value.$ref.field]);
|
|
1191
1462
|
}
|
|
1192
1463
|
if (isExpr(value)) {
|
|
1193
1464
|
const fn = value.$expr;
|
|
1194
1465
|
if (fn.fn === "now") return "now()";
|
|
1195
1466
|
const operator = fn.fn === "inc" ? "+" : "-";
|
|
1196
|
-
return `${quoteIdent(column)} ${operator} ${args
|
|
1467
|
+
return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
|
|
1197
1468
|
}
|
|
1198
|
-
return args
|
|
1469
|
+
return bindMaybeVector(args, column, vectorCols, value);
|
|
1199
1470
|
}
|
|
1200
1471
|
function renderWhere(where, args, results) {
|
|
1201
1472
|
const cols = Object.keys(where ?? {});
|
|
@@ -1207,14 +1478,21 @@ function renderWhere(where, args, results) {
|
|
|
1207
1478
|
});
|
|
1208
1479
|
return ` WHERE ${terms.join(" AND ")}`;
|
|
1209
1480
|
}
|
|
1481
|
+
function bindMaybeVector(args, column, vectorCols, value) {
|
|
1482
|
+
if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
|
|
1483
|
+
return args.bind(toVectorLiteral(value));
|
|
1484
|
+
}
|
|
1485
|
+
return args.bind(value);
|
|
1486
|
+
}
|
|
1210
1487
|
async function runPlanOp(sp, op, results) {
|
|
1488
|
+
const opVectorCols = vectorColumnsOf(currentSchema, op.table);
|
|
1211
1489
|
const args = new Args();
|
|
1212
1490
|
const table = quoteIdent(op.table);
|
|
1213
1491
|
let sql;
|
|
1214
1492
|
switch (op.op) {
|
|
1215
1493
|
case "insert": {
|
|
1216
1494
|
const cols = Object.keys(op.values ?? {});
|
|
1217
|
-
const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
|
|
1495
|
+
const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols));
|
|
1218
1496
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1219
1497
|
break;
|
|
1220
1498
|
}
|
|
@@ -1223,7 +1501,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1223
1501
|
if (rows.length === 0 || !rows[0]) return [];
|
|
1224
1502
|
const cols = Object.keys(rows[0]);
|
|
1225
1503
|
const tuples = rows.map(
|
|
1226
|
-
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
|
|
1504
|
+
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
1227
1505
|
);
|
|
1228
1506
|
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
|
|
1229
1507
|
break;
|
|
@@ -1232,7 +1510,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1232
1510
|
const cols = Object.keys(op.set ?? {});
|
|
1233
1511
|
if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
|
|
1234
1512
|
const assignments = cols.map(
|
|
1235
|
-
(c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
|
|
1513
|
+
(c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols)}`
|
|
1236
1514
|
);
|
|
1237
1515
|
sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
|
|
1238
1516
|
break;
|
|
@@ -1379,7 +1657,8 @@ function grantFor(entry, ctx, bucketLimits) {
|
|
|
1379
1657
|
filename: ctx.filename
|
|
1380
1658
|
}),
|
|
1381
1659
|
maxBytes: bucketLimits?.maxBytes ?? null,
|
|
1382
|
-
mimeTypes: bucketLimits?.mimeTypes ?? null
|
|
1660
|
+
mimeTypes: bucketLimits?.mimeTypes ?? null,
|
|
1661
|
+
ownerUid: ctx.userId ?? null
|
|
1383
1662
|
};
|
|
1384
1663
|
}
|
|
1385
1664
|
var CompletionLedger = class {
|
|
@@ -1476,6 +1755,23 @@ function installEgressFence(policy) {
|
|
|
1476
1755
|
|
|
1477
1756
|
// src/engine/index.ts
|
|
1478
1757
|
var JSON_HEADERS = { "content-type": "application/json" };
|
|
1758
|
+
function fieldErrors(err) {
|
|
1759
|
+
return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
|
|
1760
|
+
}
|
|
1761
|
+
function headersFor(raw, schema) {
|
|
1762
|
+
const shape = schema.shape;
|
|
1763
|
+
if (typeof shape !== "object" || shape === null) return raw;
|
|
1764
|
+
let aliased = null;
|
|
1765
|
+
for (const declared of Object.keys(shape)) {
|
|
1766
|
+
const lower = declared.toLowerCase();
|
|
1767
|
+
if (lower === declared) continue;
|
|
1768
|
+
const value = raw[lower];
|
|
1769
|
+
if (value === void 0) continue;
|
|
1770
|
+
aliased ??= { ...raw };
|
|
1771
|
+
aliased[declared] = value;
|
|
1772
|
+
}
|
|
1773
|
+
return aliased ?? raw;
|
|
1774
|
+
}
|
|
1479
1775
|
function envelope(error, description, status, requestId, extra) {
|
|
1480
1776
|
return new Response(
|
|
1481
1777
|
JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
|
|
@@ -1509,6 +1805,7 @@ async function defaultSqlDriver(config) {
|
|
|
1509
1805
|
async function createApp(opts) {
|
|
1510
1806
|
const { config, controllers } = opts;
|
|
1511
1807
|
setSchema(opts.schema ?? {});
|
|
1808
|
+
setSecretReader(opts.secretReader ?? null);
|
|
1512
1809
|
const routes = buildRouteTable(controllers);
|
|
1513
1810
|
if (routes.length === 0) {
|
|
1514
1811
|
throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
@@ -1621,7 +1918,8 @@ async function createApp(opts) {
|
|
|
1621
1918
|
error: "too_many_requests",
|
|
1622
1919
|
error_description: "Rate limit exceeded for this endpoint",
|
|
1623
1920
|
status: 429,
|
|
1624
|
-
request_id: requestId
|
|
1921
|
+
request_id: requestId,
|
|
1922
|
+
data: { retryAfter }
|
|
1625
1923
|
}),
|
|
1626
1924
|
{ status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
|
|
1627
1925
|
);
|
|
@@ -1640,7 +1938,7 @@ async function createApp(opts) {
|
|
|
1640
1938
|
const r = p.schema.safeParse(parsedBody);
|
|
1641
1939
|
if (!r.success) {
|
|
1642
1940
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
1643
|
-
|
|
1941
|
+
data: { fields: fieldErrors(r.error) }
|
|
1644
1942
|
});
|
|
1645
1943
|
}
|
|
1646
1944
|
args[p.index] = r.data;
|
|
@@ -1650,7 +1948,7 @@ async function createApp(opts) {
|
|
|
1650
1948
|
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
1651
1949
|
if (!r.success) {
|
|
1652
1950
|
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
1653
|
-
|
|
1951
|
+
data: { fields: fieldErrors(r.error) }
|
|
1654
1952
|
});
|
|
1655
1953
|
}
|
|
1656
1954
|
args[p.index] = r.data;
|
|
@@ -1659,9 +1957,21 @@ async function createApp(opts) {
|
|
|
1659
1957
|
case "param":
|
|
1660
1958
|
args[p.index] = hit.params[p.name];
|
|
1661
1959
|
break;
|
|
1662
|
-
case "headers":
|
|
1663
|
-
|
|
1960
|
+
case "headers": {
|
|
1961
|
+
const raw = Object.fromEntries(req.headers);
|
|
1962
|
+
if (!p.schema) {
|
|
1963
|
+
args[p.index] = raw;
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
1967
|
+
if (!r.success) {
|
|
1968
|
+
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
1969
|
+
data: { fields: fieldErrors(r.error) }
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
args[p.index] = r.data;
|
|
1664
1973
|
break;
|
|
1974
|
+
}
|
|
1665
1975
|
case "user":
|
|
1666
1976
|
case "optionalUser":
|
|
1667
1977
|
args[p.index] = claims ? {
|
|
@@ -1710,6 +2020,14 @@ async function createApp(opts) {
|
|
|
1710
2020
|
case "traceId":
|
|
1711
2021
|
args[p.index] = requestId;
|
|
1712
2022
|
break;
|
|
2023
|
+
case "client":
|
|
2024
|
+
args[p.index] = {
|
|
2025
|
+
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
2026
|
+
appVersion: req.headers.get("x-palbase-client-version"),
|
|
2027
|
+
platform: req.headers.get("x-platform"),
|
|
2028
|
+
osVersion: req.headers.get("x-os-version")
|
|
2029
|
+
};
|
|
2030
|
+
break;
|
|
1713
2031
|
case "req":
|
|
1714
2032
|
args[p.index] = req;
|
|
1715
2033
|
break;
|