@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
JavaScript
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}`
);
}