Schema hardening pass. After reading the onboarding/edit forms I
reconsidered the original 'migrate String? -> DateTime?' suggestion:
the bornDate/passedDate placeholders are 'нпр. 1960' and the column
deliberately accepts imprecise values like '1960', 'early 1990s',
'12 May 1960'. Forcing DateTime? would silently break that feature
and require users to enter exact dates they often don't know. The
responsible fix is to keep the free-text semantics but bound the
column length and document the intent in a schema comment. Future
structured-date queries should add a parallel DateTime? column.
Schema changes (prisma/schema.prisma):
- @db.VarChar bounds added to every string column, sized to match
the existing client-side maxLength / app-constant caps:
User.subdomain -> 32, User.title -> 100, User.description -> 2000,
User.bornDate/passedDate -> 50, User.email -> 255, etc.
Image.url/key -> 255, Image.id/userId -> 30.
AdminUser.username -> 50, passwordHash -> 100.
Code.code -> 12, usedByUserId -> 100 (matches Clerk userId scale).
- Image.key: added @@index('Image_key_idx') to support the
/api/image lookup we tightened in Phase 1 (currently a scan).
- updatedAt added to Image, AdminUser, Code (previously only User).
- Inline schema comment on Code.createdById documenting the existing
onDelete: Restrict behaviour (Prisma default) — the migration does
not change it, just makes the audit-trail intent explicit.
Migration:
- 20260802000000_phase6_schema_hardening/migration.sql: explicit
ALTER TABLE ... SET DATA TYPE VARCHAR(N) statements for every
bounded column; ADD COLUMN updatedAt with DEFAULT CURRENT_TIMESTAMP
(so existing rows back-fill immediately); CREATE INDEX for the
Image.key column; a defensive LEFT() truncation UPDATE for any rows
whose bornDate/passedDate exceed 50 chars (the inputs always capped
at 50 on the client side, so this is belt-and-braces). Will be
applied on next deploy via 'prisma migrate deploy'.
Seed:
- prisma/seed.ts: upserts the SUPER_ADMIN row from env
SUPER_ADMIN_USERNAME + SUPER_ADMIN_PASSWORD_HASH. This resolves
the Phase 1 problem where an env-only super-admin had no row in
AdminUser, which meant the Code.createdById FK prevented them from
ever creating codes. The seed is idempotent and safe to run on
every boot (uses upsert).
- package.json: 'prisma.seed' wired to 'tsx prisma/seed.ts'.
- 'db:seed' npm script also added for manual provisioning.
- tsx added as devDependency (executes TypeScript straight from disk
without pre-build).
44 lines
2.6 KiB
SQL
44 lines
2.6 KiB
SQL
-- Phase 6 schema hardening
|
|
-- Adds @db.VarChar() length bounds to all string columns, updatedAt to
|
|
-- Image/AdminUser/Code, an index on Image.key, and documents the
|
|
-- existing Code.createdById onDelete:RESTRICT behaviour with a schema
|
|
-- comment (no DDL change there; it was already RESTRICT by default).
|
|
|
|
-- === User: add VarChar bounds ===
|
|
ALTER TABLE "User" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "User" ALTER COLUMN "clerkId" SET DATA TYPE VARCHAR(100);
|
|
ALTER TABLE "User" ALTER COLUMN "email" SET DATA TYPE VARCHAR(255);
|
|
ALTER TABLE "User" ALTER COLUMN "name" SET DATA TYPE VARCHAR(100);
|
|
ALTER TABLE "User" ALTER COLUMN "subdomain" SET DATA TYPE VARCHAR(32);
|
|
ALTER TABLE "User" ALTER COLUMN "title" SET DATA TYPE VARCHAR(100);
|
|
ALTER TABLE "User" ALTER COLUMN "description" SET DATA TYPE VARCHAR(2000);
|
|
ALTER TABLE "User" ALTER COLUMN "bornDate" SET DATA TYPE VARCHAR(50);
|
|
ALTER TABLE "User" ALTER COLUMN "passedDate" SET DATA TYPE VARCHAR(50);
|
|
|
|
-- === Image: bounds + updatedAt + index on key ===
|
|
ALTER TABLE "Image" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "Image" ALTER COLUMN "url" SET DATA TYPE VARCHAR(255);
|
|
ALTER TABLE "Image" ALTER COLUMN "key" SET DATA TYPE VARCHAR(255);
|
|
ALTER TABLE "Image" ALTER COLUMN "userId" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "Image" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
CREATE INDEX "Image_key_idx" ON "Image"("key");
|
|
|
|
-- === AdminUser: bounds + updatedAt ===
|
|
ALTER TABLE "AdminUser" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "AdminUser" ALTER COLUMN "username" SET DATA TYPE VARCHAR(50);
|
|
ALTER TABLE "AdminUser" ALTER COLUMN "passwordHash" SET DATA TYPE VARCHAR(100);
|
|
ALTER TABLE "AdminUser" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
|
|
-- === Code: bounds + updatedAt ===
|
|
ALTER TABLE "Code" ALTER COLUMN "id" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "Code" ALTER COLUMN "code" SET DATA TYPE VARCHAR(12);
|
|
ALTER TABLE "Code" ALTER COLUMN "createdById" SET DATA TYPE VARCHAR(30);
|
|
ALTER TABLE "Code" ALTER COLUMN "usedByUserId" SET DATA TYPE VARCHAR(100);
|
|
ALTER TABLE "Code" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
|
|
-- Backfill any rows whose string columns exceed the new bounds (defensive;
|
|
-- production data was capped at the maxLength on the input side already but
|
|
-- the persistence layer was unbounded). Truncating is safer than failing.
|
|
UPDATE "User" SET "bornDate" = LEFT("bornDate", 50) WHERE "bornDate" IS NOT NULL AND LENGTH("bornDate") > 50;
|
|
UPDATE "User" SET "passedDate" = LEFT("passedDate", 50) WHERE "passedDate" IS NOT NULL AND LENGTH("passedDate") > 50;
|