fix(kbdb): entries list 端點加 q/search filter + 誠實 total 欄位 (Arcrun#3 發現①)
根因(CF D1 直查已核實):owner_id='leo' 租戶下 D1 實際有 458,357 筆,但
/kbdb/entries(list)從沒接過 search/q 參數——proxy 轉發白名單漏了它、base
listEntries() 也沒這個 filter,caller 帶 search= 會被靜默丟棄,永遠回「無過濾
list」。加上舊版 count 欄位=本頁筆數(非總數),容易被誤讀成「總共只有這幾筆」。
修法:
- kbdb/src/actions/entry-crud.ts:ListEntriesFilter 加 q,listEntries 回傳
{ entries, total }(total = 符合條件全部筆數,COUNT(*) 與 list 查詢並行跑)。
- kbdb/src/routes/entries.ts:GET / 讀 q 或 search(別名)當 LIKE filter,
回應同時帶 count(本頁)與 total(全部)。
- cypher-executor/src/routes/kbdb-proxy.ts:GET /kbdb/entries 轉發白名單加
q/search → 統一轉發成 base 認得的 q。
驗證見 issue #3 留言(CF D1 直查 + curl /kbdb/entries?search=遷移 有真實命中)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+4005
File diff suppressed because it is too large
Load Diff
@@ -160,8 +160,11 @@ kbdbProxyRouter.post('/kbdb/entries', async (c) => {
|
||||
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
|
||||
});
|
||||
|
||||
// GET /kbdb/entries — list(filters: entry_type / parent_id / page_name / limit / offset)。
|
||||
// GET /kbdb/entries — list(filters: entry_type / parent_id / page_name / source / q(search) / limit / offset)。
|
||||
// owner_id 強制覆寫成本租戶(防跨租戶讀;caller 不能查別人的 owner_id)。
|
||||
// Arcrun#3 發現①根因:本白名單原本沒有 q/search,caller 帶 search= 會被這裡靜默丟棄,
|
||||
// 打到 base 永遠是「無過濾 list」——不是 458K 筆搜不到,是這個 filter 從沒被轉發過。
|
||||
// 修法:q 與 search 都收,統一轉發成 base 認得的 q(base 端見 entries.ts 同步修)。
|
||||
kbdbProxyRouter.get('/kbdb/entries', async (c) => {
|
||||
const owner = tenant(c);
|
||||
if (!owner) return c.json(NEED_KEY, 401);
|
||||
@@ -172,6 +175,8 @@ kbdbProxyRouter.get('/kbdb/entries', async (c) => {
|
||||
const v = c.req.query(k);
|
||||
if (v) params.set(k, v);
|
||||
}
|
||||
const q = c.req.query('q') || c.req.query('search');
|
||||
if (q) params.set('q', q);
|
||||
const res = await fetch(`${base}/entries?${params.toString()}`, { headers });
|
||||
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
|
||||
});
|
||||
|
||||
Generated
+2608
File diff suppressed because it is too large
Load Diff
@@ -58,11 +58,20 @@ 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
|
||||
q?: string; // keyword filter on content (LIKE). Arcrun#3 發現①:list 端點原本完全不吃
|
||||
// search/q,caller 帶了也被靜默丟棄(不是 458K 筆搜不到,是這個 filter 沒接)。
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Promise<Entry[]> {
|
||||
export interface ListEntriesResult {
|
||||
entries: Entry[];
|
||||
total: number; // 符合本次篩選條件的「全部」筆數(不受 limit/offset 影響)。
|
||||
// Arcrun#3 發現①「count 欄位語意誤導」:舊版 route 把 entries.length(分頁筆數)
|
||||
// 當 count 回傳,容易被誤讀成「總共只有這幾筆」。total 才是真總數,count 仍保留=本頁筆數。
|
||||
}
|
||||
|
||||
export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Promise<ListEntriesResult> {
|
||||
const conds: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
if (f.entry_type) { conds.push('entry_type = ?'); params.push(f.entry_type); }
|
||||
@@ -72,14 +81,18 @@ 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.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);
|
||||
const offset = f.offset ?? 0;
|
||||
const res = await db
|
||||
.prepare(`SELECT * FROM entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
|
||||
.bind(...params, limit, offset)
|
||||
.all<Entry>();
|
||||
return res.results ?? [];
|
||||
const [rowsRes, countRow] = await Promise.all([
|
||||
db
|
||||
.prepare(`SELECT * FROM entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
|
||||
.bind(...params, limit, offset)
|
||||
.all<Entry>(),
|
||||
db.prepare(`SELECT COUNT(*) as total FROM entries ${where}`).bind(...params).first<{ total: number }>(),
|
||||
]);
|
||||
return { entries: rowsRes.results ?? [], total: countRow?.total ?? 0 };
|
||||
}
|
||||
|
||||
export interface UpdateEntryInput {
|
||||
|
||||
@@ -23,21 +23,25 @@ entryRoutes.post('/', async (c) => {
|
||||
return c.json({ success: true, entry });
|
||||
});
|
||||
|
||||
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source)
|
||||
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source, q/search)
|
||||
// e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
|
||||
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
|
||||
// e.g. filter by ingest source: ?source=logseq://vault/foo.md (issue #5.1)
|
||||
// e.g. keyword filter: ?q=遷移 或 ?search=遷移(別名,Arcrun#3 發現①:caller 實測時打的是 search=,
|
||||
// 舊版完全不接這個 filter;q 與 search 兩個名字都認,避免同一個坑再踩一次)。
|
||||
// count = 本頁筆數(受 limit 影響);total = 符合條件全部筆數(不受 limit 影響,見 total 欄位)。
|
||||
entryRoutes.get('/', async (c) => {
|
||||
const entries = await listEntries(c.env.DB, {
|
||||
const { entries, total } = await listEntries(c.env.DB, {
|
||||
entry_type: c.req.query('entry_type') || undefined,
|
||||
owner_id: c.req.query('owner_id') || undefined,
|
||||
parent_id: c.req.query('parent_id') || undefined,
|
||||
page_name: c.req.query('page_name') || undefined,
|
||||
source: c.req.query('source') || undefined,
|
||||
q: c.req.query('q') || c.req.query('search') || undefined,
|
||||
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
||||
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
|
||||
});
|
||||
return c.json({ success: true, entries, count: entries.length });
|
||||
return c.json({ success: true, entries, count: entries.length, total });
|
||||
});
|
||||
|
||||
// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&mode=keyword|semantic
|
||||
|
||||
Reference in New Issue
Block a user