// Entry CRUD — atomic data + tree (project/workflow via parent_id). Base, D1 only. import type { Bindings, Entry } from '../types'; function uid(prefix: string): string { // deterministic-enough unique id without Math.random in hot path is fine here; // crypto.randomUUID is available in Workers runtime. return `${prefix}_${crypto.randomUUID()}`; } export interface CreateEntryInput { content?: string | null; entry_type: string; owner_id?: string | null; parent_id?: string | null; page_name?: string | null; refs_json?: string; tags_json?: string; task_status?: string | null; confidence?: number | null; metadata_json?: string | null; id?: string; } export async function createEntry(db: D1Database, input: CreateEntryInput): Promise { const id = input.id ?? uid('e'); await db .prepare( `INSERT INTO entries (id, content, entry_type, owner_id, parent_id, page_name, refs_json, tags_json, task_status, confidence, metadata_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( id, input.content ?? null, input.entry_type, input.owner_id ?? null, input.parent_id ?? null, input.page_name ?? null, input.refs_json ?? '[]', input.tags_json ?? '[]', input.task_status ?? null, input.confidence ?? null, input.metadata_json ?? null, ) .run(); const row = await getEntry(db, id); if (!row) throw new Error('createEntry: insert succeeded but row not found'); return row; } export async function getEntry(db: D1Database, id: string): Promise { const row = await db.prepare('SELECT * FROM entries WHERE id = ?').bind(id).first(); return row ?? null; } export interface ListEntriesFilter { entry_type?: string; owner_id?: string; parent_id?: string; page_name?: string; // exact-match lookup (e.g. skill-/example- idempotency key) limit?: number; offset?: number; } export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Promise { const conds: string[] = []; const params: unknown[] = []; if (f.entry_type) { conds.push('entry_type = ?'); params.push(f.entry_type); } if (f.owner_id) { conds.push('owner_id = ?'); params.push(f.owner_id); } if (f.parent_id) { conds.push('parent_id = ?'); params.push(f.parent_id); } if (f.page_name) { conds.push('page_name = ?'); params.push(f.page_name); } 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(); return res.results ?? []; } export interface UpdateEntryInput { content?: string | null; parent_id?: string | null; page_name?: string | null; refs_json?: string; tags_json?: string; task_status?: string | null; confidence?: number | null; metadata_json?: string | null; } export async function updateEntry(db: D1Database, id: string, patch: UpdateEntryInput): Promise { const cols: string[] = []; const params: unknown[] = []; const map: Record = patch as Record; for (const k of ['content', 'parent_id', 'page_name', 'refs_json', 'tags_json', 'task_status', 'confidence', 'metadata_json']) { if (k in map && map[k] !== undefined) { cols.push(`${k} = ?`); params.push(map[k]); } } if (cols.length === 0) return getEntry(db, id); cols.push('updated_at = unixepoch()'); await db.prepare(`UPDATE entries SET ${cols.join(', ')} WHERE id = ?`).bind(...params, id).run(); return getEntry(db, id); } export async function deleteEntry(db: D1Database, id: string): Promise { await db.prepare('DELETE FROM entries WHERE id = ?').bind(id).run(); } // D1 LIKE keyword search (base; semantic search is the optional embed module). export async function searchEntries(db: D1Database, q: string, owner_id?: string, limit = 50): Promise { const conds = ['content LIKE ?']; const params: unknown[] = [`%${q}%`]; if (owner_id) { conds.push('owner_id = ?'); params.push(owner_id); } const res = await db .prepare(`SELECT * FROM entries WHERE ${conds.join(' AND ')} ORDER BY updated_at DESC LIMIT ?`) .bind(...params, Math.min(limit, 200)) .all(); return res.results ?? []; }