chore(db): finish Prisma cutover to Drizzle Kit tooling
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:
1 parent
d8199ea1e4
commit
ba82789166
47 files changed
+361
-2783
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
+7
-4
@@ -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 ?? "",
|
||||||
},
|
},
|
||||||
|
|||||||
Whitespace-only changes.
@@ -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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -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",
|
||||||
|
|||||||
Generated
+154
-56
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||||
|
|||||||
+136
-191
File diff suppressed because it is too large.
Load diff
@@ -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) ?? [];
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in new issue
Block a user