UNPKG

forge-sql-orm-cli

Version:

CLI tool for Forge SQL ORM

268 lines (234 loc) 8.65 kB
import "reflect-metadata"; import fs from "fs"; import path from "path"; import { execSync } from "child_process"; /** * Options for model generation */ interface GenerateModelsOptions { host: string; port: number; user: string; password: string; dbName: string; output: string; versionField: string; } /** * Interface for column metadata */ interface ColumnMetadata { autoincrement: boolean; name: string; type: string; primaryKey: boolean; notNull: boolean; } /** * Interface for table metadata */ interface TableMetadata { name: string; columns: Record<string, ColumnMetadata>; compositePrimaryKeys: Record<string, { name: string; columns: string[] }>; indexes: Record<string, any>; foreignKeys: Record<string, any>; uniqueConstraints: Record<string, any>; checkConstraint: Record<string, any>; } /** * Interface for version field metadata */ interface VersionFieldMetadata { fieldName: string; } /** * Interface for table version metadata */ interface TableVersionMetadata { tableName: string; versionField: VersionFieldMetadata; } /** * Type for additional metadata map */ type AdditionalMetadata = Record<string, TableVersionMetadata>; /** * Interface for journal entry */ interface JournalEntry { idx: number; version: string; when: number; tag: string; breakpoints: boolean; } /** * Interface for journal data */ interface JournalData { version: string; dialect: string; entries: JournalEntry[]; } /** * Replaces MySQL types with custom types in the generated schema * @param schemaContent - The content of the generated schema file * @returns Modified schema content with custom types */ function replaceMySQLTypes(schemaContent: string): string { // Add imports at the top of the file const imports = `import { forgeDateTimeString, forgeTimeString, forgeDateString, forgeTimestampString } from "forge-sql-orm";\n\n`; // Replace types in the content let modifiedContent = schemaContent // Handle datetime with column name and mode option .replace( /datetime\(['"]([^'"]+)['"],\s*{\s*mode:\s*['"]string['"]\s*}\)/g, "forgeDateTimeString('$1')", ) // Handle datetime with column name only .replace(/datetime\(['"]([^'"]+)['"]\)/g, "forgeDateTimeString('$1')") // Handle datetime with mode option only .replace(/datetime\(\s*{\s*mode:\s*['"]string['"]\s*}\s*\)/g, "forgeDateTimeString()") // Handle time with column name and mode option .replace(/time\(['"]([^'"]+)['"],\s*{\s*mode:\s*['"]string['"]\s*}\)/g, "forgeTimeString('$1')") // Handle time with column name only .replace(/time\(['"]([^'"]+)['"]\)/g, "forgeTimeString('$1')") // Handle time with mode option only .replace(/time\(\s*{\s*mode:\s*['"]string['"]\s*}\s*\)/g, "forgeTimeString()") // Handle date with column name and mode option .replace(/date\(['"]([^'"]+)['"],\s*{\s*mode:\s*['"]string['"]\s*}\)/g, "forgeDateString('$1')") // Handle date with column name only .replace(/date\(['"]([^'"]+)['"]\)/g, "forgeDateString('$1')") // Handle date with mode option only .replace(/date\(\s*{\s*mode:\s*['"]string['"]\s*}\s*\)/g, "forgeDateString()") // Handle timestamp with column name and mode option .replace( /timestamp\(['"]([^'"]+)['"],\s*{\s*mode:\s*['"]string['"]\s*}\)/g, "forgeTimestampString('$1')", ) // Handle timestamp with column name only .replace(/timestamp\(['"]([^'"]+)['"]\)/g, "forgeTimestampString('$1')") // Handle timestamp with mode option only .replace(/timestamp\(\s*{\s*mode:\s*['"]string['"]\s*}\s*\)/g, "forgeTimestampString()"); // Add imports if they don't exist if (!modifiedContent.includes("import { forgeDateTimeString")) { modifiedContent = imports + modifiedContent; } return modifiedContent; } /** * Generates models for all tables in the database using drizzle-kit * @param options - Generation options */ export const generateModels = async (options: GenerateModelsOptions) => { try { // Generate models using drizzle-kit pull await execSync( `npx drizzle-kit pull --dialect mysql --url mysql://${options.user}:${options.password}@${options.host}:${options.port}/${options.dbName} --out ${options.output}`, { encoding: "utf-8" }, ); // Process metadata to create version map const metaDir = path.join(options.output, "meta"); const additionalMetadata: AdditionalMetadata = {}; if (fs.existsSync(metaDir)) { const snapshotFile = path.join(metaDir, "0000_snapshot.json"); if (fs.existsSync(snapshotFile)) { const snapshotData = JSON.parse(fs.readFileSync(snapshotFile, "utf-8")); // Process each table from the snapshot for (const [tableName, tableData] of Object.entries(snapshotData.tables)) { const table = tableData as TableMetadata; // Find version field in columns const versionField = Object.entries(table.columns).find( ([_, col]) => col.name.toLowerCase() === options.versionField, ); if (versionField) { const [_, col] = versionField; const fieldType = col.type; const isSupportedType = fieldType === "datetime" || fieldType === "timestamp" || fieldType === "int" || fieldType === "number" || fieldType === "decimal"; if (!col.notNull) { console.warn( `Version field "${col.name}" in table ${tableName} is nullable. Versioning may not work correctly.`, ); } else if (!isSupportedType) { console.warn( `Version field "${col.name}" in table ${tableName} has unsupported type "${fieldType}". ` + `Only datetime, timestamp, int, and decimal types are supported for versioning. Versioning will be skipped.`, ); } else { additionalMetadata[tableName] = { tableName, versionField: { fieldName: col.name, }, }; } } } } } // Create version metadata file const versionMetadataContent = `/** * This file was auto-generated by forge-sql-orm * Generated at: ${new Date().toISOString()} * * DO NOT EDIT THIS FILE MANUALLY * Any changes will be overwritten on next generation */ export * from "./relations"; export * from "./schema"; export interface VersionFieldMetadata { fieldName: string; } export interface TableMetadata { tableName: string; versionField: VersionFieldMetadata; } export type AdditionalMetadata = Record<string, TableMetadata>; export const additionalMetadata: AdditionalMetadata = ${JSON.stringify(additionalMetadata, null, 2)}; `; fs.writeFileSync(path.join(options.output, "index.ts"), versionMetadataContent); // Replace MySQL types in the generated schema file const schemaPath = path.join(options.output, "schema.ts"); if (fs.existsSync(schemaPath)) { const schemaContent = fs.readFileSync(schemaPath, "utf-8"); const modifiedContent = replaceMySQLTypes(schemaContent); fs.writeFileSync(schemaPath, modifiedContent); console.log(`✅ Updated schema types in: ${schemaPath}`); } // Remove migration files and meta directory if they exist const migrationDir = path.join(options.output, "migrations"); if (fs.existsSync(migrationDir)) { fs.rmSync(migrationDir, { recursive: true, force: true }); console.log(`✅ Removed: ${migrationDir}`); } // Read journal and remove corresponding SQL file if (fs.existsSync(metaDir)) { const journalFile = path.join(metaDir, "_journal.json"); if (fs.existsSync(journalFile)) { const journalData = JSON.parse(fs.readFileSync(journalFile, "utf-8")) as JournalData; // Remove SQL files for each entry for (const entry of journalData.entries) { const sqlFile = path.join(options.output, `${entry.tag}.sql`); if (fs.existsSync(sqlFile)) { fs.rmSync(sqlFile, { force: true }); console.log(`✅ Removed SQL file: ${entry.tag}.sql`); } } } // Remove meta directory after processing fs.rmSync(metaDir, { recursive: true, force: true }); console.log(`✅ Removed: ${metaDir}`); } console.log(`✅ Successfully generated models and version metadata`); process.exit(0); } catch (error) { console.error(`❌ Error during model generation:`, error); process.exit(1); } };