mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fix: quota API field mapping + FTS5 query sanitization
- /api/v1/quotas read conn.id/provider/name — the lazy row proxy does not expose connectionId/providerId, so every connection was skipped and the endpoint always returned an empty list. - retrieveMemories/buildFtsRows now sanitize the query into a quoted FTS5 MATCH expression — natural-language queries with ? ! : ( ) etc. no longer throw 'fts5: syntax error' and silently degrade to empty results. - new unit test tests/unit/memory/fts5-query-sanitize.test.ts (5 cases).
This commit is contained in:
@@ -47,7 +47,11 @@ export async function GET(request: Request) {
|
||||
? conn.providerId
|
||||
: "";
|
||||
const connectionId =
|
||||
typeof conn.connectionId === "string" ? conn.connectionId : "";
|
||||
typeof conn.id === "string"
|
||||
? conn.id
|
||||
: typeof conn.connectionId === "string"
|
||||
? conn.connectionId
|
||||
: "";
|
||||
if (!provider || !connectionId) continue;
|
||||
|
||||
let saturation = 0;
|
||||
@@ -69,7 +73,6 @@ export async function GET(request: Request) {
|
||||
: typeof conn.name === "string" && conn.name.trim()
|
||||
? conn.name
|
||||
: connectionId;
|
||||
|
||||
connections.push({
|
||||
provider,
|
||||
connectionId,
|
||||
|
||||
@@ -78,8 +78,21 @@ function fetchMemoriesByIds(ids: string[]): Memory[] {
|
||||
return ids.map((id) => byId.get(id)).filter((m): m is Memory => m !== undefined);
|
||||
}
|
||||
|
||||
interface FtsColConfig {
|
||||
apiKeyCol: string;
|
||||
/**
|
||||
* Sanitize a free-text query into an FTS5 MATCH expression.
|
||||
* Strips FTS5 syntax characters (?, !, ", *, :, parentheses, etc.) and quotes
|
||||
* each whitespace-separated term so natural-language queries with punctuation
|
||||
* don't raise "fts5: syntax error".
|
||||
*/
|
||||
export function toFts5MatchQuery(query: string): string {
|
||||
const terms = query
|
||||
.split(/\s+/)
|
||||
.map((term) => term.replace(/["*():\[\]{}!?\^~+-]/g, ""))
|
||||
.filter((term) => term.length > 0);
|
||||
return terms.map((term) => `"${term}"`).join(" AND ");
|
||||
}
|
||||
|
||||
interface FtsColConfig { apiKeyCol: string;
|
||||
expiresCol: string;
|
||||
createdCol: string;
|
||||
sessionCol: string;
|
||||
@@ -122,7 +135,7 @@ function buildFtsRows(apiKeyId: string, config: FtsColConfig): MemoryRow[] {
|
||||
}
|
||||
ftsQueryStr += ` ORDER BY f.rank LIMIT 100`;
|
||||
|
||||
const ftsParams: unknown[] = [q, apiKeyId];
|
||||
const ftsParams: unknown[] = [toFts5MatchQuery(q), apiKeyId];
|
||||
if (scope === "session" && sessionId) ftsParams.push(sessionId);
|
||||
if (retentionDays && retentionDays > 0) {
|
||||
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
||||
@@ -918,7 +931,9 @@ export async function retrievePreview(
|
||||
const ftsQueryStr = apiKeyId
|
||||
? `SELECT m.* FROM ${tableName} m JOIN memory_fts f ON m.memory_id = f.rowid WHERE f.memory_fts MATCH ? AND m.${apiKeyCol} = ? ORDER BY f.rank LIMIT ?`
|
||||
: `SELECT m.* FROM ${tableName} m JOIN memory_fts f ON m.memory_id = f.rowid WHERE f.memory_fts MATCH ? ORDER BY f.rank LIMIT ?`;
|
||||
const ftsP: unknown[] = apiKeyId ? [query, apiKeyId, limit] : [query, limit];
|
||||
const ftsP: unknown[] = apiKeyId
|
||||
? [toFts5MatchQuery(query), apiKeyId, limit]
|
||||
: [toFts5MatchQuery(query), limit];
|
||||
try {
|
||||
ftsRows = db.prepare(ftsQueryStr).all(...ftsP) as MemoryRow[];
|
||||
} catch {
|
||||
|
||||
31
tests/unit/memory/fts5-query-sanitize.test.ts
Normal file
31
tests/unit/memory/fts5-query-sanitize.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { toFts5MatchQuery } from "../../../src/lib/memory/retrieval";
|
||||
|
||||
describe("memory retrieval toFts5MatchQuery", () => {
|
||||
it("keeps plain terms and joins with AND", () => {
|
||||
assert.strictEqual(toFts5MatchQuery("hello world"), '"hello" AND "world"');
|
||||
});
|
||||
|
||||
it("strips FTS5 syntax characters", () => {
|
||||
assert.strictEqual(
|
||||
toFts5MatchQuery("Чем занимается пользователь?"),
|
||||
'"Чем" AND "занимается" AND "пользователь"'
|
||||
);
|
||||
});
|
||||
|
||||
it("strips quotes, colons, parentheses and dashes", () => {
|
||||
assert.strictEqual(
|
||||
toFts5MatchQuery('model "gpt-4o": (fast) OR NOT [x]'),
|
||||
'"model" AND "gpt4o" AND "fast" AND "OR" AND "NOT" AND "x"'
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps punctuation-only input as a single quoted term", () => {
|
||||
assert.strictEqual(toFts5MatchQuery("???! ..."), '"..."');
|
||||
});
|
||||
|
||||
it("returns empty string for empty input", () => {
|
||||
assert.strictEqual(toFts5MatchQuery(""), "");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user