From 78f0c173f59068b38deed353b49bb619175d8544 Mon Sep 17 00:00:00 2001 From: Paulo Lopes Date: Fri, 4 Nov 2016 14:35:38 +0000 Subject: [PATCH] Add schema change to add timestamps to Sequelize meta table --- README.md | 108 ++++++------ lib/tasks/db.js | 162 +++++++++++++----- test/db/migrate/schema/add_timestamps.test.js | 111 ++++++++++++ 3 files changed, 294 insertions(+), 87 deletions(-) create mode 100644 test/db/migrate/schema/add_timestamps.test.js diff --git a/README.md b/README.md index 8da2a4d3..a7b257f6 100644 --- a/README.md +++ b/README.md @@ -6,64 +6,68 @@ The Sequelize Command Line Interface (CLI) Install this globally and you'll have access to the `sequelize` command anywhere on your system. -``` -npm install -g sequelize-cli +```bash +$ npm install -g sequelize-cli ``` or install it locally to your `node_modules` folder ```bash -npm install --save sequelize-cli +$ npm install --save sequelize-cli ``` ## Global Install Usage -``` +```bash $ sequelize [--HARMONY-FLAGS] ``` ``` -Sequelize [Node: 2.5.0, CLI: 1.8.3, ORM: 2.1.3] +Sequelize [Node: 7.8.0, CLI: 2.7.0, ORM: 3.27.0] Usage - sequelize [task] - + sequelize [task][options...] Available tasks - db:migrate Run pending migrations. - db:migrate:old_schema Update legacy migration table - db:migrate:undo Revert the last migration run. - db:migrate:undo:all Revert all migrations ran. - db:seed Run seeders. - db:seed:undo Deletes data from the database. - db:seed:undo:all Deletes data from the database. - help Display this help text. Aliases: h - init Initializes the project. - init:config Initializes the configuration. - init:migrations Initializes the migrations. - init:models Initializes the models. - init:seeders Initializes the seeders. - migration:create Generates a new migration file. Aliases: migration:generate - model:create Generates a model and its migration. Aliases: model:generate - seed:create Generates a new seed file. Aliases: seed:generate - version Prints the version number. Aliases: v - + db:migrate Run pending migrations. + db:migrate:old_schema Update legacy migration table + db:migrate:schema:timestamps:add Update migration table to have timestamps + db:migrate:status List the status of all migrations + db:migrate:undo Reverts a migration. + db:migrate:undo:all Revert all migrations ran. + db:seed Run specified seeder. + db:seed:all Run every seeder. + db:seed:undo Deletes data from the database. + db:seed:undo:all Deletes data from the database. + help Display this help text. Aliases: h + init Initializes the project. [init:config, init:migrations, init:seeders, init:models] + init:config Initializes the configuration. + init:migrations Initializes the migrations. + init:models Initializes the models. + init:seeders Initializes the seeders. + migration:create Generates a new migration file. Aliases: migration:generate + model:create Generates a model and its migration. Aliases: model:generate + seed:create Generates a new seed file. Aliases: seed:generate + version Prints the version number. Aliases: v Available manuals - help:db:migrate The documentation for "sequelize db:migrate". - help:db:migrate:old_schema The documentation for "sequelize db:migrate:old_schema". - help:db:migrate:undo The documentation for "sequelize db:migrate:undo". - help:db:migrate:undo:all The documentation for "sequelize db:migrate:undo:all". - help:db:seed The documentation for "sequelize db:seed". - help:db:seed:undo The documentation for "sequelize db:seed:undo". - help:db:seed:undo:all The documentation for "sequelize db:seed:undo:all". - help:init The documentation for "sequelize init". - help:init:config The documentation for "sequelize init:config". - help:init:migrations The documentation for "sequelize init:migrations". - help:init:models The documentation for "sequelize init:models". - help:init:seeders The documentation for "sequelize init:seeders". - help:migration:create The documentation for "sequelize migration:create". - help:model:create The documentation for "sequelize model:create". - help:seed:create The documentation for "sequelize seed:create". - help:version The documentation for "sequelize version". + help:db:migrate The documentation for "sequelize db:migrate". + help:db:migrate:old_schema The documentation for "sequelize db:migrate:old_schema". + help:db:migrate:schema:timestamps:add The documentation for "sequelize db:migrate:schema:timestamps:add". + help:db:migrate:status The documentation for "sequelize db:migrate:status". + help:db:migrate:undo The documentation for "sequelize db:migrate:undo". + help:db:migrate:undo:all The documentation for "sequelize db:migrate:undo:all". + help:db:seed The documentation for "sequelize db:seed". + help:db:seed:all The documentation for "sequelize db:seed:all". + help:db:seed:undo The documentation for "sequelize db:seed:undo". + help:db:seed:undo:all The documentation for "sequelize db:seed:undo:all". + help:init The documentation for "sequelize init". + help:init:config The documentation for "sequelize init:config". + help:init:migrations The documentation for "sequelize init:migrations". + help:init:models The documentation for "sequelize init:models". + help:init:seeders The documentation for "sequelize init:seeders". + help:migration:create The documentation for "sequelize migration:create". + help:model:create The documentation for "sequelize model:create". + help:seed:create The documentation for "sequelize seed:create". + help:version The documentation for "sequelize version". ``` ## Local Install Usage @@ -82,7 +86,7 @@ or a Node.JS script that exports a hash. ### Example for a Node.JS script -```javascript +```js var path = require('path') module.exports = { @@ -158,7 +162,7 @@ environment variable called `DB_CONNECTION_STRING` which stores the value `mysql://root:password@mysql_host.com/database_name`. In order to make the CLI use it, you have to use declare it in your config file: -``` +```json { "production": { "use_env_variable": "DB_CONNECTION_STRING" @@ -168,7 +172,7 @@ use it, you have to use declare it in your config file: With v2.0.0 of the CLI you can also just directly access the environment variables inside the `config/config.js`: -``` +```js module.exports = { "production": { "hostname": process.env.DB_HOSTNAME @@ -195,7 +199,7 @@ or the CLI will write to the file `sequelize-meta.json`. If you want to keep the database, using `sequelize`, but want to use a different table, you can change the table name using `migrationStorageTableName`. -```js +```json { "development": { "username": "root", @@ -228,7 +232,7 @@ you can specify the path of the file using `seederStoragePath` or the CLI will w `sequelize-data.json`. If you want to keep the information in the database, using `sequelize`, you can specify the table name using `seederStorageTableName`, or it will default to `SequelizeData`. -```js +```json { "development": { "username": "root", @@ -252,7 +256,7 @@ specify the table name using `seederStorageTableName`, or it will default to `Se In order to pass options to the underlying database connectors, you can add the property `dialectOptions` to your configuration like this: -``` +```js var fs = require('fs'); module.exports = { @@ -280,11 +284,19 @@ when you run a migration while having the old schema. You can opt-in for auto mi } ``` +#### Timestamps + +Since v2.8.0 the CLI supports a adding timestamps to the schema for saving the executed migrations. You can opt-in for timestamps by running the following command: + +```bash +$ sequelize db:migrate:schema:timestamps:add +``` + ### The migration schema The CLI uses [umzug](https://github.com/sequelize/umzug) and its migration schema. This means a migration has to look like this: -```javascript +```js "use strict"; module.exports = { diff --git a/lib/tasks/db.js b/lib/tasks/db.js index bbccdd10..eb363845 100644 --- a/lib/tasks/db.js +++ b/lib/tasks/db.js @@ -275,7 +275,9 @@ module.exports = { }, task: function () { - tryToMigrateFromOldSchema() + return getMigrator('migration') + .then(function (migrator) { + return tryToMigrateFromOldSchema(migrator) .then(function (items) { if (items) { console.log('Successfully migrated ' + items.length + ' migrations.'); @@ -283,45 +285,76 @@ module.exports = { process.exit(0); }, function (err) { - console.log(err.name); + console.error(err); + process.exit(1); + }); + }); + } + }, + + 'db:migrate:schema:timestamps:add': { + descriptions: { + 'short': 'Update migration table to have timestamps', + 'long': [ + 'This command updates the structure of the SequelizeMeta table to include', + '"createdAt" and "updatedAt" columns. This is an optional definition and', + 'not required to keep on working with sequelize-cli.', + '', + 'Please note that the script will create a backup of your old table schema', + 'table by renaming the original table to "Backup".' + ] + }, + + task: function () { + return getMigrator('migration') + .then(function (migrator) { + return addTimestampsToSchema(migrator) + .then(function (items) { + if (items) { + console.log('Successfully added timestamps to MetaTable.'); + } else { + console.log('MetaTable already has timestamps.'); + } + + process.exit(0); + }, function (err) { + console.error(err); process.exit(1); }); + }); } } }; function ensureCurrentMetaSchema (migrator) { - var sequelize = migrator.options.storageOptions.sequelize; + var queryInterface = migrator.options.storageOptions.sequelize.getQueryInterface(); + var tableName = migrator.options.storageOptions.tableName; var columnName = migrator.options.storageOptions.columnName; var config = helpers.config.readConfig(); - return sequelize.getQueryInterface() - .showAllTables() - .then(function (tables) { - if (tables.indexOf('SequelizeMeta') === -1) { - return; - } + return ensureMetaTable(queryInterface, tableName) + .then(function (table) { + var columns = Object.keys(table); - return sequelize.queryInterface - .describeTable('SequelizeMeta') - .then(function (table) { - var columns = Object.keys(table); + if ((columns.length === 1) && (columns[0] === columnName)) { + return; + } else if (columns.length === 3 && columns.indexOf('createdAt') >= 0) { + return; + } else { + if (!config.autoMigrateOldSchema) { + console.error( + 'Database schema was not migrated. Please run ' + + '"sequelize db:migrate:old_schema" first.' + ); + process.exit(1); + } - if ((columns.length === 1) && (columns[0] === columnName)) { - return; - } else { - if (!config.autoMigrateOldSchema) { - console.error( - 'Database schema was not migrated. Please run ' + - '"sequelize db:migrate:old_schema" first.' - ); - process.exit(1); - } - - return tryToMigrateFromOldSchema(); - } - }); - }); + return tryToMigrateFromOldSchema(migrator); + } + }, + function () { + return; + }); } function logMigrator (s) { @@ -330,6 +363,18 @@ function logMigrator (s) { } } +function ensureMetaTable (queryInterface, tableName) { + return queryInterface.showAllTables() + .then(function (tableNames) { + if (tableNames.indexOf(tableName) === -1) { + throw new Error('No MetaTable table found.'); + } + }) + .then(function () { + return queryInterface.describeTable(tableName); + }); +} + function getSequelizeInstance () { var config = null; var options = {}; @@ -433,19 +478,11 @@ function getMigrator (type) { * * @return {Promise} */ -function tryToMigrateFromOldSchema () { - var sequelize = getSequelizeInstance(); +function tryToMigrateFromOldSchema (migrator) { + var sequelize = migrator.options.storageOptions.sequelize; var queryInterface = sequelize.getQueryInterface(); - return queryInterface.showAllTables() - .then(function (tableNames) { - if (tableNames.indexOf('SequelizeMeta') === -1) { - throw new Error('No SequelizeMeta table found.'); - } - }) - .then(function () { - return queryInterface.describeTable('SequelizeMeta'); - }) + return ensureMetaTable(queryInterface, 'SequelizeMeta') .then(function (table) { if (JSON.stringify(Object.keys(table).sort()) === JSON.stringify(['id', 'from', 'to'])) { return; @@ -497,6 +534,53 @@ function tryToMigrateFromOldSchema () { }); } +/** + * Add timestamps + * + * @return {Promise} + */ +function addTimestampsToSchema (migrator) { + var sequelize = migrator.options.storageOptions.sequelize; + var queryInterface = sequelize.getQueryInterface(); + var tableName = migrator.options.storageOptions.tableName; + + return ensureMetaTable(queryInterface, tableName) + .then(function (table) { + if (table.createdAt) { + return; + } + + return ensureCurrentMetaSchema(migrator) + .then(function () { + return queryInterface.renameTable(tableName, tableName + 'Backup'); + }) + .then(function () { + var sql = queryInterface.QueryGenerator.selectQuery(tableName + 'Backup'); + + return helpers.generic.execQuery(sequelize, sql, { type: 'SELECT', raw: true }); + }) + .then(function (result) { + var SequelizeMeta = sequelize.define(tableName, { + name: { + type: Sequelize.STRING, + allowNull: false, + unique: true, + primaryKey: true, + autoIncrement: false + } + }, { + tableName: tableName, + timestamps: true + }); + + return SequelizeMeta.sync() + .then(function () { + return SequelizeMeta.bulkCreate(result); + }); + }); + }); +} + /** * ensureSeeds - checks that the `--seed` option exists */ diff --git a/test/db/migrate/schema/add_timestamps.test.js b/test/db/migrate/schema/add_timestamps.test.js new file mode 100644 index 00000000..220581a9 --- /dev/null +++ b/test/db/migrate/schema/add_timestamps.test.js @@ -0,0 +1,111 @@ +'use strict'; + +var expect = require('expect.js'); +var Support = require(__dirname + '/../../../support'); +var helpers = require(__dirname + '/../../../support/helpers'); +var gulp = require('gulp'); +var execQuery = require('../../../../lib/helpers').generic.execQuery; + +([ + 'db:migrate:schema:timestamps:add' +]).forEach(function (flag) { + var prepare = function (config, callback) { + config = helpers.getTestConfig(config); + + gulp + .src(Support.resolveSupportPath('tmp')) + .pipe(helpers.clearDirectory()) + .pipe(helpers.runCli('init')) + .pipe(helpers.copyMigration('createPerson.js')) + .pipe(helpers.copyMigration('renamePersonToUser.js')) + .pipe(helpers.overwriteFile(JSON.stringify(config), 'config/config.json')) + .pipe(helpers.runCli('db:migrate')) + .pipe(helpers.teardown(callback)); + }; + + describe(Support.getTestDialectTeaser(flag), function () { + beforeEach(function (done) { + prepare.call(this, null, function () { + return gulp + .src(Support.resolveSupportPath('tmp')) + .pipe(helpers.runCli(flag)) + .pipe(helpers.teardown(done)); + }); + }); + + it('renames the original table', function (done) { + var self = this; + + helpers.readTables(self.sequelize, function (tables) { + expect(tables).to.have.length(3); + expect(tables.indexOf('SequelizeMeta')).to.be.above(-1); + expect(tables.indexOf('SequelizeMetaBackup')).to.be.above(-1); + done(); + }); + }); + + it('keeps the data in the original table', function (done) { + execQuery( + this.sequelize, + this.sequelize.getQueryInterface().QueryGenerator.selectQuery('SequelizeMetaBackup'), + { raw: true } + ).then(function (items) { + expect(items.length).to.equal(2); + done(); + }); + }); + + it('keeps the structure of the original table', function (done) { + var self = this; + + helpers.readTables(self.sequelize, function () { + return self + .sequelize + .getQueryInterface() + .describeTable('SequelizeMetaBackup') + .then(function (fields) { + expect(Object.keys(fields).sort()).to.eql(['name']); + done(); + return null; + }); + }); + }); + + it('creates a new SequelizeMeta table with the new structure', function (done) { + this.sequelize.getQueryInterface().describeTable('SequelizeMeta').then(function (fields) { + expect(Object.keys(fields).sort()).to.eql(['createdAt', 'name', 'updatedAt']); + done(); + }); + }); + + it('copies the entries into the new table', function (done) { + execQuery( + this.sequelize, + this.sequelize.getQueryInterface().QueryGenerator.selectQuery('SequelizeMeta'), + { raw: true, type: 'SELECT' } + ).then(function (items) { + expect(items[0].name).to.eql('20111117063700-createPerson.js'); + expect(items[1].name).to.eql('20111205064000-renamePersonToUser.js'); + done(); + }); + }); + + it('is possible to undo one of the already executed migrations', function (done) { + var self = this; + + gulp + .src(Support.resolveSupportPath('tmp')) + .pipe(helpers.runCli('db:migrate:undo')) + .pipe(helpers.teardown(function () { + execQuery( + self.sequelize, + self.sequelize.getQueryInterface().QueryGenerator.selectQuery('SequelizeMeta'), + { raw: true, type: 'SELECT' } + ).then(function (items) { + expect(items[0].name).to.eql('20111117063700-createPerson.js'); + done(); + }); + })); + }); + }); +});