Apply pending migrations during development reloads
This commit is contained in:
@@ -143,8 +143,11 @@ bun run db:migrate
|
|||||||
```
|
```
|
||||||
|
|
||||||
Commit the generated `drizzle/` files alongside schema changes. `bun dev` and
|
Commit the generated `drizzle/` files alongside schema changes. `bun dev` and
|
||||||
`bun start` apply pending migrations before starting the server. The initial
|
`bun start` apply pending migrations before starting the server. Development also
|
||||||
migration creates the users and sessions tables.
|
checks for pending migrations when the server entrypoint is hot-reloaded, so new
|
||||||
|
routes do not run against an older schema. After adding only migration files,
|
||||||
|
run `bun run db:migrate` or restart `bun dev`. The initial migration creates the
|
||||||
|
users and sessions tables.
|
||||||
|
|
||||||
Import `db` from `src/db/index.ts` in backend code to query the database.
|
Import `db` from `src/db/index.ts` in backend code to query the database.
|
||||||
`GET /api/health` checks the database connection before returning success.
|
`GET /api/health` checks the database connection before returning success.
|
||||||
|
|||||||
53
src/db/development-start.test.ts
Normal file
53
src/db/development-start.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
|
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
test("development entrypoint upgrades an existing database before serving requests", async () => {
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "minabot-dev-upgrade-"));
|
||||||
|
const path = join(directory, "app.sqlite");
|
||||||
|
const sqlite = new Database(path);
|
||||||
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
||||||
|
try {
|
||||||
|
const migrations = join(directory, "migrations");
|
||||||
|
mkdirSync(join(migrations, "meta"), { recursive: true });
|
||||||
|
const journal = JSON.parse(readFileSync("drizzle/meta/_journal.json", "utf8"));
|
||||||
|
journal.entries = journal.entries.slice(0, 6);
|
||||||
|
writeFileSync(join(migrations, "meta/_journal.json"), JSON.stringify(journal));
|
||||||
|
for (const entry of journal.entries) copyFileSync(`drizzle/${entry.tag}.sql`, join(migrations, `${entry.tag}.sql`));
|
||||||
|
migrate(drizzle(sqlite), { migrationsFolder: migrations });
|
||||||
|
sqlite.exec("INSERT INTO users (id, discord_id, username, created_at, updated_at) VALUES ('existing', 'existing', 'Existing user', 0, 0)");
|
||||||
|
expect(sqlite.query("SELECT name FROM sqlite_master WHERE name = 'reminder_settings'").get()).toBeNull();
|
||||||
|
|
||||||
|
// Launch directly, as Bun does when re-evaluating the entrypoint during HMR;
|
||||||
|
// deliberately skip package.json's separate migration command.
|
||||||
|
server = Bun.spawn([process.execPath, "src/index.ts"], {
|
||||||
|
env: { ...process.env, NODE_ENV: "development", DATABASE_PATH: path, PORT: "0", APP_ORIGIN: "http://127.0.0.1", DISCORD_BOT_TOKEN: "", DISCORD_ALLOWED_USER_IDS: "" },
|
||||||
|
stdout: "pipe", stderr: "pipe",
|
||||||
|
});
|
||||||
|
const reader = (server.stdout as ReadableStream<Uint8Array>).getReader();
|
||||||
|
let output = "";
|
||||||
|
while (!output.includes("Server running at")) {
|
||||||
|
const chunk = await reader.read();
|
||||||
|
if (chunk.done) throw new Error("Development server exited before it was ready");
|
||||||
|
output += new TextDecoder().decode(chunk.value);
|
||||||
|
}
|
||||||
|
reader.releaseLock();
|
||||||
|
const origin = output.match(/Server running at (http:\/\/[^\s]+)/)?.[1];
|
||||||
|
expect(origin).toBeDefined();
|
||||||
|
expect((await fetch(new URL("/api/health", origin))).status).toBe(200);
|
||||||
|
expect(sqlite.query("SELECT COUNT(*) AS count FROM __drizzle_migrations").get()).toEqual({ count: JSON.parse(readFileSync("drizzle/meta/_journal.json", "utf8")).entries.length });
|
||||||
|
expect(sqlite.query("SELECT enabled FROM reminder_settings").all()).toEqual([]);
|
||||||
|
expect(sqlite.query("SELECT retry_at FROM reminder_deliveries").all()).toEqual([]);
|
||||||
|
expect(sqlite.query("SELECT blocked_until FROM reminder_worker_state").all()).toEqual([]);
|
||||||
|
expect(sqlite.query("SELECT username FROM users WHERE id = 'existing'").get()).toEqual({ username: "Existing user" });
|
||||||
|
} finally {
|
||||||
|
server?.kill();
|
||||||
|
if (server) await server.exited;
|
||||||
|
sqlite.close();
|
||||||
|
rmSync(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
@@ -6,6 +6,13 @@ import { createApi } from "./api";
|
|||||||
import { readAuthConfig } from "./auth/config";
|
import { readAuthConfig } from "./auth/config";
|
||||||
import { readDiscordSharingConfig } from "./sharing/config";
|
import { readDiscordSharingConfig } from "./sharing/config";
|
||||||
import index from "./index.html";
|
import index from "./index.html";
|
||||||
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
|
|
||||||
|
// Hot reloads skip the package script's migration step. Apply pending changes
|
||||||
|
// before the reloaded development server accepts requests.
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
migrate(db, { migrationsFolder: "./drizzle" });
|
||||||
|
}
|
||||||
|
|
||||||
const backups = process.env.NODE_ENV === "production" ? startBackups(sqlite, backupConfig(databasePath)) : undefined;
|
const backups = process.env.NODE_ENV === "production" ? startBackups(sqlite, backupConfig(databasePath)) : undefined;
|
||||||
if (import.meta.hot) import.meta.hot.dispose(() => backups?.stop());
|
if (import.meta.hot) import.meta.hot.dispose(() => backups?.stop());
|
||||||
|
|||||||
Reference in New Issue
Block a user