mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 23:55:43 -04:00
146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import Database from "better-sqlite3";
|
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
|
|
const MIGRATIONS_DIR = fileURLToPath(new URL("../drizzle", import.meta.url));
|
|
// Last migration shipped in the v0.2.0 release.
|
|
const V0_2_0_LAST_MIGRATION = "20260324223903_secret_rhodey";
|
|
|
|
const tempDirs: string[] = [];
|
|
const clients: Database.Database[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const client of clients.splice(0)) client.close();
|
|
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
function migrationFolders(): string[] {
|
|
return readdirSync(MIGRATIONS_DIR)
|
|
.filter((name) => existsSync(path.join(MIGRATIONS_DIR, name, "migration.sql")))
|
|
.sort();
|
|
}
|
|
|
|
function createDbAt(lastMigration: string) {
|
|
const dir = mkdtempSync(path.join(os.tmpdir(), "sofa-migrations-"));
|
|
tempDirs.push(dir);
|
|
for (const name of migrationFolders().filter((n) => n <= lastMigration)) {
|
|
cpSync(path.join(MIGRATIONS_DIR, name, ""), path.join(dir, name), { recursive: true });
|
|
}
|
|
const client = new Database(":memory:");
|
|
clients.push(client);
|
|
client.pragma("foreign_keys = ON");
|
|
const db = drizzle({ client });
|
|
migrate(db, { migrationsFolder: dir });
|
|
return { client, db };
|
|
}
|
|
|
|
const T = 1700000000;
|
|
|
|
function seedV020(client: Database.Database) {
|
|
client.exec(`
|
|
INSERT INTO "user" (id, name, email, emailVerified, createdAt, updatedAt) VALUES
|
|
('u1', 'Alice', 'alice@example.com', 1, ${T}, ${T}),
|
|
('u2', 'Bob', 'bob@example.com', 0, ${T}, ${T});
|
|
|
|
INSERT INTO titles (id, tmdbId, type, title) VALUES
|
|
('t-movie-a', 100, 'movie', 'Movie A'),
|
|
('t-show', 200, 'tv', 'Show'),
|
|
('t-movie-b', 300, 'movie', 'Movie B');
|
|
|
|
INSERT INTO seasons (id, titleId, seasonNumber) VALUES ('s1', 't-show', 1);
|
|
INSERT INTO episodes (id, seasonId, episodeNumber) VALUES ('e1', 's1', 1), ('e2', 's1', 2);
|
|
|
|
INSERT INTO userTitleStatus (userId, titleId, status, addedAt, updatedAt) VALUES
|
|
('u1', 't-show', 'completed', ${T}, ${T}),
|
|
('u1', 't-movie-a', 'in_progress', ${T}, ${T}),
|
|
('u1', 't-movie-b', 'in_progress', ${T}, ${T});
|
|
|
|
INSERT INTO userMovieWatches (id, userId, titleId, watchedAt) VALUES ('w1', 'u1', 't-movie-a', ${T});
|
|
INSERT INTO userEpisodeWatches (id, userId, episodeId, watchedAt) VALUES ('ew1', 'u1', 'e1', ${T});
|
|
INSERT INTO userRatings (userId, titleId, ratingStars, ratedAt) VALUES ('u1', 't-movie-a', 4, ${T});
|
|
|
|
INSERT INTO persons (id, tmdbId, name) VALUES ('p1', 500, 'Person');
|
|
INSERT INTO titleCast (id, titleId, personId, character, department, job, displayOrder) VALUES
|
|
('c1', 't-movie-a', 'p1', NULL, 'Directing', 'Director', 0),
|
|
('c2', 't-movie-a', 'p1', NULL, 'Directing', 'Director', 0);
|
|
|
|
INSERT INTO platforms (id, name) VALUES ('pl1', 'Platform');
|
|
INSERT INTO platformTmdbIds (platformId, tmdbProviderId) VALUES ('pl1', 8);
|
|
INSERT INTO userPlatforms (userId, platformId) VALUES ('u1', 'pl1');
|
|
INSERT INTO titleAvailability (titleId, platformId, offerType, region) VALUES ('t-movie-a', 'pl1', 'flatrate', 'US');
|
|
|
|
INSERT INTO integrations (id, userId, provider, type, token, createdAt) VALUES
|
|
('i1', 'u1', 'plex', 'webhook', 'secret-token', ${T});
|
|
|
|
INSERT INTO importJobs (id, userId, source, status, payload, importWatches, importWatchlist, importRatings, createdAt) VALUES
|
|
('j1', 'u1', 'trakt', 'success', '{}', 1, 1, 1, ${T});
|
|
|
|
INSERT INTO appSettings (key, value) VALUES ('someSetting', 'someValue');
|
|
`);
|
|
}
|
|
|
|
const SEEDED_TABLES = [
|
|
"user",
|
|
"titles",
|
|
"seasons",
|
|
"episodes",
|
|
"userTitleStatus",
|
|
"userMovieWatches",
|
|
"userEpisodeWatches",
|
|
"userRatings",
|
|
"persons",
|
|
"platforms",
|
|
"platformTmdbIds",
|
|
"userPlatforms",
|
|
"titleAvailability",
|
|
"integrations",
|
|
"importJobs",
|
|
"appSettings",
|
|
];
|
|
|
|
function count(client: Database.Database, table: string): number {
|
|
return (client.prepare(`SELECT count(*) AS c FROM "${table}"`).get() as { c: number }).c;
|
|
}
|
|
|
|
describe("migrations on an older database", () => {
|
|
test("builds a v0.2.0-schema database", () => {
|
|
const { client } = createDbAt(V0_2_0_LAST_MIGRATION);
|
|
const expected = migrationFolders().filter((n) => n <= V0_2_0_LAST_MIGRATION).length;
|
|
expect(count(client, "__drizzle_migrations")).toBe(expected);
|
|
});
|
|
|
|
test("applies newer migrations to a populated v0.2.0 database", () => {
|
|
const { client, db } = createDbAt(V0_2_0_LAST_MIGRATION);
|
|
seedV020(client);
|
|
const before = Object.fromEntries(SEEDED_TABLES.map((t) => [t, count(client, t)]));
|
|
expect(count(client, "titleCast")).toBe(2);
|
|
|
|
expect(() => migrate(db, { migrationsFolder: MIGRATIONS_DIR })).not.toThrow();
|
|
|
|
expect(count(client, "__drizzle_migrations")).toBe(migrationFolders().length);
|
|
expect(client.pragma("foreign_key_check")).toEqual([]);
|
|
expect(client.pragma("integrity_check", { simple: true })).toBe("ok");
|
|
|
|
for (const table of SEEDED_TABLES) {
|
|
expect(count(client, table), table).toBe(before[table]);
|
|
}
|
|
expect(count(client, "titleCast")).toBe(1);
|
|
|
|
const statuses = client
|
|
.prepare("SELECT titleId, status FROM userTitleStatus")
|
|
.all() as { titleId: string; status: string }[];
|
|
const byTitle = Object.fromEntries(statuses.map((s) => [s.titleId, s.status]));
|
|
expect(byTitle).toEqual({
|
|
"t-show": "in_progress",
|
|
"t-movie-a": "completed",
|
|
"t-movie-b": "watchlist",
|
|
});
|
|
});
|
|
});
|