portal-auth P1(#24 #25):KBDB library filter 地基(不 merge,待總管審) (#50)

This commit was merged in pull request #50.
This commit is contained in:
Leo
2026-07-14 03:50:06 +00:00
parent 18c0846af0
commit 7f409646e5
6 changed files with 274 additions and 14 deletions
+14
View File
@@ -58,6 +58,8 @@ export interface ListEntriesFilter {
parent_id?: string;
page_name?: string; // exact-match lookup (e.g. skill-/example- idempotency key)
source?: string; // filter by metadata_json.$.source (ingest envelope source.uri). issue #5.1
library?: string[]; // filter by metadata_json.$.library(多值 ORportal-auth P1#24/#25)。
// 未帶=不過濾(向後相容硬驗收);未標記的舊資料視同 'general'design §3.2)。
q?: string; // keyword filter on content (LIKE). Arcrun#3 發現①:list 端點原本完全不吃
// search/qcaller 帶了也被靜默丟棄(不是 458K 筆搜不到,是這個 filter 沒接)。
limit?: number;
@@ -81,6 +83,7 @@ export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Pr
// source is queryable via SQLite json_extract on the existing metadata_json TEXT column —
// no new column / no migration (表不變鐵律). Per issue #5.1 (頂層化 source 成可查 filter).
if (f.source) { conds.push("json_extract(metadata_json, '$.source') = ?"); params.push(f.source); }
if (f.library && f.library.length > 0) { conds.push(libraryPredicate(f.library)); params.push(...f.library); }
if (f.q) { conds.push('content LIKE ?'); params.push(`%${f.q}%`); }
const where = conds.length ? `WHERE ${conds.join(' AND ')}` : '';
const limit = Math.min(f.limit ?? 100, 1000);
@@ -123,19 +126,30 @@ export async function deleteEntry(db: D1Database, id: string): Promise<void> {
await db.prepare('DELETE FROM entries WHERE id = ?').bind(id).run();
}
// 「庫」filter 的 SQL 謂詞(portal-auth P1design §3.2/§3.3;零建表,同 #5.1 source 的 json_extract 先例)。
// COALESCE(x,'general') IN (…) ≡ SDD §3.3 寫的 (x IN (…) OR (x IS NULL AND 'general' IN (…)))——
// 語意完全相同(未標記/無 metadata_json 的舊資料歸 'general'),但單組佔位符、不用重複綁參數。
function libraryPredicate(libraries: string[]): string {
const placeholders = libraries.map(() => '?').join(',');
return `COALESCE(json_extract(metadata_json, '$.library'), 'general') IN (${placeholders})`;
}
// D1 LIKE keyword search (base; semantic search is the optional embed module).
// entry_type: optional base filter (generic — caller passes any type, base stays type-agnostic).
// library: optional 多值庫 filterportal-auth P1);未帶=行為與舊版一字不變(向後相容)。
export async function searchEntries(
db: D1Database,
q: string,
owner_id?: string,
entry_type?: string,
limit = 50,
library?: string[],
): Promise<Entry[]> {
const conds = ['content LIKE ?'];
const params: unknown[] = [`%${q}%`];
if (owner_id) { conds.push('owner_id = ?'); params.push(owner_id); }
if (entry_type) { conds.push('entry_type = ?'); params.push(entry_type); }
if (library && library.length > 0) { conds.push(libraryPredicate(library)); params.push(...library); }
const res = await db
.prepare(`SELECT * FROM entries WHERE ${conds.join(' AND ')} ORDER BY updated_at DESC LIMIT ?`)
.bind(...params, Math.min(limit, 200))