diff --git a/.gitignore b/.gitignore index 5ef6a52..264faa3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ /.next/ /out/ +# local database +/data/*.sqlite +/data/*.sqlite-* + # production /build diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..fea0ff7 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL CHECK (length(trim(title)) > 0), + description TEXT NOT NULL DEFAULT '', + due_date TEXT NOT NULL CHECK (due_date GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'), + topic TEXT NOT NULL CHECK (length(trim(topic)) > 0), + status TEXT NOT NULL DEFAULT 'Todo' CHECK (status IN ('Todo', 'In-Progress', 'Complete')), + archived_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_tasks_active_status +ON tasks (archived_at, status, due_date); + +CREATE INDEX IF NOT EXISTS idx_tasks_topic +ON tasks (topic COLLATE NOCASE); diff --git a/lib/db.ts b/lib/db.ts index e69de29..667c314 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -0,0 +1,36 @@ +import fs from "node:fs"; +import path from "node:path"; +import { DatabaseSync } from "node:sqlite"; + +type DatabaseGlobal = typeof globalThis & { + plannerDatabase?: DatabaseSync; + plannerDatabasePath?: string; +}; + +const schemaPath = path.join(process.cwd(), "db", "schema.sql"); +const defaultDatabasePath = path.join(process.cwd(), "data", "planner.sqlite"); + +export function getDatabase() { + const databasePath = process.env.TASKS_DB_PATH ?? defaultDatabasePath; + const globalDatabase = globalThis as DatabaseGlobal; + + if ( + globalDatabase.plannerDatabase && + globalDatabase.plannerDatabasePath === databasePath + ) { + return globalDatabase.plannerDatabase; + } + + fs.mkdirSync(path.dirname(databasePath), { recursive: true }); + + const database = new DatabaseSync(databasePath); + const schema = fs.readFileSync(schemaPath, "utf8"); + + database.exec("PRAGMA foreign_keys = ON;"); + database.exec(schema); + + globalDatabase.plannerDatabase = database; + globalDatabase.plannerDatabasePath = databasePath; + + return database; +} diff --git a/types/node-sqlite.d.ts b/types/node-sqlite.d.ts new file mode 100644 index 0000000..94ef807 --- /dev/null +++ b/types/node-sqlite.d.ts @@ -0,0 +1,19 @@ +declare module "node:sqlite" { + export type RunResult = { + changes: number; + lastInsertRowid: number | bigint; + }; + + export class StatementSync { + all(...params: unknown[]): unknown[]; + get(...params: unknown[]): unknown; + run(...params: unknown[]): RunResult; + } + + export class DatabaseSync { + constructor(location?: string | URL, options?: Record); + close(): void; + exec(sql: string): void; + prepare(sql: string): StatementSync; + } +}