e28e19069f
leo 07-29:「要在雲端子庫顯示同步了幾個 wiki,既然這樣也同步顯示有幾個三元組,
這是為了政府專案驗收。」
kbdb 兩支統計端點:
- entries.ts: COUNT(DISTINCT page_name) AS card_count
⚠️ 必須 distinct——算 block 數會膨脹 3-5 倍,政府驗收看到假數字比沒數字更糟
- records.ts: COUNT(*) AS triplet_count(三元組本來就算全部)
portal.ts 聚合兩者掛進庫目錄;前端 index.html 顯示。
驗:卡數確為 COUNT(DISTINCT page_name)/前端內嵌 JS node --check 全通過
(07-29 白畫面事故教訓)/portal-admin 測試 34 passed(改動前 31 passed,
唯一的 1 failed 是既有債:GET /portal 回 404,改動前後相同,非本次造成)。
註:此為子 CC 完成後未 commit 的懸置工作,總管收工檢查時發現並補收。
89 lines
4.0 KiB
TypeScript
89 lines
4.0 KiB
TypeScript
// Records route — structured records (entry_values composed by a template).
|
||
import { Hono } from 'hono';
|
||
import type { Bindings } from '../types';
|
||
import { createRecord, deleteRecord, getRecord, searchByTemplate, updateRecord } from '../actions/record-crud';
|
||
|
||
export const recordRoutes = new Hono<{ Bindings: Bindings }>();
|
||
|
||
// POST /records — { template, values:{slot:content}, owner_id? }
|
||
recordRoutes.post('/', async (c) => {
|
||
const body = await c.req.json().catch(() => null);
|
||
if (!body || !body.template || !body.values) {
|
||
return c.json({ success: false, error: 'template and values required' }, 400);
|
||
}
|
||
try {
|
||
const rec = await createRecord(c.env.DB, body);
|
||
return c.json({ success: true, record: rec });
|
||
} catch (e) {
|
||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
|
||
}
|
||
});
|
||
|
||
// GET /records/triplet-stats?owner_id=... — 每個庫的三元組(關聯)數。
|
||
// t142(2026-07-29):政府驗收——顯示每個庫整理出幾條知識關聯。
|
||
// 計法:依 triplet 型 record 的 'library' slot 值分組計數。無 library slot 的舊三元組歸 general。
|
||
// 使用子查詢先取 distinct triplet record IDs(針對 owner),再 LEFT JOIN library slot,
|
||
// 避免 N+1(全部一次 SQL 完成,不逐筆 getRecord)。
|
||
recordRoutes.get('/triplet-stats', async (c) => {
|
||
const owner = c.req.query('owner_id') || '';
|
||
// 子查詢:找到屬於這個 owner 的所有 triplet records;LEFT JOIN library slot 取庫名
|
||
const rows = await c.env.DB.prepare(
|
||
`SELECT
|
||
COALESCE(NULLIF(lib_e.content, ''), 'general') AS library,
|
||
COUNT(*) AS triplet_count
|
||
FROM (
|
||
SELECT DISTINCT ev.record_id
|
||
FROM entry_values ev
|
||
JOIN templates t ON ev.template_id = t.id
|
||
JOIN entries e ON ev.entry_id = e.id
|
||
WHERE t.name = 'triplet'
|
||
AND (?1 = '' OR e.owner_id = ?1)
|
||
) AS tr
|
||
LEFT JOIN entry_values lev
|
||
ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
|
||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')
|
||
ORDER BY library`,
|
||
)
|
||
.bind(owner)
|
||
.all<{ library: string; triplet_count: number }>();
|
||
const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count }));
|
||
return c.json({ success: true, stats });
|
||
});
|
||
|
||
// GET /records/by-template/:template — list records of a template
|
||
recordRoutes.get('/by-template/:template', async (c) => {
|
||
const records = await searchByTemplate(c.env.DB, c.req.param('template'), c.req.query('owner_id') || undefined);
|
||
return c.json({ success: true, records, count: records.length });
|
||
});
|
||
|
||
// GET /records/:recordId
|
||
recordRoutes.get('/:recordId', async (c) => {
|
||
const rec = await getRecord(c.env.DB, c.req.param('recordId'));
|
||
if (!rec) return c.json({ success: false, error: 'not found' }, 404);
|
||
return c.json({ success: true, record: rec });
|
||
});
|
||
|
||
// PATCH /records/:recordId — { values:{slot:content} } update existing record slot values
|
||
// (mira-dissolve T2.1 / issue #6; deprecate = flip a slot value, append-only tables untouched).
|
||
recordRoutes.patch('/:recordId', async (c) => {
|
||
const body = await c.req.json().catch(() => null);
|
||
if (!body || !body.values || typeof body.values !== 'object') {
|
||
return c.json({ success: false, error: 'values required' }, 400);
|
||
}
|
||
try {
|
||
const rec = await updateRecord(c.env.DB, c.req.param('recordId'), body.values);
|
||
if (!rec) return c.json({ success: false, error: 'not found' }, 404);
|
||
return c.json({ success: true, record: rec });
|
||
} catch (e) {
|
||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
|
||
}
|
||
});
|
||
|
||
// DELETE /records/:recordId — 刪除一筆 record 及其底層 entries。
|
||
recordRoutes.delete('/:recordId', async (c) => {
|
||
const found = await deleteRecord(c.env.DB, c.req.param('recordId'));
|
||
if (!found) return c.json({ success: false, error: 'not found' }, 404);
|
||
return c.json({ success: true });
|
||
});
|