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:
@@ -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"
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
@@ -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 ?? "",
|
||||
},
|
||||
|
||||
@@ -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`.
|
||||
+3
-3
@@ -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",
|
||||
|
||||
Generated
+154
-56
@@ -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,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
|
||||
|
||||
@@ -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";
|
||||
|
||||
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
@@ -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}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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) ?? [];
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user