refactor upgrade scripts to use individual files in src/upgrades/ as source of schema changes

v1.18.x
Julian Lam 8 years ago
parent 62e0f5f883
commit 5b8c9503c3

@ -189,13 +189,17 @@ function upgrade() {
var meta = require('./src/meta'); var meta = require('./src/meta');
var upgrade = require('./src/upgrade'); var upgrade = require('./src/upgrade');
var build = require('./src/meta/build'); var build = require('./src/meta/build');
var tasks = [db.init, meta.configs.init, upgrade.run, build.buildAll];
async.series([ if (nconf.get('upgrade') !== true) {
async.apply(db.init), // Likely an upgrade script name passed in
async.apply(meta.configs.init), tasks[2] = async.apply(upgrade.runSingle, nconf.get('upgrade'));
async.apply(upgrade.upgrade),
async.apply(build.buildAll) // Skip build
], function (err) { tasks.pop();
}
async.series(tasks, function (err) {
if (err) { if (err) {
winston.error(err.stack); winston.error(err.stack);
process.exit(1); process.exit(1);

@ -406,6 +406,18 @@ var commands = {
description: 'Run NodeBB upgrade scripts, ensure packages are up-to-date', description: 'Run NodeBB upgrade scripts, ensure packages are up-to-date',
usage: 'Usage: ' + './nodebb upgrade'.yellow, usage: 'Usage: ' + './nodebb upgrade'.yellow,
handler: function () { handler: function () {
if (process.argv[3]) {
process.stdout.write('\nUpdating NodeBB data store schema...\n'.yellow);
var arr = ['--upgrade'].concat(process.argv.slice(3));
var upgradeProc = fork(arr);
return upgradeProc.on('close', function (err) {
if (err) {
process.stdout.write('\nError'.red + ': ' + err.message + '\n');
}
});
}
async.series([ async.series([
function (next) { function (next) {
process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow); process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow);

@ -1,443 +1,74 @@
"use strict"; /* jslint node: true */
'use strict';
/* globals console, require */
var db = require('./database');
var async = require('async'); var async = require('async');
var winston = require('winston'); var path = require('path');
var Upgrade = {}; var utils = require('../public/src/utils');
var minSchemaDate = Date.UTC(2016, 8, 7); // This value gets updated every new MAJOR version var Upgrade = {};
var schemaDate;
var thisSchemaDate;
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema Upgrade.run = function (callback) {
var latestSchema = Date.UTC(2016, 11, 7); process.stdout.write('\nParsing upgrade scripts... ');
Upgrade.check = function (callback) { utils.walk(path.join(__dirname, './upgrades'), function (err, files) {
db.get('schemaDate', function (err, value) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
if (!value) { Upgrade.process(files, callback);
db.set('schemaDate', latestSchema, function (err) {
if (err) {
return callback(err);
}
callback(null);
});
return;
}
var schema_ok = parseInt(value, 10) >= latestSchema;
callback(!schema_ok ? new Error('schema-out-of-date') : null);
}); });
}; };
Upgrade.update = function (schemaDate, callback) { Upgrade.runSingle = function (query, callback) {
db.set('schemaDate', schemaDate, callback); process.stdout.write('\nParsing upgrade scripts... ');
};
Upgrade.upgrade = function (callback) {
var updatesMade = false;
winston.info('Beginning database schema update');
async.series([
function (next) {
// Prepare for upgrade & check to make sure the upgrade is possible
db.get('schemaDate', function (err, value) {
if (err) {
return next(err);
}
if(!value) {
db.set('schemaDate', latestSchema, function () {
next();
});
schemaDate = latestSchema;
} else {
schemaDate = parseInt(value, 10);
}
if (schemaDate >= minSchemaDate) {
next();
} else {
next(new Error('upgrade-not-possible'));
}
});
},
function (next) {
thisSchemaDate = Date.UTC(2016, 8, 22);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/09/22] Setting category recent tids');
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return next(err);
}
async.eachSeries(cids, function (cid, next) {
db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0, function (err, pid) {
if (err || !pid) {
return next(err);
}
db.getObjectFields('post:' + pid, ['tid', 'timestamp'], function (err, postData) {
if (err || !postData || !postData.tid) {
return next(err);
}
db.sortedSetAdd('cid:' + cid + ':recent_tids', postData.timestamp, postData.tid, next);
});
});
}, function (err) {
if (err) {
return next(err);
}
winston.info('[2016/09/22] Setting category recent tids - done');
Upgrade.update(thisSchemaDate, next);
});
});
} else {
winston.info('[2016/09/22] Setting category recent tids - skipped!');
next();
}
},
function (next) {
function upgradePosts(next) {
var batch = require('./batch');
batch.processSortedSet('posts:pid', function (ids, next) {
async.each(ids, function (id, next) {
console.log('processing pid ' + id);
async.waterfall([
function (next) {
db.rename('pid:' + id + ':users_favourited', 'pid:' + id + ':users_bookmarked', next);
},
function (next) {
db.getObjectField('post:' + id, 'reputation', next);
},
function (reputation, next) {
if (parseInt(reputation, 10)) {
db.setObjectField('post:' + id, 'bookmarks', reputation, next);
} else {
next();
}
},
function (next) {
db.deleteObjectField('post:' + id, 'reputation', next);
}
], next);
}, next);
}, {}, next);
}
function upgradeUsers(next) {
var batch = require('./batch');
batch.processSortedSet('users:joindate', function (ids, next) {
async.each(ids, function (id, next) {
console.log('processing uid ' + id);
db.rename('uid:' + id + ':favourites', 'uid:' + id + ':bookmarks', next);
}, next);
}, {}, next);
}
thisSchemaDate = Date.UTC(2016, 9, 8);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/10/8] favourite -> bookmark refactor');
async.series([upgradePosts, upgradeUsers], function (err) {
if (err) {
return next(err);
}
winston.info('[2016/08/05] favourite- bookmark refactor done!');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/10/8] favourite -> bookmark refactor - skipped!');
next();
}
},
function (next) {
thisSchemaDate = Date.UTC(2016, 9, 14);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/10/14] Creating sorted sets for post replies');
var posts = require('./posts');
var batch = require('./batch');
batch.processSortedSet('posts:pid', function (ids, next) {
posts.getPostsFields(ids, ['pid', 'toPid', 'timestamp'], function (err, data) {
if (err) {
return next(err);
}
async.eachSeries(data, function (postData, next) {
if (!parseInt(postData.toPid, 10)) {
return next(null);
}
console.log('processing pid: ' + postData.pid + ' toPid: ' + postData.toPid);
async.parallel([
async.apply(db.sortedSetAdd, 'pid:' + postData.toPid + ':replies', postData.timestamp, postData.pid),
async.apply(db.incrObjectField, 'post:' + postData.toPid, 'replies')
], next);
}, next);
});
}, function (err) {
if (err) {
return next(err);
}
winston.info('[2016/10/14] Creating sorted sets for post replies - done');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/10/14] Creating sorted sets for post replies - skipped!');
next();
}
},
function (next) {
thisSchemaDate = Date.UTC(2016, 10, 22);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/11/22] Update global and user language keys');
var user = require('./user');
var meta = require('./meta');
var batch = require('./batch');
var newLanguage;
var i = 0;
var j = 0;
async.parallel([
function (next) {
meta.configs.get('defaultLang', function (err, defaultLang) {
if (err) {
return next(err);
}
if (!defaultLang) {
return setImmediate(next);
}
newLanguage = defaultLang.replace('_', '-').replace('@', '-x-');
if (newLanguage !== defaultLang) {
meta.configs.set('defaultLang', newLanguage, next);
} else {
setImmediate(next);
}
});
},
function (next) {
batch.processSortedSet('users:joindate', function (ids, next) {
async.each(ids, function (uid, next) {
async.waterfall([
async.apply(db.getObjectField, 'user:' + uid + ':settings', 'userLang'),
function (language, next) {
++i;
if (!language) {
return setImmediate(next);
}
newLanguage = language.replace('_', '-').replace('@', '-x-');
if (newLanguage !== language) {
++j;
user.setSetting(uid, 'userLang', newLanguage, next);
} else {
setImmediate(next);
}
}
], next);
}, next);
}, next);
}
], function (err) {
if (err) {
return next(err);
}
winston.info('[2016/11/22] Update global and user language keys - done (' + i + ' processed, ' + j + ' updated)');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/11/22] Update global and user language keys - skipped!');
next();
}
},
function (next) {
thisSchemaDate = Date.UTC(2016, 10, 25);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/11/25] Creating sorted sets for pinned topics');
var topics = require('./topics');
var batch = require('./batch');
batch.processSortedSet('topics:tid', function (ids, next) {
topics.getTopicsFields(ids, ['tid', 'cid', 'pinned', 'lastposttime'], function (err, data) {
if (err) {
return next(err);
}
data = data.filter(function (topicData) {
return parseInt(topicData.pinned, 10) === 1;
});
async.eachSeries(data, function (topicData, next) {
console.log('processing tid: ' + topicData.tid);
async.parallel([
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), topicData.tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', topicData.tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', topicData.tid)
], next);
}, next);
});
}, function (err) {
if (err) {
return next(err);
}
winston.info('[2016/11/25] Creating sorted sets for pinned topics - done');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/11/25] Creating sorted sets for pinned topics - skipped!');
next();
}
},
function (next) {
thisSchemaDate = Date.UTC(2016, 11, 7);
if (schemaDate < thisSchemaDate) { async.waterfall([
updatesMade = true; async.apply(utils.walk, path.join(__dirname, './upgrades')),
winston.info('[2016/12/07] Migrating flags to new schema (#5232)'); function (files, next) {
next(null, files.filter(function (file) {
var batch = require('./batch'); return file.search(new RegExp(query)) !== -1;
var posts = require('./posts'); }));
var flags = require('./flags'); }
var migrated = 0; ], function (err, files) {
if (err) {
batch.processSortedSet('posts:pid', function (ids, next) { return callback(err);
posts.getPostsByPids(ids, 1, function (err, posts) { }
if (err) {
return next(err);
}
posts = posts.filter(function (post) { Upgrade.process(files, callback);
return post.hasOwnProperty('flags'); });
}); };
async.each(posts, function (post, next) {
async.parallel({
uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1),
reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1)
}, function (err, data) {
if (err) {
return next(err);
}
// Adding in another check here in case a post was improperly dismissed (flag count > 1 but no flags in db) Upgrade.process = function (files, callback) {
if (!data.uids.length || !data.reasons.length) { process.stdout.write('OK'.green + String(' ' + files.length).cyan + ' script(s) found\n'.cyan);
return setImmediate(next);
}
// Just take the first entry // Do I need to sort the files here? we'll see.
var datetime = data.uids[0].score; // sort();
var reason = data.reasons[0].split(':')[1];
var flagObj;
async.waterfall([ async.eachSeries(files, function (file, next) {
async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime), var scriptExport = require(file);
function (_flagObj, next) { var date = new Date(scriptExport.timestamp);
flagObj = _flagObj;
if (post['flag:state'] || post['flag:assignee']) {
flags.update(flagObj.flagId, 1, {
state: post['flag:state'],
assignee: post['flag:assignee'],
datetime: datetime
}, next);
} else {
setImmediate(next);
}
},
function (next) {
if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) {
try {
var history = JSON.parse(post['flag:history']);
history = history.filter(function (event) {
return event.type === 'notes';
})[0];
flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next); process.stdout.write(' → '.white + String('[' + [date.getFullYear(), date.getMonth() + 1, date.getDate() + 1].join('/') + '] ').gray + String(scriptExport.name).reset + '... ');
} catch (e) {
next(e);
}
} else {
setImmediate(next);
}
}
], function (err) {
if (err && err.message === '[[error:already-flagged]]') {
// Already flagged, no need to parse, but not an error
next();
} else {
next(err);
}
});
});
}, next);
});
}, function (err) {
if (err) {
return next(err);
}
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done'); // Do the upgrade...
Upgrade.update(thisSchemaDate, next); scriptExport.method(function (err) {
}); if (err) {
} else { process.stdout.write('error\n'.red);
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!'); return next(err);
next();
}
}
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
], function (err) {
if (!err) {
if(updatesMade) {
winston.info('[upgrade] Schema update complete!');
} else {
winston.info('[upgrade] Schema already up to date!');
} }
} else {
switch(err.message) {
case 'upgrade-not-possible':
winston.error('[upgrade] NodeBB upgrade could not complete, as your database schema is too far out of date.');
winston.error('[upgrade] Please ensure that you did not skip any minor version upgrades.');
winston.error('[upgrade] (e.g. v0.1.x directly to v0.3.x)');
break;
default: process.stdout.write('OK\n'.green);
winston.error('[upgrade] Errors were encountered while updating the NodeBB schema: ' + err.message); next();
break; });
} }, function (err) {
if (err) {
return callback(err);
} }
if (typeof callback === 'function') { process.stdout.write('Upgrade complete!\n\n'.green);
callback(err); callback();
} else {
process.exit();
}
}); });
}; };
module.exports = Upgrade; module.exports = Upgrade;

