UNPKG

@reliverse/rse-sdk

Version:

@reliverse/rse-sdk allows you to create new plugins for @reliverse/rse CLI, interact with reliverse.org, and even extend your own CLI functionality (you may also try @reliverse/dler-sdk for this case).

493 lines (492 loc) 16.6 kB
import path from "@reliverse/pathkit"; import fs from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { selectPrompt, confirmPrompt, inputPrompt } from "@reliverse/rempts"; import { installIntegration } from "../editor-mod.js"; import { INTEGRATION_CONFIGS } from "../feature-add.js"; import { COLUMN_TYPES } from "./manageDrizzleConstants.js"; export async function detectDatabaseProvider(cwd) { const drizzleConfigPath = path.join(cwd, "drizzle.config.ts"); if (await fs.pathExists(drizzleConfigPath)) { const content = await fs.readFile(drizzleConfigPath, "utf-8"); if (content.includes("postgres")) { return "postgres"; } if (content.includes("sqlite")) { return "sqlite"; } if (content.includes("mysql")) { return "mysql"; } } return null; } export async function setupDrizzle(cwd, isDev) { relinka( "info", "Drizzle is not set up in this project. Let's set it up first." ); const provider = await selectPrompt({ title: "Select database provider:", options: [ { label: "PostgreSQL", value: "postgres" }, { label: "SQLite", value: "sqlite" }, { label: "MySQL", value: "mysql" } ] }); if (provider === "postgres") { const pgProvider = await selectPrompt({ title: "Select PostgreSQL provider:", options: [ { label: "Neon", value: "neon" }, { label: "Railway", value: "railway" }, { label: "Other", value: "postgres" } ] }); const drizzleConfig = INTEGRATION_CONFIGS.drizzle; if (!drizzleConfig) { throw new Error("Drizzle integration configuration not found"); } const config = { ...drizzleConfig, dependencies: [ ...drizzleConfig.dependencies, pgProvider === "neon" ? "@neondatabase/serverless" : "postgres" ] }; await installIntegration(cwd, config, isDev); } else { const drizzleConfig = INTEGRATION_CONFIGS.drizzle; if (!drizzleConfig) { throw new Error("Drizzle integration configuration not found"); } await installIntegration(cwd, drizzleConfig, isDev); } return provider; } export async function getAvailableTables(cwd, useMultipleFiles) { if (useMultipleFiles) { const schemaDir = path.join(cwd, "src/db/schema"); const files = await fs.readdir(schemaDir); return files.filter((file) => file.endsWith(".ts") && file !== "index.ts").map((file) => file.replace(".ts", "")); } else { const schemaFile = path.join(cwd, "src/db/schema.ts"); if (await fs.pathExists(schemaFile)) { const content = await fs.readFile(schemaFile, "utf-8"); const tableMatches = content.match(/export const (\w+)\s*=/g); return tableMatches ? tableMatches.map((match) => match.split(" ")[2]).filter((name) => name !== void 0) : []; } } return []; } export async function addNewTable(cwd, useMultipleFiles, provider) { const tableName = await inputPrompt({ title: "Enter the table name:", validate: (value) => { if (!value?.trim()) { return "Table name is required"; } if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) { return "Table name must start with a letter and contain only letters, numbers, and underscores"; } return true; } }); const columns = []; let addingColumns = true; while (addingColumns) { const columnName = await inputPrompt({ title: "Enter column name:", validate: (value) => { if (!value?.trim()) { return "Column name is required"; } if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) { return "Column name must start with a letter and contain only letters, numbers, and underscores"; } return true; } }); const columnType = await selectPrompt({ title: "Select column type:", options: COLUMN_TYPES[provider].map((type) => ({ label: type, value: type })) }); const nullable = await confirmPrompt({ title: "Is this column nullable?", defaultValue: false }); const primaryKey = await confirmPrompt({ title: "Is this column a primary key?", defaultValue: false }); const unique = !primaryKey && await confirmPrompt({ title: "Should this column be unique?", defaultValue: false }); const hasDefaultValue = await confirmPrompt({ title: "Do you want to set a default value?", defaultValue: false }); let defaultValue; if (hasDefaultValue) { if (columnType === "timestamp" || columnType === "timestamptz" || columnType === "datetime") { const useNow = await confirmPrompt({ title: "Use current timestamp as default?", defaultValue: true }); if (useNow) { defaultValue = "sql`CURRENT_TIMESTAMP`"; } } else { defaultValue = await inputPrompt({ title: "Enter default value:" }); } } const isReference = await confirmPrompt({ title: "Is this a foreign key reference?", defaultValue: false }); let references; if (isReference) { const tables = await getAvailableTables( cwd, useMultipleFiles // singleSchemaDir, // multiSchemaDir, ); if (tables.length > 0) { const refTable = await selectPrompt({ title: "Select referenced table:", options: tables.map((t) => ({ label: t, value: t })) }); references = { table: refTable, column: "id" // Assuming referenced column is always 'id' for simplicity }; } } columns.push({ name: columnName, type: columnType, nullable, primaryKey, unique, ...defaultValue && { defaultValue }, ...references && { references } }); addingColumns = await confirmPrompt({ title: "Add another column?", defaultValue: true }); } const schema = { name: tableName, columns }; if (useMultipleFiles) { const filePath = path.join(cwd, "src/db/schema", `${tableName}.ts`); await generateTableFile(filePath, schema, provider); const indexPath = path.join(cwd, "src/db/schema/index.ts"); await updateSchemaIndex(indexPath, tableName); } else { const filePath = path.join(cwd, "src/db/schema.ts"); await appendTableToSchema(filePath, schema, provider); } relinka("success", `Table ${tableName} created successfully!`); } export async function removeTable(cwd, useMultipleFiles, provider) { const tables = await getAvailableTables(cwd, useMultipleFiles); if (tables.length === 0) { relinka("error", "No tables found to remove"); return; } const tableName = await selectPrompt({ title: "Select table to remove:", options: tables.map((t) => ({ label: t, value: t })) }); const confirm = await confirmPrompt({ title: `Are you sure you want to remove the table ${tableName}?`, content: "This action cannot be undone", defaultValue: false }); if (!confirm) { relinka("info", "Table removal cancelled"); return; } if (useMultipleFiles) { const filePath = path.join(cwd, "src/db/schema", `${tableName}.ts`); await fs.remove(filePath); const indexPath = path.join(cwd, "src/db/schema/index.ts"); await removeFromSchemaIndex(indexPath, tableName); } else { const filePath = path.join(cwd, "src/db/schema.ts"); await removeTableFromSchema(filePath, tableName, provider); } relinka("success", `Table ${tableName} removed successfully!`); } export async function renameTable(cwd, useMultipleFiles, provider) { const tables = await getAvailableTables(cwd, useMultipleFiles); if (tables.length === 0) { relinka("error", "No tables found to rename"); return; } const oldName = await selectPrompt({ title: "Select table to rename:", options: tables.map((t) => ({ label: t, value: t })) }); const newName = await inputPrompt({ title: "Enter new table name:", validate: (value) => { if (!value?.trim()) { return "Table name is required"; } if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) { return "Table name must start with a letter and contain only letters, numbers, and underscores"; } if (tables.includes(value)) { return "A table with this name already exists"; } return true; } }); if (useMultipleFiles) { const oldPath = path.join(cwd, "src/db/schema", `${oldName}.ts`); const newPath = path.join(cwd, "src/db/schema", `${newName}.ts`); await fs.move(oldPath, newPath); const indexPath = path.join(cwd, "src/db/schema/index.ts"); await updateTableNameInIndex(indexPath, oldName, newName); } else { const filePath = path.join(cwd, "src/db/schema.ts"); await renameTableInSchema(filePath, oldName, newName, provider); } relinka( "success", `Table renamed from ${oldName} to ${newName} successfully!` ); } export async function manageRelations(cwd, useMultipleFiles, provider) { const tables = await getAvailableTables(cwd, useMultipleFiles); if (tables.length < 2) { relinka("error", "Need at least two tables to manage relations"); return; } const sourceTable = await selectPrompt({ title: "Select source table:", options: tables.map((t) => ({ label: t, value: t })) }); const targetTable = await selectPrompt({ title: "Select target table:", options: tables.filter((t) => t !== sourceTable).map((t) => ({ label: t, value: t })) }); const relationType = await selectPrompt({ title: "Select relation type:", options: [ { label: "One-to-One", value: "oneToOne" }, { label: "One-to-Many", value: "oneToMany" }, { label: "Many-to-Many", value: "manyToMany" } ] }); if (relationType === "manyToMany") { const junctionTableName = `${sourceTable}_to_${targetTable}`; const schema = { name: junctionTableName, columns: [ { name: `${sourceTable}Id`, type: "integer", nullable: false, references: { table: sourceTable, column: "id" } }, { name: `${targetTable}Id`, type: "integer", nullable: false, references: { table: targetTable, column: "id" } } ] }; if (useMultipleFiles) { const filePath = path.join( cwd, "src/db/schema", `${junctionTableName}.ts` ); await generateTableFile(filePath, schema, provider); const indexPath = path.join(cwd, "src/db/schema/index.ts"); await updateSchemaIndex(indexPath, junctionTableName); } else { const filePath = path.join(cwd, "src/db/schema.ts"); await appendTableToSchema(filePath, schema, provider); } } else { const column = { name: `${targetTable}Id`, type: "integer", nullable: relationType === "oneToMany", references: { table: targetTable, column: "id" } }; if (useMultipleFiles) { const filePath = path.join(cwd, "src/db/schema", `${sourceTable}.ts`); await addColumnToTable(filePath, sourceTable, column, provider); } else { const filePath = path.join(cwd, "src/db/schema.ts"); await addColumnToTable(filePath, sourceTable, column, provider); } } relinka("success", "Relation added successfully!"); } export async function generateTableFile(filePath, schema, provider) { const dbPrefix = provider === "postgres" ? "pg" : provider; const content = `import { sql } from "drizzle-orm"; import { ${schema.columns.map((c) => c.type).join(", ")}, ${dbPrefix}Table } from "drizzle-orm/${provider}-core"; export const ${schema.name} = ${dbPrefix}Table("${schema.name}", { ${schema.columns.map((col) => { let def = `${col.name}: ${col.type}("${col.name}")`; if (col.primaryKey) { def += ".primaryKey()"; } if (col.unique) { def += ".unique()"; } if (!col.nullable) { def += ".notNull()"; } if (col.defaultValue) { def += `.default(${col.defaultValue})`; } if (col.references) { def += `.references(() => ${col.references.table}.${col.references.column})`; } return def; }).join(",\n ")} });`; await fs.writeFile(filePath, content); } export async function updateSchemaIndex(indexPath, tableName) { let content = ""; if (await fs.pathExists(indexPath)) { content = await fs.readFile(indexPath, "utf-8"); } const exportStatement = `export * from "./${tableName}";`; if (!content.includes(exportStatement)) { content += content ? ` ${exportStatement}` : exportStatement; await fs.writeFile(indexPath, content); } } export async function appendTableToSchema(filePath, schema, provider) { let content = ""; if (await fs.pathExists(filePath)) { content = await fs.readFile(filePath, "utf-8"); } const dbPrefix = provider === "postgres" ? "pg" : provider; const tableDefinition = ` export const ${schema.name} = ${dbPrefix}Table("${schema.name}", { ${schema.columns.map((col) => { let def = `${col.name}: ${col.type}("${col.name}")`; if (col.primaryKey) { def += ".primaryKey()"; } if (col.unique) { def += ".unique()"; } if (!col.nullable) { def += ".notNull()"; } if (col.defaultValue) { def += `.default(${col.defaultValue})`; } if (col.references) { def += `.references(() => ${col.references.table}.${col.references.column})`; } return def; }).join(",\n ")} });`; content += tableDefinition; await fs.writeFile(filePath, content); } export async function removeFromSchemaIndex(indexPath, tableName) { if (await fs.pathExists(indexPath)) { let content = await fs.readFile(indexPath, "utf-8"); content = content.replace(`export * from "./${tableName}";`, ""); content = content.replace(/\n\n+/g, "\n"); await fs.writeFile(indexPath, content); } } export async function removeTableFromSchema(filePath, tableName, provider) { if (await fs.pathExists(filePath)) { let content = await fs.readFile(filePath, "utf-8"); const dbPrefix = provider === "postgres" ? "pg" : provider; const regex = new RegExp( `\\nexport const ${tableName} = ${dbPrefix}Table[\\s\\S]*?\\);\\n?`, "g" ); content = content.replace(regex, "\n"); content = content.replace(/\n\n+/g, "\n\n"); await fs.writeFile(filePath, content); } } export async function renameTableInSchema(filePath, oldName, newName, provider) { if (await fs.pathExists(filePath)) { let content = await fs.readFile(filePath, "utf-8"); const dbPrefix = provider === "postgres" ? "pg" : provider; content = content.replace( new RegExp( `export const ${oldName} = ${dbPrefix}Table\\("${oldName}"`, "g" ), `export const ${newName} = ${dbPrefix}Table("${newName}"` ); await fs.writeFile(filePath, content); } } export async function updateTableNameInIndex(indexPath, oldName, newName) { if (await fs.pathExists(indexPath)) { let content = await fs.readFile(indexPath, "utf-8"); content = content.replace( `export * from "./${oldName}";`, `export * from "./${newName}";` ); await fs.writeFile(indexPath, content); } } export async function addColumnToTable(filePath, tableName, column, provider) { if (await fs.pathExists(filePath)) { let content = await fs.readFile(filePath, "utf-8"); const dbPrefix = provider === "postgres" ? "pg" : provider; const tableRegex = new RegExp( `export const ${tableName} = ${dbPrefix}Table\\([\\s\\S]*?\\);` ); const match = content.match(tableRegex); if (match) { const tableContent = match[0]; const insertPoint = tableContent.lastIndexOf("}"); let columnDef = ` ${column.name}: ${column.type}("${column.name}")`; if (column.primaryKey) { columnDef += ".primaryKey()"; } if (column.unique) { columnDef += ".unique()"; } if (!column.nullable) { columnDef += ".notNull()"; } if (column.defaultValue) { columnDef += `.default(${column.defaultValue})`; } if (column.references) { columnDef += `.references(() => ${column.references.table}.${column.references.column})`; } columnDef += ","; const newTableContent = tableContent.slice(0, insertPoint) + columnDef + tableContent.slice(insertPoint); content = content.replace(tableRegex, newTableContent); await fs.writeFile(filePath, content); } } }