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
+365 -2787

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
@@ -39,7 +39,7 @@ CREATE DATABASE IF NOT EXISTS epicnext_cms CHARACTER SET utf8mb4 COLLATE utf8mb4
The CMS reads emulator-owned tables (`users`, `items`, `rooms`, `bans`, etc.) directly. It never creates, alters, or drops them. The Drizzle schema in `src/db/schema.ts` is generated from the existing database structure and does not modify it.
> **Note:** The CMS does **not** own the database schema — it maps to tables that are managed by the emulator. All Drizzle schema definitions use `drizzle-orm`'s runtime mapping (no `drizzle-kit push/migrate` is ever run against the emulator schema). CMS-owned tables (`website_*`, `radio_*`, etc.) are created via idempotent SQL files in `prisma/migrations/`.
> **Note:** The CMS does **not** own the emulator schema — it maps to those tables via Drizzle. Never run `drizzle-kit push` / `migrate` against the shared DB. CMS-owned tables (`website_*`, `radio_*`, etc.) are created via idempotent SQL in `drizzle/migrations/` (`pnpm db:migrate`).
### 3. Configure Environment
@@ -78,23 +78,22 @@ const found = await db.select()
| Command | What it does |
| ------- | ------------ |
| `npx drizzle-kit generate --dialect mysql --schema src/db/schema.ts --out src/db/migrations` | Inspect the Drizzle schema and emit migration SQL |
| `npx drizzle-kit studio` | Open a local DB browser (dev only) |
| `npx drizzle-kit introspect` | Reverse-engineer an existing DB into a Drizzle schema |
| `pnpm db:generate` | Draft SQL from Drizzle schema into `drizzle/drafts/` (review + copy into `drizzle/migrations/`) |
| `pnpm db:studio` | Open Drizzle Studio (dev only) |
| `pnpm db:introspect` | Reverse-engineer an existing DB into a Drizzle schema draft |
| `pnpm db:schema:generate` | Regen committed `src/db/schema.ts` from previous schema names + live DB |
> The CMS does **not** use `drizzle-kit push` — the database is owned by the emulator and is never auto-migrated. CMS-owned tables are created via the SQL migration runner (step 5).
> The CMS does **not** use `drizzle-kit push` or `drizzle-kit migrate` — the database is shared with the emulator. Apply CMS DDL only via `pnpm db:migrate`.
Use `import { db } from "@/lib/db"` with table definitions from `src/db/schema.ts` for all database access. Types come from the committed Drizzle schema — no separate client code generation is required at build time.
`prisma/schema.prisma` remains as a reference model list used by `pnpm db:schema:generate` when regenerating the Drizzle schema from the live database. The Prisma client is not used at runtime.
### 5. Run CMS Migrations
```bash
pnpm db:migrate
```
Creates all CMS-owned tables (`website_*`, `radio_*`, `acl_*`, `admin_audit_log`, etc.) via idempotent SQL files in `prisma/migrations/`. Emulator tables are never touched.
Creates all CMS-owned tables (`website_*`, `radio_*`, `acl_*`, `admin_audit_log`, etc.) via idempotent SQL files in `drizzle/migrations/`. Emulator tables are never touched.
Check migration status:
@@ -154,17 +153,15 @@ The CMS runs behind an nginx reverse proxy on the default port 3000. Static asse
| `pnpm test` | Run all tests (Vitest) |
| `pnpm db:migrate` | Apply pending SQL migrations |
| `pnpm db:migrate:status` | Show migration status |
| `pnpm db:schema:generate` | Regen `src/db/schema.ts` from Prisma + live DB (needs `DATABASE_URL`) |
| `pnpm db:schema:generate` | Regen `src/db/schema.ts` from prior schema + live DB |
| `pnpm db:generate` | Draft SQL via drizzle-kit → `drizzle/drafts/` |
| `pnpm db:studio` | Drizzle Studio (dev) |
| `pnpm db:introspect` | drizzle-kit introspect (draft) |
| `pnpm analyze` | Build + open bundle analyzer |
| `pnpm jobs:worker` | Start background task worker (systemd / PM2) |
| `pnpm biome:check` | Lint and format code |
**Drizzle CLI (dev only, run with `npx`):**
| Command | Description |
| ------- | ----------- |
| `drizzle-kit generate` | Generate migration SQL from Drizzle schema |
| `drizzle-kit studio` | Local Drizzle Studio database browser |
| `drizzle-kit introspect` | Reverse-engineer DB → Drizzle schema |
**Drizzle Kit notes:** `db:generate` / `db:introspect` write drafts only. Reviewed SQL must be copied into `drizzle/migrations/` as a new numbered file, then applied with `pnpm db:migrate`. Never run `drizzle-kit push` or `drizzle-kit migrate` against production.
---
@@ -192,18 +189,18 @@ The CMS runs behind an nginx reverse proxy on the default port 3000. Static asse
## Architecture
```
├── prisma/
│ ├── schema.prisma # Reference model list — input for db:schema:generate
│ └── migrations/ # 19 SQL migrations for CMS tables (idempotent, never re-run)
├── drizzle/
│ ├── migrations/ # CMS SQL migrations (idempotent, tracked in cms_migrations)
│ └── drafts/ # drizzle-kit generate output (never auto-applied)
├── scripts/
│ ├── apply-migrations.ts # SQL migration runner (apply + status)
│ ├── jobs-worker.ts # Background task scheduler
│ ├── merge-config.cjs # Utility: merge split config files
│ └── generate-drizzle-schema.mjs # Generate src/db/schema.ts from schema.prisma + live DB
│ └── generate-drizzle-schema.mjs # Regen src/db/schema.ts from schema + live DB
├── src/
│ ├── db/
│ │ ├── schema.ts # Drizzle ORM schema (176 tables — runtime data layer)
│ │ └── migrations/ # Drizzle migration files (local dev only)
│ │ ├── schema.ts # Drizzle ORM schema (committed — runtime data layer)
│ │ └── relations.ts # Drizzle relations
│ ├── app/ # Next.js App Router (pages & API routes)
│ ├── actions/ # Server Actions
│ ├── components/ # UI components
@@ -232,13 +229,14 @@ The CMS runs behind an nginx reverse proxy on the default port 3000. Static asse
| Component | Type | Migrations |
| ------------------------------------------------- | ----------------------- | ----------------------------------- |
| Emulator tables (`users`, `items`, `rooms`, etc.) | Existing Polaris schema | None — CMS reads/writes only |
| CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `prisma/migrations/*.sql` (19 files) |
| CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `drizzle/migrations/*.sql` |
| Migration tracking | `cms_migrations` table | Auto-created by migration runner |
### ORM Architecture
- **Runtime (Drizzle ORM)**: `@/lib/db` exposes a Drizzle singleton connected to the MySQL/MariaDB database. All application and script code uses this directly. The schema is defined in `src/db/schema.ts` with typed columns against the existing database.
- **Schema regeneration**: `pnpm db:schema:generate` reads `prisma/schema.prisma` (for camelCase field names) plus the live DB and rewrites `src/db/schema.ts`. Types for the app come from that committed Drizzle schema — no Prisma client is loaded at runtime.
- **Runtime (Drizzle ORM)**: `@/lib/db` exposes a Drizzle singleton. Schema lives in `src/db/schema.ts`.
- **Schema regeneration**: `pnpm db:schema:generate` reuses field/table names from the previous `src/db/schema.ts` and refreshes column types from the live DB.
- **Drizzle Kit**: studio / generate / introspect for local tooling; CMS apply path remains `pnpm db:migrate`.
Use `import { db } from "@/lib/db"` with queries built via `src/db/schema.ts`.
@@ -304,7 +302,7 @@ pnpm biome:check # Lint and format
1. Ensure typecheck and tests pass: `pnpm typecheck && pnpm test`
2. Follow existing code conventions (Server Components where possible, minimal client boundaries)
3. Use the `src/lib/motion.ts` animation variants for consistent animations
4. SQL migrations in `prisma/migrations/` must be idempotent
4. SQL migrations in `drizzle/migrations/` must be idempotent
5. For new database code, use the Drizzle runtime directly (`import { db } from "@/lib/db"`) — see [ORM Setup](#4-orm-setup--type-generation)
6. Avoid `any` — use `eslint-disable` or `biome-ignore` comments only when unavoidable
+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
@@ -144,9 +144,6 @@ importers:
'@next/bundle-analyzer':
specifier: ^16.2.12
version: 16.2.12
'@prisma/client':
specifier: ^7.9.1
version: 7.9.1([email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected])
'@tailwindcss/forms':
specifier: ^0.5.11
version: 0.5.11([email protected])
@@ -192,9 +189,6 @@ importers:
postcss:
specifier: ^8.5.25
version: 8.5.25
prisma:
specifier: ^7.9.1
version: 7.9.1(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])
tailwindcss:
specifier: ^4.3.3
version: 4.3.3
@@ -5275,12 +5269,15 @@ snapshots:
'@electric-sql/[email protected](@electric-sql/[email protected])':
dependencies:
'@electric-sql/pglite': 0.4.3
optional: true
'@electric-sql/[email protected](@electric-sql/[email protected])':
dependencies:
'@electric-sql/pglite': 0.4.3
optional: true
'@electric-sql/[email protected]': {}
'@electric-sql/[email protected]':
optional: true
'@emnapi/[email protected]':
dependencies:
@@ -6053,7 +6050,8 @@ snapshots:
'@polka/[email protected]': {}
'@prisma/[email protected]': {}
'@prisma/[email protected]':
optional: true
'@prisma/[email protected]([email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected])':
dependencies:
@@ -6061,6 +6059,7 @@ snapshots:
optionalDependencies:
prisma: 7.9.1(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])
typescript: 7.0.2
optional: true
'@prisma/[email protected]([email protected])':
dependencies:
@@ -6070,10 +6069,13 @@ snapshots:
empathic: 2.0.0
transitivePeerDependencies:
- magicast
optional: true
'@prisma/[email protected]': {}
'@prisma/[email protected]':
optional: true
'@prisma/[email protected]': {}
'@prisma/[email protected]':
optional: true
'@prisma/[email protected]([email protected])':
dependencies:
@@ -6094,8 +6096,10 @@ snapshots:
zeptomatch: 2.1.0
transitivePeerDependencies:
- typescript
optional: true
'@prisma/[email protected]': {}
'@prisma/[email protected]':
optional: true
'@prisma/[email protected]':
dependencies:
@@ -6103,22 +6107,27 @@ snapshots:
'@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad
'@prisma/fetch-engine': 7.9.1
'@prisma/get-platform': 7.9.1
optional: true
'@prisma/[email protected]':
dependencies:
'@prisma/debug': 7.9.1
'@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad
'@prisma/get-platform': 7.9.1
optional: true
'@prisma/[email protected]':
dependencies:
'@prisma/debug': 7.2.0
optional: true
'@prisma/[email protected]':
dependencies:
'@prisma/debug': 7.9.1
optional: true
'@prisma/[email protected]': {}
'@prisma/[email protected]':
optional: true
'@prisma/[email protected]':
dependencies:
@@ -6126,6 +6135,7 @@ snapshots:
better-result: 2.10.0
env-paths: 3.0.0
proper-lockfile: 4.1.2
optional: true
'@prisma/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
@@ -6145,8 +6155,10 @@ snapshots:
react-dom: 19.2.8([email protected])
transitivePeerDependencies:
- '@types/react-dom'
optional: true
'@radix-ui/[email protected]': {}
'@radix-ui/[email protected]':
optional: true
'@radix-ui/[email protected]': {}
@@ -6155,6 +6167,7 @@ snapshots:
react: 19.2.8
optionalDependencies:
'@types/react': 19.2.18
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6264,6 +6277,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.18
'@types/react-dom': 19.2.4(@types/[email protected])
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6271,6 +6285,7 @@ snapshots:
react: 19.2.8
optionalDependencies:
'@types/react': 19.2.18
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6289,6 +6304,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.18
'@types/react-dom': 19.2.4(@types/[email protected])
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6303,6 +6319,7 @@ snapshots:
react: 19.2.8
optionalDependencies:
'@types/react': 19.2.18
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6319,6 +6336,7 @@ snapshots:
react: 19.2.8
optionalDependencies:
'@types/react': 19.2.18
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6332,6 +6350,7 @@ snapshots:
react: 19.2.8
optionalDependencies:
'@types/react': 19.2.18
optional: true
'@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies:
@@ -6871,45 +6890,58 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]':
dependencies:
'@types/geojson': 7946.0.16
optional: true
'@types/[email protected]':
dependencies:
'@types/d3-color': 3.1.0
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]':
dependencies:
'@types/d3-time': 3.0.0
optional: true
'@types/[email protected]':
dependencies:
'@types/d3-path': 3.1.1
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]':
optional: true
'@types/[email protected]':
dependencies:
@@ -6989,11 +7021,13 @@ snapshots:
'@visx/[email protected]':
dependencies:
'@visx/vendor': 4.0.0-alpha.0
optional: true
'@visx/[email protected]':
dependencies:
'@types/react': 19.2.18
'@visx/point': 4.0.1-alpha.0
optional: true
'@visx/[email protected]([email protected])':
dependencies:
@@ -7005,14 +7039,17 @@ snapshots:
'@visx/shape': 4.0.1-alpha.0([email protected])
classnames: 2.5.1
react: 19.2.8
optional: true
'@visx/[email protected]([email protected])':
dependencies:
'@types/react': 19.2.18
classnames: 2.5.1
react: 19.2.8
optional: true
'@visx/[email protected]': {}
'@visx/[email protected]':
optional: true
'@visx/[email protected]([email protected])':
dependencies:
@@ -7020,10 +7057,12 @@ snapshots:
'@types/react': 19.2.18
lodash: 4.18.1
react: 19.2.8
optional: true
'@visx/[email protected]':
dependencies:
'@visx/vendor': 4.0.0-alpha.0
optional: true
'@visx/[email protected]([email protected])':
dependencies:
@@ -7036,6 +7075,7 @@ snapshots:
classnames: 2.5.1
lodash: 4.18.1
react: 19.2.8
optional: true
'@visx/[email protected]':
dependencies:
@@ -7062,6 +7102,7 @@ snapshots:
d3-time: 3.1.0
d3-time-format: 4.1.0
internmap: 2.0.3
optional: true
'@vitest/[email protected]([email protected])':
dependencies:
@@ -7262,7 +7303,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -7298,6 +7340,7 @@ snapshots:
rc9: 3.0.1
optionalDependencies:
magicast: 0.5.3
optional: true
[email protected]: {}
@@ -7306,6 +7349,7 @@ snapshots:
[email protected]:
dependencies:
readdirp: 5.0.0
optional: true
[email protected]: {}
@@ -7315,7 +7359,8 @@ snapshots:
dependencies:
clsx: 2.1.1
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -7349,7 +7394,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -7377,28 +7423,36 @@ snapshots:
[email protected]:
dependencies:
internmap: 2.0.3
optional: true
[email protected]:
dependencies:
internmap: 2.0.3
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
delaunator: 5.1.0
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
d3-array: 3.2.4
optional: true
[email protected]:
dependencies:
d3-color: 3.1.0
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -7407,18 +7461,22 @@ snapshots:
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
optional: true
[email protected]:
dependencies:
d3-path: 3.1.0
optional: true
[email protected]:
dependencies:
d3-time: 3.1.0
optional: true
[email protected]:
dependencies:
d3-array: 3.2.4
optional: true
[email protected](@noble/[email protected]):
dependencies:
@@ -7437,17 +7495,21 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
robust-predicates: 3.0.3
optional: true
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -7485,16 +7547,19 @@ snapshots:
dependencies:
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
optional: true
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -7507,7 +7572,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -7625,21 +7691,25 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
pure-rand: 6.1.0
optional: true
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]:
dependencies:
fast-decode-uri-component: 1.0.1
optional: true
[email protected]: {}
@@ -7669,6 +7739,7 @@ snapshots:
fast-deep-equal: 3.1.3
fast-querystring: 1.1.2
safe-regex2: 5.1.1
optional: true
[email protected]:
dependencies:
@@ -7707,13 +7778,15 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
resolve-pkg-maps: 1.0.0
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -7726,9 +7799,11 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -7777,7 +7852,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -8036,7 +8112,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8147,6 +8224,7 @@ snapshots:
named-placeholders: 1.1.6
seq-queue: 0.0.5
sqlstring: 2.3.3
optional: true
[email protected](@types/[email protected]):
dependencies:
@@ -8235,7 +8313,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8328,7 +8407,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8375,6 +8455,7 @@ snapshots:
confbox: 0.2.4
exsolve: 1.1.1
pathe: 2.0.3
optional: true
[email protected]:
optional: true
@@ -8401,7 +8482,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
[email protected]: {}
[email protected]:
optional: true
[email protected]([email protected]):
dependencies:
@@ -8425,6 +8507,7 @@ snapshots:
- magicast
- react
- react-dom
optional: true
[email protected]: {}
@@ -8437,6 +8520,7 @@ snapshots:
graceful-fs: 4.2.11
retry: 0.12.0
signal-exit: 3.0.7
optional: true
[email protected]: {}
@@ -8447,7 +8531,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8455,6 +8540,7 @@ snapshots:
dependencies:
defu: 6.1.7
destr: 2.0.5
optional: true
[email protected]([email protected]):
dependencies:
@@ -8504,7 +8590,8 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8516,7 +8603,8 @@ snapshots:
dependencies:
redis-errors: 1.2.0
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8536,11 +8624,14 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
@@ -8599,6 +8690,7 @@ snapshots:
[email protected]:
dependencies:
ret: 0.5.0
optional: true
[email protected]: {}
@@ -8625,7 +8717,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8672,7 +8765,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8706,7 +8800,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8721,7 +8816,8 @@ snapshots:
'@stablelib/base64': 1.0.1
fast-sha256: 1.3.0
[email protected]: {}
[email protected]:
optional: true
[email protected]: {}
@@ -8909,6 +9005,7 @@ snapshots:
[email protected]([email protected]):
optionalDependencies:
typescript: 7.0.2
optional: true
[email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]):
dependencies:
@@ -9091,5 +9188,6 @@ snapshots:
dependencies:
grammex: 3.1.13
graphmatch: 1.1.1
optional: true
[email protected]: {}
-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 {
+140 -195
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env node
// Generates src/db/schema.ts from:
// 1. prisma/schema.prisma -> TS field names (camelCase) + @map column names + table names
// 2. live MySQL introspection -> real column types (DB is authoritative for DDL)
// 1. existing src/db/schema.ts -> TS export names, camelCase fields, column maps, keys
// 2. live MySQL introspection -> real column types (DB is authoritative for DDL)
//
// The DB is owned by the Arcturus emulator; we never run drizzle-kit migrate/push.
// This schema is only used for the query builder + TypeScript types.
// CMS DDL lands in drizzle/migrations/*.sql via `pnpm db:migrate`.
// drizzle-kit (`pnpm db:generate` / studio / introspect) is draft/browse tooling only.
//
// Usage: pnpm db:schema:generate
import "dotenv/config";
@@ -27,84 +28,124 @@ function mysqlConnectionUrl(value) {
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const SCHEMA_PRISMA = resolve(ROOT, "prisma/schema.prisma");
const OUT = resolve(ROOT, "src/db/schema.ts");
const SCHEMA_TS = resolve(ROOT, "src/db/schema.ts");
const OUT = SCHEMA_TS;
const prismaSource = readFileSync(SCHEMA_PRISMA, "utf-8");
// ---------- Parse existing Drizzle schema (naming source of truth) ----------
const schemaSource = readFileSync(SCHEMA_TS, "utf-8");
// ---------- Parse prisma/schema.prisma ----------
const modelBlocks = [
...prismaSource.matchAll(/^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm),
];
const modelNames = new Set(modelBlocks.map((m) => m[1]));
/**
* @returns {{ name: string, table: string, fields: object[], ids: string[]|null, uniques: string[][] }}
*/
function parseDrizzleTables(source) {
const models = [];
const re2 =
/^export const (\w+) = mysqlTable\(\s*"([^"]+)"\s*,\s*\{([\s\S]*?)\n\}(?:,\s*\(t\)\s*=>\s*\[([\s\S]*?)\])?\s*\);/gm;
/** parse a model block -> { name, table, fields, ids, uniques, maps } */
function parseModel(block) {
const [_full, name, body] = block;
const table =
body.match(/@@map\(\s*"([^"]+)"\s*\)/)?.[1] ?? name.toLowerCase();
let match;
const seen = new Set();
while ((match = re2.exec(source)) !== null) {
const [, name, table, body, extras] = match;
if (seen.has(name)) continue;
seen.add(name);
models.push(parseTableBody(name, table, body, extras ?? ""));
}
if (models.length === 0) {
throw new Error(
`[schema-gen] Failed to parse any mysqlTable exports from ${SCHEMA_TS}`,
);
}
return models;
}
function parseTableBody(name, table, body, extras) {
const fields = [];
for (const line of body.split("\n")) {
const trimmed = line.trim();
if (
!trimmed ||
trimmed.startsWith("//") ||
trimmed.startsWith("@@") ||
trimmed.startsWith("///")
) {
continue;
}
const m = trimmed.match(/^(\w+)\s+(.+)$/);
if (!trimmed || trimmed.startsWith("//")) continue;
const m = trimmed.match(/^(\w+)\s*:\s*(.+?),?\s*$/);
if (!m) continue;
const [fieldName, rest] = [m[1], m[2]];
const typeMatch = rest.match(/^([^\s]+)/);
const prismaType = typeMatch[1];
const baseType = prismaType.replace(/\?$/, "").replace(/\[\]$/, "");
const isList = prismaType.endsWith("[]");
// relation field (points at another model) -> skip
if (modelNames.has(baseType)) continue;
const fieldName = m[1];
const expr = m[2];
const colMatch = expr.match(/\(\s*"([^"]+)"/);
if (!colMatch) continue;
const column = colMatch[1];
const isBoolean = /\bboolean\s*\(/.test(expr);
const enumMatch = expr.match(/mysqlEnum\s*\(\s*"[^"]+"\s*,\s*(\[[^\]]*\])/);
let enumValues = null;
if (enumMatch) {
try {
enumValues = JSON.parse(enumMatch[1].replace(/'/g, '"'));
} catch {
enumValues = [...enumMatch[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]);
}
}
let defaultContent = null;
const defIdx = expr.indexOf(".default(");
if (defIdx >= 0) {
const start = defIdx + ".default(".length;
let depth = 0;
for (let i = start; i < expr.length; i++) {
const ch = expr[i];
if (ch === "(") depth++;
else if (ch === ")") {
if (depth === 0) {
defaultContent = expr.slice(start, i).trim();
break;
}
depth--;
}
}
}
const attrs = rest;
const optional = prismaType.endsWith("?");
const map = attrs.match(/@map\(\s*"([^"]+)"\s*\)/)?.[1] ?? fieldName;
const isId = /@id\b/.test(attrs);
const isUnique = /@unique\b/.test(attrs);
const autoIncrement = /@default\(autoincrement\(\)\)/.test(attrs);
const updatedAt = /@updatedAt\b/.test(attrs);
const dbHint = attrs.match(/@db\.(\w+)(?:\((\d+)(?:\s*,\s*(\d+))?\))?/);
fields.push({
fieldName,
column: map,
prismaType: baseType,
optional,
isList,
isId,
isUnique,
autoIncrement,
updatedAt,
attrs,
dbHint: dbHint
? { type: dbHint[1], param1: dbHint[2], param2: dbHint[3] }
: null,
column,
optional: !/\.notNull\s*\(/.test(expr),
isId: /\.primaryKey\s*\(/.test(expr),
isUnique: /\.unique\s*\(/.test(expr),
autoIncrement: /\.autoincrement\s*\(/.test(expr),
isBoolean,
enumValues,
defaultContent,
// legacy shape used by columnExpr / fallback
prismaType: isBoolean
? "Boolean"
: enumValues
? fieldName === "gender"
? "users_gender"
: fieldName === "type" && table === "bans"
? "bans_type"
: "String"
: "String",
attrs: defaultContent ? `@default(${defaultContent})` : "",
dbHint: null,
});
}
// model-level keys
const compIds = body.match(/@@id\(\s*\[([^\]]+)\]\s*\)/)?.[1];
const ids = compIds
? compIds.split(",").map((s) => s.trim().replace(/`/g, ""))
: null;
let ids = null;
const uniques = [];
for (const u of body.matchAll(/@@unique\(\s*\[([^\]]+)\]\s*/g)) {
uniques.push(u[1].split(",").map((s) => s.trim().replace(/`/g, "")));
if (extras) {
const pk = extras.match(
/primaryKey\(\s*\{\s*columns:\s*\[([^\]]+)\]\s*\}\s*\)/,
);
if (pk) {
ids = [...pk[1].matchAll(/t\.(\w+)/g)].map((m) => m[1]);
}
for (const u of extras.matchAll(
/uniqueIndex\([^)]*\)\.on\(([^)]+)\)/g,
)) {
uniques.push([...u[1].matchAll(/t\.(\w+)/g)].map((m) => m[1]));
}
}
return { name, table, fields, ids, uniques };
}
const models = modelBlocks.map(parseModel);
const models = parseDrizzleTables(schemaSource);
// ---------- Introspect live MySQL ----------
let url;
@@ -152,21 +193,20 @@ function quote(v) {
/** Map a DB column row to a drizzle column expression string. */
function columnExpr(field, dbCol) {
const col = field.column;
let expr = "";
let type = "";
const unsigned = /unsigned/.test(dbCol?.column_type ?? "");
const decimalMatch = dbCol?.column_type?.match(/decimal\((\d+),(\d+)\)/);
const enumMatch = dbCol?.column_type?.match(/^enum\((.+)\)$/);
// Prisma enum types -> mysqlEnum
if (field.prismaType === "users_gender" || field.prismaType === "bans_type") {
use("mysqlEnum");
const values = enumMatch
? [...enumMatch[1].matchAll(/'([^']+)'/g)].map((m) => m[1])
: field.prismaType === "users_gender"
? ["M", "F"]
: ["account", "ip", "machine", "super"];
: (field.enumValues ??
(field.prismaType === "users_gender"
? ["M", "F"]
: ["account", "ip", "machine", "super"]));
return `mysqlEnum(${quote(col)}, ${JSON.stringify(values)})`;
}
@@ -179,7 +219,8 @@ function columnExpr(field, dbCol) {
break;
case "tinyint": {
const isBool =
dbCol.column_type === "tinyint(1)" && field.prismaType === "Boolean";
dbCol.column_type === "tinyint(1)" &&
(field.isBoolean || field.prismaType === "Boolean");
if (isBool) {
use("boolean");
type = `boolean(${quote(col)})`;
@@ -209,8 +250,6 @@ function columnExpr(field, dbCol) {
break;
case "varchar":
case "enum": {
// DB enums become plain varchar in the schema: TS contract is `string`
// (only users_gender / bans_type are typed as mysqlEnum above).
use("varchar");
const maxLen = enumMatch
? Math.max(
@@ -284,14 +323,13 @@ function columnExpr(field, dbCol) {
break;
default:
console.warn(
`[schema-gen] WARN unhandled DB type "${dbCol?.data_type}" for ${col} (prisma:${field.prismaType})`,
`[schema-gen] WARN unhandled DB type "${dbCol?.data_type}" for ${col}`,
);
use("varchar");
type = `varchar(${quote(col)}, { length: 255 })`;
}
expr += type;
return expr;
return type;
}
function modifiers(field, dbCol) {
@@ -306,55 +344,35 @@ function modifiers(field, dbCol) {
} else if (field.isUnique) {
expr += `.unique()`;
}
// Mirror the Prisma TS contract: required fields (no `?`) are not-null on
// select, and fields with `@default` are optional on insert.
if (!field.optional) {
expr += `.notNull()`;
}
expr += parseDefault(field, dbCol);
expr += emitDefault(field, dbCol);
return expr;
}
/** Extract `@default(...)` and emit a drizzle `.default(...)` (or ""). */
function parseDefault(field, dbCol) {
const m = field.attrs.match(/@default\(/);
if (!m) return "";
const start = m.index + "@default(".length;
let depth = 0;
let content = "";
for (let i = start; i < field.attrs.length; i++) {
const ch = field.attrs[i];
if (ch === "(") {
depth++;
} else if (ch === ")") {
if (depth === 0) {
content = field.attrs.slice(start, i);
break;
}
depth--;
}
}
function emitDefault(field, dbCol) {
const content = field.defaultContent;
if (!content) return "";
if (content === "now()") {
if (content === "sql`CURRENT_TIMESTAMP`" || content.includes("CURRENT_TIMESTAMP")) {
markSqlUsed();
return `.default(sql\`CURRENT_TIMESTAMP\`)`;
}
if (content === "autoincrement()" || content.startsWith("dbgenerated("))
return "";
if (content === "true" || content === "false") return `.default(${content})`;
if (/^-?\d+n$/.test(content)) return `.default(${content})`;
if (/^-?\d+$/.test(content)) {
// integer literal; bigint64 data type is `bigint`, others are `number`
return dbCol?.data_type === "bigint"
? `.default(${content}n)`
: `.default(${content})`;
}
if (/^-?\d+\.\d+$/.test(content)) return `.default(${content})`;
// quoted string or bare enum/string identifier (e.g. @default(M))
const s =
content.startsWith('"') && content.endsWith('"')
? content.slice(1, -1)
: content;
return `.default(${JSON.stringify(s)})`;
if (
(content.startsWith('"') && content.endsWith('"')) ||
(content.startsWith("'") && content.endsWith("'"))
) {
return `.default(${JSON.stringify(content.slice(1, -1))})`;
}
return `.default(${content})`;
}
function modelTable(model) {
@@ -362,8 +380,6 @@ function modelTable(model) {
for (const f of model.fields) {
const dbCol = colByTable.get(`${model.table}.${f.column}`);
if (!dbCol) {
// Column not found in live DB. Prisma schema may be ahead of the DB.
// Fall back to a best-effort type from the Prisma @db hint / base type.
const fallback = fallbackColumn(f) + modifiers(f, null);
rows.push(`\t${f.fieldName}: ${fallback},`);
console.warn(
@@ -407,96 +423,23 @@ ${uniqueRows.join("\n")}
],
);`;
}
return `export const ${model.name} = mysqlTable(
"${model.table}",
{
return `export const ${model.name} = mysqlTable("${model.table}", {
${rows.join("\n")}
},
);`;
});`;
}
function fallbackColumn(field) {
const hint = field.dbHint;
const name = field.column;
switch (hint?.type) {
case "VarChar":
use("varchar");
return `varchar(${quote(name)}, { length: ${Number(hint.param1 ?? 191)} })`;
case "Char":
use("char");
return `char(${quote(name)}, { length: ${Number(hint.param1 ?? 8)} })`;
case "Text":
use("text");
return `text(${quote(name)})`;
case "MediumText":
use("mediumtext");
return `mediumtext(${quote(name)})`;
case "LongText":
use("longtext");
return `longtext(${quote(name)})`;
case "Decimal":
use("decimal");
return `decimal(${quote(name)}, { precision: ${Number(hint.param1 ?? 10)}, scale: ${Number(hint.param2 ?? 0)}, mode: "number" })`;
case "UnsignedBigInt":
use("bigint");
return `bigint(${quote(name)}, { mode: "bigint", unsigned: true })`;
case "UnsignedInt":
use("int");
return `int(${quote(name)}, { unsigned: true })`;
case "Double":
use("double");
return `double(${quote(name)})`;
case "Float":
use("float");
return `float(${quote(name)})`;
case "Json":
use("json");
return `json(${quote(name)})`;
case "Timestamp":
use("timestamp");
return `timestamp(${quote(name)})`;
case "Time":
return `timeAsDate(${quote(name)})`;
case "Date":
return `dateAsDate(${quote(name)})`;
case "TinyInt":
use("tinyint");
return `tinyint(${quote(name)})`;
default:
break;
if (field.enumValues) {
use("mysqlEnum");
return `mysqlEnum(${quote(name)}, ${JSON.stringify(field.enumValues)})`;
}
switch (field.prismaType) {
case "Int":
use("int");
return `int(${quote(name)})`;
case "BigInt":
use("bigint");
return `bigint(${quote(name)}, { mode: "bigint" })`;
case "Boolean":
use("boolean");
return `boolean(${quote(name)})`;
case "DateTime":
use("datetime");
return `datetime(${quote(name)})`;
case "String":
use("varchar");
return `varchar(${quote(name)}, { length: 255 })`;
case "Float":
use("double");
return `double(${quote(name)})`;
case "Decimal":
use("decimal");
return `decimal(${quote(name)}, { precision: 10, scale: 2, mode: "number" })`;
case "Json":
use("json");
return `json(${quote(name)})`;
case "Bytes":
use("binary");
return `binary(${quote(name)})`;
default:
use("varchar");
return `varchar(${quote(name)}, { length: 255 })`;
if (field.isBoolean || field.prismaType === "Boolean") {
use("boolean");
return `boolean(${quote(name)})`;
}
use("varchar");
return `varchar(${quote(name)}, { length: 255 })`;
}
// ---------- Generate file ----------
@@ -532,8 +475,8 @@ const IMPORTABLE = [
const importList = IMPORTABLE.filter((b) => used.has(b));
const helpers = [
"// MySQL TIME / DATE columns are hydrated by Prisma as JS Date (epoch 1970-01-01",
"// for TIME). Mirror that so existing call sites keep working unchanged.",
"// MySQL TIME / DATE columns are hydrated as JS Date (epoch 1970-01-01",
"// for TIME). Keep this so existing call sites stay unchanged.",
"const timeAsDate = customType<{ data: Date; driverData: string }>({",
" dataType() {",
' return "time";',
@@ -563,13 +506,15 @@ const helpers = [
"",
].join("\n");
const sqlImport = usedSql ? 'import { sql } from "drizzle-orm";\n' : "";
const out = `// 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.
import {
// 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.
${sqlImport}import {
${importList.map((b) => `\t${b},`).join("\n")}
} from "drizzle-orm/mysql-core";
${usedSql ? `import { sql } from "drizzle-orm";\n` : ""}
${helpers}${body}
`;
+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");