@ -0,0 +1,33 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
var winston = require('winston');
module.exports = {
name: 'Category recent tids',
timestamp: Date.UTC(2016, 8, 22),
method: function (callback) {
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
async.eachSeries(cids, function (cid, next) {
db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0, function (err, pid) {
if (err || !pid) {
return next(err);
}
db.getObjectFields('post:' + pid, ['tid', 'timestamp'], function (err, postData) {
if (err || !postData || !postData.tid) {
return next(err);
}
db.sortedSetAdd('cid:' + cid + ':recent_tids', postData.timestamp, postData.tid, next);
});
});
}, callback);
});
}
};

@ -0,0 +1,52 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
var winston = require('winston');
module.exports = {
name: 'Favourites to Bookmarks',
timestamp: Date.UTC(2016, 9, 8),
method: function (callback) {
function upgradePosts(next) {
var batch = require('../batch');
batch.processSortedSet('posts:pid', function (ids, next) {
async.each(ids, function (id, next) {
async.waterfall([
function (next) {
db.rename('pid:' + id + ':users_favourited', 'pid:' + id + ':users_bookmarked', next);
},
function (next) {
db.getObjectField('post:' + id, 'reputation', next);
},
function (reputation, next) {
if (parseInt(reputation, 10)) {
db.setObjectField('post:' + id, 'bookmarks', reputation, next);
} else {
next();
}
},
function (next) {
db.deleteObjectField('post:' + id, 'reputation', next);
}
], next);
}, next);
}, {}, next);
}
function upgradeUsers(next) {
var batch = require('../batch');
batch.processSortedSet('users:joindate', function (ids, next) {
async.each(ids, function (id, next) {
db.rename('uid:' + id + ':favourites', 'uid:' + id + ':bookmarks', next);
}, next);
}, {}, next);
}
async.series([upgradePosts, upgradeUsers], callback);
}
};

