import { mkdirSync } from 'node:fs'; import { Simple_KeyValue_Store } from './kv-store.mjs'; mkdirSync('./data', { recursive: true }); const store = new Simple_KeyValue_Store('./data/tasks.ndjson', { auto_load: true, auto_store: true, debounce_flush_timeout: 5000, }); // --------------------------------------------------------------------------- // Migration from old flat-key format (task:1, task:2, ...) // --------------------------------------------------------------------------- function migrate_if_needed() { const old_keys = []; for (const [key] of store.data.entries()) { if (key.startsWith('task:')) { old_keys.push(key); } } if (!old_keys.length) { return; } console.log(`[storage] Migrating ${old_keys.length} old flat-key entries...`); const task_bucket = store.get('task') ?? {}; let max_id = store.get('meta')?.next_id ?? 0; for (const key of old_keys) { const entry = store.get(key); task_bucket[entry.id] = { ...entry, type: 'task' }; if (typeof entry.id === 'number' && entry.id > max_id) { max_id = entry.id; } store.data.delete(key); } store.set('task', task_bucket); // Absorb old ids.ndjson counter if present try { const ids_store = new Simple_KeyValue_Store('./data/ids.ndjson', { auto_load: true }); const old_counter = ids_store.get('seq:task'); if (old_counter && old_counter > max_id) { max_id = old_counter; } } catch {} store.set('meta', { ...(store.get('meta') ?? {}), next_id: max_id }); store.store(); console.log(`[storage] Migration complete. next_id=${max_id}`); } // --------------------------------------------------------------------------- // Seed default entry types on a fresh store // --------------------------------------------------------------------------- function seed_defaults() { const now = Date.now(); store.set('entry_types', { task: { id: 'task', title: 'Task', description: 'A unit of work to be completed.', created_at: now, updated_at: now }, idea: { id: 'idea', title: 'Idea', description: 'A thought, concept, or possibility to explore.', created_at: now, updated_at: now }, note: { id: 'note', title: 'Note', description: 'A reference, observation, or piece of information.', created_at: now, updated_at: now }, }); if (!store.get('task')) { store.set('task', {}); } if (!store.get('idea')) { store.set('idea', {}); } if (!store.get('note')) { store.set('note', {}); } store.store(); } migrate_if_needed(); if (!store.get('entry_types')) { seed_defaults(); } if (!store.get('meta')) { store.set('meta', { next_id: 0 }); } // --------------------------------------------------------------------------- // ID generation // --------------------------------------------------------------------------- export function next_id() { const meta = store.get('meta') ?? { next_id: 0 }; meta.next_id = (meta.next_id ?? 0) + 1; store.set('meta', meta); return meta.next_id; } // --------------------------------------------------------------------------- // Entry types // --------------------------------------------------------------------------- export function list_entry_types() { return Object.values(store.get('entry_types') ?? {}); } export function get_entry_type(id) { return (store.get('entry_types') ?? {})[id] ?? null; } export function set_entry_type(type) { const types = store.get('entry_types') ?? {}; types[type.id] = type; store.set('entry_types', types); if (!store.get(type.id)) { store.set(type.id, {}); } } export function delete_entry_type(id) { const types = store.get('entry_types') ?? {}; if (!types[id]) { return false; } delete types[id]; store.set('entry_types', types); return true; } // --------------------------------------------------------------------------- // Entries // --------------------------------------------------------------------------- export function list_entries(type = null) { const type_keys = type ? [type] : Object.keys(store.get('entry_types') ?? {}); const result = []; for (const t of type_keys) { result.push(...Object.values(store.get(t) ?? {})); } return result.sort((a, b) => b.created_at - a.created_at); } export function get_entry(id) { for (const t of Object.keys(store.get('entry_types') ?? {})) { const entry = (store.get(t) ?? {})[id]; if (entry) { return entry; } } return null; } export function set_entry(entry) { // Remove from any other bucket in case type changed for (const t of Object.keys(store.get('entry_types') ?? {})) { if (t === entry.type) { continue; } const bucket = store.get(t); if (bucket?.[entry.id]) { delete bucket[entry.id]; store.set(t, bucket); } } const bucket = store.get(entry.type) ?? {}; bucket[entry.id] = entry; store.set(entry.type, bucket); } export function delete_entry(id) { for (const t of Object.keys(store.get('entry_types') ?? {})) { const bucket = store.get(t); if (bucket?.[id]) { delete bucket[id]; store.set(t, bucket); return true; } } return false; } export function has_child_entries(parent_id) { for (const t of Object.keys(store.get('entry_types') ?? {})) { if (Object.values(store.get(t) ?? {}).some(e => e.parent_id === parent_id)) { return true; } } return false; }