From 5f6029b2bcdd9067ef2bc605b21452a5d3c32c30 Mon Sep 17 00:00:00 2001 From: HugoPoi Date: Tue, 4 Dec 2018 15:19:44 +0100 Subject: [PATCH] Add onDelete onUpdate foreign key config options --- README.md | 4 +- lib/migration.js | 116 +++++++++++++++------- test/mysql.autoupdate.test.js | 180 +++++++++++++++++++++++++++++++++- 3 files changed, 256 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index bb411de55..9fdbda868 100644 --- a/README.md +++ b/README.md @@ -432,7 +432,9 @@ Example: "name": "authorId", "foreignKey": "authorId", "entityKey": "aId", - "entity": "Author" + "entity": "Author", + "onUpdate": "restrict", + "onDelete": "restrict" } } } diff --git a/lib/migration.js b/lib/migration.js index d25fe7945..08c93a376 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -37,6 +37,35 @@ function mixinMigration(MySQL, mysql) { }); }; + MySQL.prototype.getConstraintTrigger = function(model, actualFks, cb) { + var table = this.tableEscaped(model); + var sql = 'SHOW CREATE TABLE ' + table; + this.execute(sql, function(err, createTable) { + if (err) { + return cb(err); + } else { + var matchConstraint = new RegExp('CONSTRAINT `([^`]+)` FOREIGN KEY \\(`([^`]+)`\\)' + + ' REFERENCES `([^`]+)` \\(`([^`]+)`\\)' + + '(?: ON DELETE (RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT))?' + + '(?: ON UPDATE (RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT))?', 'g'); + var rawConstraints = []; + do { + var match = matchConstraint.exec(createTable[0]['Create Table']); + if (match) { + actualFks.forEach(function(fk) { + if (fk.fkName === match[1]) { + fk.onDelete = match[5] ? match[5].toLowerCase() : 'restrict'; + fk.onUpdate = match[6] ? match[6].toLowerCase() : 'restrict'; + } + }); + rawConstraints.push(match); + } + } while (match != null); + cb(err, rawConstraints); + } + }); + }; + /** * Perform autoupdate for the given models * @param {String[]} [models] A model name or an array of model names. @@ -69,35 +98,36 @@ function mixinMigration(MySQL, mysql) { self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { if (err) console.log('Failed to discover "' + self.table(model) + '" foreign keys', err); - - if (!err && fields && fields.length) { - // if we already have a definition, update this table - self.alterTable(model, fields, indexes, foreignKeys, function(err, result) { - if (!err) { - // foreignKeys is a list of EXISTING fkeys here, so you don't need to recreate them again - // prepare fkSQL for new foreign keys - var fkSQL = self.getForeignKeySQL(model, - self.getModelDefinition(model).settings.foreignKeys, - foreignKeys); - self.addForeignKeys(model, fkSQL, function(err, result) { + self.getConstraintTrigger(model, foreignKeys, function(err) { + if (!err && fields && fields.length) { + // if we already have a definition, update this table + self.alterTable(model, fields, indexes, foreignKeys, function(err, result) { + if (!err) { + // foreignKeys is a list of EXISTING fkeys here, so you don't need to recreate them again + // prepare fkSQL for new foreign keys + var fkSQL = self.getForeignKeySQL(model, + self.getModelDefinition(model).settings.foreignKeys, + foreignKeys); + self.addForeignKeys(model, fkSQL, function(err, result) { + done(err); + }); + } else { done(err); - }); - } else { - done(err); - } - }); - } else { - // if there is not yet a definition, create this table - self.createTable(model, function(err) { - if (!err) { - self.addForeignKeys(model, function(err, result) { + } + }); + } else { + // if there is not yet a definition, create this table + self.createTable(model, function(err) { + if (!err) { + self.addForeignKeys(model, function(err, result) { + done(err); + }); + } else { done(err); - }); - } else { - done(err); - } - }); - } + } + }); + } + }); }); }); }, function(err) { @@ -147,15 +177,16 @@ function mixinMigration(MySQL, mysql) { self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { if (err) console.log('Failed to discover "' + self.table(model) + '" foreign keys', err); - - self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) { - if (err) { - return done(err); - } else { - ok = ok || needAlter; - done(err); - } - }, true); + self.getConstraintTrigger(model, foreignKeys, function(err) { + self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) { + if (err) { + return done(err); + } else { + ok = ok || needAlter; + done(err); + } + }, true); + }); }); }); }, function(err) { @@ -468,7 +499,9 @@ function mixinMigration(MySQL, mysql) { var fkRefTable = self.table(fkEntityName); needsToDrop = fkCol != fk.fkColumnName || fkRefKey != fk.pkColumnName || - fkRefTable != fk.pkTableName; + fkRefTable != fk.pkTableName || + (newFk.onDelete || 'restrict') != fk.onDelete || + (newFk.onUpdate || 'restrict') != fk.onUpdate; } else { needsToDrop = true; } @@ -563,10 +596,17 @@ function mixinMigration(MySQL, mysql) { // verify that the other model in the same DB if (this._models[fkEntityName]) { - return ' CONSTRAINT ' + this.client.escapeId(fk.name) + + var constraint = ' CONSTRAINT ' + this.client.escapeId(fk.name) + ' FOREIGN KEY (`' + expectedColNameForModel(fk.foreignKey, definition) + '`)' + ' REFERENCES ' + this.tableEscaped(fkEntityName) + '(' + this.client.escapeId(fk.entityKey) + ')'; + if (fk.onDelete) { + constraint += ' ON DELETE ' + fk.onDelete.toUpperCase(); + } + if (fk.onUpdate) { + constraint += ' ON UPDATE ' + fk.onUpdate.toUpperCase(); + } + return constraint; } } return ''; diff --git a/test/mysql.autoupdate.test.js b/test/mysql.autoupdate.test.js index 241ec9e24..14d030228 100644 --- a/test/mysql.autoupdate.test.js +++ b/test/mysql.autoupdate.test.js @@ -476,10 +476,9 @@ describe('MySQL connector', function() { ds.autoupdate(function(err, result) { if (err) return done(err); - // should be actual after autoupdate ds.isActual(function(err, isEqual) { if (err) return done(err); - assert(isEqual); + assert(isEqual, 'Should be actual after autoupdate'); // get and validate the properties on this model ds.discoverModelProperties('order_test', function(err, props) { @@ -528,7 +527,6 @@ describe('MySQL connector', function() { }); it('should auto migrate/update foreign keys in tables multiple times without error', function(done) { - var customer3_schema = { 'name': 'CustomerTest3', 'options': { @@ -602,8 +600,180 @@ describe('MySQL connector', function() { // do initial update/creation of table ds.autoupdate(function(err) { assert(!err, err); - ds.autoupdate(function(err, result) { - return done(err); + ds.isActual(function(err, isActual) { + if (err) return done(err); + assert(isActual, 'isActual should be true after autoupdate'); + ds.autoupdate(function(err) { + return done(err); + }); + }); + }); + }); + + it('should auto migrate/update foreign keys with onUpdate and onDelete in tables', function(done) { + var customer2_schema = { + 'name': 'CustomerTest2', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test2', + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'name': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + 'email': { + 'type': 'String', + 'required': true, + 'length': 40, + }, + 'age': { + 'type': 'Number', + 'required': false, + }, + }, + }; + + var schema_v1 = { + 'name': 'OrderTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'order_test', + }, + 'foreignKeys': { + 'fk_ordertest_customerId': { + 'name': 'fk_ordertest_customerId', + 'entity': 'CustomerTest2', + 'entityKey': 'id', + 'foreignKey': 'customerId', + 'onUpdate': 'no action', + 'onDelete': 'cascade', + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'customerId': { + 'type': 'String', + 'length': 20, + }, + 'description': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + }, + }; + + var schema_v2 = { + 'name': 'OrderTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'order_test', + }, + 'foreignKeys': { + 'fk_ordertest_customerId': { + 'name': 'fk_ordertest_customerId', + 'entity': 'CustomerTest2', + 'entityKey': 'id', + 'foreignKey': 'customerId', + 'onUpdate': 'restrict', + 'onDelete': 'restrict', + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'customerId': { + 'type': 'String', + 'length': 20, + }, + 'description': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + }, + }; + + var foreignKeySelect = + 'SELECT COLUMN_NAME,CONSTRAINT_NAME,REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ' + + 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE ' + + 'WHERE REFERENCED_TABLE_SCHEMA = "myapp_test" ' + + 'AND TABLE_NAME = "order_test"'; + var getCreateTable = 'SHOW CREATE TABLE `myapp_test`.`order_test`'; + + ds.createModel(customer2_schema.name, customer2_schema.properties, customer2_schema.options); + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); + + // do initial update/creation of table + ds.autoupdate(function(err) { + assert(!err, err); + ds.discoverModelProperties('order_test', function(err, props) { + // validate that we have the correct number of properties + assert.equal(props.length, 3); + + // get the foreign keys for this table + ds.connector.execute(foreignKeySelect, function(err, foreignKeys) { + if (err) return done(err); + // validate that the foreign key exists and points to the right column + assert(foreignKeys); + assert(foreignKeys.length.should.be.equal(1)); + assert.equal(foreignKeys[0].REFERENCED_TABLE_NAME, 'customer_test2'); + assert.equal(foreignKeys[0].COLUMN_NAME, 'customerId'); + assert.equal(foreignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId'); + assert.equal(foreignKeys[0].REFERENCED_COLUMN_NAME, 'id'); + + // get the create table for this table + ds.connector.execute(getCreateTable, function(err, createTable) { + if (err) return done(err); + // validate that the foreign key exists and points to the right column + assert(createTable); + assert(createTable.length.should.be.equal(1)); + assert(/ON DELETE CASCADE ON UPDATE NO ACTION/.test(createTable[0]['Create Table']), 'Constraint must have correct trigger'); + + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); + ds.isActual(function(err, isActual) { + if (err) return done(err); + assert(!isActual, 'isActual should return false before autoupdate'); + ds.autoupdate(function(err) { + if (err) return done(err); + ds.isActual(function(err, isActual) { + if (err) return done(err); + assert(isActual, 'isActual should be true after autoupdate'); + ds.connector.execute(getCreateTable, function(err, createTable) { + if (err) return done(err); + assert(createTable); + assert(createTable.length.should.be.equal(1)); + assert(!/ON DELETE CASCADE ON UPDATE NO ACTION/.test(createTable[0]['Create Table']), 'Constraint must not have on delete trigger'); + done(err, createTable); + }); + }); + }); + }); + }); + }); }); }); });