datacops-cms
Version:
A modern, extensible CMS built with Next.js and Prisma.
210 lines (183 loc) • 7.95 kB
text/typescript
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.");