UNPKG

datacops-cms

Version:

A modern, extensible CMS built with Next.js and Prisma.

210 lines (183 loc) 7.95 kB
import fs from "fs"; import path from "path"; // --- MARKERS for auto-generated section --- const GENERATED_START = "// *** AUTO-GENERATED MODELS START ***"; const GENERATED_END = "// *** AUTO-GENERATED MODELS END ***"; // --- Utility to escape for regex --- function escapeRegex(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // --- Utility: Sanitize Prisma field/model names --- function toSafeFieldName(name: string): string { return name .trim() .replace(/\s+/g, '_') // Replace spaces with underscores .replace(/[^a-zA-Z0-9_]/g, '') // Remove any char that's not alphanumeric or underscore .replace(/^(\d)/, '_$1'); // Prefix with _ if starts with number } // --- Paths --- const contentTypesFolder = path.resolve(process.cwd(), "content-types"); const prismaSchemaPath = path.resolve(process.cwd(), "prisma/schema.prisma"); const typeMap: Record<string, string> = { text: "String", textarea: "String", richtext: "String", number: "Int", date: "DateTime", checkbox: "Boolean", select: "String", radio: "String", image: "String", file: "String", slug: "String", // Support slug as string }; function capitalize(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } function getContentTypeModels(): string { if (!fs.existsSync(contentTypesFolder)) return ""; const files = fs.readdirSync(contentTypesFolder).filter(f => f.endsWith(".json")); const schemasByName: Record<string, any> = {}; const reverseRelations: { [target: string]: Array<{ source: string, fieldName: string, relType: string, relName: string, }> } = {}; // --- First pass: parse schemas and collect relations --- for (const file of files) { const schema = JSON.parse(fs.readFileSync(path.join(contentTypesFolder, file), "utf-8")); const safeModelName = toSafeFieldName(capitalize(schema.name)); schemasByName[safeModelName] = schema; for (const field of schema.fields) { if (field.type === "relation" && field.relation) { const safeTarget = toSafeFieldName(capitalize(field.relation.target)); const relType = field.relation.relationType; const relName = `${safeModelName}_${field.name}`; if (!reverseRelations[safeTarget]) reverseRelations[safeTarget] = []; reverseRelations[safeTarget].push({ source: safeModelName, fieldName: field.name, relType, relName, }); } } } // --- Second pass: generate models including reverse fields --- const allModels: Record<string, string[]> = {}; for (const [modelName, schema] of Object.entries(schemasByName)) { let modelLines: string[] = []; modelLines.push(`model ${modelName} {`); modelLines.push(` id String @id @default(uuid())`); // Forward fields for (const field of schema.fields) { const safeName = toSafeFieldName(field.name); // --- Relation fields --- if (field.type === "relation" && field.relation) { const safeTarget = toSafeFieldName(capitalize(field.relation.target)); const relType = field.relation.relationType; const relName = `${modelName}_${field.name}`; if (relType === "one-one") { modelLines.push( ` ${safeName} ${safeTarget}? @relation("${relName}", fields: [${safeName}Id], references: [id])` ); modelLines.push(` ${safeName}Id String? @unique`); } else if (relType === "one-many") { modelLines.push( ` ${safeName} ${safeTarget}? @relation("${relName}", fields: [${safeName}Id], references: [id])` ); modelLines.push(` ${safeName}Id String?`); } else if (relType === "many-many") { modelLines.push( ` ${safeName} ${safeTarget}[] @relation("${relName}")` ); } } else { // --- Regular fields --- let fieldType = typeMap[field.type] || "String"; let unique = ""; if (safeName.toLowerCase() === "slug") { unique = " @unique"; // Mark slug fields unique } modelLines.push(` ${safeName} ${fieldType}${field.required ? "" : "?"}${unique}`); } } // --- Add status and schedule fields --- modelLines.push(` status Status @default(Draft)`); modelLines.push(` schedule DateTime?`); modelLines.push(` createdAt DateTime @default(now())`); modelLines.push(` updatedAt DateTime @updatedAt`); // Reverse relation fields (arrays for many, singular for one-to-one) if (reverseRelations[modelName]) { for (const rel of reverseRelations[modelName]) { // Avoid duplicate field names const forwardFields = schema.fields.map((f: any) => toSafeFieldName(f.name)); let reverseFieldName = rel.source.toLowerCase(); if (["one-many", "many-many"].includes(rel.relType)) reverseFieldName += "s"; let fieldType = rel.source; let fieldLine = ""; if (rel.relType === "one-one") { fieldLine = ` ${reverseFieldName} ${fieldType}? @relation("${rel.relName}")`; } else if (rel.relType === "one-many" || rel.relType === "many-many") { fieldLine = ` ${reverseFieldName} ${fieldType}[] @relation("${rel.relName}")`; } // Only add if not already present if (!forwardFields.includes(reverseFieldName)) { modelLines.push(fieldLine); } } } modelLines.push("}"); allModels[modelName] = modelLines; } // Join all models return Object.values(allModels).map(lines => lines.join("\n")).join("\n\n"); } // --- Read the current schema.prisma, or create base if missing --- let schemaPrisma = ""; if (fs.existsSync(prismaSchemaPath)) { schemaPrisma = fs.readFileSync(prismaSchemaPath, "utf-8"); } else { schemaPrisma = ` generator client { provider = "prisma-client-js" } datasource db { provider = "${process.env.DATABASE_TYPE || "sqlite"}" url = env("DATABASE_URL") } ${GENERATED_START} ${GENERATED_END} `; } // --- Ensure Status enum is present --- const statusEnum = ` enum Status { Draft Published Scheduled } `; if (!schemaPrisma.includes("enum Status {")) { schemaPrisma = statusEnum.trim() + "\n\n" + schemaPrisma.trim(); } // --- Replace (or insert) the generated section --- const generatedModels = getContentTypeModels(); const startPattern = escapeRegex(GENERATED_START); const endPattern = escapeRegex(GENERATED_END); const regex = new RegExp(`${startPattern}[\\s\\S]*?${endPattern}`); if (schemaPrisma.includes(GENERATED_START) && schemaPrisma.includes(GENERATED_END)) { schemaPrisma = schemaPrisma.replace( regex, `${GENERATED_START}\n${generatedModels}\n${GENERATED_END}` ); } else { // If marker not found, append at end schemaPrisma += `\n${GENERATED_START}\n${generatedModels}\n${GENERATED_END}\n`; } // --- Write back --- fs.writeFileSync(prismaSchemaPath, schemaPrisma); console.log("✅ Prisma schema generated.");