feat: add SQLite database and persistent task schema
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,6 +17,10 @@
|
|||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
|
# local database
|
||||||
|
/data/*.sqlite
|
||||||
|
/data/*.sqlite-*
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
|||||||
17
db/schema.sql
Normal file
17
db/schema.sql
Normal file
@@ -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);
|
||||||
36
lib/db.ts
36
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;
|
||||||
|
}
|
||||||
|
|||||||
19
types/node-sqlite.d.ts
vendored
Normal file
19
types/node-sqlite.d.ts
vendored
Normal file
@@ -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<string, unknown>);
|
||||||
|
close(): void;
|
||||||
|
exec(sql: string): void;
|
||||||
|
prepare(sql: string): StatementSync;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user