From 5b8c9503c314aac3eece30c6abca98c634895a98 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 22 Feb 2017 13:20:47 -0500 Subject: [PATCH] refactor upgrade scripts to use individual files in src/upgrades/ as source of schema changes --- app.js | 16 +- nodebb | 12 + src/upgrade.js | 463 ++---------------- src/upgrades/20160922-category-recent-tids.js | 33 ++ .../20161008-favourites-to-bookmarks.js | 52 ++ .../20161014-sorted-sets-for-post-replies.js | 34 ++ .../20161122-global-and-user-language-keys.js | 63 +++ .../20161125-sorted-set-for-pinned-topics.js | 37 ++ src/upgrades/20161207-flags-refactor.js | 89 ++++ 9 files changed, 377 insertions(+), 422 deletions(-) create mode 100644 src/upgrades/20160922-category-recent-tids.js create mode 100644 src/upgrades/20161008-favourites-to-bookmarks.js create mode 100644 src/upgrades/20161014-sorted-sets-for-post-replies.js create mode 100644 src/upgrades/20161122-global-and-user-language-keys.js create mode 100644 src/upgrades/20161125-sorted-set-for-pinned-topics.js create mode 100644 src/upgrades/20161207-flags-refactor.js diff --git a/app.js b/app.js index 57c57d61b6..271e16fee5 100644 --- a/app.js +++ b/app.js @@ -189,13 +189,17 @@ function upgrade() { var meta = require('./src/meta'); var upgrade = require('./src/upgrade'); var build = require('./src/meta/build'); + var tasks = [db.init, meta.configs.init, upgrade.run, build.buildAll]; - async.series([ - async.apply(db.init), - async.apply(meta.configs.init), - async.apply(upgrade.upgrade), - async.apply(build.buildAll) - ], function (err) { + if (nconf.get('upgrade') !== true) { + // Likely an upgrade script name passed in + tasks[2] = async.apply(upgrade.runSingle, nconf.get('upgrade')); + + // Skip build + tasks.pop(); + } + + async.series(tasks, function (err) { if (err) { winston.error(err.stack); process.exit(1); diff --git a/nodebb b/nodebb index ee87b24288..8ee1628855 100755 --- a/nodebb +++ b/nodebb @@ -406,6 +406,18 @@ var commands = { description: 'Run NodeBB upgrade scripts, ensure packages are up-to-date', usage: 'Usage: ' + './nodebb upgrade'.yellow, 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([ function (next) { process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow); diff --git a/src/upgrade.js b/src/upgrade.js index 12939ad99d..1aee44d060 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -1,443 +1,74 @@ -"use strict"; +/* jslint node: true */ +'use strict'; -/* globals console, require */ - -var db = require('./database'); 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 schemaDate; -var thisSchemaDate; +var Upgrade = {}; -// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema -var latestSchema = Date.UTC(2016, 11, 7); +Upgrade.run = function (callback) { + process.stdout.write('\nParsing upgrade scripts... '); -Upgrade.check = function (callback) { - db.get('schemaDate', function (err, value) { + utils.walk(path.join(__dirname, './upgrades'), function (err, files) { if (err) { return callback(err); } - if (!value) { - 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.process(files, callback); }); }; -Upgrade.update = function (schemaDate, callback) { - db.set('schemaDate', schemaDate, callback); -}; - -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); +Upgrade.runSingle = function (query, callback) { + process.stdout.write('\nParsing upgrade scripts... '); - if (schemaDate < thisSchemaDate) { - updatesMade = true; - winston.info('[2016/12/07] Migrating flags to new schema (#5232)'); - - 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); - } + async.waterfall([ + async.apply(utils.walk, path.join(__dirname, './upgrades')), + function (files, next) { + next(null, files.filter(function (file) { + return file.search(new RegExp(query)) !== -1; + })); + } + ], function (err, files) { + if (err) { + return callback(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); - } + Upgrade.process(files, callback); + }); +}; - // 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); - } +Upgrade.process = function (files, callback) { + process.stdout.write('OK'.green + String(' ' + files.length).cyan + ' script(s) found\n'.cyan); - // Just take the first entry - var datetime = data.uids[0].score; - var reason = data.reasons[0].split(':')[1]; - var flagObj; + // Do I need to sort the files here? we'll see. + // sort(); - 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]; + async.eachSeries(files, function (file, next) { + var scriptExport = require(file); + var date = new Date(scriptExport.timestamp); - 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); - }); - }, function (err) { - if (err) { - return next(err); - } + process.stdout.write(' → '.white + String('[' + [date.getFullYear(), date.getMonth() + 1, date.getDate() + 1].join('/') + '] ').gray + String(scriptExport.name).reset + '... '); - winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done'); - Upgrade.update(thisSchemaDate, next); - }); - } else { - winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!'); - 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!'); + // Do the upgrade... + scriptExport.method(function (err) { + if (err) { + process.stdout.write('error\n'.red); + return next(err); } - } 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: - winston.error('[upgrade] Errors were encountered while updating the NodeBB schema: ' + err.message); - break; - } + process.stdout.write('OK\n'.green); + next(); + }); + }, function (err) { + if (err) { + return callback(err); } - if (typeof callback === 'function') { - callback(err); - } else { - process.exit(); - } + process.stdout.write('Upgrade complete!\n\n'.green); + callback(); }); }; -module.exports = Upgrade; +module.exports = Upgrade; \ No newline at end of file diff --git a/src/upgrades/20160922-category-recent-tids.js b/src/upgrades/20160922-category-recent-tids.js new file mode 100644 index 0000000000..ccb6f5c818 --- /dev/null +++ b/src/upgrades/20160922-category-recent-tids.js @@ -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); + }); + } +}; \ No newline at end of file diff --git a/src/upgrades/20161008-favourites-to-bookmarks.js b/src/upgrades/20161008-favourites-to-bookmarks.js new file mode 100644 index 0000000000..288d32c511 --- /dev/null +++ b/src/upgrades/20161008-favourites-to-bookmarks.js @@ -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); + } +}; \ No newline at end of file diff --git a/src/upgrades/20161014-sorted-sets-for-post-replies.js b/src/upgrades/20161014-sorted-sets-for-post-replies.js new file mode 100644 index 0000000000..7442b614d4 --- /dev/null +++ b/src/upgrades/20161014-sorted-sets-for-post-replies.js @@ -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); + } +}; \ No newline at end of file diff --git a/src/upgrades/20161122-global-and-user-language-keys.js b/src/upgrades/20161122-global-and-user-language-keys.js new file mode 100644 index 0000000000..d08b3fea74 --- /dev/null +++ b/src/upgrades/20161122-global-and-user-language-keys.js @@ -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); + } +}; \ No newline at end of file diff --git a/src/upgrades/20161125-sorted-set-for-pinned-topics.js b/src/upgrades/20161125-sorted-set-for-pinned-topics.js new file mode 100644 index 0000000000..d10a6ee902 --- /dev/null +++ b/src/upgrades/20161125-sorted-set-for-pinned-topics.js @@ -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); + } +}; \ No newline at end of file diff --git a/src/upgrades/20161207-flags-refactor.js b/src/upgrades/20161207-flags-refactor.js new file mode 100644 index 0000000000..c61a3a4747 --- /dev/null +++ b/src/upgrades/20161207-flags-refactor.js @@ -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); + } +}; \ No newline at end of file