index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // █████╗ ██╗ ████████╗███████╗██████╗
  2. // ██╔══██╗██║ ╚══██╔══╝██╔════╝██╔══██╗
  3. // ███████║██║ ██║ █████╗ ██████╔╝
  4. // ██╔══██║██║ ██║ ██╔══╝ ██╔══██╗
  5. // ██║ ██║███████╗██║ ███████╗██║ ██║
  6. // ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝
  7. //
  8. /**
  9. * Module dependencies
  10. */
  11. var _ = require('@sailshq/lodash');
  12. var async = require('async');
  13. var flaverr = require('flaverr');
  14. var informReFailedAlterStratagem = require('./private/inform-re-failed-alter-stratagem');
  15. /**
  16. * runAlterStrategy()
  17. *
  18. * Drop each table in the database and rebuild it with the new model definition,
  19. * coercing and reinserting the existing table data (if possible).
  20. *
  21. * @param {[type]} orm [description]
  22. * @param {Function} cb [description]
  23. * @return {[type]} [description]
  24. */
  25. module.exports = function runAlterStrategy(orm, cb) {
  26. // Refuse to run this migration strategy in production.
  27. if (process.env.NODE_ENV === 'production' && !process.env.ALLOW_UNSAFE_MIGRATIONS) {
  28. return cb(new Error('`migrate: \'alter\'` strategy is not supported in production, please change to `migrate: \'safe\'`.'));
  29. }
  30. // The alter strategy works by looping through each collection in the ORM and
  31. // pulling out the data that is currently in the database and keeping it in
  32. // memory. It then drops the table and rebuilds it based on the collection's
  33. // schema definition and the `autoMigrations` settings on the attributes.
  34. async.each(_.keys(orm.collections), function simultaneouslyMigrateEachModel(modelIdentity, next) {
  35. var WLModel = orm.collections[modelIdentity];
  36. // Grab the adapter to perform the query on
  37. var datastoreName = WLModel.datastore;
  38. var WLAdapter = orm.datastores[datastoreName].adapter;
  39. // Set a tableName to use
  40. var tableName = WLModel.tableName;
  41. // Build a dictionary to represent the underlying physical database structure.
  42. var tableDDLSpec = {};
  43. try {
  44. _.each(WLModel.schema, function parseAttribute(wlsAttrDef, wlsAttrName) {
  45. // If this is a plural association, then skip it.
  46. // (it is impossible for a key from this error to match up with one of these-- they don't even have column names)
  47. if (wlsAttrDef.collection) {
  48. return;
  49. }
  50. var columnName = wlsAttrDef.columnName;
  51. // If the attribute doesn't have an `autoMigrations` key on it, throw an error.
  52. if (!_.has(wlsAttrDef, 'autoMigrations')) {
  53. throw new Error('An attribute in the model definition: `' + wlsAttrName + '` is missing an `autoMigrations` property. When running the `alter` migration, each attribute must have an autoMigrations key so that you don\'t end up with an invalid data schema.');
  54. }
  55. tableDDLSpec[columnName] = wlsAttrDef.autoMigrations;
  56. });
  57. } catch (e) {
  58. return next(e);
  59. }
  60. // Set Primary Key flag on the primary key attribute
  61. var primaryKeyAttrName = WLModel.primaryKey;
  62. var primaryKey = WLModel.schema[primaryKeyAttrName];
  63. if (primaryKey) {
  64. var pkColumnName = primaryKey.columnName;
  65. tableDDLSpec[pkColumnName].primaryKey = true;
  66. }
  67. // Make sure that the backup data is sorted by primary key (if the model has one).
  68. var sort = primaryKey ? primaryKeyAttrName + ' ASC' : [];
  69. // ╔═╗╔═╗╔╦╗ ┌┐ ┌─┐┌─┐┬┌─┬ ┬┌─┐ ┌┬┐┌─┐┌┬┐┌─┐
  70. // ║ ╦║╣ ║ ├┴┐├─┤│ ├┴┐│ │├─┘ ││├─┤ │ ├─┤
  71. // ╚═╝╚═╝ ╩ └─┘┴ ┴└─┘┴ ┴└─┘┴ ─┴┘┴ ┴ ┴ ┴ ┴
  72. WLModel.find()
  73. .meta({
  74. skipAllLifecycleCallbacks: true,
  75. skipRecordVerification: true,
  76. decrypt: true,
  77. skipExpandingDefaultSelectClause: true
  78. })
  79. .sort(sort)
  80. .exec(function findCallback(err, backupRecords) {
  81. if (err) {
  82. // Ignore the error if it's an adapter error. For example, this could error out
  83. // on an empty database when the table doesn't yet exist (which is perfectly fine).
  84. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  85. // FUTURE: further negotiate this error and only ignore failure due to "no such table"
  86. // (other errors are still relevant and important). The database-specific piece of
  87. // this should happen in the adapter (and where supported, use a newly standardized
  88. // footprint from the underlying driver)
  89. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  90. if (err.name === 'AdapterError') {
  91. // Ignore.
  92. //
  93. // (But note that we also set backupRecords to an empty array so that it matches
  94. // what we'd expect if everything had worked out.)
  95. backupRecords = [];
  96. // But otherwise, this is NOT an adapter error, so still bail w/ a fatal error
  97. // (because this means something else completely unexpected has happened.)
  98. } else {
  99. return next(flaverr({
  100. message: 'When attempting to perform the `alter` auto-migration strategy '+
  101. 'on model `' + WLModel.identity + '`, Sails encountered an error. '+err.message+'\n'+
  102. 'Tip: Could there be existing records in the database that are not compatible '+
  103. 'with a recent change to this model\'s definition? If so, you might need to '+
  104. 'migrate them manually or, if you don\'t care about the data, wipe them; e.g. --drop.\n'+
  105. '--\n'+
  106. 'For help with auto-migrations, visit:\n'+
  107. ' [?] https://sailsjs.com/docs/concepts/models-and-orm/model-settings#?migrate\n'
  108. }, err));
  109. }
  110. }//>-•
  111. // ╔╦╗╦═╗╔═╗╔═╗ ┌┬┐┌─┐┌┐ ┬ ┌─┐
  112. // ║║╠╦╝║ ║╠═╝ │ ├─┤├┴┐│ ├┤
  113. // ═╩╝╩╚═╚═╝╩ ┴ ┴ ┴└─┘┴─┘└─┘
  114. WLAdapter.drop(datastoreName, tableName, undefined, function dropCallback(err) {
  115. if (err) {
  116. informReFailedAlterStratagem(err, 'drop', WLModel.identity, backupRecords, next);//_∏_
  117. return;
  118. }//-•
  119. // ╔╦╗╔═╗╔═╗╦╔╗╔╔═╗ ┌┬┐┌─┐┌┐ ┬ ┌─┐
  120. // ║║║╣ ╠╣ ║║║║║╣ │ ├─┤├┴┐│ ├┤
  121. // ═╩╝╚═╝╚ ╩╝╚╝╚═╝ ┴ ┴ ┴└─┘┴─┘└─┘
  122. WLAdapter.define(datastoreName, tableName, tableDDLSpec, function defineCallback(err) {
  123. if (err) {
  124. informReFailedAlterStratagem(err, 'define', WLModel.identity, backupRecords, next);//_∏_
  125. return;
  126. }//-•
  127. // ╦═╗╔═╗ ╦╔╗╔╔═╗╔═╗╦═╗╔╦╗ ┬─┐┌─┐┌─┐┌─┐┬─┐┌┬┐┌─┐
  128. // ╠╦╝║╣───║║║║╚═╗║╣ ╠╦╝ ║ ├┬┘├┤ │ │ │├┬┘ ││└─┐
  129. // ╩╚═╚═╝ ╩╝╚╝╚═╝╚═╝╩╚═ ╩ ┴└─└─┘└─┘└─┘┴└──┴┘└─┘
  130. WLModel.createEach(backupRecords)
  131. .meta({
  132. skipAllLifecycleCallbacks: true
  133. })
  134. .exec(function createEachCallback(err) {
  135. if (err) {
  136. informReFailedAlterStratagem(err, 'createEach', WLModel.identity, backupRecords, next);//_∏_
  137. return;
  138. }//-•
  139. // ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┌─┐ ┬ ┬┌─┐┌┐┌┌─┐┌─┐
  140. // ╚═╗║╣ ║ └─┐├┤ │─┼┐│ │├┤ ││││ ├┤
  141. // ╚═╝╚═╝ ╩ └─┘└─┘└─┘└└─┘└─┘┘└┘└─┘└─┘
  142. // If this primary key attribute is not auto-incrementing, it won't have
  143. // a sequence attached. So we can skip it.
  144. if (WLModel.schema[primaryKeyAttrName].autoMigrations.autoIncrement !== true) {
  145. return next();
  146. }
  147. // If there were no pre-existing records, we can also skip this step,
  148. // since the previous sequence number ought to be fine.
  149. if (backupRecords.length === 0) {
  150. return next();
  151. }
  152. // Otherwise, this model's primary key is auto-incrementing, so we'll expect
  153. // the adapter to have a setSequence method.
  154. if (!_.has(WLAdapter, 'setSequence')) {
  155. // If it doesn't, log a warning, then skip setting the sequence number.
  156. console.warn('\n' +
  157. 'Warning: Although `autoIncrement: true` was specified for the primary key\n' +
  158. 'of this model (`' + WLModel.identity + '`), this adapter does not support the\n' +
  159. '`setSequence()` method, so the sequence number cannot be reset during the\n' +
  160. 'auto-migration process.\n' +
  161. '(Proceeding without resetting the auto-increment sequence...)\n'
  162. );
  163. return next();
  164. }
  165. // Now try to reset the sequence so that the next record created has a reasonable ID.
  166. var lastRecord = _.last(backupRecords);
  167. var primaryKeyColumnName = WLModel.schema[primaryKeyAttrName].columnName;
  168. var sequenceName = WLModel.tableName + '_' + primaryKeyColumnName + '_seq';
  169. var sequenceValue = lastRecord[primaryKeyColumnName];
  170. WLAdapter.setSequence(datastoreName, sequenceName, sequenceValue, function setSequenceCb(err) {
  171. if (err) {
  172. return next(err);
  173. }
  174. return next();
  175. });//</ setSequence >
  176. });//</ createEach >
  177. });//</ define >
  178. });//</ drop >
  179. });//</ find >
  180. }, function afterMigrate(err) {
  181. if (err) {
  182. return cb(err);
  183. }
  184. return cb();
  185. });
  186. };