Skip to content

Commit

Permalink
Add onDelete onUpdate foreign key config options
Browse files Browse the repository at this point in the history
  • Loading branch information
HugoPoi committed Dec 4, 2018
1 parent 086ae41 commit 5f6029b
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 44 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,9 @@ Example:
"name": "authorId",
"foreignKey": "authorId",
"entityKey": "aId",
"entity": "Author"
"entity": "Author",
"onUpdate": "restrict",
"onDelete": "restrict"
}
}
}
Expand Down
116 changes: 78 additions & 38 deletions lib/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 '';
Expand Down
180 changes: 175 additions & 5 deletions test/mysql.autoupdate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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);
});
});
});
});
});
});
});
});
});
Expand Down

0 comments on commit 5f6029b

Please sign in to comment.