Files
task-inventory/lib/storage.mjs
mikael-lovqvists-claude-agent 0a985c591b Refactor tasks to configurable entry types
- Storage: nested object buckets per type, integrated next_id into meta,
  migration from old flat task:N key format, seeded default types (task/idea/note)
- Server: /api/entry-types CRUD + /api/entries replacing /api/tasks, type
  validation, blocks deletion if entries exist
- Frontend: dynamic nav per type, Entry_Type_Dialog, Entry_Dialog with type
  selector, type chip in All view, Manage Types view

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:53:39 +00:00

153 lines
4.8 KiB
JavaScript

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) {
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;
}