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 BCRYPT_ROUNDS=4
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 typecheck
pnpm test
@@ -139,7 +139,7 @@ jobs:
echo "STAGE DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE}"
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
pnpm typecheck
# Validate production env (AUTH_SECRET, DATABASE_URL, …) during build.
@@ -292,7 +292,7 @@ jobs:
export NODE_ENV=production
export SKIP_ENV_VALIDATION=1
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
# (same path as push-to-main deploy), using the production .env.
LIVE="/var/www/atom-nexst"
-1
View File
@@ -5,7 +5,6 @@ node_modules/
next-env.d.ts
.env
*.tsbuildinfo
# Prisma client is generated by `prisma generate`
src/generated/
# Runtime avatar/badge imaging disk 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 { defineConfig } from "drizzle-kit";
// The schema is generated by scripts/generate-drizzle-schema.mjs from
// prisma/schema.prisma + live DB introspection. This DB is shared live with the
// 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`.
// Schema source of truth for the query builder: src/db/schema.ts
// (regenerated via `pnpm db:schema:generate` from the previous schema + live DB).
//
// 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({
dialect: "mysql",
schema: "./src/db/schema.ts",
out: "./drizzle/drafts",
dbCredentials: {
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",
"build": "next build",
"start": "next start",
"prisma:generate": "prisma generate",
"typecheck": "tsc --noEmit --incremental",
"biome:check": "biome check --write .",
"biome:lint": "biome lint .",
@@ -21,6 +20,9 @@
"db:migrate": "tsx scripts/apply-migrations.ts",
"db:migrate:status": "tsx scripts/apply-migrations.ts --status",
"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",
"prepare": "husky"
},
@@ -76,7 +78,6 @@
"devDependencies": {
"@biomejs/biome": "2.5.6",
"@next/bundle-analyzer": "^16.2.12",
"@prisma/client": "^7.9.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.3.3",
"@tailwindcss/typography": "^0.5.20",
@@ -92,7 +93,6 @@
"lint-staged": "^17.3.0",
"pino-pretty": "^13.1.3",
"postcss": "^8.5.25",
"prisma": "^7.9.1",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.1",
"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
allowBuilds:
esbuild: true
prisma: true
"@prisma/client": true
"@prisma/engines": true
sharp: true
"@parcel/watcher": 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";
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";
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", () => {
it("adds every column idempotently for partially migrated databases", () => {
const sql = readFileSync(
resolve("prisma/migrations/0009_radio_contests_giveaways_columns.sql"),
resolve("drizzle/migrations/0009_radio_contests_giveaways_columns.sql"),
"utf8",
);
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" };
// 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.
const TABS = [
{ 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
// client component stays free of i18n/prisma dependencies.
// client component stays free of i18n/db dependencies.
const items: LightboxPhoto[] = photos.map((p) => ({
id: String(p.id),
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.
*
* 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 = {
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
* 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
* (/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.
// TS field names mirror prisma/schema.prisma (camelCase); column names are the
// real MySQL columns. Run `pnpm db:schema:generate` after schema changes.
// TS field names come from the previous src/db/schema.ts; column types from the
// live MySQL DB. Run `pnpm db:schema:generate` after schema/map changes.
import { sql } from "drizzle-orm";
import {
@@ -19,15 +19,17 @@ const tables = [
describe("admin content module port", () => {
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(
resolve("prisma/migrations/0013_admin_events_polls_banners_prefixes.sql"),
resolve(
"drizzle/migrations/0013_admin_events_polls_banners_prefixes.sql",
),
"utf8",
);
for (const table of tables) {
expect(migration).toContain(`CREATE TABLE IF NOT EXISTS \`${table}\``);
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", () => {
const sql = readFileSync(
"prisma/migrations/0014_complete_acl_and_import_permissions.sql",
"drizzle/migrations/0014_complete_acl_and_import_permissions.sql",
"utf8",
);
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";
describe("production ACL migration", () => {
const migrationsDir = resolve(process.cwd(), "prisma/migrations");
const migrationsDir = resolve(process.cwd(), "drizzle/migrations");
const allSql = readdirSync(migrationsDir)
.filter((name) => name.endsWith(".sql"))
.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:"));
expect(release).toContain("pnpm db:migrate");
expect(release).not.toContain("pnpm prisma:generate");
expect(release).toContain("pnpm db:migrate");
expect(release).toContain("src/db/schema.ts");
const migrateAt = release.indexOf("pnpm db:migrate");
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") {
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");
return fail("Internal server error");
+1 -1
View File
@@ -1,6 +1,6 @@
// ── Permission Slug Constants ───────────────────────────────────────
// 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 = {
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).not.toContain('from "@/lib/auth"');
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(
/*turbopackIgnore: true*/ process.cwd(),
"prisma/migrations",
"drizzle/migrations",
);
let itemsBaseIdAllocChain: Promise<unknown> = Promise.resolve();
+15
View File
@@ -49,6 +49,21 @@ describe("staff smoke contract", () => {
).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", () => {
expect(existsSync("src/lib/admin/ops-health.ts")).toBe(true);
const ops = readFileSync("src/lib/admin/ops-health.ts", "utf8");