forge-sql-orm-cli
Version:
CLI tool for Forge SQL ORM
212 lines (181 loc) • 7.16 kB
text/typescript
import "reflect-metadata";
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
/**
* Options for migration creation
*/
export interface CreateMigrationOptions {
output: string;
entitiesPath: string;
force?: boolean;
}
/**
* Loads the current migration version from `migrationCount.ts`.
* @param migrationPath - Path to the migration folder.
* @returns The latest migration version.
*/
export const loadMigrationVersion = async (migrationPath: string): Promise<number> => {
try {
const migrationCountFilePath = path.resolve(path.join(migrationPath, "migrationCount.ts"));
if (!fs.existsSync(migrationCountFilePath)) {
console.log(`✅ Current migration version: 0`);
return 0;
}
const { MIGRATION_VERSION } = await import(migrationCountFilePath);
console.log(`✅ Current migration version: ${MIGRATION_VERSION}`);
return MIGRATION_VERSION as number;
} catch (error) {
console.error(`❌ Error loading migrationCount:`, error);
process.exit(1);
}
};
/**
* Cleans SQL statements by removing unnecessary database options.
* @param sql - The raw SQL statement.
* @returns The cleaned SQL statement.
*/
export function cleanSQLStatement(sql: string): string {
// Add IF NOT EXISTS to CREATE TABLE statements
sql = sql.replace(/create\s+table\s+(\w+)/gi, "create table if not exists $1");
// Add IF NOT EXISTS to CREATE INDEX statements
sql = sql.replace(/create\s+index\s+(\w+)/gi, "create index if not exists $1");
// Add IF NOT EXISTS to ADD INDEX statements
sql = sql.replace(
/alter\s+table\s+(\w+)\s+add\s+index\s+(\w+)/gi,
"alter table $1 add index if not exists $2",
);
// Add IF NOT EXISTS to ADD CONSTRAINT statements
sql = sql.replace(
/alter\s+table\s+(\w+)\s+add\s+constraint\s+(\w+)/gi,
"alter table $1 add constraint if not exists $2",
);
// Remove unnecessary database options
return sql.replace(/\s+default\s+character\s+set\s+utf8mb4\s+engine\s*=\s*InnoDB;?/gi, "").trim();
}
/**
* Generates a migration file using the provided SQL statements.
* @param createStatements - Array of SQL statements.
* @param version - Migration version number.
* @returns TypeScript migration file content.
*/
export function generateMigrationFile(createStatements: string[], version: number): string {
const versionPrefix = `v${version}_MIGRATION`;
// Clean each SQL statement and generate migration lines with .enqueue()
const migrationLines = createStatements
.map(
(stmt, index) =>
` .enqueue("${versionPrefix}${index}", "${cleanSQLStatement(stmt).replace(/\s+/g, " ")}")`,
)
.join("\n");
// Migration template
return `import { MigrationRunner } from "@forge/sql/out/migration";
export default (migrationRunner: MigrationRunner): MigrationRunner => {
return migrationRunner
${migrationLines};
};`;
}
/**
* Saves the generated migration file along with `migrationCount.ts` and `index.ts`.
* @param migrationCode - The migration code to be written to the file.
* @param version - Migration version number.
* @param outputDir - Directory where the migration files will be saved.
*/
export function saveMigrationFiles(migrationCode: string, version: number, outputDir: string) {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const migrationFilePath = path.join(outputDir, `migrationV${version}.ts`);
const migrationCountPath = path.join(outputDir, `migrationCount.ts`);
const indexFilePath = path.join(outputDir, `index.ts`);
// Write the migration file
fs.writeFileSync(migrationFilePath, migrationCode);
// Write the migration count file
fs.writeFileSync(migrationCountPath, `export const MIGRATION_VERSION = ${version};`);
// Generate the migration index file
const indexFileContent = `import { MigrationRunner } from "@forge/sql/out/migration";
import { MIGRATION_VERSION } from "./migrationCount";
export type MigrationType = (
migrationRunner: MigrationRunner,
) => MigrationRunner;
export default async (
migrationRunner: MigrationRunner,
): Promise<MigrationRunner> => {
for (let i = 1; i <= MIGRATION_VERSION; i++) {
const migrations = (await import(\`./migrationV\${i}\`)) as {
default: MigrationType;
};
migrations.default(migrationRunner);
}
return migrationRunner;
};`;
fs.writeFileSync(indexFilePath, indexFileContent);
console.log(`✅ Migration file created: ${migrationFilePath}`);
console.log(`✅ Migration count file updated: ${migrationCountPath}`);
console.log(`✅ Migration index file created: ${indexFilePath}`);
}
/**
* Extracts only the relevant SQL statements for migration.
* @param schema - The full database schema as SQL.
* @returns Filtered list of SQL statements.
*/
export const extractCreateStatements = (schema: string): string[] => {
// Split by statement-breakpoint and semicolon
const statements = schema
.split(/--> statement-breakpoint|;/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
return statements.filter(
(stmt) =>
stmt.toLowerCase().startsWith("create table") ||
stmt.toLowerCase().startsWith("alter table") ||
stmt.toLowerCase().includes("add index") ||
stmt.toLowerCase().includes("create index") ||
stmt.toLowerCase().includes("add unique index") ||
stmt.toLowerCase().includes("add constraint"),
);
};
/**
* Creates a full database migration.
* @param options - Database connection settings and output paths.
*/
export const createMigration = async (options: CreateMigrationOptions) => {
try {
let version = await loadMigrationVersion(options.output);
if (version > 0) {
if (options.force) {
console.warn(
`⚠️ Warning: Migration already exists. Creating new migration with force flag...`,
);
} else {
console.error(
`❌ Error: Migration has already been created. Use --force flag to override.`,
);
process.exit(1);
}
}
// Generate SQL using drizzle-kit
await execSync(
`npx drizzle-kit generate --name=init --dialect mysql --out ${options.output} --schema ${options.entitiesPath}`,
{ encoding: "utf-8" },
);
const initSqlFile = path.join(options.output, "0000_init.sql");
const sql = fs.readFileSync(initSqlFile, "utf-8");
// Extract and clean statements
const createStatements = extractCreateStatements(sql);
// Generate and save migration files
const migrationFile = generateMigrationFile(createStatements, 1);
saveMigrationFiles(migrationFile, 1, options.output);
fs.rmSync(initSqlFile, { force: true });
console.log(`✅ Removed SQL file: ${initSqlFile}`);
// Remove meta directory after processing
let metaDir = path.join(options.output, "meta");
fs.rmSync(metaDir, { recursive: true, force: true });
console.log(`✅ Removed: ${metaDir}`);
console.log(`✅ Migration successfully created!`);
process.exit(0);
} catch (error) {
console.error(`❌ Error during migration creation:`, error);
process.exit(1);
}
};