UNPKG

@reliverse/rse

Version:

@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power

238 lines (237 loc) 7.75 kB
import path from "@reliverse/pathkit"; import { ensuredir } from "@reliverse/relifso"; import fs from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { readPackageJSON, writePackageJSON } from "pkg-types"; function parsePrismaSchema(content) { const models = []; const modelRegex = /model\s+(\w+)\s*{([^}]+)}/g; const fieldRegex = /(\w+)\s+(\w+)(\[\])?\s*(\?)?\s*(@[^@\n]+)?/g; let match; while ((match = modelRegex.exec(content)) !== null) { const [_, modelName, fieldsStr] = match; const fields = []; let fieldMatch; if (fieldsStr) { while ((fieldMatch = fieldRegex.exec(fieldsStr)) !== null) { const [_2, name, type, isList, isOptional, attributes] = fieldMatch; if (name && type) { fields.push({ name, type, isOptional: !!isOptional, isList: !!isList, attributes: parseAttributes(attributes ?? "") }); } } } if (modelName) { models.push({ name: modelName, fields }); } } return models; } function parseAttributes(attributesStr) { const attributes = {}; const attrRegex = /@(\w+)(?:\((.*?)\))?/g; let match; while ((match = attrRegex.exec(attributesStr)) !== null) { const [_, name, args] = match; if (name) { attributes[name] = args ? parseAttributeArgs(args) : true; } } return attributes; } function parseAttributeArgs(args) { if (args.startsWith("[") && args.endsWith("]")) { return args.slice(1, -1).split(",").map((s) => s.trim()); } if (args === "true") { return true; } if (args === "false") { return false; } if (/^\d+$/.test(args)) { return parseInt(args, 10); } return args.replace(/['"]/g, ""); } function convertPrismaToDrizzleType(field, dbType) { const typeMap = { postgres: { Int: "integer", String: "text", Boolean: "boolean", DateTime: "timestamp", Float: "real", BigInt: "bigint", Decimal: "decimal", Json: "jsonb", Bytes: "bytea" }, mysql: { Int: "int", String: "text", Boolean: "boolean", DateTime: "timestamp", Float: "float", BigInt: "bigint", Decimal: "decimal", Json: "json", Bytes: "blob" }, sqlite: { Int: "integer", String: "text", Boolean: "integer", DateTime: "integer", Float: "real", BigInt: "integer", Decimal: "real", Json: "text", Bytes: "blob" } }; return typeMap[dbType]?.[field.type] ?? "text"; } function generateDrizzleSchema(models, dbType) { const tablePrefix = dbType === "postgres" ? "pg" : dbType === "mysql" ? "mysql" : "sqlite"; const imports = /* @__PURE__ */ new Set([`${tablePrefix}Table`]); const modelSchemas = models.map((model) => { const fields = model.fields.map((field) => { const drizzleType = convertPrismaToDrizzleType(field, dbType); imports.add(drizzleType); let fieldDef = `${field.name}: ${drizzleType}("${field.name}")`; if (field.attributes.id) { fieldDef += ".primaryKey()"; } if (field.attributes.unique) { fieldDef += ".unique()"; } if (!field.isOptional) { fieldDef += ".notNull()"; } if (field.attributes.default) { if (field.type === "DateTime" && field.attributes.default === "now()") { fieldDef += ".default(sql`CURRENT_TIMESTAMP`)"; imports.add("sql"); } else { fieldDef += `.default(${field.attributes.default})`; } } return fieldDef; }); return `export const ${model.name.toLowerCase()} = ${tablePrefix}Table("${model.name.toLowerCase()}", { ${fields.join(",\n ")} });`; }); let importStatement = `import { ${Array.from(imports).join(", ")} } from "drizzle-orm/${dbType}";`; if (imports.has("sql")) { importStatement = `import { sql } from "drizzle-orm"; ${importStatement}`; } return `${importStatement} ${modelSchemas.join("\n\n")}`; } export async function convertPrismaToDrizzle(cwd, targetDbType) { const prismaSchemaPath = path.join(cwd, "prisma/schema.prisma"); if (!await fs.pathExists(prismaSchemaPath)) { relinka("error", "No Prisma schema found"); return; } const schemaContent = await fs.readFile(prismaSchemaPath, "utf-8"); const models = parsePrismaSchema(schemaContent); const drizzleSchema = generateDrizzleSchema(models, targetDbType); const drizzleConfig = `import type { Config } from "drizzle-kit"; export default { schema: "./src/db/schema.ts", out: "./drizzle", } satisfies Config;`; await ensuredir(path.join(cwd, "src/db")); await fs.writeFile(path.join(cwd, "src/db/schema.ts"), drizzleSchema); await fs.writeFile(path.join(cwd, "drizzle.config.ts"), drizzleConfig); const packageJsonPath = path.join(cwd, "package.json"); if (await fs.pathExists(packageJsonPath)) { const packageJson = await readPackageJSON(packageJsonPath); if (packageJson.dependencies) { delete packageJson.dependencies["@prisma/client"]; } if (packageJson.devDependencies) { delete packageJson.devDependencies.prisma; } packageJson.dependencies = { ...packageJson.dependencies, "drizzle-orm": "latest" }; packageJson.devDependencies = { ...packageJson.devDependencies, "drizzle-kit": "latest" }; if (targetDbType === "postgres") { packageJson.dependencies.postgres = "latest"; } else if (targetDbType === "mysql") { packageJson.dependencies.mysql2 = "latest"; } else if (targetDbType === "sqlite") { packageJson.dependencies["better-sqlite3"] = "latest"; packageJson.devDependencies["@types/better-sqlite3"] = "latest"; } await writePackageJSON(packageJsonPath, packageJson); } relinka("success", "Converted Prisma schema to Drizzle schema"); } export async function convertDatabaseProvider(cwd, fromProvider, toProvider) { const packageJsonPath = path.join(cwd, "package.json"); if (!await fs.pathExists(packageJsonPath)) { relinka("error", "No package.json found"); return; } const packageJson = await readPackageJSON(packageJsonPath); if (fromProvider === "postgres" && toProvider === "libsql") { if (packageJson.dependencies) { delete packageJson.dependencies.postgres; delete packageJson.dependencies["@neondatabase/serverless"]; } packageJson.dependencies = { ...packageJson.dependencies, "@libsql/client": "latest" }; const envPath = path.join(cwd, ".env"); if (await fs.pathExists(envPath)) { let envContent = await fs.readFile(envPath, "utf-8"); envContent = envContent.replace( /DATABASE_URL=.*postgres:\/\/.*/g, "DATABASE_URL=libsql://[YOUR-DATABASE].turso.io" ); await fs.writeFile(envPath, envContent); } const dbClientPaths = [ "src/lib/db.ts", "src/db/client.ts", "src/database/index.ts" ]; for (const memoryPath of dbClientPaths) { const fullPath = path.join(cwd, memoryPath); if (await fs.pathExists(fullPath)) { let content = await fs.readFile(fullPath, "utf-8"); content = content.replace( /import .*postgres.*/g, 'import { createClient } from "@libsql/client";' ); content = content.replace( /const .*= new Pool\(.*\)/g, "const client = createClient({ url: process.env.DATABASE_URL! });" ); await fs.writeFile(fullPath, content); break; } } } await writePackageJSON(packageJsonPath, packageJson); relinka( "success", `Converted database provider from ${fromProvider} to ${toProvider}` ); }