Skip to content

Commit 77ff2af

Browse files
committed
add support for create and delete foreign key
1 parent b8e6232 commit 77ff2af

File tree

4 files changed

+447
-11
lines changed

4 files changed

+447
-11
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules
22
.project
33
.idea
4+
.vscode
45
coverage
56
lib-cov
67
*.seed

README.md

+60
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,66 @@ The auto-migrate method:
430430

431431
Destroying models may result in errors due to foreign key integrity. First delete any related models by calling delete on models with relationships.
432432

433+
### Auto-migrate/Auto-update models with foreign keys
434+
435+
Foreign key constraints can be defined in the model `options`. Removing or updating the value of `foreignKeys` will be updated or delete or update the constraints in the db tables.
436+
437+
If there is a reference to an object being deleted then the `DELETE` will fail. Likewise if there is a create with an invalid FK id then the `POST` will fail.
438+
439+
**Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint.
440+
441+
```json
442+
{
443+
"name": "Customer",
444+
"options": {
445+
"idInjection": false
446+
},
447+
"properties": {
448+
"id": {
449+
"type": "String",
450+
"length": 20,
451+
"id": 1
452+
},
453+
"name": {
454+
"type": "String",
455+
"required": false,
456+
"length": 40
457+
}
458+
}
459+
},
460+
461+
{
462+
"name": "Order",
463+
"options": {
464+
"idInjection": false,
465+
"foreignKeys": {
466+
"fk_order_customerId": {
467+
"name": "fk_order_customerId",
468+
"entity": "Customer",
469+
"entityKey": "id",
470+
"foreignKey": "customerId"
471+
}
472+
}
473+
},
474+
"properties": {
475+
"id": {
476+
"type": "String",
477+
"length": 20,
478+
"id": 1
479+
},
480+
"customerId": {
481+
"type": "String",
482+
"length": 20
483+
},
484+
"description": {
485+
"type": "String",
486+
"required": false,
487+
"length": 40
488+
}
489+
}
490+
}
491+
```
492+
433493
## Running tests
434494

435495
### Own instance

lib/migration.js

+147-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
var SG = require('strong-globalize');
88
var g = SG();
99
var async = require('async');
10+
var debug = require('debug')('loopback:connector:postgresql:migration');
1011

1112
module.exports = mixinMigration;
1213