@ -0,0 +1,34 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
var winston = require('winston');
module.exports = {
name: 'Sorted sets for post replies',
timestamp: Date.UTC(2016, 9, 14),
method: function (callback) {
var posts = require('../posts');
var batch = require('../batch');
batch.processSortedSet('posts:pid', function (ids, next) {
posts.getPostsFields(ids, ['pid', 'toPid', 'timestamp'], function (err, data) {
if (err) {
return next(err);
}
async.eachSeries(data, function (postData, next) {
if (!parseInt(postData.toPid, 10)) {
return next(null);
}
winston.verbose('processing pid: ' + postData.pid + ' toPid: ' + postData.toPid);
async.parallel([
async.apply(db.sortedSetAdd, 'pid:' + postData.toPid + ':replies', postData.timestamp, postData.pid),
async.apply(db.incrObjectField, 'post:' + postData.toPid, 'replies')
], next);
}, next);
});
}, callback);
}
};

@ -0,0 +1,63 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
var winston = require('winston');
module.exports = {
name: 'Update global and user language keys',
timestamp: Date.UTC(2016, 10, 22),
method: function (callback) {
var user = require('../user');
var meta = require('../meta');
var batch = require('../batch');
var newLanguage;
var i = 0;
var j = 0;
async.parallel([
function (next) {
meta.configs.get('defaultLang', function (err, defaultLang) {
if (err) {
return next(err);
}
if (!defaultLang) {
return setImmediate(next);
}
newLanguage = defaultLang.replace('_', '-').replace('@', '-x-');
if (newLanguage !== defaultLang) {
meta.configs.set('defaultLang', newLanguage, next);
} else {
setImmediate(next);
}
});
},
function (next) {
batch.processSortedSet('users:joindate', function (ids, next) {
async.each(ids, function (uid, next) {
async.waterfall([
async.apply(db.getObjectField, 'user:' + uid + ':settings', 'userLang'),
function (language, next) {
++i;
if (!language) {
return setImmediate(next);
}
newLanguage = language.replace('_', '-').replace('@', '-x-');
if (newLanguage !== language) {
++j;
user.setSetting(uid, 'userLang', newLanguage, next);
} else {
setImmediate(next);
}
}
], next);
}, next);
}, next);
}
], callback);
}
};

