chore(db): finish Prisma cutover to Drizzle Kit tooling
CI / check (push) Failing after 9s
CI / release (push) Skipped
CI / deploy (push) Skipped

Move CMS SQL to drizzle/migrations, drop prisma packages/schema, wire drizzle-kit scripts, and regenerate schema names from src/db/schema.ts.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 15:02:21 +02:00
1 parent d8199ea1e4
commit ba82789166
47 files changed
+361 -2783

No files matched your search

+3 -3
View File
@@ -43,7 +43,7 @@ jobs:
export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1" export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1"
export BCRYPT_ROUNDS=4 export BCRYPT_ROUNDS=4
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
# Types come from the committed Drizzle schema (src/db/schema.ts); no prisma:generate. # Types come from the committed Drizzle schema (src/db/schema.ts).
pnpm biome:lint pnpm biome:lint
pnpm typecheck pnpm typecheck
pnpm test pnpm test
@@ -139,7 +139,7 @@ jobs:
echo "STAGE DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE}" echo "STAGE DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE}"
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
# Types come from the committed Drizzle schema (src/db/schema.ts); no prisma:generate. # Types come from the committed Drizzle schema (src/db/schema.ts).
export BCRYPT_ROUNDS=4 export BCRYPT_ROUNDS=4
pnpm typecheck pnpm typecheck
# Validate production env (AUTH_SECRET, DATABASE_URL, …) during build. # Validate production env (AUTH_SECRET, DATABASE_URL, …) during build.
@@ -292,7 +292,7 @@ jobs:
export NODE_ENV=production export NODE_ENV=production
export SKIP_ENV_VALIDATION=1 export SKIP_ENV_VALIDATION=1
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
# Types come from the committed Drizzle schema (src/db/schema.ts); no prisma:generate. # Types come from the committed Drizzle schema (src/db/schema.ts).
# Tag releases must apply CMS SQL migrations against the live DB # Tag releases must apply CMS SQL migrations against the live DB
# (same path as push-to-main deploy), using the production .env. # (same path as push-to-main deploy), using the production .env.
LIVE="/var/www/atom-nexst" LIVE="/var/www/atom-nexst"
-1
View File
@@ -5,7 +5,6 @@ node_modules/
next-env.d.ts next-env.d.ts
.env .env
*.tsbuildinfo *.tsbuildinfo
# Prisma client is generated by `prisma generate`
src/generated/ src/generated/
# Runtime avatar/badge imaging disk cache # Runtime avatar/badge imaging disk cache
public/cache/ public/cache/
+23 -25
View File
File diff suppressed because it is too large. Load diff
+7 -4
View File
@@ -1,13 +1,16 @@
import "dotenv/config"; import "dotenv/config";
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
// The schema is generated by scripts/generate-drizzle-schema.mjs from // Schema source of truth for the query builder: src/db/schema.ts
// prisma/schema.prisma + live DB introspection. This DB is shared live with the // (regenerated via `pnpm db:schema:generate` from the previous schema + live DB).
// Arcturus emulator, so we NEVER run `drizzle-kit migrate`/`push` against it — //
// CMS-only schema changes stay in prisma/migrations/*.sql via `pnpm db:migrate`. // This DB is shared with the Arcturus emulator — NEVER run `drizzle-kit migrate`
// or `push` against it. CMS DDL stays in drizzle/migrations/*.sql applied by
// `pnpm db:migrate`. Use `pnpm db:generate` only for draft SQL under drizzle/drafts/.
export default defineConfig({ export default defineConfig({
dialect: "mysql", dialect: "mysql",
schema: "./src/db/schema.ts", schema: "./src/db/schema.ts",
out: "./drizzle/drafts",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL ?? "", url: process.env.DATABASE_URL ?? "",
}, },
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
Draft SQL from `pnpm db:generate` (drizzle-kit). Never apply these automatically — copy reviewed statements into drizzle/migrations/ as numbered CMS migrations, then `pnpm db:migrate`.
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -10,7 +10,6 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"prisma:generate": "prisma generate",
"typecheck": "tsc --noEmit --incremental", "typecheck": "tsc --noEmit --incremental",
"biome:check": "biome check --write .", "biome:check": "biome check --write .",
"biome:lint": "biome lint .", "biome:lint": "biome lint .",
@@ -21,6 +20,9 @@
"db:migrate": "tsx scripts/apply-migrations.ts", "db:migrate": "tsx scripts/apply-migrations.ts",
"db:migrate:status": "tsx scripts/apply-migrations.ts --status", "db:migrate:status": "tsx scripts/apply-migrations.ts --status",
"db:schema:generate": "node scripts/generate-drizzle-schema.mjs", "db:schema:generate": "node scripts/generate-drizzle-schema.mjs",
"db:generate": "drizzle-kit generate",
"db:studio": "drizzle-kit studio",
"db:introspect": "drizzle-kit introspect",
"jobs:worker": "tsx scripts/jobs-worker.ts", "jobs:worker": "tsx scripts/jobs-worker.ts",
"prepare": "husky" "prepare": "husky"
}, },
@@ -76,7 +78,6 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.6", "@biomejs/biome": "2.5.6",
"@next/bundle-analyzer": "^16.2.12", "@next/bundle-analyzer": "^16.2.12",
"@prisma/client": "^7.9.1",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@tailwindcss/typography": "^0.5.20", "@tailwindcss/typography": "^0.5.20",
@@ -92,7 +93,6 @@
"lint-staged": "^17.3.0", "lint-staged": "^17.3.0",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"postcss": "^8.5.25", "postcss": "^8.5.25",
"prisma": "^7.9.1",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"tsx": "^4.23.1", "tsx": "^4.23.1",
"typescript": "^7.0.2", "typescript": "^7.0.2",
+154 -56
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -3,9 +3,6 @@
# pnpm v11 vervanger voor onlyBuiltDependencies # pnpm v11 vervanger voor onlyBuiltDependencies
allowBuilds: allowBuilds:
esbuild: true esbuild: true
prisma: true
"@prisma/client": true
"@prisma/engines": true
sharp: true sharp: true
"@parcel/watcher": true "@parcel/watcher": true
"@swc/core": true "@swc/core": true
-16
View File
@@ -1,16 +0,0 @@
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
// Prisma 7 config. The datasource URL lives here (not in schema.prisma).
// We NEVER run `prisma migrate`/`db push` against this database — it is shared
// live with the Arcturus emulator. CMS-only schema changes go in
// prisma/migrations/*.sql (idempotent) applied via `pnpm db:migrate`.
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
-2457
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -6,7 +6,7 @@ import { mysqlConnectionUrl } from "./db-url";
import { splitSqlStatements } from "./sql-statements"; import { splitSqlStatements } from "./sql-statements";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const MIGRATIONS_DIR = resolve(__dirname, "../prisma/migrations"); const MIGRATIONS_DIR = resolve(__dirname, "../drizzle/migrations");
const TRACKING_TABLE = "cms_migrations"; const TRACKING_TABLE = "cms_migrations";
interface MigrationFile { interface MigrationFile {
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
describe("radio columns migration", () => { describe("radio columns migration", () => {
it("adds every column idempotently for partially migrated databases", () => { it("adds every column idempotently for partially migrated databases", () => {
const sql = readFileSync( const sql = readFileSync(
resolve("prisma/migrations/0009_radio_contests_giveaways_columns.sql"), resolve("drizzle/migrations/0009_radio_contests_giveaways_columns.sql"),
"utf8", "utf8",
); );
const additions = sql.match(/ADD COLUMN(?! IF NOT EXISTS)/gi) ?? []; const additions = sql.match(/ADD COLUMN(?! IF NOT EXISTS)/gi) ?? [];
+1 -1
View File
@@ -10,7 +10,7 @@ export const revalidate = 300;
export const metadata = { title: "Leaderboard" }; export const metadata = { title: "Leaderboard" };
// Currency type ids (see prisma/schema.prisma UsersCurrency): // Currency type ids (see UsersCurrency in src/db/schema.ts):
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5. // Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
const TABS = [ const TABS = [
{ key: "credits", label: "Credits", coin: "credits" }, { key: "credits", label: "Credits", coin: "credits" },
+1 -1
View File
@@ -35,7 +35,7 @@ export default async function PhotosPage() {
} }
// Pre-shape for the client lightbox: translate captions server-side so the // Pre-shape for the client lightbox: translate captions server-side so the
// client component stays free of i18n/prisma dependencies. // client component stays free of i18n/db dependencies.
const items: LightboxPhoto[] = photos.map((p) => ({ const items: LightboxPhoto[] = photos.map((p) => ({
id: String(p.id), id: String(p.id),
url: p.url, url: p.url,
+1 -1
View File
@@ -10,7 +10,7 @@ import { useCallback, useEffect, useState } from "react";
* toggled by state — closes on backdrop click, on the ✕ button, or with Esc. * toggled by state — closes on backdrop click, on the ✕ button, or with Esc.
* *
* Photos are pre-shaped on the server (caption strings already translated) so * Photos are pre-shaped on the server (caption strings already translated) so
* this component carries no prisma/i18n dependency. * this component carries no db/i18n dependency.
*/ */
export type LightboxPhoto = { export type LightboxPhoto = {
id: string; id: string;
+1 -1
View File
@@ -10,7 +10,7 @@ import { useEventSource } from "@/lib/use-event-source";
* radio via a hidden <audio> element, with play/pause, a volume slider, the * radio via a hidden <audio> element, with play/pause, a volume slider, the
* current DJ / now-playing text, and a live listener count. * current DJ / now-playing text, and a live listener count.
* *
* It is a CLIENT component and must NOT import prisma / siteSettings. * It is a CLIENT component and must NOT import db / siteSettings.
* Config is fetched once on mount; now-playing + listeners come via SSE * Config is fetched once on mount; now-playing + listeners come via SSE
* (/api/radio/stream) for real-time updates. * (/api/radio/stream) for real-time updates.
* *
+2 -2
View File
@@ -1,6 +1,6 @@
// AUTO-GENERATED by scripts/generate-drizzle-schema.mjs — DO NOT EDIT. // AUTO-GENERATED by scripts/generate-drizzle-schema.mjs — DO NOT EDIT.
// TS field names mirror prisma/schema.prisma (camelCase); column names are the // TS field names come from the previous src/db/schema.ts; column types from the
// real MySQL columns. Run `pnpm db:schema:generate` after schema changes. // live MySQL DB. Run `pnpm db:schema:generate` after schema/map changes.
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { import {
@@ -19,15 +19,17 @@ const tables = [
describe("admin content module port", () => { describe("admin content module port", () => {
it("owns every required table through schema and migration contracts", () => { it("owns every required table through schema and migration contracts", () => {
const schema = readFileSync(resolve("prisma/schema.prisma"), "utf8"); const schema = readFileSync(resolve("src/db/schema.ts"), "utf8");
const migration = readFileSync( const migration = readFileSync(
resolve("prisma/migrations/0013_admin_events_polls_banners_prefixes.sql"), resolve(
"drizzle/migrations/0013_admin_events_polls_banners_prefixes.sql",
),
"utf8", "utf8",
); );
for (const table of tables) { for (const table of tables) {
expect(migration).toContain(`CREATE TABLE IF NOT EXISTS \`${table}\``); expect(migration).toContain(`CREATE TABLE IF NOT EXISTS \`${table}\``);
if (!table.startsWith("custom_prefix")) if (!table.startsWith("custom_prefix"))
expect(schema).toContain(`@@map("${table}")`); expect(schema).toContain(`"${table}"`);
} }
}); });
@@ -13,7 +13,7 @@ describe("ACL management contract", () => {
it("ships an idempotent ACL completion migration", () => { it("ships an idempotent ACL completion migration", () => {
const sql = readFileSync( const sql = readFileSync(
"prisma/migrations/0014_complete_acl_and_import_permissions.sql", "drizzle/migrations/0014_complete_acl_and_import_permissions.sql",
"utf8", "utf8",
); );
expect(sql).toContain("admin.assets.import"); expect(sql).toContain("admin.assets.import");
+1 -1
View File
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
import { PERMS } from "@/lib/permission-slugs"; import { PERMS } from "@/lib/permission-slugs";
describe("production ACL migration", () => { describe("production ACL migration", () => {
const migrationsDir = resolve(process.cwd(), "prisma/migrations"); const migrationsDir = resolve(process.cwd(), "drizzle/migrations");
const allSql = readdirSync(migrationsDir) const allSql = readdirSync(migrationsDir)
.filter((name) => name.endsWith(".sql")) .filter((name) => name.endsWith(".sql"))
.map((name) => readFileSync(resolve(migrationsDir, name), "utf8")) .map((name) => readFileSync(resolve(migrationsDir, name), "utf8"))
+1
View File
@@ -36,6 +36,7 @@ describe("CI workflow", () => {
const release = workflow.slice(workflow.indexOf("\n release:")); const release = workflow.slice(workflow.indexOf("\n release:"));
expect(release).toContain("pnpm db:migrate"); expect(release).toContain("pnpm db:migrate");
expect(release).not.toContain("pnpm prisma:generate"); expect(release).not.toContain("pnpm prisma:generate");
expect(release).toContain("pnpm db:migrate");
expect(release).toContain("src/db/schema.ts"); expect(release).toContain("src/db/schema.ts");
const migrateAt = release.indexOf("pnpm db:migrate"); const migrateAt = release.indexOf("pnpm db:migrate");
const buildAt = release.indexOf("pnpm build"); const buildAt = release.indexOf("pnpm build");
-8
View File
@@ -249,14 +249,6 @@ export function handleActionError(error: unknown): ActionFailure {
if (error instanceof Error && error.name === "ZodError") { if (error instanceof Error && error.name === "ZodError") {
return fail("Validation failed"); return fail("Validation failed");
} }
if (
error instanceof Error &&
(error.constructor.name === "PrismaClientKnownRequestError" ||
error.name === "PrismaClientKnownRequestError") &&
(error as Error & { code?: string }).code === "P2025"
) {
return fail("Not found");
}
reportError(error, "Action error"); reportError(error, "Action error");
return fail("Internal server error"); return fail("Internal server error");
+1 -1
View File
@@ -1,6 +1,6 @@
// ── Permission Slug Constants ─────────────────────────────────────── // ── Permission Slug Constants ───────────────────────────────────────
// Extracted to its own file so it can be imported by client components // Extracted to its own file so it can be imported by client components
// (modules.ts → sidebar.tsx) without pulling in prisma/server-only deps. // (modules.ts → sidebar.tsx) without pulling in db/server-only deps.
export const PERMS = { export const PERMS = {
ADMIN_DASHBOARD: "admin.dashboard", ADMIN_DASHBOARD: "admin.dashboard",
+1 -1
View File
@@ -9,7 +9,7 @@ describe("proxy authentication boundary", () => {
expect(proxy).toContain('from "@/lib/proxy-auth"'); expect(proxy).toContain('from "@/lib/proxy-auth"');
expect(proxy).not.toContain('from "@/lib/auth"'); expect(proxy).not.toContain('from "@/lib/auth"');
expect(proxyAuth).not.toMatch( expect(proxyAuth).not.toMatch(
/from\s+["'][^"']*(prisma|site-settings|credentials)[^"']*["']/i, /from\s+["'][^"']*(db|site-settings|credentials|prisma)[^"']*["']/i,
); );
}); });
}); });
+1 -1
View File
@@ -38,7 +38,7 @@ export interface UploadParams {
const MIGRATIONS_DIR = path.resolve( const MIGRATIONS_DIR = path.resolve(
/*turbopackIgnore: true*/ process.cwd(), /*turbopackIgnore: true*/ process.cwd(),
"prisma/migrations", "drizzle/migrations",
); );
let itemsBaseIdAllocChain: Promise<unknown> = Promise.resolve(); let itemsBaseIdAllocChain: Promise<unknown> = Promise.resolve();
+15
View File
@@ -49,6 +49,21 @@ describe("staff smoke contract", () => {
).toContain("UserSanctionTimeline"); ).toContain("UserSanctionTimeline");
}); });
it("applies CMS SQL from drizzle/migrations (not Prisma)", () => {
expect(existsSync("drizzle/migrations")).toBe(true);
expect(existsSync("prisma")).toBe(false);
expect(readFileSync("scripts/apply-migrations.ts", "utf8")).toContain(
"drizzle/migrations",
);
expect(readFileSync("package.json", "utf8")).not.toContain('"prisma"');
expect(readFileSync("package.json", "utf8")).not.toContain(
'"@prisma/client"',
);
expect(readFileSync("drizzle.config.ts", "utf8")).toContain(
"./drizzle/drafts",
);
});
it("ships shared ops + ticket queue helpers", () => { it("ships shared ops + ticket queue helpers", () => {
expect(existsSync("src/lib/admin/ops-health.ts")).toBe(true); expect(existsSync("src/lib/admin/ops-health.ts")).toBe(true);
const ops = readFileSync("src/lib/admin/ops-health.ts", "utf8"); const ops = readFileSync("src/lib/admin/ops-health.ts", "utf8");