Error statuses follow one rule: APIException takes a status and derives the code from it (BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403, NOT_FOUND 404, CONFLICT 409, RATE_LIMITED 429, SERVER_ERROR 500, BAD_GATEWAY 502), so a code can't contradict its status. That fixed real bugs: a wrong current password on change-password answered 401, on which authFetch signs the user out; editing or deleting someone else's comment or chat message answered 401 rather than 403; one invalid dump edit answered 422. A taken username is 409, like a taken email. Along the way: the database is created from schema.sql by connection.ts when the file doesn't exist, so every task works on a fresh checkout (dev and serve never ran db/init.ts, which is gone); the startup purge compared ISO dates with SQLite's datetime() as text and kept anything from the current UTC day, and now compares ISO cutoffs; the seeded admin's hash comes from lib/password.ts (split out of jwt.ts, which needs the JWT secret) rather than a copy of its format; avatar, thumbnail and playlist-cover uploads go through replaceUploadedFile, which writes beside the old file and renames over it before the record announces it (a failed avatar write lost the old avatar; a thumbnail was announced before it existed); upload.ts derives its size message from MAX_IMAGE_SIZE_BYTES and stops re-exporting the upload folders; ALLOWED_IMAGE_MIMES, which duplicated detectImageMime, is gone; the SPA fallback no longer calls next() after answering; a reply must answer a comment on the same dump (a foreign or unknown parent was a 500 or a cross-dump thread). 211 backend and 628 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
/**
|
|
* Brings the database up to date with the migrations under migrations/, and
|
|
* records each one it applies in `schema_migrations`. connection.ts runs it
|
|
* each time the server opens the database.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { DatabaseSync } from "node:sqlite";
|
|
import { up as up0001CommentLikes } from "./migrations/0001_comment_likes.ts";
|
|
import { up as up0002DumpThumbnail } from "./migrations/0002_dump_thumbnail.ts";
|
|
import { up as up0003DumpBacklinks } from "./migrations/0003_dump_backlinks.ts";
|
|
import { up as up0004UserRoles } from "./migrations/0004_user_roles.ts";
|
|
import { up as up0005DropIsAdmin } from "./migrations/0005_drop_is_admin.ts";
|
|
import { up as up0006Categories } from "./migrations/0006_categories.ts";
|
|
import { up as up0007PasswordResetTokens } from "./migrations/0007_password_reset_tokens.ts";
|
|
import { up as up0008ChatMessages } from "./migrations/0008_chat_messages.ts";
|
|
import { up as up0009ChatReply } from "./migrations/0009_chat_reply.ts";
|
|
import { up as up0010YoutubeEmbedStart } from "./migrations/0010_youtube_embed_start.ts";
|
|
import { up as up0011SplitFaviconThumbnail } from "./migrations/0011_split_favicon_thumbnail.ts";
|
|
import { up as up0012FixFaviconReclassification } from "./migrations/0012_fix_favicon_reclassification.ts";
|
|
import { up as up0013DumpUrlCanonical } from "./migrations/0013_dump_url_canonical.ts";
|
|
import { up as up0014ChatReadState } from "./migrations/0014_chat_read_state.ts";
|
|
import { up as up0015TokenRevocation } from "./migrations/0015_token_revocation.ts";
|
|
import { up as up0016PerformanceIndexes } from "./migrations/0016_performance_indexes.ts";
|
|
import { up as up0017CaseInsensitiveUsers } from "./migrations/0017_case_insensitive_users.ts";
|
|
import { up as up0018AttachmentResourceType } from "./migrations/0018_attachment_resource_type.ts";
|
|
|
|
interface Migration {
|
|
name: string;
|
|
up: (db: DatabaseSync) => void;
|
|
}
|
|
|
|
/**
|
|
* Every migration, in the order they run. A new one goes at the end, along
|
|
* with the same change to schema.sql, and a shipped one is never edited or
|
|
* moved. A database connection.ts made from schema.sql records none of them,
|
|
* so each `up` must find its change already made and leave it be.
|
|
*/
|
|
const MIGRATIONS: Migration[] = [
|
|
{ name: "0001_comment_likes", up: up0001CommentLikes },
|
|
{ name: "0002_dump_thumbnail", up: up0002DumpThumbnail },
|
|
{ name: "0003_dump_backlinks", up: up0003DumpBacklinks },
|
|
{ name: "0004_user_roles", up: up0004UserRoles },
|
|
{ name: "0005_drop_is_admin", up: up0005DropIsAdmin },
|
|
{ name: "0006_categories", up: up0006Categories },
|
|
{ name: "0007_password_reset_tokens", up: up0007PasswordResetTokens },
|
|
{ name: "0008_chat_messages", up: up0008ChatMessages },
|
|
{ name: "0009_chat_reply", up: up0009ChatReply },
|
|
{ name: "0010_youtube_embed_start", up: up0010YoutubeEmbedStart },
|
|
{ name: "0011_split_favicon_thumbnail", up: up0011SplitFaviconThumbnail },
|
|
{
|
|
name: "0012_fix_favicon_reclassification",
|
|
up: up0012FixFaviconReclassification,
|
|
},
|
|
{ name: "0013_dump_url_canonical", up: up0013DumpUrlCanonical },
|
|
{ name: "0014_chat_read_state", up: up0014ChatReadState },
|
|
{ name: "0015_token_revocation", up: up0015TokenRevocation },
|
|
{ name: "0016_performance_indexes", up: up0016PerformanceIndexes },
|
|
{ name: "0017_case_insensitive_users", up: up0017CaseInsensitiveUsers },
|
|
{
|
|
name: "0018_attachment_resource_type",
|
|
up: up0018AttachmentResourceType,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Applies each migration `db` hasn't recorded yet, in order, each in its own
|
|
* transaction.
|
|
*
|
|
* @throws {Error} When a migration fails. That one is rolled back, the later
|
|
* ones don't run, and the server doesn't start.
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|