diff --git a/package.json b/package.json index f20d44a..25da7d4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "npm run storage:legacy-boundary && prisma generate && next build", + "build": "npm run storage:legacy-boundary && prisma generate && npm run prisma:verify-client && next build", "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", @@ -22,6 +22,7 @@ "history:scan": "tsx scripts/scan-history-materials.ts", "history:import": "tsx scripts/import-history-materials.ts", "prisma:generate": "prisma generate", + "prisma:verify-client": "node scripts/verify-prisma-client-schema.mjs", "prisma:push": "prisma db push", "prisma:studio": "prisma studio" }, diff --git a/scripts/verify-prisma-client-schema.mjs b/scripts/verify-prisma-client-schema.mjs new file mode 100644 index 0000000..e7656cf --- /dev/null +++ b/scripts/verify-prisma-client-schema.mjs @@ -0,0 +1,55 @@ +import fs from 'node:fs' +import path from 'node:path' +import { createRequire } from 'node:module' + +const ROOT = process.cwd() +const SOURCE_SCHEMA = path.join(ROOT, 'prisma', 'schema.prisma') +const require = createRequire(import.meta.url) +const clientPackage = require.resolve('@prisma/client/package.json') +const GENERATED_SCHEMA = path.resolve(path.dirname(clientPackage), '..', '..', '.prisma', 'client', 'schema.prisma') + +function readSchemaShape(schemaPath) { + const source = fs.readFileSync(schemaPath, 'utf8') + const shape = {} + const declaration = /(?:^|\n)(model|enum)\s+(\w+)\s*\{([\s\S]*?)\n\}/g + + for (const match of source.matchAll(declaration)) { + const [, kind, name, body] = match + const members = [] + + for (const rawLine of body.split(/\r?\n/)) { + const line = rawLine.trim() + if (!line || line.startsWith('//') || line.startsWith('@@')) continue + const [memberName, memberType] = line.split(/\s+/) + members.push(kind === 'model' ? `${memberName}:${memberType}` : memberName) + } + + shape[`${kind}:${name}`] = members.sort() + } + + return shape +} + +if (!fs.existsSync(GENERATED_SCHEMA)) { + throw new Error(`Prisma Client schema is missing: ${GENERATED_SCHEMA}`) +} + +const sourceShape = readSchemaShape(SOURCE_SCHEMA) +const generatedShape = readSchemaShape(GENERATED_SCHEMA) + +if (JSON.stringify(sourceShape) !== JSON.stringify(generatedShape)) { + const sourceKeys = new Set(Object.keys(sourceShape)) + const generatedKeys = new Set(Object.keys(generatedShape)) + const missingDeclarations = [...sourceKeys].filter((key) => !generatedKeys.has(key)) + const staleDeclarations = [...sourceKeys].filter((key) => ( + generatedKeys.has(key) + && JSON.stringify(sourceShape[key]) !== JSON.stringify(generatedShape[key]) + )) + throw new Error( + `Prisma Client does not match prisma/schema.prisma; regenerate it before building. ` + + `Missing declarations: ${missingDeclarations.join(', ') || 'none'}. ` + + `Stale declarations: ${staleDeclarations.join(', ') || 'none'}.`, + ) +} + +console.log(`Prisma Client schema verified (${Object.keys(sourceShape).length} declarations)`)