snapshot
This commit is contained in:
+96
-18
@@ -15,6 +15,38 @@ function escapeLike(raw: string): string {
|
||||
return raw.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
||||
}
|
||||
|
||||
/** `tag:x`, `title:y`, `content:z` (case-insensitive); remainder = default (title OR tag names, LIKE). All parts AND. */
|
||||
function parseSearchQuery(raw: string): {
|
||||
defaultText: string | null;
|
||||
tagTerms: string[];
|
||||
titleTerms: string[];
|
||||
contentTerms: string[];
|
||||
} {
|
||||
const tagTerms: string[] = [];
|
||||
const titleTerms: string[] = [];
|
||||
const contentTerms: string[] = [];
|
||||
const re = /(?:^|\s)(tag|title|content):(\S+)/gi;
|
||||
const spans: [number, number][] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
const kind = m[1].toLowerCase();
|
||||
const val = m[2];
|
||||
if (!val) continue;
|
||||
const escaped = escapeLike(val);
|
||||
if (kind === "tag") tagTerms.push(escaped);
|
||||
else if (kind === "title") titleTerms.push(escaped);
|
||||
else if (kind === "content") contentTerms.push(escaped);
|
||||
spans.push([m.index, m.index + m[0].length]);
|
||||
}
|
||||
let rest = raw;
|
||||
for (const [start, end] of [...spans].sort((a, b) => b[0] - a[0])) {
|
||||
rest = `${rest.slice(0, start)} ${rest.slice(end)}`;
|
||||
}
|
||||
const defaultTrim = rest.replace(/\s+/g, " ").trim();
|
||||
const defaultText = defaultTrim === "" ? null : escapeLike(defaultTrim);
|
||||
return { defaultText, tagTerms, titleTerms, contentTerms };
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
@@ -101,22 +133,59 @@ export function listSnippets(q: string | null, sort: string | null): SnippetRow[
|
||||
)
|
||||
.all() as typeof rows;
|
||||
} else {
|
||||
const term = escapeLike(trimmed);
|
||||
rows = database
|
||||
.query(
|
||||
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
||||
FROM snippets s
|
||||
WHERE s.title LIKE '%' || ?1 || '%' ESCAPE '\\'
|
||||
OR s.body LIKE '%' || ?1 || '%' ESCAPE '\\'
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM snippet_tags st
|
||||
JOIN tags t ON t.id = st.tag_id
|
||||
WHERE st.snippet_id = s.id
|
||||
AND t.name LIKE '%' || ?1 || '%' ESCAPE '\\'
|
||||
)
|
||||
${order}`,
|
||||
)
|
||||
.all(term) as typeof rows;
|
||||
const parsed = parseSearchQuery(trimmed);
|
||||
const conditions: string[] = [];
|
||||
const params: string[] = [];
|
||||
|
||||
if (parsed.defaultText !== null) {
|
||||
conditions.push(`(
|
||||
(s.title IS NOT NULL AND s.title LIKE '%' || ? || '%' ESCAPE '\\')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM snippet_tags st
|
||||
JOIN tags t ON t.id = st.tag_id
|
||||
WHERE st.snippet_id = s.id
|
||||
AND t.name LIKE '%' || ? || '%' ESCAPE '\\'
|
||||
)
|
||||
)`);
|
||||
params.push(parsed.defaultText, parsed.defaultText);
|
||||
}
|
||||
for (const term of parsed.tagTerms) {
|
||||
conditions.push(`EXISTS (
|
||||
SELECT 1 FROM snippet_tags st
|
||||
JOIN tags t ON t.id = st.tag_id
|
||||
WHERE st.snippet_id = s.id
|
||||
AND t.name LIKE '%' || ? || '%' ESCAPE '\\'
|
||||
)`);
|
||||
params.push(term);
|
||||
}
|
||||
for (const term of parsed.titleTerms) {
|
||||
conditions.push(`(s.title IS NOT NULL AND s.title LIKE '%' || ? || '%' ESCAPE '\\')`);
|
||||
params.push(term);
|
||||
}
|
||||
for (const term of parsed.contentTerms) {
|
||||
conditions.push(`(s.body LIKE '%' || ? || '%' ESCAPE '\\')`);
|
||||
params.push(term);
|
||||
}
|
||||
|
||||
if (conditions.length === 0) {
|
||||
rows = database
|
||||
.query(
|
||||
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
||||
FROM snippets s
|
||||
${order}`,
|
||||
)
|
||||
.all() as typeof rows;
|
||||
} else {
|
||||
const where = conditions.join(" AND ");
|
||||
rows = database
|
||||
.query(
|
||||
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
||||
FROM snippets s
|
||||
WHERE ${where}
|
||||
${order}`,
|
||||
)
|
||||
.all(...params) as typeof rows;
|
||||
}
|
||||
}
|
||||
|
||||
const ids = rows.map(r => r.id);
|
||||
@@ -174,7 +243,12 @@ export function createSnippet(input: {
|
||||
}): SnippetRow {
|
||||
const database = getDb();
|
||||
const t = nowIso();
|
||||
const title = input.title === undefined || input.title === "" ? null : String(input.title);
|
||||
const title =
|
||||
input.title === undefined || input.title === null
|
||||
? null
|
||||
: String(input.title).trim() === ""
|
||||
? null
|
||||
: String(input.title).trim();
|
||||
const body = input.body ?? "";
|
||||
const sortOrder = typeof input.sort_order === "number" ? input.sort_order : Date.now();
|
||||
|
||||
@@ -210,7 +284,11 @@ export function updateSnippet(
|
||||
};
|
||||
|
||||
const title =
|
||||
input.title === undefined ? current.title : input.title === "" || input.title === null ? null : String(input.title);
|
||||
input.title === undefined
|
||||
? current.title
|
||||
: input.title === null || String(input.title).trim() === ""
|
||||
? null
|
||||
: String(input.title).trim();
|
||||
const body = input.body === undefined ? current.body : String(input.body);
|
||||
const sort_order = input.sort_order === undefined ? current.sort_order : input.sort_order;
|
||||
const t = nowIso();
|
||||
|
||||
Reference in New Issue
Block a user