fix(db): add down migration, template-DB test harness, EntryStatus type
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
da04adb286
commit
ff595c1c5f
@@ -389,4 +389,37 @@ export const initial: Migration = {
|
||||
{ indexes: [["status", "run_at"], ["lock_key"]] },
|
||||
);
|
||||
},
|
||||
|
||||
async down(db: Kysely<any>) {
|
||||
for (const table of [
|
||||
"jobs",
|
||||
"change_events",
|
||||
"source_record_versions",
|
||||
"source_records",
|
||||
"source_runs",
|
||||
"match_suggestions",
|
||||
"user_history",
|
||||
"attachment_links",
|
||||
"attachments",
|
||||
"copies",
|
||||
"custom_field_definitions",
|
||||
"list_items",
|
||||
"lists",
|
||||
"listens",
|
||||
"engagement_sources",
|
||||
"source_connections",
|
||||
"engagements",
|
||||
"entries",
|
||||
"work_history",
|
||||
"work_aliases",
|
||||
"external_ids",
|
||||
"works",
|
||||
"app_tokens",
|
||||
"invites",
|
||||
"sessions",
|
||||
"users",
|
||||
]) {
|
||||
await db.schema.dropTable(table).execute();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
export type Role = "admin" | "member";
|
||||
export type EntryStatus = "planned" | "in_progress" | "paused" | "completed" | "dropped" | "none";
|
||||
export type Visibility = "private" | "friends" | "public";
|
||||
export type TokenScope = "read" | "write" | "ingest";
|
||||
export type CustomFieldLevel = "entry" | "engagement" | "copy";
|
||||
@@ -105,7 +106,7 @@ export interface EntriesTable {
|
||||
id: string;
|
||||
user_id: string;
|
||||
work_id: string;
|
||||
status: string;
|
||||
status: EntryStatus;
|
||||
rating: number | null;
|
||||
review: string | null;
|
||||
private_notes: string | null;
|
||||
|
||||
+44
-19
@@ -1,5 +1,5 @@
|
||||
import { SQL } from "bun";
|
||||
import { afterAll, afterEach, beforeEach, describe } from "bun:test";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe } from "bun:test";
|
||||
import { createDb, type Db } from "./db.ts";
|
||||
import type { Dialect } from "./dialect.ts";
|
||||
import { migrateToLatest } from "./migrate.ts";
|
||||
@@ -17,33 +17,50 @@ export function forEachDialect(name: string, fn: (getDb: () => Db) => void): voi
|
||||
for (const dialect of dialects) {
|
||||
describe(`${name} [${dialect}]`, () => {
|
||||
let current: Db | undefined;
|
||||
const created: string[] = [];
|
||||
let currentName: string | undefined;
|
||||
const template = `st_tpl_${crypto.randomUUID().replaceAll("-", "")}`;
|
||||
|
||||
if (dialect === "postgres") {
|
||||
// Migrate one template per describe block, then clone it per test (much faster).
|
||||
beforeAll(async () => {
|
||||
await withAdmin(pgUrl!, (admin) => admin.unsafe(`create database "${template}"`));
|
||||
const tpl = await createDb(urlFor(pgUrl!, template));
|
||||
try {
|
||||
await migrateToLatest(tpl);
|
||||
} finally {
|
||||
await tpl.close();
|
||||
}
|
||||
}, HOOK_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let url = "sqlite://:memory:";
|
||||
if (dialect === "postgres") {
|
||||
const dbName = `st_${crypto.randomUUID().replaceAll("-", "")}`;
|
||||
await withAdmin(pgUrl!, (admin) => admin.unsafe(`create database "${dbName}"`));
|
||||
created.push(dbName);
|
||||
const u = new URL(pgUrl!);
|
||||
u.pathname = `/${dbName}`;
|
||||
url = u.toString();
|
||||
currentName = `st_${crypto.randomUUID().replaceAll("-", "")}`;
|
||||
await withAdmin(pgUrl!, (admin) =>
|
||||
admin.unsafe(`create database "${currentName}" template "${template}"`),
|
||||
);
|
||||
current = await createDb(urlFor(pgUrl!, currentName));
|
||||
} else {
|
||||
current = await createDb("sqlite://:memory:");
|
||||
await migrateToLatest(current);
|
||||
}
|
||||
current = await createDb(url);
|
||||
await migrateToLatest(current);
|
||||
});
|
||||
}, HOOK_TIMEOUT_MS);
|
||||
|
||||
afterEach(async () => {
|
||||
await current?.close();
|
||||
current = undefined;
|
||||
});
|
||||
if (currentName) {
|
||||
const dbName = currentName;
|
||||
currentName = undefined;
|
||||
await withAdmin(pgUrl!, (admin) => admin.unsafe(`drop database if exists "${dbName}" with (force)`));
|
||||
}
|
||||
}, HOOK_TIMEOUT_MS);
|
||||
|
||||
afterAll(async () => {
|
||||
if (created.length === 0) return;
|
||||
await withAdmin(pgUrl!, async (admin) => {
|
||||
for (const dbName of created) await admin.unsafe(`drop database if exists "${dbName}" with (force)`);
|
||||
});
|
||||
});
|
||||
if (dialect === "postgres") {
|
||||
afterAll(async () => {
|
||||
await withAdmin(pgUrl!, (admin) => admin.unsafe(`drop database if exists "${template}" with (force)`));
|
||||
}, HOOK_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
fn(() => {
|
||||
if (!current) throw new Error("getDb() called outside a test");
|
||||
@@ -53,6 +70,14 @@ export function forEachDialect(name: string, fn: (getDb: () => Db) => void): voi
|
||||
}
|
||||
}
|
||||
|
||||
const HOOK_TIMEOUT_MS = 30_000;
|
||||
|
||||
function urlFor(base: string, dbName: string): string {
|
||||
const u = new URL(base);
|
||||
u.pathname = `/${dbName}`;
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
async function withAdmin<T>(url: string, f: (admin: SQL) => Promise<T>): Promise<T> {
|
||||
const admin = new SQL(url);
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { sql, type Kysely } from "kysely";
|
||||
import { Migrator } from "kysely/migration";
|
||||
import { newId, nowIso } from "../src/ids.ts";
|
||||
import { toJson } from "../src/json.ts";
|
||||
import { migrations } from "../src/migrations/index.ts";
|
||||
import { forEachDialect } from "../src/testing.ts";
|
||||
|
||||
forEachDialect("schema", (getDb) => {
|
||||
@@ -57,7 +60,7 @@ forEachDialect("schema", (getDb) => {
|
||||
const entry = {
|
||||
user_id: userId,
|
||||
work_id: workId,
|
||||
status: "planned",
|
||||
status: "planned" as const,
|
||||
rating: null,
|
||||
review: null,
|
||||
private_notes: null,
|
||||
@@ -75,4 +78,13 @@ forEachDialect("schema", (getDb) => {
|
||||
await k.insertInto("entries").values({ id: newId(), ...entry }).execute();
|
||||
await expect(k.insertInto("entries").values({ id: newId(), ...entry }).execute()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("migrating down removes the tables", async () => {
|
||||
const k = getDb().k as Kysely<any>;
|
||||
const migrator = new Migrator({ db: k, provider: { getMigrations: async () => migrations } });
|
||||
const { error } = await migrator.migrateDown();
|
||||
expect(error).toBeUndefined();
|
||||
const probe = sql`select 1 from users`.execute(k);
|
||||
await expect(probe).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user