chore: initial commit — monorepo ReadaBook (API NestJS, web PWA, Docker)
This commit is contained in:
124
apps/api/src/database/database.service.ts
Normal file
124
apps/api/src/database/database.service.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { Injectable, OnModuleDestroy } from "@nestjs/common";
|
||||
import Database from "better-sqlite3";
|
||||
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { AppConfig, loadConfig } from "../config/env.js";
|
||||
import * as schema from "./schema.js";
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService implements OnModuleDestroy {
|
||||
readonly config: AppConfig;
|
||||
readonly sqlite: Database.Database;
|
||||
readonly db: BetterSQLite3Database<typeof schema>;
|
||||
|
||||
constructor() {
|
||||
this.config = loadConfig();
|
||||
this.sqlite = new Database(this.config.databasePath);
|
||||
this.sqlite.pragma("journal_mode = WAL");
|
||||
this.sqlite.pragma("foreign_keys = ON");
|
||||
this.sqlite.pragma("busy_timeout = 5000");
|
||||
this.db = drizzle(this.sqlite, { schema });
|
||||
this.migrate();
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
this.sqlite.close();
|
||||
}
|
||||
|
||||
now(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
private migrate(): void {
|
||||
this.sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin','user')),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS books (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
library_id INTEGER NOT NULL REFERENCES libraries(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT,
|
||||
description TEXT,
|
||||
isbn TEXT,
|
||||
language TEXT,
|
||||
publisher TEXT,
|
||||
published_date TEXT,
|
||||
format TEXT NOT NULL CHECK (format IN ('epub','pdf')),
|
||||
file_path TEXT NOT NULL UNIQUE,
|
||||
cover_path TEXT,
|
||||
file_size INTEGER NOT NULL,
|
||||
file_mtime TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS progress (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
|
||||
locator TEXT NOT NULL,
|
||||
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(user_id, book_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('queued','running','succeeded','failed')),
|
||||
detail TEXT,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS book_fts USING fts5(
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
isbn,
|
||||
content='books',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS books_library_idx ON books(library_id);
|
||||
CREATE INDEX IF NOT EXISTS books_title_idx ON books(title);
|
||||
CREATE INDEX IF NOT EXISTS jobs_status_idx ON jobs(status);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
|
||||
INSERT INTO book_fts(rowid, title, author, description, isbn)
|
||||
VALUES (new.id, new.title, new.author, new.description, new.isbn);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS books_ad AFTER DELETE ON books BEGIN
|
||||
INSERT INTO book_fts(book_fts, rowid, title, author, description, isbn)
|
||||
VALUES('delete', old.id, old.title, old.author, old.description, old.isbn);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS books_au AFTER UPDATE ON books BEGIN
|
||||
INSERT INTO book_fts(book_fts, rowid, title, author, description, isbn)
|
||||
VALUES('delete', old.id, old.title, old.author, old.description, old.isbn);
|
||||
INSERT INTO book_fts(rowid, title, author, description, isbn)
|
||||
VALUES (new.id, new.title, new.author, new.description, new.isbn);
|
||||
END;
|
||||
`);
|
||||
this.sqlite.exec("INSERT INTO book_fts(book_fts) VALUES('rebuild')");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user