chore(db): finish Prisma cutover to Drizzle Kit tooling
Move CMS SQL to drizzle/migrations, drop prisma packages/schema, wire drizzle-kit scripts, and regenerate schema names from src/db/schema.ts. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
d8199ea1e4
commit
ba82789166
47 files changed
+361
-2783
No files matched your search
@@ -43,7 +43,7 @@ jobs:
|
||||
export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1"
|
||||
export 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 ?? "",
|
||||
},
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
Draft SQL from `pnpm db:generate` (drizzle-kit). Never apply these automatically — copy reviewed statements into drizzle/migrations/ as numbered CMS migrations, then `pnpm db:migrate`.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -10,7 +10,6 @@
|
||||
"dev": "next dev",
|
||||
"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 {
|
||||
|
||||
+136
-191
@@ -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
|
||||
// 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"
|
||||
: (field.enumValues ??
|
||||
(field.prismaType === "users_gender"
|
||||
? ["M", "F"]
|
||||
: ["account", "ip", "machine", "super"];
|
||||
: ["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":
|
||||
if (field.isBoolean || field.prismaType === "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 })`;
|
||||
}
|
||||
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