Init
This commit is contained in:
+142
@@ -0,0 +1,142 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { hashSync } from "bcryptjs";
|
||||
import { getDatabasePath, getUploadDir } from "./env";
|
||||
|
||||
declare global {
|
||||
var __mopedDatabase: DatabaseSync | undefined;
|
||||
var __mopedDatabaseInitialized: boolean | undefined;
|
||||
}
|
||||
|
||||
export const db =
|
||||
globalThis.__mopedDatabase ??
|
||||
new DatabaseSync(getDatabasePath(), {
|
||||
enableForeignKeyConstraints: true,
|
||||
});
|
||||
|
||||
globalThis.__mopedDatabase = db;
|
||||
|
||||
function ensureSchema() {
|
||||
db.exec(`
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA busy_timeout = 5000;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS motorcycles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nickname TEXT NOT NULL,
|
||||
brand TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
year INTEGER,
|
||||
vin TEXT,
|
||||
plate TEXT,
|
||||
current_km INTEGER NOT NULL DEFAULT 0 CHECK (current_km >= 0),
|
||||
notes TEXT,
|
||||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maintenance_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL CHECK (type IN ('reparatur', 'tuev_hu', 'inspektion', 'reifenwechsel', 'oelwechsel', 'sonstiges')),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
event_date TEXT NOT NULL,
|
||||
km INTEGER CHECK (km IS NULL OR km >= 0),
|
||||
cost_cents INTEGER CHECK (cost_cents IS NULL OR cost_cents >= 0),
|
||||
due_date TEXT,
|
||||
due_km INTEGER CHECK (due_km IS NULL OR due_km >= 0),
|
||||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
motorcycle_id INTEGER NOT NULL REFERENCES motorcycles(id) ON DELETE CASCADE,
|
||||
trip_date TEXT NOT NULL,
|
||||
driver_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
start_km INTEGER NOT NULL CHECK (start_km >= 0),
|
||||
end_km INTEGER NOT NULL CHECK (end_km >= start_km),
|
||||
purpose TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
maintenance_event_id INTEGER NOT NULL UNIQUE REFERENCES maintenance_events(id) ON DELETE CASCADE,
|
||||
stored_name TEXT NOT NULL UNIQUE,
|
||||
original_name TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_maintenance_motorcycle
|
||||
ON maintenance_events(motorcycle_id, event_date DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_maintenance_due
|
||||
ON maintenance_events(motorcycle_id, type, event_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_trips_motorcycle
|
||||
ON trips(motorcycle_id, trip_date DESC, id DESC);
|
||||
`);
|
||||
}
|
||||
|
||||
function ensureInitialAdmin() {
|
||||
const row = db.prepare("SELECT COUNT(*) AS count FROM users").get() as
|
||||
| { count: number }
|
||||
| undefined;
|
||||
|
||||
if ((row?.count ?? 0) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = process.env.ADMIN_EMAIL?.trim().toLowerCase();
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
const name = process.env.ADMIN_NAME?.trim();
|
||||
|
||||
if (!email || !password || !name) {
|
||||
throw new Error(
|
||||
"Keine Benutzer vorhanden. ADMIN_EMAIL, ADMIN_PASSWORD und ADMIN_NAME müssen gesetzt sein.",
|
||||
);
|
||||
}
|
||||
|
||||
if (password.length < 10) {
|
||||
throw new Error("ADMIN_PASSWORD muss mindestens 10 Zeichen lang sein.");
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
"INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, 'admin')",
|
||||
).run(name, email, hashSync(password, 12));
|
||||
}
|
||||
|
||||
export function initDatabase() {
|
||||
if (globalThis.__mopedDatabaseInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
getUploadDir();
|
||||
ensureSchema();
|
||||
ensureInitialAdmin();
|
||||
globalThis.__mopedDatabaseInitialized = true;
|
||||
}
|
||||
|
||||
export function transaction<T>(work: () => T): T {
|
||||
db.exec("BEGIN IMMEDIATE");
|
||||
try {
|
||||
const result = work();
|
||||
db.exec("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
db.exec("ROLLBACK");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user