UNPKG

@athenox/talos

Version:

Talos: a backend you describe instead of write. Declare a schema, write the rules in plain language, map URLs to sentences; a small built-in model turns them into query plans.

68 lines (67 loc) • 3.89 kB
import { mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { sqliteStore } from './stores.js'; /** * Talos's own database, for an app that gives it none: a SQLite file * (TALOS_DB, default `.talos/data.db` in the working directory) whose tables * are the schema's. Nothing to configure — the schema already says it: * * - every physical column of a table (and of its relations' keys) exists; * - `id` is the integer primary key; * - a declared value column defaults to its first value (`role: ['member', 'admin']`); * - `types: { minutes: 'number' }` makes it numeric; the rest keeps what is written. * * Run on every start: missing tables are created and missing columns added, * nothing is ever dropped. `seed` rows (logical or physical column names) go * into tables that are still empty, hashed and defaulted as the schema says. */ export async function localStore(schema, o = {}) { const file = o.file ?? process.env.TALOS_DB ?? resolve(process.cwd(), '.talos', 'data.db'); if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true }); // Loaded here, not at the top: an app with its own store never loads node:sqlite (nor sees its warning). const { DatabaseSync } = await import('node:sqlite'); const db = new DatabaseSync(file); db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON'); const q = (s) => `"${s.replace(/"/g, '""')}"`; for (const t of schema.tables) { const cols = new Set([...Object.values(t.columns), ...Object.values(t.relations ?? {}).map(r => r.on), ...(t.ownerColumn ? [t.ownerColumn] : []), ...Object.keys(t.files ?? {}), ...Object.values(t.files ?? {}).flatMap(f => [...Object.keys(f.variants ?? {}), ...Object.values(f.record ?? {})])]); const logical = Object.fromEntries(Object.entries(t.columns).map(([l, p]) => [p, l])); const def = (c) => { if (c === 'id') return `${q(c)} INTEGER PRIMARY KEY`; const values = t.enums?.[logical[c] ?? c]; if (values?.length) return `${q(c)} TEXT DEFAULT '${values[0].replace(/'/g, "''")}'`; const type = t.types?.[c]; return `${q(c)}${type === 'number' || type === 'boolean' ? ' NUMERIC' : Object.values(t.relations ?? {}).some(r => r.on === c) ? ' INTEGER' : ''}`; }; db.exec(`CREATE TABLE IF NOT EXISTS ${q(t.table)} (${[...cols].map(def).join(', ')})`); const have = new Set(db.prepare(`PRAGMA table_info(${q(t.table)})`).all().map(r => r.name)); for (const c of cols) if (!have.has(c)) db.exec(`ALTER TABLE ${q(t.table)} ADD COLUMN ${def(c).replace(' PRIMARY KEY', '')}`); } for (const [name, rows] of Object.entries(o.seed ?? {})) { const t = schema.tables.find(x => x.as === name || x.table === name); if (!t) throw new Error(`talos seed: no table ${name}`); if (db.prepare(`SELECT 1 FROM ${q(t.table)} LIMIT 1`).get()) continue; for (const r of rows) { const row = Object.fromEntries(Object.entries(r).map(([k, v]) => [t.columns[k] ?? k, v])); for (const [c, f] of Object.entries(t.defaults?.insert ?? {})) if (row[c] === undefined) row[c] = f(); for (const [c, f] of Object.entries(t.hash ?? {})) if (typeof row[c] === 'string') row[c] = await f(row[c]); const keys = Object.keys(row); db.prepare(`INSERT INTO ${q(t.table)} (${keys.map(q).join(', ')}) VALUES (${keys.map(() => '?').join(', ')})`) .run(...keys.map(k => { const v = row[k]; return v instanceof Date ? v.toISOString() : typeof v === 'boolean' ? Number(v) : (v ?? null); })); } } return { ...sqliteStore(db), file }; }