@ -0,0 +1,37 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
var winston = require('winston');
module.exports = {
name: 'Sorted set for pinned topics',
timestamp: Date.UTC(2016, 10, 25),
method: function (callback) {
var topics = require('../topics');
var batch = require('../batch');
batch.processSortedSet('topics:tid', function (ids, next) {
topics.getTopicsFields(ids, ['tid', 'cid', 'pinned', 'lastposttime'], function (err, data) {
if (err) {
return next(err);
}
data = data.filter(function (topicData) {
return parseInt(topicData.pinned, 10) === 1;
});
async.eachSeries(data, function (topicData, next) {
winston.verbose('processing tid: ' + topicData.tid);
async.parallel([
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), topicData.tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', topicData.tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', topicData.tid)
], next);
}, next);
});
}, callback);
}
};

@ -0,0 +1,89 @@
/* jslint node: true */
'use strict';
var db = require('../database');
var async = require('async');
module.exports = {
name: 'Migrating flags to new schema',
timestamp: Date.UTC(2016, 11, 7),
method: function (callback) {
var batch = require('../batch');
var posts = require('../posts');
var flags = require('../flags');
var migrated = 0;
batch.processSortedSet('posts:pid', function (ids, next) {
posts.getPostsByPids(ids, 1, function (err, posts) {
if (err) {
return next(err);
}
posts = posts.filter(function (post) {
return post.hasOwnProperty('flags');
});
async.each(posts, function (post, next) {
async.parallel({
uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1),
reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1)
}, function (err, data) {
if (err) {
return next(err);
}
// Adding in another check here in case a post was improperly dismissed (flag count > 1 but no flags in db)
if (!data.uids.length || !data.reasons.length) {
return setImmediate(next);
}
// Just take the first entry
var datetime = data.uids[0].score;
var reason = data.reasons[0].split(':')[1];
var flagObj;
async.waterfall([
async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime),
function (_flagObj, next) {
flagObj = _flagObj;
if (post['flag:state'] || post['flag:assignee']) {
flags.update(flagObj.flagId, 1, {
state: post['flag:state'],
assignee: post['flag:assignee'],
datetime: datetime
}, next);
} else {
setImmediate(next);
}
},
function (next) {
if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) {
try {
var history = JSON.parse(post['flag:history']);
history = history.filter(function (event) {
return event.type === 'notes';
})[0];
flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next);
} catch (e) {
next(e);
}
} else {
setImmediate(next);
}
}
], function (err) {
if (err && err.message === '[[error:already-flagged]]') {
// Already flagged, no need to parse, but not an error
next();
} else {
next(err);
}
});
});
}, next);
});
}, callback);
}
};
Loading…
Cancel
Save