Skip to content

Instantly share code, notes, and snippets.

@pipethedev
Last active April 7, 2024 08:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pipethedev/68d7157369ff5cd29db473edd33a5510 to your computer and use it in GitHub Desktop.
Save pipethedev/68d7157369ff5cd29db473edd33a5510 to your computer and use it in GitHub Desktop.
Convert object model to knex migrations
import fs from 'fs';
import path from 'path';
import { exec } from 'child_process';
enum KnexMigrationType {
Id = 'uuid',
String = 'string',
Number = 'integer',
Float = 'float',
Array = 'enum',
Date = 'timestamp',
Boolean = 'boolean',
}
enum DataTypes {
String = 'string',
Number = 'number',
Boolean = 'boolean',
Date = 'Date',
}
interface ModelField {
field: string;
type: KnexMigrationType;
nullable: boolean;
}
interface ModelFile {
filePath: string;
tableName: string;
}
//Use your own directories (I could make it passed as a flag, later on)
const modelsFolderPath = path.join(__dirname, '../../../models');
const migrationsDir = path.join(__dirname, '../../../database/migrations');
const modelFiles = getTableNamesFromModelsFolder(modelsFolderPath);
const transformedFiles = modelFiles.map(({ filePath, tableName }) => {
const newFilePath = filePath.replace('dist/', '').replace('.js', '.ts');
return { filePath: newFilePath, tableName };
});
function getTableNamesFromModelsFolder(modelsFolderPath: string): ModelFile[] {
const modelFiles: ModelFile[] = [];
fs.readdirSync(modelsFolderPath).forEach((file) => {
const filePath = path.join(modelsFolderPath, file);
const tableName = getTableNameFromFile(filePath);
if (tableName) {
modelFiles.push({ filePath, tableName });
}
});
return modelFiles;
}
function getTableNameFromFile(filePath: string): string | null {
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
const match = fileContent.match(/static theTableName\(\) {\s*return '(.*)';\s*}/);
if (match && match[1]) {
return match[1];
}
} catch (err) {
console.error('Error reading file:', err);
}
return null;
}
async function writeMigrationFile(migrationScript: string, fileName: string): Promise<void> {
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir);
}
const filePath = path.join(migrationsDir, fileName).replace(/\/dist\//, '/');
await fs.promises.writeFile(filePath, migrationScript);
}
async function createKnexMigrationFromModel(modelPath: string): Promise<string> {
const timestamp = new Date().toISOString().replace(/[^0-9]/g, '');
const modelData = await fs.promises.readFile(modelPath, 'utf8');
const modelName = path.basename(modelPath, '.ts');
const tableNameMatch = modelData.match(/static theTableName\(\) {\s*return '(.*)';\s*}/);
const tableName = tableNameMatch ? tableNameMatch[1] : modelName.toLowerCase() + 's';
const fileName = `${timestamp}_${tableName}.ts`;
const fieldRegex = /(\w+)\s*(!|\?)?\s*:\s*(\w+\s*\|\s*\w+|\w+)(\[\])?;/g;
const fields: ModelField[] = [];
const supportedTypes = Object.values(DataTypes);
const notNullableFields = ['id', 'created_at', 'createdAt'];
let match: Array<any>;
while ((match = fieldRegex.exec(modelData)) !== null) {
let [, field, isRequired, type, isArray] = match;
if (!isRequired || isRequired === '!' || isRequired === '?') {
const nullable = isRequired === '?' && !notNullableFields.includes(field);
if (type.includes('|')) type = KnexMigrationType.String;
if (isArray) type = KnexMigrationType.Array;
if (!supportedTypes.includes(type as DataTypes) && !isArray) type = DataTypes.String;
if (field.includes('id')) type = KnexMigrationType.Id;
if (type === 'Date') type = KnexMigrationType.Date;
if (type === 'number') type = KnexMigrationType.Number;
if (field.includes('balance')) type = KnexMigrationType.Float;
fields.push({ field, type, nullable });
}
}
let migrationScript = `import { Knex as KnexType } from 'knex';\n\n`;
migrationScript += ` export async function up(knex: KnexType): Promise<void> {\n`;
migrationScript += ` await knex.schema.createTable('${tableName}', (table) => {\n`;
fields.forEach((field) => {
if (field.field === 'created_at' || field.field === 'updated_at') {
migrationScript += ` table.${field.type}('${field.field}').defaultTo(knex.fn.now());\n`;
} else if (field.type == KnexMigrationType.String) {
migrationScript += ` table.${field.type}('${field.field}', 225);\n`;
} else if (field.type == KnexMigrationType.Array) {
migrationScript += ` table.${field.type}('${field.field}', []);\n`;
} else {
migrationScript += ` table.${field.type}('${field.field}')${field.field === 'id' ? '.primary()' : ''}${
field.nullable ? '.nullable()' : ''
};\n`;
}
});
migrationScript += ` });\n`;
migrationScript += `};\n\n`;
migrationScript += `export async function down(knex: KnexType): Promise<void> {\n`;
migrationScript += ` await knex.schema.dropTable('${tableName}');\n`;
migrationScript += `};\n`;
await writeMigrationFile(migrationScript, fileName);
return migrationScript;
}
function main() {
transformedFiles.forEach(async (transformedFile) => {
await createKnexMigrationFromModel(transformedFile.filePath);
});
exec('npx prettier --write .', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
}
main();
//Add to package.json [yarn auto:migrate OR npm run auto:migrate]
"auto:migrate": "npm run build && ts-node -r tsconfig-paths/register ./<REPLACE_BUILD_PATH>/auto-migrate.script.js",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment