/** * In-memory entity store backing the KROW demo. * * Mirrors the Base44 entity API the app was built against * (`list` / `filter` / `get` / `create` / `update` / `delete`) so every hook, * page and component consumes data exactly as it did against the real backend. * Records live in memory and are mirrored to localStorage, so demo edits — * screening a candidate, hiring, creating a position — survive a reload. */ const STORAGE_KEY = 'krow_demo_db'; const STORAGE_VERSION = 8; /** Simulated network latency, in ms, so loading states are real. */ const LATENCY = { read: 140, write: 220 }; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** Structured clone with a JSON fallback for older Safari. */ const clone = (value) => typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value)); let _counter = 0; export function makeId(prefix = 'rec') { _counter += 1; return `${prefix}_${Date.now().toString(36)}${_counter.toString(36).padStart(3, '0')}`; } /* ── Persistence ───────────────────────────────────────────────────────── */ let db = {}; function persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ version: STORAGE_VERSION, db })); } catch { // Private browsing / quota — the demo still works from memory. } } function readPersisted() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); // A seed-data change bumps STORAGE_VERSION, which invalidates stale copies. return parsed?.version === STORAGE_VERSION ? parsed.db : null; } catch { return null; } } /** * Loads the store: persisted snapshot if one matches the current seed version, * otherwise the seed itself. */ export function initStore(seed) { db = readPersisted() || clone(seed); // A seed that gained an entity after the snapshot was written still resolves. for (const [name, records] of Object.entries(seed)) { if (!db[name]) db[name] = clone(records); } persist(); } /** Drops all local edits and restores the shipped demo data. */ export function resetStore(seed) { db = clone(seed); persist(); } /* ── Query helpers ─────────────────────────────────────────────────────── */ /** * Applies a Base44-style sort string: `'-ai_score'` descending, * `'created_date'` ascending. Unknown fields leave order untouched. */ function applySort(records, sort) { if (!sort) return records; const desc = sort.startsWith('-'); const field = desc ? sort.slice(1) : sort; return [...records].sort((a, b) => { const av = a?.[field]; const bv = b?.[field]; if (av === bv) return 0; if (av === undefined || av === null) return 1; if (bv === undefined || bv === null) return -1; const result = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv)); return desc ? -result : result; }); } /** Shallow equality match across every key of `query`, array fields included. */ function matches(record, query) { return Object.entries(query).every(([key, want]) => { const got = record?.[key]; if (Array.isArray(want)) return want.includes(got); return got === want; }); } /* ── Entity API ────────────────────────────────────────────────────────── */ /** * Builds the client surface for one entity. Every method is async and returns * cloned records, so callers can never mutate the store by reference. */ export function createEntity(name) { const table = () => (db[name] ||= []); return { entityName: name, async list(sort = '-created_date', limit = 100) { await delay(LATENCY.read); return clone(applySort(table(), sort).slice(0, limit)); }, async filter(query = {}, sort = '-created_date', limit = 100) { await delay(LATENCY.read); const found = table().filter((record) => matches(record, query)); return clone(applySort(found, sort).slice(0, limit)); }, async get(id) { await delay(LATENCY.read); const found = table().find((record) => record.id === id); if (!found) throw new Error(`${name} ${id} not found`); return clone(found); }, async create(data) { await delay(LATENCY.write); const now = new Date().toISOString(); const record = { id: makeId(name.toLowerCase()), created_date: now, updated_date: now, ...data, }; table().unshift(record); persist(); return clone(record); }, async update(id, data) { await delay(LATENCY.write); const index = table().findIndex((record) => record.id === id); if (index === -1) throw new Error(`${name} ${id} not found`); const next = { ...table()[index], ...data, updated_date: new Date().toISOString() }; table()[index] = next; persist(); return clone(next); }, async delete(id) { await delay(LATENCY.write); db[name] = table().filter((record) => record.id !== id); persist(); return { id }; }, async bulkCreate(records = []) { const created = []; for (const record of records) created.push(await this.create(record)); return created; }, }; }