@@ -83,7 +84,7 @@ function mixinMigration(PostgreSQL) {
8384

8485
var applyPending = function(actions, cb, err, result) {
8586
var action = actions.shift();
86-
var pendingChanges = action && action.call(self, model, actualFields) || [];
87+
var pendingChanges = action && action() || [];
8788
if (pendingChanges.length) {
8889
self.applySqlChanges(model, pendingChanges, function(err, result) {
8990
if (!err) {
@@ -99,15 +100,37 @@ function mixinMigration(PostgreSQL) {
99100
}
100101
};
101102

102-
async.series([
103-
function(cb) {
104-
applyPending([self.getAddModifyColumns, self.getDropColumns], cb);
105-
},
106-
function(cb) {
107-
self.addIndexes(model, actualIndexes, cb);
108-
},
109-
], function(err, result) {
110-
cb(err, result[0]);
103+
self.discoverForeignKeys(self.table(model), {}, function(err, actualFks) {
104+
if (err) {
105+
debug('Failed to discover "%s" foreign keys %s', self.table(model), err);
106+
cb(err);
107+
return;
108+
}
109+
110+
// actualFks is a list of EXISTING fkeys here,
111+
// so you don't need to recreate them again
112+
// prepare fkSQL for new foreign keys
113+
var fkSQL = self.getForeignKeySQL(model,
114+
self.getModelDefinition(model).settings.foreignKeys,
115+
actualFks);
116+
117+
async.series([
118+
function(cb) {
119+
applyPending([
120+
self.getAddModifyColumns.bind(self, model, actualFields),
121+
self.getDropColumns.bind(self, model, actualFields),
122+
self.getDropForeignKeys.bind(self, model, actualFks),
123+
], cb);
124+
},
125+
function(cb) {
126+
self.addIndexes(model, actualIndexes, cb);
127+
},
128+
function(cb) {
129+
self.addForeignKeys(model, fkSQL, cb);
130+
},
131+
], function(err, result) {
132+
cb(err, result[0]);
133+
});
111134
});
112135
};
113136

@@ -302,7 +325,14 @@ function mixinMigration(PostgreSQL) {
302325
if (err) {
303326
return cb(err, info);
304327
}
305-
self.addIndexes(model, undefined, cb);
328+
self.addIndexes(model, undefined, function(err) {
329+
if (err) {
330+
return cb(err);
331+
}
332+
self.addForeignKeys(model, function(err, result) {
333+
cb(err);
334+
});
335+
});
306336
}
307337
);
308338
});
@@ -443,6 +473,112 @@ function mixinMigration(PostgreSQL) {
443473
}
444474
};
445475

476+
PostgreSQL.prototype.addForeignKeys = function(model, fkSQL, cb) {
477+
var self = this;
478+
var m = this.getModelDefinition(model);
479+
480+
if ((!cb) && ('function' === typeof fkSQL)) {
481+
cb = fkSQL;
482+
fkSQL = undefined;
483+
}
484+
485+
if (!fkSQL || fkSQL.length === 0) {
486+
var newFks = m.settings.foreignKeys;
487+
if (newFks)
488+
fkSQL = self.getForeignKeySQL(model, newFks);
489+
}
490+
491+
if (fkSQL && fkSQL.length) {
492+
self.applySqlChanges(model, [fkSQL.toString()], function(err, result) {
493+
if (err) cb(err);
494+
else
495+
cb(null, result);
496+
});
497+
} else cb(null, {});
498+
};
499+
500+
PostgreSQL.prototype.getDropForeignKeys = function(model, actualFks) {
501+
var self = this;
502+
var m = this.getModelDefinition(model);
503+
504+
var fks = actualFks;
505+
var sql = [];
506+
var correctFks = m.settings.foreignKeys || {};
507+
508+
// drop foreign keys for removed fields
509+
if (fks && fks.length) {
510+
var removedFks = [];
511+
fks.forEach(function(fk) {
512+
var needsToDrop = false;
513+
var newFk = correctFks[fk.fkName];
514+
if (newFk) {
515+
var fkCol = newFk.foreignKey;
516+
var fkRefKey = newFk.entityKey;
517+
var fkEntityName = (typeof newFk.entity === 'object') ? newFk.entity.name : newFk.entity;
518+
var fkRefTable = self.table(fkEntityName);
519+
needsToDrop = fkCol != fk.fkColumnName ||
520+
fkRefKey != fk.pkColumnName ||
521+
fkRefTable != fk.pkTableName;
522+
} else {
523+
needsToDrop = true;
524+
}
525+
526+
if (needsToDrop) {
527+
sql.push('DROP CONSTRAINT ' + self.escapeName(fk.fkName));
528+
removedFks.push(fk); // keep track that we removed these
529+
}
530+
});
531+
532+
// update out list of existing keys by removing dropped keys
533+
removedFks.forEach(function(k) {
534+
var index = actualFks.indexOf(k);
535+
if (index !== -1) actualFks.splice(index, 1);
536+
});
537+
}
538+
return sql;
539+
};
540+
541+
PostgreSQL.prototype.getForeignKeySQL = function getForeignKeySQL(model, actualFks, existingFks) {
542+
var self = this;
543+
var addFksSql = [];
544+
existingFks = existingFks || [];
545+
546+
if (actualFks) {
547+
var keys = Object.keys(actualFks);
548+
for (var i = 0; i < keys.length; i++) {
549+
// all existing fks are already checked in PostgreSQL.prototype.dropForeignKeys
550+
// so we need check only names - skip if found
551+
if (existingFks.filter(function(fk) {
552+
return fk.fkName === keys[i];
553+
}).length > 0) continue;
554+
var constraint = self.buildForeignKeyDefinition(model, keys[i]);
555+
556+
if (constraint) {
557+
addFksSql.push('ADD ' + constraint);
558+
}
559+
}
560+
}
561+
return addFksSql;
562+
};
563+
564+
PostgreSQL.prototype.buildForeignKeyDefinition = function buildForeignKeyDefinition(model, keyName) {
565+
var definition = this.getModelDefinition(model);
566+
567+
var fk = definition.settings.foreignKeys[keyName];
568+
if (fk) {
569+
// get the definition of the referenced object
570+
var fkEntityName = (typeof fk.entity === 'object') ? fk.entity.name : fk.entity;
571+
572+
// verify that the other model in the same DB
573+
if (this._models[fkEntityName]) {
574+
return 'CONSTRAINT ' + this.escapeName(fk.name) + ' ' +
575+
'FOREIGN KEY (' + fk.foreignKey + ') ' +
576+
'REFERENCES ' + this.tableEscaped(fkEntityName) + '(' + fk.entityKey + ')';
577+
}
578+
}
579+
return '';
580+
};
581+
446582
/*!
447583
* Map postgresql data types to json types
448584
* @param {String} postgresqlType

0 commit comments

Comments
 (0)