Implement local SQLite backend and reactive UI

This commit is contained in:
2026-05-14 21:28:32 -04:00
parent 4aa3f7b362
commit f95b0ae912
35 changed files with 5444 additions and 2009 deletions

View File

@@ -1,16 +1,50 @@
import type { Db } from "../db/database.js";
import { getModel, updateModelCell } from "../db/queries.js";
import { ok } from "./result.js";
import { createModelRow, deleteModelRow, getModel, resolveCompany, updateModelCell } from "../db/queries.js";
import { fail, ok } from "./result.js";
import type { RpcHandlers } from "./types.js";
export function modelHandlers(db: Db): RpcHandlers<"model.get" | "model.updateCell" | "model.runScenario"> {
function errorDetail(operation: string, error: unknown): { operation: string; message?: string } {
return {
"model.get": ({ companyId, tab }) => ok("model.get", getModel(db, companyId, tab)),
"model.updateCell": ({ companyId, tab, row, col, value }) =>
ok("model.updateCell", updateModelCell(db, companyId, tab, row, col, value)),
"model.runScenario": ({ companyId }) => {
const model = getModel(db, companyId, "income");
return ok("model.runScenario", model);
operation,
message: error instanceof Error ? error.message : undefined,
};
}
export function modelHandlers(db: Db): RpcHandlers<"model.get" | "model.updateCell" | "model.createRow" | "model.deleteRow" | "model.runScenario"> {
return {
"model.get": ({ companyId, tab }) => {
const company = resolveCompany(db, companyId);
if (!company) return fail("NOT_FOUND", `Company "${companyId}" not found.`);
try {
return ok("model.get", getModel(db, company.id, tab));
} catch (error) {
return fail("INTERNAL_ERROR", "Could not load model for company.", errorDetail("getModel", error));
}
},
"model.updateCell": ({ companyId, tab, row, col, value }) => {
const company = resolveCompany(db, companyId);
if (!company) return fail("NOT_FOUND", `Company "${companyId}" not found.`);
return ok("model.updateCell", updateModelCell(db, company.id, tab, row, col, value));
},
"model.createRow": ({ companyId, tab, label, kind, values }) => {
const company = resolveCompany(db, companyId);
if (!company) return fail("NOT_FOUND", `Company "${companyId}" not found.`);
return ok("model.createRow", createModelRow(db, company.id, tab, { label, kind, values: values ?? [] }));
},
"model.deleteRow": ({ companyId, tab, row }) => {
const company = resolveCompany(db, companyId);
if (!company) return fail("NOT_FOUND", `Company "${companyId}" not found.`);
return ok("model.deleteRow", { ok: deleteModelRow(db, company.id, tab, row) });
},
"model.runScenario": ({ companyId, overrides }) => {
const company = resolveCompany(db, companyId);
if (!company) return fail("NOT_FOUND", `Company "${companyId}" not found.`);
const model = getModel(db, company.id, "operating");
const rows = model.rows.map((row, rowIndex) => ({
...row,
values: row.values.map((value, colIndex) => overrides[`${rowIndex}-${colIndex}`] ?? value),
}));
return ok("model.runScenario", { headers: model.headers, rows });
},
};
}