Initial commit
This commit is contained in:
173
src/api/store.js
Normal file
173
src/api/store.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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 = 3;
|
||||
|
||||
/** 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user