v3: fixed docker image, updated locales
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 39s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 39s
This commit is contained in:
14
api/db/init.ts
Normal file
14
api/db/init.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { DB_PATH } from "../config.ts";
|
||||
|
||||
try {
|
||||
await Deno.stat(DB_PATH);
|
||||
console.log("Database already exists, skipping initialization.");
|
||||
} catch {
|
||||
console.log("Initializing database from schema...");
|
||||
const schema = Deno.readTextFileSync("api/db/schema.sql");
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
db.exec(schema);
|
||||
db.close();
|
||||
console.log("Database initialized.");
|
||||
}
|
||||
43
api/db/migrate.ts
Normal file
43
api/db/migrate.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { up as up0001CommentLikes } from "./migrations/0001_comment_likes.ts";
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
up: (db: DatabaseSync) => void;
|
||||
}
|
||||
|
||||
// Append-only — never edit or reorder a migration once it has shipped.
|
||||
// Each `up` must be idempotent: it also runs against fresh databases created
|
||||
// straight from schema.sql, where the change it makes already exists.
|
||||
const MIGRATIONS: Migration[] = [
|
||||
{ name: "0001_comment_likes", up: up0001CommentLikes },
|
||||
];
|
||||
|
||||
export function runMigrations(db: DatabaseSync): void {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL
|
||||
);`);
|
||||
|
||||
const applied = new Set(
|
||||
(db.prepare(`SELECT name FROM schema_migrations;`).all() as {
|
||||
name: string;
|
||||
}[]).map((r) => r.name),
|
||||
);
|
||||
|
||||
for (const { name, up } of MIGRATIONS) {
|
||||
if (applied.has(name)) continue;
|
||||
db.exec("BEGIN;");
|
||||
try {
|
||||
up(db);
|
||||
db.prepare(
|
||||
`INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?);`,
|
||||
).run(name, new Date().toISOString());
|
||||
db.exec("COMMIT;");
|
||||
console.log(`[migrate] applied ${name}`);
|
||||
} catch (err) {
|
||||
db.exec("ROLLBACK;");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
api/db/migrations/0001_comment_likes.ts
Normal file
27
api/db/migrations/0001_comment_likes.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
// Idempotent: safe to run against a fresh db (already created from schema.sql
|
||||
// with these columns/tables) or an existing one that predates them.
|
||||
export function up(db: DatabaseSync): void {
|
||||
const commentCols = db.prepare(`PRAGMA table_info(comments);`).all() as {
|
||||
name: string;
|
||||
}[];
|
||||
if (!commentCols.some((c) => c.name === "like_count")) {
|
||||
db.exec(
|
||||
`ALTER TABLE comments ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;`,
|
||||
);
|
||||
}
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS comment_likes (
|
||||
comment_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (comment_id, user_id),
|
||||
FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);`);
|
||||
|
||||
db.exec(
|
||||
`CREATE INDEX IF NOT EXISTS idx_comment_likes_user ON comment_likes(user_id);`,
|
||||
);
|
||||
}
|
||||
161
api/db/schema.sql
Normal file
161
api/db/schema.sql
Normal file
@@ -0,0 +1,161 @@
|
||||
CREATE TABLE dumps (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
comment TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
url TEXT,
|
||||
slug TEXT,
|
||||
rich_content TEXT,
|
||||
file_name TEXT,
|
||||
file_mime TEXT,
|
||||
file_size INTEGER,
|
||||
vote_count INTEGER NOT NULL DEFAULT 0,
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
avatar_mime TEXT,
|
||||
description TEXT,
|
||||
invited_by TEXT REFERENCES users(id),
|
||||
email TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE votes (
|
||||
dump_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (dump_id, user_id),
|
||||
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE playlists (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT,
|
||||
description TEXT,
|
||||
is_public INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
image_mime TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE playlist_dumps (
|
||||
playlist_id TEXT NOT NULL,
|
||||
dump_id TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
PRIMARY KEY (playlist_id, dump_id),
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
dump_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
body TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT,
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
like_count INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE comment_likes (
|
||||
comment_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (comment_id, user_id),
|
||||
FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dumps_user ON dumps(user_id);
|
||||
CREATE INDEX idx_votes_user ON votes(user_id);
|
||||
CREATE INDEX idx_playlists_user ON playlists(user_id);
|
||||
CREATE INDEX idx_playlist_dumps_order ON playlist_dumps(playlist_id, position);
|
||||
CREATE INDEX idx_playlist_dumps_dump ON playlist_dumps(dump_id);
|
||||
CREATE INDEX idx_comments_dump ON comments(dump_id, created_at);
|
||||
CREATE INDEX idx_comment_likes_user ON comment_likes(user_id);
|
||||
|
||||
CREATE TABLE follows (
|
||||
id TEXT PRIMARY KEY,
|
||||
follower_id TEXT NOT NULL,
|
||||
followed_user_id TEXT,
|
||||
followed_playlist_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (followed_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (followed_playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
CHECK (
|
||||
(followed_user_id IS NOT NULL AND followed_playlist_id IS NULL)
|
||||
OR
|
||||
(followed_user_id IS NULL AND followed_playlist_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_follows_user
|
||||
ON follows(follower_id, followed_user_id)
|
||||
WHERE followed_user_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX idx_follows_playlist
|
||||
ON follows(follower_id, followed_playlist_id)
|
||||
WHERE followed_playlist_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_follows_follower ON follows(follower_id);
|
||||
|
||||
CREATE TABLE invites (
|
||||
token TEXT PRIMARY KEY,
|
||||
inviter_id TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
resource_id TEXT,
|
||||
mime TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_attachments_resource ON attachments(resource_id);
|
||||
|
||||
CREATE TABLE password_reset_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
source_key TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_notifications_user ON notifications(user_id, created_at);
|
||||
CREATE UNIQUE INDEX idx_notifications_dedup
|
||||
ON notifications(user_id, source_key)
|
||||
WHERE source_key IS NOT NULL;
|
||||
Reference in New Issue
Block a user