174 lines
5.5 KiB
JavaScript
174 lines
5.5 KiB
JavaScript
/**
|
|
* 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;
|
|
},
|
|
};
|
|
}
|