public
anord
read
Ledger
Why work hard when you can work easier?
Languages
Repository composition by tracked source files.
TypeScript
86%
CSS
10%
SQL
3%
Shell
1%
HTML
0%
Trace
apps/api/src/services/search.ts
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import type { PageSummary, SessionUser } from "@ledger/shared";
2
import { canReadVisibility } from "@ledger/shared";
3
import { pool } from "../db/pool.js";
4
import { getSearchCandidates } from "./pages.js";
6
export function matchesSearchQuery(query: string, title: string, bodyMarkdown: string): boolean {
7
const normalizedQuery = query.trim().toLowerCase();
8
if (!normalizedQuery) {
9
return false;
10
}
12
return (
13
title.toLowerCase().includes(normalizedQuery) ||
14
bodyMarkdown.toLowerCase().includes(normalizedQuery)
15
);
16
}
18
export async function searchPages(query: string, user: SessionUser | null): Promise<PageSummary[]> {
19
const candidates = await getSearchCandidates();
20
const visible: PageSummary[] = [];
22
for (const row of candidates) {
23
if (!matchesSearchQuery(query, row.title, row.body_markdown)) {
24
continue;
25
}
27
const permissions = await pool.query(
28
`SELECT role_key, group_id FROM page_permissions WHERE page_id = $1`,
29
[row.id]
30
);
32
const roleKeys = permissions.rows.map((permission) => permission.role_key).filter(Boolean);
33
const groupIds = permissions.rows.map((permission) => permission.group_id).filter(Boolean);
35
if (!canReadVisibility(user, row.visibility, roleKeys, groupIds)) {
36
continue;
37
}
39
const tags = await pool.query(
40
`SELECT t.name FROM page_tags pt JOIN tags t ON t.id = pt.tag_id WHERE pt.page_id = $1`,
41
[row.id]
42
);
44
visible.push({
45
id: row.id,
46
spaceId: row.space_id,
47
title: row.title,
48
slug: row.slug,
49
excerpt: row.excerpt,
50
visibility: row.visibility,
51
state: row.state,
52
isPublic: row.is_public,
53
parentPageId: row.parent_page_id,
54
tags: tags.rows.map((tag) => tag.name),
55
updatedAt: row.updated_at.toISOString()
56
});
57
}
59
return visible.slice(0, 20);
60
}
62
export async function recordSearch(query: string, userId: string | null, results: PageSummary[]) {
63
const search = await pool.query(
64
`
65
INSERT INTO searches (user_id, query, visible_scope, results_count)
66
VALUES ($1, $2, $3, $4)
67
RETURNING id
68
`,
69
[userId, query, userId ? "authenticated" : "public", results.length]
70
);
72
for (const [index, result] of results.entries()) {
73
await pool.query(
74
`INSERT INTO search_results (search_id, page_id, position) VALUES ($1, $2, $3)`,
75
[search.rows[0].id, result.id, index + 1]
76
);
77
}
79
return search.rows[0].id as string;
80
}