Switch to JSON store so Electron runs without native sqlite rebuild.
Auto-seeds 10 presets on first launch; visual designer with palette, drag, resize, and bricks CRUD. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+58
-89
@@ -1,47 +1,58 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
import type { CharacterSheet, SystemDefinition } from '../shared/types.ts';
|
||||
import { validateSystemDefinition } from '../shared/systemValidate.ts';
|
||||
|
||||
type Meta = {
|
||||
systems: Record<string, SystemDefinition>;
|
||||
characters: Record<string, CharacterSheet>;
|
||||
};
|
||||
|
||||
export class AppStore {
|
||||
private readonly db: Database.Database;
|
||||
private readonly rootDir: string;
|
||||
private readonly dbPath: string;
|
||||
private data: Meta = { systems: {}, characters: {} };
|
||||
|
||||
constructor(dbPath: string) {
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
this.db = new Database(dbPath);
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS systems (
|
||||
id TEXT PRIMARY KEY,
|
||||
revision INTEGER NOT NULL,
|
||||
schema_version INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS characters (
|
||||
id TEXT PRIMARY KEY,
|
||||
system_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
// dbPath historically pointed at *.sqlite — keep parent dir, use store.json
|
||||
this.rootDir = path.dirname(dbPath);
|
||||
this.dbPath = path.join(this.rootDir, 'store.json');
|
||||
fs.mkdirSync(this.rootDir, { recursive: true });
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
try {
|
||||
const raw = fs.readFileSync(this.dbPath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as Meta;
|
||||
this.data = {
|
||||
systems: parsed.systems ?? {},
|
||||
characters: parsed.characters ?? {},
|
||||
};
|
||||
} catch {
|
||||
this.data = { systems: {}, characters: {} };
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
private persist(): void {
|
||||
fs.writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
listSystems(): Array<{ id: string; name: string; revision: number; schemaVersion: number }> {
|
||||
return this.db
|
||||
.prepare('SELECT id, name, revision, schema_version AS schemaVersion FROM systems ORDER BY name')
|
||||
.all() as Array<{ id: string; name: string; revision: number; schemaVersion: number }>;
|
||||
return Object.values(this.data.systems)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
revision: s.revision,
|
||||
schemaVersion: s.schemaVersion,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
getSystem(id: string): SystemDefinition | null {
|
||||
const row = this.db.prepare('SELECT json FROM systems WHERE id = ?').get(id) as
|
||||
| { json: string }
|
||||
| undefined;
|
||||
return row ? (JSON.parse(row.json) as SystemDefinition) : null;
|
||||
return this.data.systems[id] ?? null;
|
||||
}
|
||||
|
||||
upsertSystem(system: SystemDefinition): SystemDefinition {
|
||||
@@ -49,89 +60,47 @@ export class AppStore {
|
||||
if (!check.ok) {
|
||||
throw new Error(check.errors.map((e) => e.message).join('; '));
|
||||
}
|
||||
const existing = this.getSystem(system.id);
|
||||
const existing = this.data.systems[system.id];
|
||||
const next: SystemDefinition = {
|
||||
...system,
|
||||
revision: existing ? existing.revision + 1 : system.revision || 1,
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO systems (id, revision, schema_version, name, json, updated_at)
|
||||
VALUES (@id, @revision, @schemaVersion, @name, @json, @updatedAt)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
revision=excluded.revision,
|
||||
schema_version=excluded.schema_version,
|
||||
name=excluded.name,
|
||||
json=excluded.json,
|
||||
updated_at=excluded.updated_at`,
|
||||
)
|
||||
.run({
|
||||
id: next.id,
|
||||
revision: next.revision,
|
||||
schemaVersion: next.schemaVersion,
|
||||
name: next.name,
|
||||
json: JSON.stringify(next),
|
||||
updatedAt: now,
|
||||
});
|
||||
this.data.systems[next.id] = next;
|
||||
this.persist();
|
||||
return next;
|
||||
}
|
||||
|
||||
deleteSystem(id: string): void {
|
||||
const tx = this.db.transaction(() => {
|
||||
this.db.prepare('DELETE FROM characters WHERE system_id = ?').run(id);
|
||||
this.db.prepare('DELETE FROM systems WHERE id = ?').run(id);
|
||||
});
|
||||
tx();
|
||||
delete this.data.systems[id];
|
||||
for (const [cid, ch] of Object.entries(this.data.characters)) {
|
||||
if (ch.systemId === id) delete this.data.characters[cid];
|
||||
}
|
||||
this.persist();
|
||||
}
|
||||
|
||||
listCharacters(systemId?: string): Array<{ id: string; name: string; systemId: string }> {
|
||||
if (systemId) {
|
||||
return this.db
|
||||
.prepare(
|
||||
'SELECT id, name, system_id AS systemId FROM characters WHERE system_id = ? ORDER BY name',
|
||||
)
|
||||
.all(systemId) as Array<{ id: string; name: string; systemId: string }>;
|
||||
}
|
||||
return this.db
|
||||
.prepare('SELECT id, name, system_id AS systemId FROM characters ORDER BY name')
|
||||
.all() as Array<{ id: string; name: string; systemId: string }>;
|
||||
return Object.values(this.data.characters)
|
||||
.filter((c) => (systemId ? c.systemId === systemId : true))
|
||||
.map((c) => ({ id: c.id, name: c.name, systemId: c.systemId }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
getCharacter(id: string): CharacterSheet | null {
|
||||
const row = this.db.prepare('SELECT json FROM characters WHERE id = ?').get(id) as
|
||||
| { json: string }
|
||||
| undefined;
|
||||
return row ? (JSON.parse(row.json) as CharacterSheet) : null;
|
||||
return this.data.characters[id] ?? null;
|
||||
}
|
||||
|
||||
upsertCharacter(character: CharacterSheet): CharacterSheet {
|
||||
const now = new Date().toISOString();
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO characters (id, system_id, name, json, updated_at)
|
||||
VALUES (@id, @systemId, @name, @json, @updatedAt)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
system_id=excluded.system_id,
|
||||
name=excluded.name,
|
||||
json=excluded.json,
|
||||
updated_at=excluded.updated_at`,
|
||||
)
|
||||
.run({
|
||||
id: character.id,
|
||||
systemId: character.systemId,
|
||||
name: character.name,
|
||||
json: JSON.stringify(character),
|
||||
updatedAt: now,
|
||||
});
|
||||
this.data.characters[character.id] = character;
|
||||
this.persist();
|
||||
return character;
|
||||
}
|
||||
|
||||
deleteCharacter(id: string): void {
|
||||
this.db.prepare('DELETE FROM characters WHERE id = ?').run(id);
|
||||
delete this.data.characters[id];
|
||||
this.persist();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close();
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user