From b6040f32ec6b48b0e1980b005b4d7b63b8ce38d9 Mon Sep 17 00:00:00 2001 From: DyatkoGleb Date: Fri, 11 Nov 2022 01:26:24 +0400 Subject: [PATCH] 1.2.0 --- Changelog.md | 10 +- Readme.md | 29 +++-- lib/CommandNameParser.js | 55 +++++---- lib/FileMigration.js | 159 +++++++++++++++++++++++++ lib/LogHelper.js | 27 +++++ lib/connection.js | 4 +- lib/index.js | 21 ++-- lib/migration.js | 251 ++++++++++++--------------------------- package.json | 4 +- 9 files changed, 333 insertions(+), 227 deletions(-) create mode 100644 lib/FileMigration.js create mode 100644 lib/LogHelper.js diff --git a/Changelog.md b/Changelog.md index a3ed43d..18761d7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,8 @@ +## Version 1.2.0 +- Added the possibility create migrations for work with columns: add/change/drop. +- Redesigned work with errors. +- Refactor. + ## Version 1.1.0 - Added colors for informational messages in the console. - Checking the availability of the database configuration directory. @@ -7,12 +12,11 @@ - Added error handling for failed migration UP. - Added command "help". - ## Version 1.0.3 -- Hotfix +- Hotfix. ## Version 1.0.2 -- Hotfix +- Hotfix. ## Version 1.0.1 - Create migration. diff --git a/Readme.md b/Readme.md index 9bdcbc5..b308f2d 100644 --- a/Readme.md +++ b/Readme.md @@ -1,18 +1,20 @@ # node-mysql2-migrations +![Node.js Version][node-version-image] + A tool to support migration using the mysql 2 package in node.js ## Prerequisites -A node project with [mysql2](https://github.com/sidorares/node-mysql2) used for database. +A node project with [mysql2] used for database. ## Install -It can be installed using npm. +It can be installed using npm. ``` npm i mysql2-migration ``` ## Setup -1. Create a file in config directory where you define your database config. (./config/database.js) +1. Create a file in config directory where you define your database config. (./config/database.js) **database.js:** ``` module.exports = { @@ -23,29 +25,38 @@ module.exports = { password: 'password' } ``` -2. In the root of your project, create a file with the package connection. +2. In the root of your project, create a file with the package connection. (Should be on one level with node_modules) **migration.js:** - ``` require('mysql2-migrations') ``` ## Create a migration -Run +Run ``` node migration.js create create_users_table +node migration.js create drop_users_table +node migration.js create add_phone_column_to_users_table +node migration.js create change_phone_column_users_table +node migration.js create drop_phone_column_from_users_table ``` - After that, you can find your migration in ./database/migrations. ## Executing Migrations -There are few ways to run migrations. +There are few ways to run migrations. 1. Run `node migration.js up`. Runs all the pending `up` migrations. 2. Run `node migration.js up 1`. Runs only 1 `up` migrations. 3. Run `node migration.js up 2022_22_07_00675_create_users_table.js`. Runs a migration with name 2022_22_07_00675_create_users_table.js. 4. Run `node migration.js down`. Runs all the migrations `down` from last upped. 5. Run `node migration.js down 1`. Runs only 1 `down` migrations. -6. Run `node migration.js help`. If you forgot something. + +## If you forgot something +``` +node migration.js help +``` + +[node-version-image]: https://img.shields.io/badge/dynamic/xml?color=success&label=node&query=%27%20%3E%3D%20%27&suffix=v12.22.12&url=https%3A%2F%2Fnodejs.org%2F +[mysql2]: (https://github.com/sidorares/node-mysql2) \ No newline at end of file diff --git a/lib/CommandNameParser.js b/lib/CommandNameParser.js index 0f5b506..172c072 100644 --- a/lib/CommandNameParser.js +++ b/lib/CommandNameParser.js @@ -1,33 +1,40 @@ +const logHelper = require('./LogHelper') + + class CommandNameParser { parse = (enteredCommand) => { - if (!enteredCommand) { - console.log("Error: Invalid migration name\nHint: [create/update/drop]_[your_table_name]_table") - process.exit() - } - if (!enteredCommand.replace(/[^a-z_]/g, '').length) { - console.log("Error: Invalid migration name\nHint: [create/update/drop]_[your_table_name]_table") - process.exit() - } - if (enteredCommand.indexOf('_') == -1) { - console.log("Error: Invalid migration name\nHint: [create/update/drop]_[your_table_name]_table") - process.exit() - } + const columnAddAction = new RegExp('^(add_)[a-z0-9_]*(column_to)[a-z0-9_]*(_table)$') + const columnChangeAction = new RegExp('^(change_)[a-z0-9_]*(column)[a-z0-9_]*(_table)$') + const columnDropAction = new RegExp('^(drop_)[a-z0-9_]*(column_from)[a-z0-9_]*(_table)$') + const tableAction = new RegExp('^(?!.*column)^((create_)|(drop_))[a-z0-9_]*(_table)$') + let action = '' + let tableName = '' + let columnName = '' + + enteredCommand = enteredCommand ? enteredCommand.toLowerCase() : enteredCommand - enteredCommand = enteredCommand.replace(/[^a-z_]/g, '') - const migrationCommand = enteredCommand - const allowedActions = ['create', 'update', 'drop'] - const action = enteredCommand.split('_')[0] - const tableName = enteredCommand.split('_')[1].split('_table')[0] - const isEnteredCommandContainsAllowedAction = allowedActions.indexOf(action) != -1 - const isEnteredCommandContainsKeywordObject = enteredCommand.indexOf('_table') == -1 - - if (!isEnteredCommandContainsAllowedAction || isEnteredCommandContainsKeywordObject || !tableName) { - console.log("Error: Invalid migration name\nHint: [create/update/drop]_[your_table_name]_table") + if (columnAddAction.test(enteredCommand) || columnChangeAction.test(enteredCommand) || columnDropAction.test(enteredCommand)) { + action = enteredCommand.split('_')[0] + const withoutPreAndPostfoxes = enteredCommand.split(action+'_')[1].slice(0, -6) + if (action === 'add') { + [columnName, tableName] = withoutPreAndPostfoxes.split('_column_to_') + } else if (action === 'change') { + [columnName, tableName] = withoutPreAndPostfoxes.split('_column_') + } else if (action === 'drop'){ + [columnName, tableName] = withoutPreAndPostfoxes.split('_column_from_') + } + } else if (tableAction.test(enteredCommand)) { + action = enteredCommand.split('_')[0] + tableName = enteredCommand.split(action+'_')[1].slice(0, -6) + } else { + logHelper.error('Error: Invalid migration name') + logHelper.hint('\n[create/drop]_[table_name]_table\n[add/change/drop]_[column_name]_[to_/from_/ ][table_name]_table') process.exit() } - - return [action, migrationCommand, tableName] + + return [enteredCommand, action, tableName, columnName] } } + module.exports = new CommandNameParser() \ No newline at end of file diff --git a/lib/FileMigration.js b/lib/FileMigration.js new file mode 100644 index 0000000..a8113a4 --- /dev/null +++ b/lib/FileMigration.js @@ -0,0 +1,159 @@ +const comandNameParser = require('mysql2-migration/lib/CommandNameParser') +const logHelper = require('mysql2-migration/lib/LogHelper') +const fs = require('fs') +const migrationsFolder = 'database/migrations/' + + +class FileMigration { + create = () => { + const enteredCommand = process.argv[3] + const [migrationCommand, action, tableName, columnName] = comandNameParser.parse(enteredCommand) + const migrationName = `${migrationsFolder + this.getCurrentDate()}_${this.getSecondsToday()}_${migrationCommand}.js` + + this.createMigrationsDir() + + fs.writeFile(migrationName, this.makeMigrationText(action, tableName, columnName), (err) => { + if (err) throw err + logHelper.message(`Migration created ${migrationsFolder + migrationName}`) + }) + } + + getCurrentDate = () => { + const today = new Date() + const year = today.getFullYear() + let month = today.getMonth() + 12 + let day = today.getDate() + + if (month < 10) month = '0' + month + if (day < 10) day = '0' + day + + return year + '_' + month + '_' + day + } + + getSecondsToday = () => { + const date = new Date() + const counterLength = 5 + const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + + return String(seconds).padStart(counterLength, '0') + } + + createMigrationsDir = () => { + fs.mkdirSync(migrationsFolder, { recursive: true }) + } + + makeMigrationText = (action, tableName, columnName) => { + if (columnName) { + switch(action) { + case 'add': + return this.makeTextMigrationAddColumn(tableName, columnName) + case 'change': + return this.makeTextMigrationChangeColumn(tableName, columnName) + case 'drop': + return this.makeTextMigrationDropColumn(tableName, columnName) + } + } else { + switch(action) { + case 'create': + return this.makeTextMigrationCreateTable(tableName) + case 'drop': + return this.makeTextMigrationDropTable(tableName) + } + } + } + + makeTextMigrationCreateTable = (tableName) => { + return `const connection = require('mysql2-migration/lib/connection') + +class CreateMigration +{ + up = async () => { + return await connection.query(\`CREATE TABLE ${tableName} ( + id int not null primary key auto_increment, + + created_at datetime not null, + updated_at datetime not null + )\`) + } + + down = async () => { + return await connection.query(\`DROP TABLE ${tableName}\`) + } +} + +module.exports = new CreateMigration()` + } + + makeTextMigrationDropTable = (tableName) => { + return `const connection = require('mysql2-migration/lib/connection') + +class CreateMigration +{ + up = async () => { + return await connection.query(\`DROP TABLE ${tableName}\`) + } + + down = async () => { + return await connection.query(\`CREATE TABLE ${tableName} ( + id int not null primary key auto_increment + )\`) + } +} + +module.exports = new CreateMigration()` + } + + makeTextMigrationAddColumn = (tableName, columnName) => { + return `const connection = require('mysql2-migration/lib/connection') + +class CreateMigration +{ + up = async () => { + return await connection.query(\`ALTER TABLE ${tableName} ADD ${columnName}\`) + } + + down = async () => { + return await connection.query(\`ALTER TABLE ${tableName} DROP ${columnName}\`) + } +} + +module.exports = new CreateMigration()` + } + + makeTextMigrationChangeColumn = (tableName, columnName) => { + return `const connection = require('mysql2-migration/lib/connection') + +class CreateMigration +{ + up = async () => { + return await connection.query(\`ALTER TABLE ${tableName} ALTER COLUMN ${columnName}\`) + } + + down = async () => { + return await connection.query(\`ALTER TABLE ${tableName} ALTER COLUMN ${columnName}\`) + } +} + +module.exports = new CreateMigration()` + } + + makeTextMigrationDropColumn = (tableName, columnName) => { + return `const connection = require('mysql2-migration/lib/connection') + +class CreateMigration +{ + up = async () => { + return await connection.query(\`ALTER TABLE ${tableName} DROP ${columnName}\`) + } + + down = async () => { + return await connection.query(\`ALTER TABLE ${tableName} ADD ${columnName}\`) + } +} + +module.exports = new CreateMigration()` + } +} + + +module.exports = new FileMigration() \ No newline at end of file diff --git a/lib/LogHelper.js b/lib/LogHelper.js new file mode 100644 index 0000000..44df280 --- /dev/null +++ b/lib/LogHelper.js @@ -0,0 +1,27 @@ +const colors = require('colors') + + +class LogHelper { + error = (text) => { + console.log(colors.red(text)) + } + + warning = (text) => { + console.log(colors.yellow(text)) + } + + success = (text) => { + console.log(colors.green(text)) + } + + hint = (text) => { + console.log(colors.cyan('Hint: ' + text)) + } + + message = (text) => { + console.log(text) + } +} + + +module.exports = new LogHelper() \ No newline at end of file diff --git a/lib/connection.js b/lib/connection.js index ca7b5e3..822d028 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -2,8 +2,8 @@ const config = require('../../../config/database') const mysql = require('mysql2') const pool = mysql.createConnection({ - host: config.host ?? '127.0.0.1', - port: config.port ?? '3306', + host: config.host ? config.host : '127.0.0.1', + port: config.port ? config.port : '3306', user: config.user, database: config.database, password: config.password diff --git a/lib/index.js b/lib/index.js index 7d4f93d..661b3df 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,11 +1,12 @@ const migration = require('./migration') -const colors = require('colors') +const logHelper = require('./LogHelper') +const fileMigration = require('./FileMigration') const fs = require('fs') const action = process.argv[2] if (!fs.existsSync('./config/database.js')) { - console.log(colors.red('Error: File configuration config/database.js does not exist')) + logHelper.error('Error: File configuration config/database.js does not exist') process.exit() } @@ -13,17 +14,19 @@ if (!fs.existsSync('./config/database.js')) { if (action === '-h' || action === '--help' || action === 'help') { console.log('Usage: node [file_with_package_connection] [commands] [additional_parameter]\n') console.log('Commands:') - console.log(' create Create mirgation [create/update/drop]_[your_table_name]_table') - console.log(' up [N] Runs all or [N/migration_name] the pending UP migrations, or UP a specific migration. Ex.: up 2022_22_07_00675_create_users_table.js') - console.log(' down [N] Runs all or [N] the migrations DOWN from last upped.') - console.log(' -h, --help, help') + console.log(' create Create mirgation: \n\t\t\t [create/drop]_[table_name]_table \n\t\t\t [add/change/drop]_[column_name]_[to_/from_/ ][table_name]_table') + console.log(' up [N/migration_name] Runs all or [N/migration_name] the pending UP migrations, or UP a specific migration. Ex.: up 2022_22_07_00675_create_users_table.js') + console.log(' down [N] Runs all or [N] the migrations DOWN from last upped.') + console.log('\n -h, --help, help') } else if (action === 'down' || action === 'up') { migration.startMigration(action) } else if (action === 'create') { - migration.createMigration() + fileMigration.create() } else if (action === undefined) { - console.log(colors.red("Error: You didn't pass the command.\nHint: [create/update/drop]_[your_table_name]_table")) + logHelper.error(`Error: You didn't pass the command.`) + logHelper.hint('Allowed command: create, up [N], down [N], help') } else { - console.log(colors.red("Error: Not allowed command.\nHint: Allowed command: create, up [N], down [N], help")) + logHelper.error(`Error: Not allowed command.`) + logHelper.hint('Allowed command: create, up [N], down [N], help') } diff --git a/lib/migration.js b/lib/migration.js index 5e33c12..3d6ec8f 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -1,109 +1,33 @@ -const comandNameParser = require('./CommandNameParser') +const logHelper = require('./LogHelper') const colors = require('colors') const fs = require('fs') const migrationsFolder = 'database/migrations/' class Migration { - constructor() {} - - getCurrentDate = () => { - const today = new Date() - const year = today.getFullYear() - let month = today.getMonth() + 12 - let day = today.getDate() - - if (month < 10) month = '0' + month - if (day < 10) day = '0' + day - - return year + '_' + month + '_' + day - } - - getSecondsToday = () => { - const date = new Date() - const counterLength = 5 - const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() - - return String(seconds).padStart(counterLength, '0') - } - - - makeMigrationCreateText = (tableName) => { - return `const connection = require('mysql2-migration/lib/connection') - -class CreateMigration -{ - up = async () => { - return await connection.query(\`CREATE TABLE ${tableName} ( - id int not null primary key auto_increment, - - created_at datetime not null, - updated_at datetime not null - )\`) - } - - down = async () => { - return await connection.query(\`DROP TABLE ${tableName}\`) - } -} - -module.exports = new CreateMigration()` - } - - makeMigrationUpdateText = (tableName) => { - return `const connection = require('mysql2-migration/lib/connection') - -class CreateMigration -{ - up = () => { - connection.query(\`UPDATE ${tableName}\`) - } - - down = () => { - connection.query(\`UPDATE ${tableName}\`) - } -} + startMigration = async (action) => { + this.connection = require('./connection') + const migrateParam = process.argv[3] -module.exports = new CreateMigration()` - } - - makeMigrationDropText = (tableName) => { - return `const connection = require('mysql2-migration/lib/connection') - -class CreateMigration -{ - up = () => { - connection.query(\`DROP TABLE ${tableName}\`) - } - - down = () => { - connection.query(\`CREATE TABLE ${tableName} ( - id int not null primary key auto_increment - )\`) - } -} + await this.checkExistsMigrationFiles(migrationsFolder) -module.exports = new CreateMigration()` - } - - makeMigrationText = (action, tableName) => { switch(action) { - case 'create': - return this.makeMigrationCreateText(tableName) - case 'update': - return this.makeMigrationUpdateText(tableName) - case 'drop': - return this.makeMigrationDropText(tableName) + case 'up': + const isParam = !migrateParam + const isMigrationsCount = (typeof (+migrateParam) == 'number' && !isNaN(+migrateParam)) + + if (isParam || isMigrationsCount) { + await this.migrationUp(action, migrateParam) + } else { + await this.upSpecificMigration(action, migrateParam) + } + break + case 'down': + await this.migrationDown(action, migrateParam) + break } - } - - createMigrationsDir = () => { - fs.mkdirSync(migrationsFolder, { recursive: true }) - } - - stopProcessAndShowReason = (reason) => { - console.log(colors.red(reason)) - process.exit() + + this.connection.end() } isRowsInMigrationsTableExists = async () => { @@ -137,7 +61,8 @@ module.exports = new CreateMigration()` if (err.sqlState === '42S02' && err.errno === 1146) { return false } else { - this.stopProcessAndShowReason(err) + logHelper.error('Error: File configuration config/database.js does not exist') + process.exit() } } } @@ -156,7 +81,25 @@ module.exports = new CreateMigration()` } } - upSpecificMigration = async (file, action) => { + getMigrationFiles = async (lastMigrationName, countMigrate) => { + let files = fs.readdirSync(migrationsFolder) + + if (lastMigrationName) { + const migrationName = lastMigrationName.migration_name + let migrationIndexInFilesArray = files.indexOf(migrationName) + + if (migrationIndexInFilesArray === -1 ) { + logHelper.error('Error: The migration files do not match the records in the database. File migration /' + migrationsFolder + migrationName + ' does not Exists') + } else { + files = files.slice(migrationIndexInFilesArray + 1) + } + } + + files = files.slice(0, countMigrate) + return files + } + + upSpecificMigration = async (action, file) => { const migration = require('../../../' + migrationsFolder + file) process.stdout.write('Migration ' + migrationsFolder + file + ' ' + action) @@ -164,51 +107,52 @@ module.exports = new CreateMigration()` try { if (await migration.up()) { await this.updateMigrationsTable(file, action) - - console.log(colors.green(' completed')) + logHelper.success(' completed') } } catch (err) { - console.log(colors.red(' failed')) - console.log(err) + console.log() + logHelper.error(err) } } migrationUp = async (action, countMigrate) => { await this.createMigartionsTableIfNotExists() - let lastMigration = await this.getLastUppedMigration().then(data => { return data[0][0] }) - let files = fs.readdirSync(migrationsFolder) - - if (lastMigration) { - const migrationName = lastMigration.migration_name - let migrationIndexInFilesArray = files.indexOf(migrationName) + let lastMigrationName = await this.getLastUppedMigration().then(data => { return data[0][0] }) - if (migrationIndexInFilesArray == -1 ) { - this.stopProcessAndShowReason('Error: The migration files do not match the records in the database. File migration /' + migrationsFolder + migrationName + ' does not Exists') - } else { - files = files.slice(migrationIndexInFilesArray + 1) - } - } - - files = files.slice(0, countMigrate) + const migrationFiles = await this.getMigrationFiles(lastMigrationName, countMigrate) - if (!files.length) { - console.log(colors.yellow('Nothing to up')) + if (!migrationFiles.length) { + logHelper.warning('Nothing to up') return } - for (let file of files) { - await this.upSpecificMigration(file, action) + for (let file of migrationFiles) { + await this.upSpecificMigration(action, file) + } + } + + downSpecificMigration = async (action, files, migrationName) => { + if (files.indexOf(migrationName) === -1 ) { + logHelper.error('Error: The migration files do not match the records in the database. File migration /' + migrationsFolder + migrationName + ' does not Exists') + process.exit() + } + + const migration = require('../../../' + migrationsFolder + migrationName) + if (await migration.down()) { + await this.updateMigrationsTable(migrationName, action) } + + logHelper.message('Migration ' + migrationsFolder + migrationName + ' ' + action + colors.green(' completed')) } migrationDown = async (action, countMigrate) => { if (!await this.isMigrationTableExists()) { - console.log(colors.yellow('Nothing to down')) + logHelper.warning('Nothing to up') return } if (!await this.isRowsInMigrationsTableExists().then(result => { return result[0].length })) { - console.log(colors.yellow('Nothing to down')) + logHelper.warning('Nothing to up') return } @@ -216,79 +160,30 @@ module.exports = new CreateMigration()` const files = fs.readdirSync(migrationsFolder) let indexDown = 0 - for (let name of migrations[0]) { - if (indexDown == countMigrate) { - break - } - - const migrationName = name.migration_name + for (let migration of migrations[0]) { + if (indexDown === countMigrate) break - if (files.indexOf(migrationName) == -1 ) { - this.stopProcessAndShowReason('Error: The migration files do not match the records in the database. File migration /' + migrationsFolder + migrationName + ' does not Exists') - } + await this.downSpecificMigration(action, files, migration.migration_name) - const migration = require('../../../' + migrationsFolder + migrationName) - - if (await migration.down()) { - await this.updateMigrationsTable(migrationName, action) - } - - console.log('Migration ' + migrationsFolder + migrationName + ' ' + action + colors.green(' completed')) indexDown++ } } checkExistsMigrationFiles = async (migrationsFolder) => { if (!fs.existsSync(migrationsFolder)) { - this.stopProcessAndShowReason(`Error: Migrations in ${migrationsFolder} does not Exists`) + logHelper.error(`Error: Migrations in ${migrationsFolder} does not Exists`) + process.exit() } fs.readdir(migrationsFolder, (err, files) => { if (err) throw err if (!files.length) { - this.stopProcessAndShowReason(`Error: Migrations in ${migrationsFolder} does not Exists`) + logHelper.error(`Error: Migrations in ${migrationsFolder} does not Exists`) + process.exit() } }) } - - createMigration = () => { - const enteredCommand = process.argv[3] - const [action, migrationCommand, tableName] = comandNameParser.parse(enteredCommand) - const migrationName = `${migrationsFolder + this.getCurrentDate()}_${this.getSecondsToday()}_${migrationCommand}.js` - - this.createMigrationsDir() - - fs.writeFile(migrationName, this.makeMigrationText(action, tableName), (err) => { - if (err) throw err - console.log(`Migration created ${migrationsFolder + migrationName}`) - }) - } - - startMigration = async (action) => { - this.connection = require('./connection') - const migrateParam = process.argv[3] - - await this.checkExistsMigrationFiles(migrationsFolder) - - switch(action) { - case 'up': - const isParam = !migrateParam - const isMigrationsCount = (typeof (+migrateParam) == 'number' && !isNaN(+migrateParam)) - - if (isParam || isMigrationsCount) { - await this.migrationUp(action, migrateParam) - } else { - await this.upSpecificMigration(migrateParam, action) - } - break - case 'down': - await this.migrationDown(action, migrateParam) - break - } - - this.connection.end() - } } diff --git a/package.json b/package.json index 0adbc44..6f59b7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mysql2-migration", - "version": "1.1.0", + "version": "1.2.0", "description": "A tool to support migration using the mysql 2 package in node.js", "main": "index.js", "scripts": { @@ -22,7 +22,7 @@ }, "homepage": "https://github.com/DyatkoGleb/node-mysql2-migrations#readme", "engines": { - "node": ">= 14.6.15" + "node": ">= 12.22.12" }, "dependencies": { "colors": "^1.4.0"