@palbase/backend 22.0.0 → 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/README.md +12 -7
- package/docs/endpoints.md +1 -1
- package/docs/errors.md +9 -0
- package/docs/llms-full.txt +22 -8
- package/package.json +1 -1
- package/dist/chunk-POYAFBLF.js.map +0 -1
- package/dist/chunk-QYOHMVUW.js.map +0 -1
- package/dist/chunk-SSGAMC26.js.map +0 -1
|
@@ -425,7 +425,8 @@ 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)
|
|
429
430
|
};
|
|
430
431
|
}
|
|
431
432
|
}
|
|
@@ -440,7 +441,8 @@ function makeTypedSurface(raw) {
|
|
|
440
441
|
update: (table, id, data) => raw.update(table, id, data),
|
|
441
442
|
delete: (table, id) => raw.delete(table, id),
|
|
442
443
|
findById: (table, id) => raw.findById(table, id),
|
|
443
|
-
findMany: (table, query) => raw.findMany(table, query)
|
|
444
|
+
findMany: (table, query) => raw.findMany(table, query),
|
|
445
|
+
search: (table, params) => raw.search(table, params)
|
|
444
446
|
};
|
|
445
447
|
return Object.assign(ops, {
|
|
446
448
|
tables: makeTablesAccessor(() => raw),
|
|
@@ -971,6 +973,194 @@ function asWireRow(row) {
|
|
|
971
973
|
function asWireRows(rows) {
|
|
972
974
|
return rows.map((row) => asWireRow(row));
|
|
973
975
|
}
|
|
976
|
+
function toVectorLiteral(v) {
|
|
977
|
+
return `[${v.join(",")}]`;
|
|
978
|
+
}
|
|
979
|
+
function vectorColumnsOf(schema, table) {
|
|
980
|
+
const out = /* @__PURE__ */ new Set();
|
|
981
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
982
|
+
if ((def.name ?? key) !== table) continue;
|
|
983
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
984
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
985
|
+
if (d !== null && typeof d === "object" && d.type === "vector") out.add(col);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
function reviveVectors(row, vectorCols) {
|
|
991
|
+
if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
|
|
992
|
+
const out = row;
|
|
993
|
+
for (const col of vectorCols) {
|
|
994
|
+
const v = out[col];
|
|
995
|
+
if (typeof v === "string") out[col] = JSON.parse(v);
|
|
996
|
+
}
|
|
997
|
+
return row;
|
|
998
|
+
}
|
|
999
|
+
function asTableRow(table, row) {
|
|
1000
|
+
return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1001
|
+
}
|
|
1002
|
+
function asTableRows(table, rows) {
|
|
1003
|
+
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1004
|
+
return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
|
|
1005
|
+
}
|
|
1006
|
+
function asBindParams(table, cols, data) {
|
|
1007
|
+
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1008
|
+
return cols.map((c) => {
|
|
1009
|
+
const v = data[c];
|
|
1010
|
+
return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
1014
|
+
var METRIC_OPERATOR = {
|
|
1015
|
+
cosine: "<=>",
|
|
1016
|
+
euclidean: "<->",
|
|
1017
|
+
inner_product: "<#>"
|
|
1018
|
+
};
|
|
1019
|
+
function searchConfigFor(table) {
|
|
1020
|
+
const t = currentSchema.tables?.[table];
|
|
1021
|
+
if (!t) return null;
|
|
1022
|
+
const columns = t.columns ?? {};
|
|
1023
|
+
const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
1024
|
+
const cols = Object.keys(columns);
|
|
1025
|
+
const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
|
|
1026
|
+
const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
1027
|
+
const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
|
|
1028
|
+
const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
|
|
1029
|
+
if (pk === null) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
const search = t.search;
|
|
1035
|
+
const ftsCols = search?.text ?? [];
|
|
1036
|
+
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [search.vector];
|
|
1037
|
+
let legs;
|
|
1038
|
+
if (rawLegs.length > 0) {
|
|
1039
|
+
legs = rawLegs.map((leg) => {
|
|
1040
|
+
const l = leg;
|
|
1041
|
+
const column = l.column ?? (vectorCols.length === 1 ? vectorCols[0] : void 0);
|
|
1042
|
+
if (column === void 0) {
|
|
1043
|
+
throw new Error(`search(${table}): birden \xE7ok vector kolonu var \u2014 beyanda 'column' zorunlu (FR-010)`);
|
|
1044
|
+
}
|
|
1045
|
+
const model = l.model;
|
|
1046
|
+
return {
|
|
1047
|
+
column,
|
|
1048
|
+
metric: l.metric ?? "cosine",
|
|
1049
|
+
...model !== void 0 ? { embed: {
|
|
1050
|
+
model: model.model,
|
|
1051
|
+
apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
|
|
1052
|
+
...model.baseURL !== void 0 ? { baseURL: model.baseURL } : {},
|
|
1053
|
+
...model.dimensions !== void 0 ? { dimensions: model.dimensions } : {}
|
|
1054
|
+
} } : {}
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1057
|
+
} else {
|
|
1058
|
+
legs = vectorCols.map((column) => ({ column, metric: "cosine" }));
|
|
1059
|
+
}
|
|
1060
|
+
if (ftsCols.length === 0 && legs.length === 0) return null;
|
|
1061
|
+
return { pk, cols, colSet: new Set(cols), ftsCols, legs };
|
|
1062
|
+
}
|
|
1063
|
+
function pickLeg(table, legs, using) {
|
|
1064
|
+
if (legs.length === 0) return null;
|
|
1065
|
+
if (using !== void 0) {
|
|
1066
|
+
const hit = legs.find((l) => l.column === using);
|
|
1067
|
+
if (!hit) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return hit;
|
|
1073
|
+
}
|
|
1074
|
+
if (legs.length === 1) return legs[0];
|
|
1075
|
+
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`);
|
|
1076
|
+
}
|
|
1077
|
+
var WHERE_OPS = { gt: ">", gte: ">=", lt: "<", lte: "<=", neq: "<>" };
|
|
1078
|
+
function compileWhere(table, colSet, where, add) {
|
|
1079
|
+
const parts = [];
|
|
1080
|
+
for (const [col, cond] of Object.entries(where)) {
|
|
1081
|
+
if (!colSet.has(col)) {
|
|
1082
|
+
throw new Error(`search(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1083
|
+
}
|
|
1084
|
+
const q = `t.${quoteIdent(col)}`;
|
|
1085
|
+
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1086
|
+
for (const [op, v] of Object.entries(cond)) {
|
|
1087
|
+
if (op === "in") {
|
|
1088
|
+
if (!Array.isArray(v)) throw new Error(`search(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
1089
|
+
if (v.length === 0) {
|
|
1090
|
+
parts.push("false");
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
|
|
1094
|
+
} else if (op in WHERE_OPS) {
|
|
1095
|
+
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
1096
|
+
} else {
|
|
1097
|
+
throw new Error(`search(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
parts.push(`${q} = ${add(cond)}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1105
|
+
}
|
|
1106
|
+
var cachedVectorSchema = null;
|
|
1107
|
+
async function vectorSchemaWithGuc(runner) {
|
|
1108
|
+
if (cachedVectorSchema !== null) {
|
|
1109
|
+
await runner.unsafe(
|
|
1110
|
+
"select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
|
|
1111
|
+
);
|
|
1112
|
+
return cachedVectorSchema;
|
|
1113
|
+
}
|
|
1114
|
+
const rows = await runner.unsafe(
|
|
1115
|
+
"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"
|
|
1116
|
+
);
|
|
1117
|
+
const name = rows?.[0]?.nspname;
|
|
1118
|
+
if (typeof name !== "string" || name === "") {
|
|
1119
|
+
throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
|
|
1120
|
+
}
|
|
1121
|
+
cachedVectorSchema = name;
|
|
1122
|
+
return name;
|
|
1123
|
+
}
|
|
1124
|
+
var secretReader = null;
|
|
1125
|
+
function setSecretReader(fn) {
|
|
1126
|
+
secretReader = fn;
|
|
1127
|
+
}
|
|
1128
|
+
var embedFetch = (url, init) => fetch(url, init);
|
|
1129
|
+
async function embedQuery(embed, text) {
|
|
1130
|
+
if (secretReader === null) {
|
|
1131
|
+
throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
|
|
1132
|
+
}
|
|
1133
|
+
const key = await secretReader(embed.apiKeyName);
|
|
1134
|
+
if (key === null || key === "") {
|
|
1135
|
+
throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
|
|
1136
|
+
}
|
|
1137
|
+
const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
|
|
1138
|
+
const controller = new AbortController();
|
|
1139
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1140
|
+
try {
|
|
1141
|
+
const res = await embedFetch(url, {
|
|
1142
|
+
method: "POST",
|
|
1143
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
1144
|
+
body: JSON.stringify({
|
|
1145
|
+
model: embed.model,
|
|
1146
|
+
input: [text],
|
|
1147
|
+
...embed.dimensions !== void 0 ? { dimensions: embed.dimensions } : {}
|
|
1148
|
+
}),
|
|
1149
|
+
signal: controller.signal
|
|
1150
|
+
});
|
|
1151
|
+
if (!res.ok) {
|
|
1152
|
+
throw new Error(`query embed: sa\u011Flay\u0131c\u0131 ${res.status} d\xF6nd\xFC (${embed.apiKeyName} ile) \u2014 anahtar/model do\u011Fru mu?`);
|
|
1153
|
+
}
|
|
1154
|
+
const data = await res.json();
|
|
1155
|
+
const vec = data.data?.[0]?.embedding;
|
|
1156
|
+
if (!Array.isArray(vec)) {
|
|
1157
|
+
throw new Error("query embed: sa\u011Flay\u0131c\u0131 yan\u0131t\u0131nda data[0].embedding yok (CLAIM-N1 \u015Fekli)");
|
|
1158
|
+
}
|
|
1159
|
+
return vec;
|
|
1160
|
+
} finally {
|
|
1161
|
+
clearTimeout(timer);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
974
1164
|
function createOps(tx) {
|
|
975
1165
|
const at = () => resolveTx(tx);
|
|
976
1166
|
const ops = {
|
|
@@ -982,22 +1172,22 @@ function createOps(tx) {
|
|
|
982
1172
|
if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
|
|
983
1173
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
984
1174
|
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
|
|
985
|
-
const rows = await (await at()).unsafe(sql,
|
|
1175
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
986
1176
|
const inserted = rows[0];
|
|
987
1177
|
if (!inserted) {
|
|
988
1178
|
throw new Error(
|
|
989
1179
|
`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
990
1180
|
);
|
|
991
1181
|
}
|
|
992
|
-
return
|
|
1182
|
+
return asTableRow(table, inserted);
|
|
993
1183
|
},
|
|
994
1184
|
async update(table, id, data) {
|
|
995
1185
|
const cols = Object.keys(data);
|
|
996
1186
|
if (cols.length === 0) return ops.findById(table, id);
|
|
997
1187
|
const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
|
|
998
1188
|
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] ?
|
|
1189
|
+
const rows = await (await at()).unsafe(sql, [...asBindParams(table, cols, data), id]);
|
|
1190
|
+
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1001
1191
|
},
|
|
1002
1192
|
async delete(table, id) {
|
|
1003
1193
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
@@ -1007,16 +1197,96 @@ function createOps(tx) {
|
|
|
1007
1197
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
1008
1198
|
[id]
|
|
1009
1199
|
);
|
|
1010
|
-
return rows[0] ?
|
|
1200
|
+
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1011
1201
|
},
|
|
1012
1202
|
async findMany(table, query = {}) {
|
|
1013
1203
|
const cols = Object.keys(query);
|
|
1014
1204
|
const where = cols.length ? ` WHERE ${cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ")}` : "";
|
|
1015
|
-
return
|
|
1205
|
+
return asTableRows(table, await (await at()).unsafe(
|
|
1016
1206
|
`SELECT * FROM ${quoteIdent(table)}${where}`,
|
|
1017
1207
|
cols.map((c) => query[c])
|
|
1018
1208
|
));
|
|
1019
1209
|
},
|
|
1210
|
+
/**
|
|
1211
|
+
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
1212
|
+
* (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
|
|
1213
|
+
* hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
|
|
1214
|
+
* RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
|
|
1215
|
+
* T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
|
|
1216
|
+
*/
|
|
1217
|
+
async search(table, params = {}) {
|
|
1218
|
+
const cfg = searchConfigFor(table);
|
|
1219
|
+
if (!cfg) {
|
|
1220
|
+
throw new Error(`search(${table}): tablo aranabilir de\u011Fil \u2014 ne vector kolonu ne search beyan\u0131 var (FR-013)`);
|
|
1221
|
+
}
|
|
1222
|
+
const rawLimit = params.limit ?? 20;
|
|
1223
|
+
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
1224
|
+
throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
|
|
1225
|
+
}
|
|
1226
|
+
const limit = Math.min(Math.max(1, Math.trunc(rawLimit)), 100);
|
|
1227
|
+
const pool = Math.max(limit * 3, 30);
|
|
1228
|
+
const wantText = params.mode !== "vector" && cfg.ftsCols.length > 0 && typeof params.query === "string" && params.query !== "";
|
|
1229
|
+
const anyEmbed = cfg.legs.some((l) => l.embed !== void 0);
|
|
1230
|
+
const vectorAsked = params.mode !== "text" && (Array.isArray(params.vector) || params.using !== void 0 || params.mode === "vector" || anyEmbed && typeof params.query === "string" && params.query !== "");
|
|
1231
|
+
const leg = vectorAsked ? pickLeg(table, cfg.legs, params.using) : null;
|
|
1232
|
+
let qv = Array.isArray(params.vector) ? params.vector : null;
|
|
1233
|
+
if (qv === null && leg?.embed !== void 0 && typeof params.query === "string" && params.query !== "") {
|
|
1234
|
+
try {
|
|
1235
|
+
qv = await embedQuery(leg.embed, params.query);
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
if (!wantText) throw e;
|
|
1238
|
+
qv = null;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const wantVector = leg !== null && qv !== null;
|
|
1242
|
+
if (!wantText && !wantVector) {
|
|
1243
|
+
throw new Error(
|
|
1244
|
+
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const bind = [];
|
|
1248
|
+
const add = (v) => {
|
|
1249
|
+
bind.push(v);
|
|
1250
|
+
return `$${bind.length}`;
|
|
1251
|
+
};
|
|
1252
|
+
const whereSql = compileWhere(table, cfg.colSet, params.where ?? {}, add);
|
|
1253
|
+
const live = await at();
|
|
1254
|
+
const K = 50;
|
|
1255
|
+
let semSql = "";
|
|
1256
|
+
let kwSql = "";
|
|
1257
|
+
if (wantVector && leg) {
|
|
1258
|
+
const sch = await vectorSchemaWithGuc(live);
|
|
1259
|
+
const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
|
|
1260
|
+
let exactOrder = "";
|
|
1261
|
+
if (whereSql !== "") {
|
|
1262
|
+
const probeRows = await live.unsafe(
|
|
1263
|
+
`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`,
|
|
1264
|
+
bind.slice()
|
|
1265
|
+
);
|
|
1266
|
+
const n = probeRows?.[0]?.n;
|
|
1267
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1268
|
+
exactOrder = " + 0.0";
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const vp = add(toVectorLiteral(qv));
|
|
1272
|
+
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}`;
|
|
1273
|
+
}
|
|
1274
|
+
if (wantText) {
|
|
1275
|
+
const qp = add(params.query);
|
|
1276
|
+
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}`;
|
|
1277
|
+
}
|
|
1278
|
+
const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1279
|
+
let sql;
|
|
1280
|
+
if (semSql !== "" && kwSql !== "") {
|
|
1281
|
+
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}`;
|
|
1282
|
+
} else {
|
|
1283
|
+
const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwSql})`;
|
|
1284
|
+
const alias = semSql !== "" ? "sem" : "kw";
|
|
1285
|
+
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}`;
|
|
1286
|
+
}
|
|
1287
|
+
const rows = await live.unsafe(sql, bind);
|
|
1288
|
+
return asTableRows(table, rows);
|
|
1289
|
+
},
|
|
1020
1290
|
/** A real SAVEPOINT inside the request's transaction. */
|
|
1021
1291
|
async transaction(cb) {
|
|
1022
1292
|
const live = await at();
|
|
@@ -1045,7 +1315,7 @@ function createOps(tx) {
|
|
|
1045
1315
|
return live.savepoint(async (sp) => {
|
|
1046
1316
|
const results = [];
|
|
1047
1317
|
for (const op of plan.ops) {
|
|
1048
|
-
const rows = await runPlanOp(sp, op, results);
|
|
1318
|
+
const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
|
|
1049
1319
|
const result = { rows, rows_affected: rows.length };
|
|
1050
1320
|
results.push(result);
|
|
1051
1321
|
assertGuard(op, result);
|
|
@@ -1058,6 +1328,7 @@ function createOps(tx) {
|
|
|
1058
1328
|
}
|
|
1059
1329
|
var currentSchema = {};
|
|
1060
1330
|
function setSchema(schema) {
|
|
1331
|
+
cachedVectorSchema = null;
|
|
1061
1332
|
const s = schema;
|
|
1062
1333
|
currentSchema = (s && "default" in s ? s.default : s) ?? {};
|
|
1063
1334
|
}
|
|
@@ -1144,7 +1415,7 @@ function isRef(v) {
|
|
|
1144
1415
|
function isExpr(v) {
|
|
1145
1416
|
return typeof v === "object" && v !== null && "$expr" in v;
|
|
1146
1417
|
}
|
|
1147
|
-
function renderValue(value, column, args, results) {
|
|
1418
|
+
function renderValue(value, column, args, results, vectorCols) {
|
|
1148
1419
|
if (isRef(value)) {
|
|
1149
1420
|
const source = results[value.$ref.op];
|
|
1150
1421
|
const row = source?.rows[0];
|
|
@@ -1153,15 +1424,15 @@ function renderValue(value, column, args, results) {
|
|
|
1153
1424
|
error_code: "tx_ref_unresolved"
|
|
1154
1425
|
});
|
|
1155
1426
|
}
|
|
1156
|
-
return args
|
|
1427
|
+
return bindMaybeVector(args, column, vectorCols, row[value.$ref.field]);
|
|
1157
1428
|
}
|
|
1158
1429
|
if (isExpr(value)) {
|
|
1159
1430
|
const fn = value.$expr;
|
|
1160
1431
|
if (fn.fn === "now") return "now()";
|
|
1161
1432
|
const operator = fn.fn === "inc" ? "+" : "-";
|
|
1162
|
-
return `${quoteIdent(column)} ${operator} ${args
|
|
1433
|
+
return `${quoteIdent(column)} ${operator} ${bindMaybeVector(args, column, vectorCols, fn.by)}`;
|
|
1163
1434
|
}
|
|
1164
|
-
return args
|
|
1435
|
+
return bindMaybeVector(args, column, vectorCols, value);
|
|
1165
1436
|
}
|
|
1166
1437
|
function renderWhere(where, args, results) {
|
|
1167
1438
|
const cols = Object.keys(where ?? {});
|
|
@@ -1173,14 +1444,21 @@ function renderWhere(where, args, results) {
|
|
|
1173
1444
|
});
|
|
1174
1445
|
return ` WHERE ${terms.join(" AND ")}`;
|
|
1175
1446
|
}
|
|
1447
|
+
function bindMaybeVector(args, column, vectorCols, value) {
|
|
1448
|
+
if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
|
|
1449
|
+
return args.bind(toVectorLiteral(value));
|
|
1450
|
+
}
|
|
1451
|
+
return args.bind(value);
|
|
1452
|
+
}
|
|
1176
1453
|
async function runPlanOp(sp, op, results) {
|
|
1454
|
+
const opVectorCols = vectorColumnsOf(currentSchema, op.table);
|
|
1177
1455
|
const args = new Args();
|
|
1178
1456
|
const table = quoteIdent(op.table);
|
|
1179
1457
|
let sql;
|
|
1180
1458
|
switch (op.op) {
|
|
1181
1459
|
case "insert": {
|
|
1182
1460
|
const cols = Object.keys(op.values ?? {});
|
|
1183
|
-
const rendered = cols.map((c) => renderValue(op.values[c], c, args, results));
|
|
1461
|
+
const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols));
|
|
1184
1462
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1185
1463
|
break;
|
|
1186
1464
|
}
|
|
@@ -1189,7 +1467,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1189
1467
|
if (rows.length === 0 || !rows[0]) return [];
|
|
1190
1468
|
const cols = Object.keys(rows[0]);
|
|
1191
1469
|
const tuples = rows.map(
|
|
1192
|
-
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results)).join(", ")})`
|
|
1470
|
+
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
1193
1471
|
);
|
|
1194
1472
|
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
|
|
1195
1473
|
break;
|
|
@@ -1198,7 +1476,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1198
1476
|
const cols = Object.keys(op.set ?? {});
|
|
1199
1477
|
if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
|
|
1200
1478
|
const assignments = cols.map(
|
|
1201
|
-
(c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results)}`
|
|
1479
|
+
(c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols)}`
|
|
1202
1480
|
);
|
|
1203
1481
|
sql = `UPDATE ${table} SET ${assignments.join(", ")}${renderWhere(op.where, args, results)} RETURNING *`;
|
|
1204
1482
|
break;
|
|
@@ -1345,7 +1623,8 @@ function grantFor(entry, ctx, bucketLimits) {
|
|
|
1345
1623
|
filename: ctx.filename
|
|
1346
1624
|
}),
|
|
1347
1625
|
maxBytes: bucketLimits?.maxBytes ?? null,
|
|
1348
|
-
mimeTypes: bucketLimits?.mimeTypes ?? null
|
|
1626
|
+
mimeTypes: bucketLimits?.mimeTypes ?? null,
|
|
1627
|
+
ownerUid: ctx.userId ?? null
|
|
1349
1628
|
};
|
|
1350
1629
|
}
|
|
1351
1630
|
var CompletionLedger = class {
|
|
@@ -1381,6 +1660,23 @@ function verifySignature(presented, expected) {
|
|
|
1381
1660
|
|
|
1382
1661
|
// src/engine/index.ts
|
|
1383
1662
|
var JSON_HEADERS = { "content-type": "application/json" };
|
|
1663
|
+
function fieldErrors(err) {
|
|
1664
|
+
return err.issues.map((i) => ({ field: i.path.join("."), message: i.message }));
|
|
1665
|
+
}
|
|
1666
|
+
function headersFor(raw, schema) {
|
|
1667
|
+
const shape = schema.shape;
|
|
1668
|
+
if (typeof shape !== "object" || shape === null) return raw;
|
|
1669
|
+
let aliased = null;
|
|
1670
|
+
for (const declared of Object.keys(shape)) {
|
|
1671
|
+
const lower = declared.toLowerCase();
|
|
1672
|
+
if (lower === declared) continue;
|
|
1673
|
+
const value = raw[lower];
|
|
1674
|
+
if (value === void 0) continue;
|
|
1675
|
+
aliased ??= { ...raw };
|
|
1676
|
+
aliased[declared] = value;
|
|
1677
|
+
}
|
|
1678
|
+
return aliased ?? raw;
|
|
1679
|
+
}
|
|
1384
1680
|
function envelope(error, description, status, requestId, extra) {
|
|
1385
1681
|
return new Response(
|
|
1386
1682
|
JSON.stringify({ error, error_description: description, status, request_id: requestId, ...extra }),
|
|
@@ -1414,6 +1710,7 @@ async function defaultSqlDriver(config) {
|
|
|
1414
1710
|
async function createApp(opts) {
|
|
1415
1711
|
const { config, controllers } = opts;
|
|
1416
1712
|
setSchema(opts.schema ?? {});
|
|
1713
|
+
setSecretReader(opts.secretReader ?? null);
|
|
1417
1714
|
const routes = buildRouteTable(controllers);
|
|
1418
1715
|
if (routes.length === 0) {
|
|
1419
1716
|
throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
@@ -1526,7 +1823,8 @@ async function createApp(opts) {
|
|
|
1526
1823
|
error: "too_many_requests",
|
|
1527
1824
|
error_description: "Rate limit exceeded for this endpoint",
|
|
1528
1825
|
status: 429,
|
|
1529
|
-
request_id: requestId
|
|
1826
|
+
request_id: requestId,
|
|
1827
|
+
data: { retryAfter }
|
|
1530
1828
|
}),
|
|
1531
1829
|
{ status: 429, headers: { ...JSON_HEADERS, "retry-after": String(retryAfter) } }
|
|
1532
1830
|
);
|
|
@@ -1545,7 +1843,7 @@ async function createApp(opts) {
|
|
|
1545
1843
|
const r = p.schema.safeParse(parsedBody);
|
|
1546
1844
|
if (!r.success) {
|
|
1547
1845
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
1548
|
-
|
|
1846
|
+
data: { fields: fieldErrors(r.error) }
|
|
1549
1847
|
});
|
|
1550
1848
|
}
|
|
1551
1849
|
args[p.index] = r.data;
|
|
@@ -1555,7 +1853,7 @@ async function createApp(opts) {
|
|
|
1555
1853
|
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
1556
1854
|
if (!r.success) {
|
|
1557
1855
|
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
1558
|
-
|
|
1856
|
+
data: { fields: fieldErrors(r.error) }
|
|
1559
1857
|
});
|
|
1560
1858
|
}
|
|
1561
1859
|
args[p.index] = r.data;
|
|
@@ -1564,9 +1862,21 @@ async function createApp(opts) {
|
|
|
1564
1862
|
case "param":
|
|
1565
1863
|
args[p.index] = hit.params[p.name];
|
|
1566
1864
|
break;
|
|
1567
|
-
case "headers":
|
|
1568
|
-
|
|
1865
|
+
case "headers": {
|
|
1866
|
+
const raw = Object.fromEntries(req.headers);
|
|
1867
|
+
if (!p.schema) {
|
|
1868
|
+
args[p.index] = raw;
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
1872
|
+
if (!r.success) {
|
|
1873
|
+
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
1874
|
+
data: { fields: fieldErrors(r.error) }
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
args[p.index] = r.data;
|
|
1569
1878
|
break;
|
|
1879
|
+
}
|
|
1570
1880
|
case "user":
|
|
1571
1881
|
case "optionalUser":
|
|
1572
1882
|
args[p.index] = claims ? {
|
|
@@ -1615,6 +1925,14 @@ async function createApp(opts) {
|
|
|
1615
1925
|
case "traceId":
|
|
1616
1926
|
args[p.index] = requestId;
|
|
1617
1927
|
break;
|
|
1928
|
+
case "client":
|
|
1929
|
+
args[p.index] = {
|
|
1930
|
+
sdkVersion: req.headers.get("x-palbase-sdk-version"),
|
|
1931
|
+
appVersion: req.headers.get("x-palbase-client-version"),
|
|
1932
|
+
platform: req.headers.get("x-platform"),
|
|
1933
|
+
osVersion: req.headers.get("x-os-version")
|
|
1934
|
+
};
|
|
1935
|
+
break;
|
|
1618
1936
|
case "req":
|
|
1619
1937
|
args[p.index] = req;
|
|
1620
1938
|
break;
|