From 805dcd7ca282cd348947b92e25699cf6329e2e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 Jul 2019 12:46:49 -0400 Subject: [PATCH] Async refactor in place (#7736) * feat: allow both callback&and await * feat: ignore async key * feat: callbackify and promisify in same file * Revert "feat: callbackify and promisify in same file" This reverts commit cea206a9b8e6d8295310074b18cc82a504487862. * feat: no need to store .callbackify * feat: change getTopics to async * feat: remove .async * fix: byScore * feat: rewrite topics/index and social with async/await * fix: rewrite topics/data.js fix issue with async.waterfall, only pass result if its not undefined * feat: add callbackify to redis/psql * feat: psql use await * fix: redis :volcano: * feat: less returns * feat: more await rewrite * fix: redis tests * feat: convert sortedSetAdd rewrite psql transaction to async/await * feat: :dog: * feat: test * feat: log client and query * feat: log bind * feat: more logs * feat: more logs * feat: check perform * feat: dont callbackify transaction * feat: remove logs * fix: main functions * feat: more logs * fix: increment * fix: rename * feat: remove cls * fix: remove console.log * feat: add deprecation message to .async usage * feat: update more dbal methods * fix: redis :voodoo: * feat: fix redis zrem, convert setObject * feat: upgrade getObject methods * fix: psql getObjectField * fix: redis tests * feat: getObjectKeys * feat: getObjectValues * feat: isObjectField * fix: add missing return * feat: delObjectField * feat: incrObjectField * fix: add missing await * feat: remove exposed helpers * feat: list methods * feat: flush/empty * feat: delete * fix: redis delete all * feat: get/set * feat: incr/rename * feat: type * feat: expire * feat: setAdd * feat: setRemove * feat: isSetMember * feat: getSetMembers * feat: setCount, setRemoveRandom * feat: zcard,zcount * feat: sortedSetRank * feat: isSortedSetMember * feat: zincrby * feat: sortedSetLex * feat: processSortedSet * fix: add mising await * feat: debug psql * fix: psql test * fix: test * fix: another test * fix: test fix * fix: psql tests * feat: remove logs * feat: user arrow func use builtin async promises * feat: topic bookmarks * feat: topic.delete * feat: topic.restore * feat: topics.purge * feat: merge * feat: suggested * feat: topics/user.js * feat: topics modules * feat: topics/follow * fix: deprecation msg * feat: fork * feat: topics/posts * feat: sorted/recent * feat: topic/teaser * feat: topics/tools * feat: topics/unread * feat: add back node versions disable deprecation notice wrap async controllers in try/catch * feat: use db directly * feat: promisify in place * fix: redis/psql * feat: deprecation message logs for psql * feat: more logs * feat: more logs * feat: logs again * feat: more logs * fix: call release * feat: restore travis, remove logs * fix: loops * feat: remove .async. usage --- .eslintrc | 2 + install/package.json | 1 - src/batch.js | 133 ++--- src/controllers/topics.js | 18 +- src/database/mongo.js | 4 - src/database/mongo/hash.js | 234 +++----- src/database/mongo/list.js | 107 ++-- src/database/mongo/main.js | 191 +++---- src/database/mongo/sets.js | 191 +++---- src/database/mongo/sorted.js | 499 +++++++---------- src/database/mongo/sorted/add.js | 54 +- src/database/mongo/sorted/intersect.js | 36 +- src/database/mongo/sorted/remove.js | 50 +- src/database/mongo/sorted/union.js | 41 +- src/database/postgres.js | 34 +- src/database/postgres/hash.js | 322 ++++------- src/database/postgres/helpers.js | 123 ++-- src/database/postgres/list.js | 140 ++--- src/database/postgres/main.js | 247 ++++----- src/database/postgres/sets.js | 218 +++----- src/database/postgres/sorted.js | 515 +++++++---------- src/database/postgres/sorted/add.js | 157 +++--- src/database/postgres/sorted/intersect.js | 59 +- src/database/postgres/sorted/remove.js | 43 +- src/database/postgres/sorted/union.js | 56 +- src/database/postgres/transaction.js | 70 +-- src/database/redis.js | 8 +- src/database/redis/hash.js | 231 ++++---- src/database/redis/helpers.js | 7 + src/database/redis/list.js | 54 +- src/database/redis/main.js | 126 ++--- src/database/redis/promisify.js | 66 +++ src/database/redis/sets.js | 92 ++- src/database/redis/sorted.js | 315 +++++------ src/database/redis/sorted/add.js | 49 +- src/database/redis/sorted/intersect.js | 48 +- src/database/redis/sorted/remove.js | 46 +- src/database/redis/sorted/union.js | 50 +- src/file.js | 16 +- src/image.js | 48 +- src/messaging/rooms.js | 12 +- src/posts/uploads.js | 4 +- src/promisify.js | 80 ++- src/routes/helpers.js | 15 +- src/search.js | 2 + src/social.js | 66 +-- src/socket.io/user.js | 2 +- src/topics/bookmarks.js | 102 ++-- src/topics/create.js | 469 ++++++---------- src/topics/data.js | 78 +-- src/topics/delete.js | 373 +++++-------- src/topics/follow.js | 347 +++++------- src/topics/fork.js | 235 +++----- src/topics/index.js | 429 ++++++-------- src/topics/merge.js | 53 +- src/topics/posts.js | 501 ++++++----------- src/topics/recent.js | 118 ++-- src/topics/sorted.js | 204 +++---- src/topics/suggested.js | 119 ++-- src/topics/tags.js | 648 ++++++++-------------- src/topics/teaser.js | 208 +++---- src/topics/thumb.js | 97 ++-- src/topics/tools.js | 456 ++++++--------- src/topics/unread.js | 609 ++++++++------------ src/topics/user.js | 13 +- src/user/blocks.js | 4 +- test/authentication.js | 1 - test/batch.js | 115 ++++ test/controllers.js | 12 +- test/database/keys.js | 11 + test/database/list.js | 25 +- test/database/sorted.js | 27 +- test/topics.js | 4 +- 73 files changed, 4030 insertions(+), 6110 deletions(-) create mode 100644 src/database/redis/promisify.js create mode 100644 test/batch.js diff --git a/.eslintrc b/.eslintrc index bbaa357442..c7ae618ffa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,8 @@ "exports": "always-multiline", "functions": "never" }], + "no-return-await": "off", + "no-constant-condition": "off", "no-empty": ["error", { "allowEmptyCatch": true }], "no-underscore-dangle": "off", "no-console": "off", diff --git a/install/package.json b/install/package.json index 14ca928e27..59c7369f8a 100644 --- a/install/package.json +++ b/install/package.json @@ -50,7 +50,6 @@ "connect-multiparty": "^2.1.0", "connect-pg-simple": "^5.0.0", "connect-redis": "3.4.1", - "continuation-local-storage": "^3.2.1", "cookie-parser": "^1.4.3", "cron": "^1.3.0", "cropperjs": "^1.2.2", diff --git a/src/batch.js b/src/batch.js index 9f7e8720e4..14f60d4dc3 100644 --- a/src/batch.js +++ b/src/batch.js @@ -2,39 +2,32 @@ 'use strict'; -var async = require('async'); +const util = require('util'); + var db = require('./database'); var utils = require('./utils'); var DEFAULT_BATCH_SIZE = 100; -exports.processSortedSet = function (setKey, process, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; - } +const sleep = util.promisify(setTimeout); - callback = typeof callback === 'function' ? callback : function () {}; +exports.processSortedSet = async function (setKey, process, options) { options = options || {}; if (typeof process !== 'function') { - return callback(new Error('[[error:process-not-a-function]]')); + throw new Error('[[error:process-not-a-function]]'); } // Progress bar handling (upgrade scripts) if (options.progress) { - db.sortedSetCard(setKey, function (err, total) { - if (!err) { - options.progress.total = total; - } - }); + options.progress.total = await db.sortedSetCard(setKey); } options.batch = options.batch || DEFAULT_BATCH_SIZE; // use the fast path if possible if (db.processSortedSet && typeof options.doneIf !== 'function' && !utils.isNumber(options.alwaysStartAt)) { - return db.processSortedSet(setKey, process, options, callback); + return await db.processSortedSet(setKey, process, options); } // custom done condition @@ -42,90 +35,58 @@ exports.processSortedSet = function (setKey, process, options, callback) { var start = 0; var stop = options.batch; - var done = false; - async.whilst( - function (next) { - next(null, !done); - }, - function (next) { - async.waterfall([ - function (next) { - db['getSortedSetRange' + (options.withScores ? 'WithScores' : '')](setKey, start, stop, next); - }, - function (ids, _next) { - if (!ids.length || options.doneIf(start, stop, ids)) { - done = true; - return next(); - } - process(ids, function (err) { - _next(err); - }); - }, - function (next) { - start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : options.batch + 1; - stop = start + options.batch; - - if (options.interval) { - setTimeout(next, options.interval); - } else { - next(); - } - }, - ], next); - }, - callback - ); -}; - -exports.processArray = function (array, process, options, callback) { - if (typeof options === 'function') { - callback = options; - options = {}; + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); } - callback = typeof callback === 'function' ? callback : function () {}; + while (true) { + /* eslint-disable no-await-in-loop */ + const ids = await db['getSortedSetRange' + (options.withScores ? 'WithScores' : '')](setKey, start, stop); + if (!ids.length || options.doneIf(start, stop, ids)) { + return; + } + await process(ids); + + start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : options.batch + 1; + stop = start + options.batch; + + if (options.interval) { + await sleep(options.interval); + } + } +}; + +exports.processArray = async function (array, process, options) { options = options || {}; if (!Array.isArray(array) || !array.length) { - return callback(); + return; } if (typeof process !== 'function') { - return callback(new Error('[[error:process-not-a-function]]')); + throw new Error('[[error:process-not-a-function]]'); } var batch = options.batch || DEFAULT_BATCH_SIZE; var start = 0; - var done = false; + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); + } - async.whilst( - function (next) { - next(null, !done); - }, - function (next) { - var currentBatch = array.slice(start, start + batch); - if (!currentBatch.length) { - done = true; - return next(); - } - async.waterfall([ - function (next) { - process(currentBatch, function (err) { - next(err); - }); - }, - function (next) { - start += batch; - if (options.interval) { - setTimeout(next, options.interval); - } else { - next(); - } - }, - ], next); - }, - function (err) { - callback(err); + while (true) { + var currentBatch = array.slice(start, start + batch); + + if (!currentBatch.length) { + return; } - ); + + await process(currentBatch); + + start += batch; + + if (options.interval) { + await sleep(options.interval); + } + } }; +require('./promisify')(exports); diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 960b91d4cd..3638e842e8 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -31,10 +31,10 @@ topicsController.get = async function getTopic(req, res, callback) { topicData, rssToken, ] = await Promise.all([ - privileges.async.topics.get(tid, req.uid), - user.async.getSettings(req.uid), - topics.async.getTopicData(tid), - user.async.auth.getFeedToken(req.uid), + privileges.topics.get(tid, req.uid), + user.getSettings(req.uid), + topics.getTopicData(tid), + user.auth.getFeedToken(req.uid), ]); var currentPage = parseInt(req.query.page, 10) || 1; @@ -52,7 +52,7 @@ topicsController.get = async function getTopic(req, res, callback) { } if (postIndex === 'unread') { - postIndex = await topics.async.getUserBookmark(tid, req.uid); + postIndex = await topics.getUserBookmark(tid, req.uid); } if (utils.isNumber(postIndex) && (postIndex < 1 || postIndex > topicData.postcount)) { @@ -67,11 +67,11 @@ topicsController.get = async function getTopic(req, res, callback) { } const { start, stop } = calculateStartStop(currentPage, postIndex, settings); - await topics.async.getTopicWithPosts(topicData, set, req.uid, start, stop, reverse); + await topics.getTopicWithPosts(topicData, set, req.uid, start, stop, reverse); topics.modifyPostsByPrivilege(topicData, userPrivileges); - const hookData = await plugins.async.fireHook('filter:controllers.topic.get', { topicData: topicData, uid: req.uid }); + const hookData = await plugins.fireHook('filter:controllers.topic.get', { topicData: topicData, uid: req.uid }); await Promise.all([ buildBreadcrumbs(hookData.topicData), addTags(topicData, req, res), @@ -161,7 +161,7 @@ async function buildBreadcrumbs(topicData) { text: topicData.title, }, ]; - const parentCrumbs = await helpers.async.buildCategoryBreadcrumbs(topicData.category.parentCid); + const parentCrumbs = await helpers.buildCategoryBreadcrumbs(topicData.category.parentCid); topicData.breadcrumbs = parentCrumbs.concat(breadcrumbs); } @@ -239,7 +239,7 @@ async function addTags(topicData, req, res) { } async function addOGImageTags(res, topicData, postAtIndex) { - const uploads = postAtIndex ? await posts.async.uploads.listWithSizes(postAtIndex.pid) : []; + const uploads = postAtIndex ? await posts.uploads.listWithSizes(postAtIndex.pid) : []; const images = uploads.map((upload) => { upload.name = nconf.get('url') + nconf.get('upload_url') + '/files/' + upload.name; return upload; diff --git a/src/database/mongo.js b/src/database/mongo.js index 0fd8ad9169..be883bb7b8 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -59,9 +59,6 @@ mongoModule.questions = [ }, ]; -mongoModule.helpers = mongoModule.helpers || {}; -mongoModule.helpers.mongo = require('./mongo/helpers'); - mongoModule.getConnectionString = function (mongo) { mongo = mongo || nconf.get('mongo'); var usernamePassword = ''; @@ -130,7 +127,6 @@ mongoModule.init = function (callback) { require('./mongo/transaction')(db, mongoModule); mongoModule.async = require('../promisify')(mongoModule, ['client', 'sessionStore']); - callback(); }); }; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index b6be45b776..8d7e1c8123 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -1,86 +1,73 @@ 'use strict'; -var async = require('async'); - - module.exports = function (db, module) { - var helpers = module.helpers.mongo; + var helpers = require('./helpers'); var _ = require('lodash'); const cache = require('../cache').create('mongo'); module.objectCache = cache; - module.setObject = function (key, data, callback) { - callback = callback || helpers.noop; + module.setObject = async function (key, data) { if (!key || !data) { - return callback(); + return; } const writeData = helpers.serializeData(data); - db.collection('objects').updateOne({ _key: key }, { $set: writeData }, { upsert: true, w: 1 }, function (err) { - if (err) { - return callback(err); - } - cache.delObjectCache(key); - callback(); - }); + await db.collection('objects').updateOne({ _key: key }, { $set: writeData }, { upsert: true, w: 1 }); + cache.delObjectCache(key); }; - module.setObjectField = function (key, field, value, callback) { - callback = callback || helpers.noop; + module.setObjectField = async function (key, field, value) { if (!field) { - return callback(); + return; } var data = {}; data[field] = value; - module.setObject(key, data, callback); + await module.setObject(key, data); }; - module.getObject = function (key, callback) { + module.getObject = async function (key) { if (!key) { - return setImmediate(callback, null, null); + return null; } - module.getObjects([key], function (err, data) { - callback(err, data && data.length ? data[0] : null); - }); + const data = await module.getObjects([key]); + return data && data.length ? data[0] : null; }; - module.getObjects = function (keys, callback) { - module.getObjectsFields(keys, [], callback); + module.getObjects = async function (keys) { + return await module.getObjectsFields(keys, []); }; - module.getObjectField = function (key, field, callback) { + module.getObjectField = async function (key, field) { if (!key) { - return setImmediate(callback, null, null); + return null; } const cachedData = {}; cache.getUnCachedKeys([key], cachedData); if (cachedData[key]) { - return setImmediate(callback, null, cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null); + return cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null; } field = helpers.fieldToString(field); - db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, [field]: 1 } }, function (err, item) { - if (err || !item) { - return callback(err, null); - } - callback(null, item.hasOwnProperty(field) ? item[field] : null); - }); - }; - - module.getObjectFields = function (key, fields, callback) { - if (!key) { - return setImmediate(callback, null, null); + const item = await db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, [field]: 1 } }); + if (!item) { + return null; } - module.getObjectsFields([key], fields, function (err, data) { - callback(err, data ? data[0] : null); - }); + return item.hasOwnProperty(field) ? item[field] : null; }; - module.getObjectsFields = function (keys, fields, callback) { + module.getObjectFields = async function (key, fields) { + if (!key) { + return null; + } + const data = await module.getObjectsFields([key], fields); + return data ? data[0] : null; + }; + + module.getObjectsFields = async function (keys, fields) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } const cachedData = {}; function returnData() { @@ -97,104 +84,72 @@ module.exports = function (db, module) { return result; }); - callback(null, mapped); + return mapped; } const unCachedKeys = cache.getUnCachedKeys(keys, cachedData); if (!unCachedKeys.length) { - return process.nextTick(returnData); + return returnData(); } var query = { _key: { $in: unCachedKeys } }; if (unCachedKeys.length === 1) { query._key = unCachedKeys[0]; } - db.collection('objects').find(query, { projection: { _id: 0 } }).toArray(function (err, data) { - if (err) { - return callback(err); - } - data = data.map(helpers.deserializeData); - var map = helpers.toMap(data); - unCachedKeys.forEach(function (key) { - cachedData[key] = map[key] || null; - cache.set(key, cachedData[key]); - }); + let data = await db.collection('objects').find(query, { projection: { _id: 0 } }).toArray(); - returnData(); + data = data.map(helpers.deserializeData); + var map = helpers.toMap(data); + unCachedKeys.forEach(function (key) { + cachedData[key] = map[key] || null; + cache.set(key, cachedData[key]); }); + + return returnData(); }; - module.getObjectKeys = function (key, callback) { - module.getObject(key, function (err, data) { - callback(err, data ? Object.keys(data) : []); - }); + module.getObjectKeys = async function (key) { + const data = await module.getObject(key); + return data ? Object.keys(data) : []; }; - module.getObjectValues = function (key, callback) { - module.getObject(key, function (err, data) { - if (err) { - return callback(err); - } - - var values = []; - for (var key in data) { - if (data && data.hasOwnProperty(key)) { - values.push(data[key]); - } - } - callback(null, values); - }); + module.getObjectValues = async function (key) { + const data = await module.getObject(key); + return data ? Object.values(data) : []; }; - module.isObjectField = function (key, field, callback) { + module.isObjectField = async function (key, field) { + const data = await module.isObjectFields(key, [field]); + return Array.isArray(data) && data.length ? data[0] : false; + }; + + module.isObjectFields = async function (key, fields) { if (!key) { - return callback(); - } - var data = {}; - field = helpers.fieldToString(field); - data[field] = 1; - db.collection('objects').findOne({ _key: key }, { projection: data }, function (err, item) { - callback(err, !!item && item[field] !== undefined && item[field] !== null); - }); - }; - - module.isObjectFields = function (key, fields, callback) { - if (!key) { - return callback(); + return; } - var data = {}; + const data = {}; fields.forEach(function (field) { field = helpers.fieldToString(field); data[field] = 1; }); - db.collection('objects').findOne({ _key: key }, { projection: data }, function (err, item) { - if (err) { - return callback(err); - } - var results = []; - - fields.forEach(function (field, index) { - results[index] = !!item && item[field] !== undefined && item[field] !== null; - }); - - callback(null, results); - }); + const item = await db.collection('objects').findOne({ _key: key }, { projection: data }); + const results = fields.map(f => !!item && item[f] !== undefined && item[f] !== null); + return results; }; - module.deleteObjectField = function (key, field, callback) { - module.deleteObjectFields(key, [field], callback); + module.deleteObjectField = async function (key, field) { + await module.deleteObjectFields(key, [field]); }; - module.deleteObjectFields = function (key, fields, callback) { - callback = callback || helpers.noop; + module.deleteObjectFields = async function (key, fields) { if (!key || !Array.isArray(fields) || !fields.length) { - return callback(); + return; } fields = fields.filter(Boolean); if (!fields.length) { - return callback(); + return; } var data = {}; @@ -203,68 +158,41 @@ module.exports = function (db, module) { data[field] = ''; }); - db.collection('objects').updateOne({ _key: key }, { $unset: data }, function (err) { - if (err) { - return callback(err); - } - cache.delObjectCache(key); - callback(); - }); + await db.collection('objects').updateOne({ _key: key }, { $unset: data }); + cache.delObjectCache(key); }; - module.incrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, 1, callback); + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); }; - module.decrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, -1, callback); + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); }; - module.incrObjectFieldBy = function (key, field, value, callback) { - callback = callback || helpers.noop; + module.incrObjectFieldBy = async function (key, field, value) { value = parseInt(value, 10); if (!key || isNaN(value)) { - return callback(null, null); + return null; } - var data = {}; + var increment = {}; field = helpers.fieldToString(field); - data[field] = value; + increment[field] = value; if (Array.isArray(key)) { var bulk = db.collection('objects').initializeUnorderedBulkOp(); key.forEach(function (key) { - bulk.find({ _key: key }).upsert().update({ $inc: data }); + bulk.find({ _key: key }).upsert().update({ $inc: increment }); }); - - async.waterfall([ - function (next) { - bulk.execute(function (err) { - next(err); - }); - }, - function (next) { - cache.delObjectCache(key); - - module.getObjectsFields(key, [field], next); - }, - function (data, next) { - data = data.map(function (data) { - return data && data[field]; - }); - next(null, data); - }, - ], callback); - return; + await bulk.execute(); + cache.delObjectCache(key); + const result = await module.getObjectsFields(key, [field]); + return result.map(data => data && data[field]); } - - db.collection('objects').findOneAndUpdate({ _key: key }, { $inc: data }, { returnOriginal: false, upsert: true }, function (err, result) { - if (err) { - return callback(err); - } - cache.delObjectCache(key); - callback(null, result && result.value ? result.value[field] : null); - }); + const result = await db.collection('objects').findOneAndUpdate({ _key: key }, { $inc: increment }, { returnOriginal: false, upsert: true }); + cache.delObjectCache(key); + return result && result.value ? result.value[field] : null; }; }; diff --git a/src/database/mongo/list.js b/src/database/mongo/list.js index 219cd53be5..1a8ba3df1d 100644 --- a/src/database/mongo/list.js +++ b/src/database/mongo/list.js @@ -1,112 +1,75 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.mongo; - - module.listPrepend = function (key, value, callback) { - callback = callback || helpers.noop; + var helpers = require('./helpers'); + module.listPrepend = async function (key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - module.isObjectField(key, 'array', function (err, exists) { - if (err) { - return callback(err); - } - - if (exists) { - db.collection('objects').updateOne({ _key: key }, { $push: { array: { $each: [value], $position: 0 } } }, { upsert: true, w: 1 }, function (err) { - callback(err); - }); - } else { - module.listAppend(key, value, callback); - } - }); + const exists = await module.isObjectField(key, 'array'); + if (exists) { + await db.collection('objects').updateOne({ _key: key }, { $push: { array: { $each: [value], $position: 0 } } }, { upsert: true, w: 1 }); + } else { + await module.listAppend(key, value); + } }; - module.listAppend = function (key, value, callback) { - callback = callback || helpers.noop; + module.listAppend = async function (key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - db.collection('objects').updateOne({ _key: key }, { $push: { array: value } }, { upsert: true, w: 1 }, function (err) { - callback(err); - }); + await db.collection('objects').updateOne({ _key: key }, { $push: { array: value } }, { upsert: true, w: 1 }); }; - module.listRemoveLast = function (key, callback) { - callback = callback || helpers.noop; + module.listRemoveLast = async function (key) { if (!key) { - return callback(); + return; } - module.getListRange(key, -1, -1, function (err, value) { - if (err) { - return callback(err); - } - - db.collection('objects').updateOne({ _key: key }, { $pop: { array: 1 } }, function (err) { - callback(err, (value && value.length) ? value[0] : null); - }); - }); + const value = await module.getListRange(key, -1, -1); + db.collection('objects').updateOne({ _key: key }, { $pop: { array: 1 } }); + return (value && value.length) ? value[0] : null; }; - module.listRemoveAll = function (key, value, callback) { - callback = callback || helpers.noop; + module.listRemoveAll = async function (key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - db.collection('objects').updateOne({ _key: key }, { $pull: { array: value } }, function (err) { - callback(err); - }); + await db.collection('objects').updateOne({ _key: key }, { $pull: { array: value } }); }; - module.listTrim = function (key, start, stop, callback) { - callback = callback || helpers.noop; + module.listTrim = async function (key, start, stop) { if (!key) { - return callback(); + return; } - module.getListRange(key, start, stop, function (err, value) { - if (err) { - return callback(err); - } - - db.collection('objects').updateOne({ _key: key }, { $set: { array: value } }, function (err) { - callback(err); - }); - }); + const value = await module.getListRange(key, start, stop); + await db.collection('objects').updateOne({ _key: key }, { $set: { array: value } }); }; - module.getListRange = function (key, start, stop, callback) { + module.getListRange = async function (key, start, stop) { if (!key) { - return callback(); + return; } - db.collection('objects').findOne({ _key: key }, { array: 1 }, function (err, data) { - if (err || !(data && data.array)) { - return callback(err, []); - } + const data = await db.collection('objects').findOne({ _key: key }, { array: 1 }); + if (!(data && data.array)) { + return []; + } - if (stop === -1) { - data.array = data.array.slice(start); - } else { - data.array = data.array.slice(start, stop + 1); - } - callback(null, data.array); - }); + return data.array.slice(start, stop !== -1 ? stop + 1 : undefined); }; - module.listLength = function (key, callback) { - db.collection('objects').aggregate([ + module.listLength = async function (key) { + const result = await db.collection('objects').aggregate([ { $match: { _key: key } }, { $project: { count: { $size: '$array' } } }, - ]).toArray(function (err, result) { - callback(err, Array.isArray(result) && result.length && result[0].count); - }); + ]).toArray(); + return Array.isArray(result) && result.length && result[0].count; }; }; diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 7c7d430a10..5a95c15f51 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -1,170 +1,121 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.mongo; - - module.flushdb = function (callback) { - callback = callback || helpers.noop; - db.dropDatabase(function (err) { - callback(err); - }); + module.flushdb = async function () { + await db.dropDatabase(); }; - module.emptydb = function (callback) { - callback = callback || helpers.noop; - db.collection('objects').deleteMany({}, function (err) { - if (err) { - return callback(err); - } - module.objectCache.resetObjectCache(); - callback(); - }); + module.emptydb = async function () { + await db.collection('objects').deleteMany({}); + module.objectCache.resetObjectCache(); }; - module.exists = function (key, callback) { + module.exists = async function (key) { if (!key) { - return callback(); + return; } if (Array.isArray(key)) { - db.collection('objects').find({ _key: { $in: key } }).toArray(function (err, data) { - if (err) { - return callback(err); - } - - var map = {}; - data.forEach(function (item) { - map[item._key] = true; - }); - - callback(null, key.map(key => !!map[key])); - }); - } else { - db.collection('objects').findOne({ _key: key }, function (err, item) { - callback(err, item !== undefined && item !== null); + const data = await db.collection('objects').find({ _key: { $in: key } }).toArray(); + var map = {}; + data.forEach(function (item) { + map[item._key] = true; }); + + return key.map(key => !!map[key]); } + const item = await db.collection('objects').findOne({ _key: key }); + return item !== undefined && item !== null; }; - module.delete = function (key, callback) { - callback = callback || helpers.noop; + module.delete = async function (key) { if (!key) { - return callback(); + return; } - db.collection('objects').deleteMany({ _key: key }, function (err) { - if (err) { - return callback(err); - } - module.objectCache.delObjectCache(key); - callback(); - }); + await db.collection('objects').deleteMany({ _key: key }); + module.objectCache.delObjectCache(key); }; - module.deleteAll = function (keys, callback) { - callback = callback || helpers.noop; + module.deleteAll = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } - db.collection('objects').deleteMany({ _key: { $in: keys } }, function (err) { - if (err) { - return callback(err); - } - - module.objectCache.delObjectCache(keys); - - callback(null); - }); + await db.collection('objects').deleteMany({ _key: { $in: keys } }); + module.objectCache.delObjectCache(keys); }; - module.get = function (key, callback) { + module.get = async function (key) { if (!key) { - return callback(); + return; } - db.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }, function (err, objectData) { - if (err) { - return callback(err); + const objectData = await db.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }); + + // fallback to old field name 'value' for backwards compatibility #6340 + var value = null; + if (objectData) { + if (objectData.hasOwnProperty('data')) { + value = objectData.data; + } else if (objectData.hasOwnProperty('value')) { + value = objectData.value; } - // fallback to old field name 'value' for backwards compatibility #6340 - var value = null; - if (objectData) { - if (objectData.hasOwnProperty('data')) { - value = objectData.data; - } else if (objectData.hasOwnProperty('value')) { - value = objectData.value; - } - } - callback(null, value); - }); + } + return value; }; - module.set = function (key, value, callback) { - callback = callback || helpers.noop; + module.set = async function (key, value) { if (!key) { - return callback(); + return; } var data = { data: value }; - module.setObject(key, data, callback); + await module.setObject(key, data); }; - module.increment = function (key, callback) { - callback = callback || helpers.noop; + module.increment = async function (key) { if (!key) { - return callback(); + return; } - db.collection('objects').findOneAndUpdate({ _key: key }, { $inc: { data: 1 } }, { returnOriginal: false, upsert: true }, function (err, result) { - callback(err, result && result.value ? result.value.data : null); - }); + const result = await db.collection('objects').findOneAndUpdate({ _key: key }, { $inc: { data: 1 } }, { returnOriginal: false, upsert: true }); + return result && result.value ? result.value.data : null; }; - module.rename = function (oldKey, newKey, callback) { - callback = callback || helpers.noop; - db.collection('objects').updateMany({ _key: oldKey }, { $set: { _key: newKey } }, function (err) { - if (err) { - return callback(err); - } - module.objectCache.delObjectCache(oldKey); - module.objectCache.delObjectCache(newKey); - callback(); - }); + module.rename = async function (oldKey, newKey) { + await db.collection('objects').updateMany({ _key: oldKey }, { $set: { _key: newKey } }); + module.objectCache.delObjectCache([oldKey, newKey]); }; - module.type = function (key, callback) { - db.collection('objects').findOne({ _key: key }, function (err, data) { - if (err) { - return callback(err); - } - if (!data) { - return callback(null, null); - } - delete data.expireAt; - var keys = Object.keys(data); - if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { - return callback(null, 'zset'); - } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('members')) { - return callback(null, 'set'); - } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('array')) { - return callback(null, 'list'); - } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('data')) { - return callback(null, 'string'); - } - callback(null, 'hash'); - }); + module.type = async function (key) { + const data = await db.collection('objects').findOne({ _key: key }); + if (!data) { + return null; + } + delete data.expireAt; + var keys = Object.keys(data); + if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { + return 'zset'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('members')) { + return 'set'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('array')) { + return 'list'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('data')) { + return 'string'; + } + return 'hash'; }; - module.expire = function (key, seconds, callback) { - module.expireAt(key, Math.round(Date.now() / 1000) + seconds, callback); + module.expire = async function (key, seconds) { + await module.expireAt(key, Math.round(Date.now() / 1000) + seconds); }; - module.expireAt = function (key, timestamp, callback) { - module.setObjectField(key, 'expireAt', new Date(timestamp * 1000), callback); + module.expireAt = async function (key, timestamp) { + await module.setObjectField(key, 'expireAt', new Date(timestamp * 1000)); }; - module.pexpire = function (key, ms, callback) { - module.pexpireAt(key, Date.now() + parseInt(ms, 10), callback); + module.pexpire = async function (key, ms) { + await module.pexpireAt(key, Date.now() + parseInt(ms, 10)); }; - module.pexpireAt = function (key, timestamp, callback) { + module.pexpireAt = async function (key, timestamp) { timestamp = Math.min(timestamp, 8640000000000000); - module.setObjectField(key, 'expireAt', new Date(timestamp), callback); + await module.setObjectField(key, 'expireAt', new Date(timestamp)); }; }; diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js index 97f8371a35..bfaa58dd6e 100644 --- a/src/database/mongo/sets.js +++ b/src/database/mongo/sets.js @@ -1,19 +1,16 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.mongo; + var helpers = require('./helpers'); - module.setAdd = function (key, value, callback) { - callback = callback || helpers.noop; + module.setAdd = async function (key, value) { if (!Array.isArray(value)) { value = [value]; } - value.forEach(function (element, index, array) { - array[index] = helpers.valueToString(element); - }); + value = value.map(v => helpers.valueToString(v)); - db.collection('objects').updateOne({ + await db.collection('objects').updateOne({ _key: key, }, { $addToSet: { @@ -24,25 +21,19 @@ module.exports = function (db, module) { }, { upsert: true, w: 1, - }, function (err) { - callback(err); }); }; - module.setsAdd = function (keys, value, callback) { - callback = callback || helpers.noop; - + module.setsAdd = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } if (!Array.isArray(value)) { value = [value]; } - value.forEach(function (element, index, array) { - array[index] = helpers.valueToString(element); - }); + value = value.map(v => helpers.valueToString(v)); var bulk = db.collection('objects').initializeUnorderedBulkOp(); @@ -53,168 +44,118 @@ module.exports = function (db, module) { }, } }); } - - bulk.execute(function (err) { + try { + await bulk.execute(); + } catch (err) { if (err && err.message.startsWith('E11000 duplicate key error')) { - return process.nextTick(module.setsAdd, keys, value, callback); + return await module.setsAdd(keys, value); } - callback(err); - }); + throw err; + } }; - module.setRemove = function (key, value, callback) { - callback = callback || helpers.noop; + module.setRemove = async function (key, value) { if (!Array.isArray(value)) { value = [value]; } - value.forEach(function (element, index, array) { - array[index] = helpers.valueToString(element); - }); + value = value.map(v => helpers.valueToString(v)); - if (Array.isArray(key)) { - db.collection('objects').updateMany({ _key: { $in: key } }, { $pullAll: { members: value } }, function (err) { - callback(err); - }); - } else { - db.collection('objects').updateOne({ _key: key }, { $pullAll: { members: value } }, function (err) { - callback(err); - }); - } + await db.collection('objects').updateMany({ _key: Array.isArray(key) ? { $in: key } : key }, { $pullAll: { members: value } }); }; - module.setsRemove = function (keys, value, callback) { - callback = callback || helpers.noop; + module.setsRemove = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } value = helpers.valueToString(value); - db.collection('objects').updateMany({ _key: { $in: keys } }, { $pull: { members: value } }, function (err) { - callback(err); - }); + await db.collection('objects').updateMany({ _key: { $in: keys } }, { $pull: { members: value } }); }; - module.isSetMember = function (key, value, callback) { + module.isSetMember = async function (key, value) { if (!key) { - return callback(null, false); + return false; } value = helpers.valueToString(value); - db.collection('objects').findOne({ _key: key, members: value }, { projection: { _id: 0, members: 0 } }, function (err, item) { - callback(err, item !== null && item !== undefined); - }); + const item = await db.collection('objects').findOne({ _key: key, members: value }, { projection: { _id: 0, members: 0 } }); + return item !== null && item !== undefined; }; - module.isSetMembers = function (key, values, callback) { + module.isSetMembers = async function (key, values) { if (!key || !Array.isArray(values) || !values.length) { - return callback(null, []); + return []; } + values = values.map(v => helpers.valueToString(v)); - for (var i = 0; i < values.length; i += 1) { - values[i] = helpers.valueToString(values[i]); - } - - db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, _key: 0 } }, function (err, items) { - if (err) { - return callback(err); - } - - const membersSet = new Set(items && Array.isArray(items.members) ? items.members : []); - values = values.map(value => membersSet.has(value)); - callback(null, values); - }); + const result = await db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, _key: 0 } }); + const membersSet = new Set(result && Array.isArray(result.members) ? result.members : []); + return values.map(v => membersSet.has(v)); }; - module.isMemberOfSets = function (sets, value, callback) { + module.isMemberOfSets = async function (sets, value) { if (!Array.isArray(sets) || !sets.length) { - return callback(null, []); + return []; } value = helpers.valueToString(value); - db.collection('objects').find({ _key: { $in: sets }, members: value }, { projection: { _id: 0, members: 0 } }).toArray(function (err, result) { - if (err) { - return callback(err); - } - var map = {}; - result.forEach(function (item) { - map[item._key] = true; - }); + const result = await db.collection('objects').find({ _key: { $in: sets }, members: value }, { projection: { _id: 0, members: 0 } }).toArray(); - result = sets.map(function (set) { - return !!map[set]; - }); - - callback(null, result); + var map = {}; + result.forEach(function (item) { + map[item._key] = true; }); + + return sets.map(set => !!map[set]); }; - module.getSetMembers = function (key, callback) { + module.getSetMembers = async function (key) { if (!key) { - return callback(null, []); + return []; } - db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, _key: 0 } }, function (err, data) { - callback(err, data ? data.members : []); - }); + const data = await db.collection('objects').findOne({ _key: key }, { projection: { _id: 0, _key: 0 } }); + return data ? data.members : []; }; - module.getSetsMembers = function (keys, callback) { + module.getSetsMembers = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - db.collection('objects').find({ _key: { $in: keys } }, { projection: { _id: 0 } }).toArray(function (err, data) { - if (err) { - return callback(err); - } + const data = await db.collection('objects').find({ _key: { $in: keys } }, { projection: { _id: 0 } }).toArray(); - var sets = {}; - data.forEach(function (set) { - sets[set._key] = set.members || []; - }); - - var returnData = new Array(keys.length); - for (var i = 0; i < keys.length; i += 1) { - returnData[i] = sets[keys[i]] || []; - } - callback(null, returnData); + var sets = {}; + data.forEach(function (set) { + sets[set._key] = set.members || []; }); + + return keys.map(k => sets[k] || []); }; - module.setCount = function (key, callback) { + module.setCount = async function (key) { if (!key) { - return callback(null, 0); + return 0; } - db.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }, function (err, data) { - callback(err, data ? data.members.length : 0); - }); + const data = await db.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }); + return data ? data.members.length : 0; }; - module.setsCount = function (keys, callback) { - module.getSetsMembers(keys, function (err, setsMembers) { - if (err) { - return callback(err); - } - - var counts = setsMembers.map(function (members) { - return (members && members.length) || 0; - }); - callback(null, counts); - }); + module.setsCount = async function (keys) { + const setsMembers = await module.getSetsMembers(keys); + var counts = setsMembers.map(members => (members && members.length) || 0); + return counts; }; - module.setRemoveRandom = function (key, callback) { - callback = callback || function () {}; - db.collection('objects').findOne({ _key: key }, function (err, data) { - if (err || !data) { - return callback(err); - } + module.setRemoveRandom = async function (key) { + const data = await db.collection('objects').findOne({ _key: key }); + if (!data) { + return; + } - var randomIndex = Math.floor(Math.random() * data.members.length); - var value = data.members[randomIndex]; - module.setRemove(data._key, value, function (err) { - callback(err, value); - }); - }); + var randomIndex = Math.floor(Math.random() * data.members.length); + var value = data.members[randomIndex]; + await module.setRemove(data._key, value); + return value; }; }; diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 874b9b7a01..d73ee9f50d 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -1,44 +1,43 @@ 'use strict'; -var async = require('async'); var utils = require('../../utils'); module.exports = function (db, module) { - var helpers = module.helpers.mongo; + var helpers = require('./helpers'); + const util = require('util'); + const sleep = util.promisify(setTimeout); require('./sorted/add')(db, module); require('./sorted/remove')(db, module); require('./sorted/union')(db, module); require('./sorted/intersect')(db, module); - module.getSortedSetRange = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, '-inf', '+inf', 1, false, callback); + module.getSortedSetRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', 1, false); }; - module.getSortedSetRevRange = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, '-inf', '+inf', -1, false, callback); + module.getSortedSetRevRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', -1, false); }; - module.getSortedSetRangeWithScores = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, '-inf', '+inf', 1, true, callback); + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', 1, true); }; - module.getSortedSetRevRangeWithScores = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, '-inf', '+inf', -1, true, callback); + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', -1, true); }; - function getSortedSetRange(key, start, stop, min, max, sort, withScores, callback) { + async function getSortedSetRange(key, start, stop, min, max, sort, withScores) { if (!key) { - return callback(); + return; } - if (start < 0 && start > stop) { - return callback(null, []); + const isArray = Array.isArray(key); + if ((start < 0 && start > stop) || (isArray && !key.length)) { + return []; } - if (Array.isArray(key)) { - if (!key.length) { - return setImmediate(callback, null, []); - } + if (isArray) { if (key.length > 1) { key = { $in: key }; } else { @@ -82,53 +81,49 @@ module.exports = function (db, module) { limit = 0; } - db.collection('objects').find(query, { projection: fields }) + let data = await db.collection('objects').find(query, { projection: fields }) .sort({ score: sort }) .skip(start) .limit(limit) - .toArray(function (err, data) { - if (err || !data) { - return callback(err); - } + .toArray(); - if (reverse) { - data.reverse(); - } - if (!withScores) { - data = data.map(item => item.value); - } + if (reverse) { + data.reverse(); + } + if (!withScores) { + data = data.map(item => item.value); + } - callback(null, data); - }); + return data; } - module.getSortedSetRangeByScore = function (key, start, count, min, max, callback) { - getSortedSetRangeByScore(key, start, count, min, max, 1, false, callback); + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, false); }; - module.getSortedSetRevRangeByScore = function (key, start, count, max, min, callback) { - getSortedSetRangeByScore(key, start, count, min, max, -1, false, callback); + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, false); }; - module.getSortedSetRangeByScoreWithScores = function (key, start, count, min, max, callback) { - getSortedSetRangeByScore(key, start, count, min, max, 1, true, callback); + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, true); }; - module.getSortedSetRevRangeByScoreWithScores = function (key, start, count, max, min, callback) { - getSortedSetRangeByScore(key, start, count, min, max, -1, true, callback); + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, true); }; - function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores, callback) { + async function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores) { if (parseInt(count, 10) === 0) { - return setImmediate(callback, null, []); + return []; } const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); - getSortedSetRange(key, start, stop, min, max, sort, withScores, callback); + return await getSortedSetRange(key, start, stop, min, max, sort, withScores); } - module.sortedSetCount = function (key, min, max, callback) { + module.sortedSetCount = async function (key, min, max) { if (!key) { - return callback(); + return; } var query = { _key: key }; @@ -140,334 +135,268 @@ module.exports = function (db, module) { query.score.$lte = max; } - db.collection('objects').countDocuments(query, function (err, count) { - callback(err, count || 0); - }); + const count = await db.collection('objects').countDocuments(query); + return count || 0; }; - module.sortedSetCard = function (key, callback) { + module.sortedSetCard = async function (key) { if (!key) { - return callback(null, 0); + return 0; } - db.collection('objects').countDocuments({ _key: key }, function (err, count) { - count = parseInt(count, 10); - callback(err, count || 0); - }); + const count = await db.collection('objects').countDocuments({ _key: key }); + return parseInt(count, 10) || 0; }; - module.sortedSetsCard = function (keys, callback) { + module.sortedSetsCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - async.map(keys, module.sortedSetCard, callback); + const promises = keys.map(k => module.sortedSetCard(k)); + return await Promise.all(promises); }; - module.sortedSetsCardSum = function (keys, callback) { + module.sortedSetsCardSum = async function (keys) { if (!keys || (Array.isArray(keys) && !keys.length)) { - return callback(null, 0); + return 0; } - db.collection('objects').countDocuments({ _key: Array.isArray(keys) ? { $in: keys } : keys }, function (err, count) { - count = parseInt(count, 10); - callback(err, count || 0); - }); + const count = await db.collection('objects').countDocuments({ _key: Array.isArray(keys) ? { $in: keys } : keys }); + return parseInt(count, 10) || 0; }; - module.sortedSetRank = function (key, value, callback) { - getSortedSetRank(false, key, value, callback); + module.sortedSetRank = async function (key, value) { + return await getSortedSetRank(false, key, value); }; - module.sortedSetRevRank = function (key, value, callback) { - getSortedSetRank(true, key, value, callback); + module.sortedSetRevRank = async function (key, value) { + return await getSortedSetRank(true, key, value); }; - function getSortedSetRank(reverse, key, value, callback) { + async function getSortedSetRank(reverse, key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - module.sortedSetScore(key, value, function (err, score) { - if (err || score === null) { - return callback(err, null); - } + const score = await module.sortedSetScore(key, value); + if (score === null) { + return null; + } - db.collection('objects').countDocuments({ - $or: [ - { - _key: key, - score: reverse ? { $gt: score } : { $lt: score }, - }, - { - _key: key, - score: score, - value: reverse ? { $gt: value } : { $lt: value }, - }, - ], - }, function (err, rank) { callback(err, rank); }); + return await db.collection('objects').countDocuments({ + $or: [ + { + _key: key, + score: reverse ? { $gt: score } : { $lt: score }, + }, + { + _key: key, + score: score, + value: reverse ? { $gt: value } : { $lt: value }, + }, + ], }); } - module.sortedSetsRanks = function (keys, values, callback) { - sortedSetsRanks(module.sortedSetRank, keys, values, callback); + module.sortedSetsRanks = async function (keys, values) { + return await sortedSetsRanks(module.sortedSetRank, keys, values); }; - module.sortedSetsRevRanks = function (keys, values, callback) { - sortedSetsRanks(module.sortedSetRevRank, keys, values, callback); + module.sortedSetsRevRanks = async function (keys, values) { + return await sortedSetsRanks(module.sortedSetRevRank, keys, values); }; - function sortedSetsRanks(method, keys, values, callback) { + async function sortedSetsRanks(method, keys, values) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } var data = new Array(values.length); for (var i = 0; i < values.length; i += 1) { data[i] = { key: keys[i], value: values[i] }; } - - async.map(data, function (item, next) { - method(item.key, item.value, next); - }, callback); + const promises = data.map(item => method(item.key, item.value)); + return await Promise.all(promises); } - module.sortedSetRanks = function (key, values, callback) { - sortedSetRanks(module.getSortedSetRange, key, values, callback); + module.sortedSetRanks = async function (key, values) { + return await sortedSetRanks(module.getSortedSetRange, key, values); }; - module.sortedSetRevRanks = function (key, values, callback) { - sortedSetRanks(module.getSortedSetRevRange, key, values, callback); + module.sortedSetRevRanks = async function (key, values) { + return await sortedSetRanks(module.getSortedSetRevRange, key, values); }; - function sortedSetRanks(method, key, values, callback) { - method(key, 0, -1, function (err, sortedSet) { - if (err) { - return callback(err); + async function sortedSetRanks(method, key, values) { + const sortedSet = await method(key, 0, -1); + + var result = values.map(function (value) { + if (!value) { + return null; } - - var result = values.map(function (value) { - if (!value) { - return null; - } - var index = sortedSet.indexOf(value.toString()); - return index !== -1 ? index : null; - }); - - callback(null, result); + var index = sortedSet.indexOf(value.toString()); + return index !== -1 ? index : null; }); + return result; } - module.sortedSetScore = function (key, value, callback) { + module.sortedSetScore = async function (key, value) { if (!key) { - return callback(null, null); + return null; } value = helpers.valueToString(value); - db.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, value: 0 } }, function (err, result) { - callback(err, result ? result.score : null); - }); + const result = await db.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, value: 0 } }); + return result ? result.score : null; }; - module.sortedSetsScore = function (keys, value, callback) { + module.sortedSetsScore = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } value = helpers.valueToString(value); - db.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, value: 0 } }).toArray(function (err, result) { - if (err) { - return callback(err); + const result = await db.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, value: 0 } }).toArray(); + var map = {}; + result.forEach(function (item) { + if (item) { + map[item._key] = item; } - - var map = {}; - result.forEach(function (item) { - if (item) { - map[item._key] = item; - } - }); - - result = keys.map(function (key) { - return map[key] ? map[key].score : null; - }); - - callback(null, result); }); + + return keys.map(key => (map[key] ? map[key].score : null)); }; - module.sortedSetScores = function (key, values, callback) { + module.sortedSetScores = async function (key, values) { if (!key) { - return setImmediate(callback, null, null); + return null; } if (!values.length) { - return setImmediate(callback, null, []); + return []; } values = values.map(helpers.valueToString); - db.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0 } }).toArray(function (err, result) { - if (err) { - return callback(err); + const result = await db.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0 } }).toArray(); + + var valueToScore = {}; + result.forEach(function (item) { + if (item) { + valueToScore[item.value] = item.score; } - - var map = {}; - result.forEach(function (item) { - map[item.value] = item.score; - }); - - var returnData = new Array(values.length); - var score; - - for (var i = 0; i < values.length; i += 1) { - score = map[values[i]]; - returnData[i] = utils.isNumber(score) ? score : null; - } - - callback(null, returnData); }); + + return values.map(v => (utils.isNumber(valueToScore[v]) ? valueToScore[v] : null)); }; - module.isSortedSetMember = function (key, value, callback) { + module.isSortedSetMember = async function (key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - db.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, score: 0 } }, function (err, result) { - callback(err, !!result); - }); + const result = await db.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, score: 0 } }); + return !!result; }; - module.isSortedSetMembers = function (key, values, callback) { + module.isSortedSetMembers = async function (key, values) { if (!key) { - return callback(); + return; } values = values.map(helpers.valueToString); - db.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0, score: 0 } }).toArray(function (err, results) { - if (err) { - return callback(err); - } - var isMember = {}; - results.forEach(function (item) { - if (item) { - isMember[item.value] = true; - } - }); + const results = await db.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0, score: 0 } }).toArray(); - values = values.map(function (value) { - return !!isMember[value]; - }); - callback(null, values); + var isMember = {}; + results.forEach(function (item) { + if (item) { + isMember[item.value] = true; + } }); + + return values.map(value => !!isMember[value]); }; - module.isMemberOfSortedSets = function (keys, value, callback) { + module.isMemberOfSortedSets = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } value = helpers.valueToString(value); - db.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, score: 0 } }).toArray(function (err, results) { - if (err) { - return callback(err); - } - var isMember = {}; - results.forEach(function (item) { - if (item) { - isMember[item._key] = true; - } - }); + const results = await db.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, score: 0 } }).toArray(); - results = keys.map(function (key) { - return !!isMember[key]; - }); - callback(null, results); + var isMember = {}; + results.forEach(function (item) { + if (item) { + isMember[item._key] = true; + } }); + + return keys.map(key => !!isMember[key]); }; - module.getSortedSetsMembers = function (keys, callback) { + module.getSortedSetsMembers = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } - db.collection('objects').find({ _key: { $in: keys } }, { projection: { _id: 0, score: 0 } }).sort({ score: 1 }).toArray(function (err, data) { - if (err) { - return callback(err); - } + const data = await db.collection('objects').find({ _key: { $in: keys } }, { projection: { _id: 0, score: 0 } }).sort({ score: 1 }).toArray(); - var sets = {}; - data.forEach(function (set) { - sets[set._key] = sets[set._key] || []; - sets[set._key].push(set.value); - }); - - var returnData = new Array(keys.length); - for (var i = 0; i < keys.length; i += 1) { - returnData[i] = sets[keys[i]] || []; - } - callback(null, returnData); + var sets = {}; + data.forEach(function (set) { + sets[set._key] = sets[set._key] || []; + sets[set._key].push(set.value); }); + + return keys.map(k => sets[k] || []); }; - module.sortedSetIncrBy = function (key, increment, value, callback) { - callback = callback || helpers.noop; + module.sortedSetIncrBy = async function (key, increment, value) { if (!key) { - return callback(); + return; } var data = {}; value = helpers.valueToString(value); data.score = parseFloat(increment); - db.collection('objects').findOneAndUpdate({ _key: key, value: value }, { $inc: data }, { returnOriginal: false, upsert: true }, function (err, result) { + try { + const result = await db.collection('objects').findOneAndUpdate({ _key: key, value: value }, { $inc: data }, { returnOriginal: false, upsert: true }); + return result && result.value ? result.value.score : null; + } catch (err) { // if there is duplicate key error retry the upsert // https://github.com/NodeBB/NodeBB/issues/4467 // https://jira.mongodb.org/browse/SERVER-14322 // https://docs.mongodb.org/manual/reference/command/findAndModify/#upsert-and-unique-index if (err && err.message.startsWith('E11000 duplicate key error')) { - return process.nextTick(module.sortedSetIncrBy, key, increment, value, callback); + return await module.sortedSetIncrBy(key, increment, value); } - callback(err, result && result.value ? result.value.score : null); - }); - }; - - module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { - sortedSetLex(key, min, max, 1, start, count, callback); - }; - - module.getSortedSetRevRangeByLex = function (key, max, min, start, count, callback) { - sortedSetLex(key, min, max, -1, start, count, callback); - }; - - module.sortedSetLexCount = function (key, min, max, callback) { - sortedSetLex(key, min, max, 1, 0, 0, function (err, data) { - callback(err, data ? data.length : null); - }); - }; - - function sortedSetLex(key, min, max, sort, start, count, callback) { - if (!callback) { - callback = start; - start = 0; - count = 0; + throw err; } + }; + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex(key, min, max, 1, start, count); + }; + + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex(key, min, max, -1, start, count); + }; + + module.sortedSetLexCount = async function (key, min, max) { + const data = await sortedSetLex(key, min, max, 1, 0, 0); + return data ? data.length : null; + }; + + async function sortedSetLex(key, min, max, sort, start, count) { var query = { _key: key }; + start = start !== undefined ? start : 0; + count = count !== undefined ? count : 0; buildLexQuery(query, min, max); - db.collection('objects').find(query, { projection: { _id: 0, _key: 0, score: 0 } }) + const data = await db.collection('objects').find(query, { projection: { _id: 0, _key: 0, score: 0 } }) .sort({ value: sort }) .skip(start) .limit(count === -1 ? 0 : count) - .toArray(function (err, data) { - if (err) { - return callback(err); - } - data = data.map(function (item) { - return item && item.value; - }); - callback(err, data); - }); + .toArray(); + + return data.map(item => item && item.value); } - module.sortedSetRemoveRangeByLex = function (key, min, max, callback) { - callback = callback || helpers.noop; - + module.sortedSetRemoveRangeByLex = async function (key, min, max) { var query = { _key: key }; buildLexQuery(query, min, max); - db.collection('objects').deleteMany(query, function (err) { - callback(err); - }); + await db.collection('objects').deleteMany(query); }; function buildLexQuery(query, min, max) { @@ -492,51 +421,39 @@ module.exports = function (db, module) { } } - module.processSortedSet = function (setKey, processFn, options, callback) { + module.processSortedSet = async function (setKey, processFn, options) { var done = false; var ids = []; var project = { _id: 0, _key: 0 }; + if (!options.withScores) { project.score = 0; } - var cursor = db.collection('objects').find({ _key: setKey }, { projection: project }) + var cursor = await db.collection('objects').find({ _key: setKey }, { projection: project }) .sort({ score: 1 }) .batchSize(options.batch); - async.whilst( - function (next) { - next(null, !done); - }, - function (next) { - async.waterfall([ - function (next) { - cursor.next(next); - }, - function (item, _next) { - if (item === null) { - done = true; - } else { - ids.push(options.withScores ? item : item.value); - } + if (processFn && processFn.constructor && processFn.constructor.name !== 'AsyncFunction') { + processFn = util.promisify(processFn); + } - if (ids.length < options.batch && (!done || ids.length === 0)) { - return process.nextTick(next, null); - } - processFn(ids, function (err) { - _next(err); - }); - }, - function (next) { - ids = []; - if (options.interval) { - setTimeout(next, options.interval); - } else { - process.nextTick(next); - } - }, - ], next); - }, - callback - ); + while (!done) { + /* eslint-disable no-await-in-loop */ + const item = await cursor.next(); + if (item === null) { + done = true; + } else { + ids.push(options.withScores ? item : item.value); + } + + if (ids.length >= options.batch || (done && ids.length !== 0)) { + await processFn(ids); + + ids.length = 0; + if (options.interval) { + await sleep(options.interval); + } + } + } }; }; diff --git a/src/database/mongo/sorted/add.js b/src/database/mongo/sorted/add.js index 61c03602f6..ec7401ffaf 100644 --- a/src/database/mongo/sorted/add.js +++ b/src/database/mongo/sorted/add.js @@ -1,88 +1,82 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.mongo; + var helpers = require('../helpers'); var utils = require('../../../utils'); - module.sortedSetAdd = function (key, score, value, callback) { - callback = callback || helpers.noop; + module.sortedSetAdd = async function (key, score, value) { if (!key) { - return callback(); + return; } if (Array.isArray(score) && Array.isArray(value)) { - return sortedSetAddBulk(key, score, value, callback); + return await sortedSetAddBulk(key, score, value); } if (!utils.isNumber(score)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + score + ']]')); + throw new Error('[[error:invalid-score, ' + score + ']]'); } value = helpers.valueToString(value); - db.collection('objects').updateOne({ _key: key, value: value }, { $set: { score: parseFloat(score) } }, { upsert: true, w: 1 }, function (err) { + try { + await db.collection('objects').updateOne({ _key: key, value: value }, { $set: { score: parseFloat(score) } }, { upsert: true, w: 1 }); + } catch (err) { if (err && err.message.startsWith('E11000 duplicate key error')) { - return process.nextTick(module.sortedSetAdd, key, score, value, callback); + return await module.sortedSetAdd(key, score, value); } - callback(err); - }); + throw err; + } }; - function sortedSetAddBulk(key, scores, values, callback) { + async function sortedSetAddBulk(key, scores, values) { if (!scores.length || !values.length) { - return callback(); + return; } if (scores.length !== values.length) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } for (let i = 0; i < scores.length; i += 1) { if (!utils.isNumber(scores[i])) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores[i] + ']]')); + throw new Error('[[error:invalid-score, ' + scores[i] + ']]'); } } values = values.map(helpers.valueToString); var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for (var i = 0; i < scores.length; i += 1) { bulk.find({ _key: key, value: values[i] }).upsert().updateOne({ $set: { score: parseFloat(scores[i]) } }); } - - bulk.execute(function (err) { - callback(err); - }); + await bulk.execute(); } - module.sortedSetsAdd = function (keys, scores, value, callback) { - callback = callback || helpers.noop; + module.sortedSetsAdd = async function (keys, scores, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback); + return; } const isArrayOfScores = Array.isArray(scores); if (!isArrayOfScores && !utils.isNumber(scores)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores + ']]')); + throw new Error('[[error:invalid-score, ' + scores + ']]'); } if (isArrayOfScores && scores.length !== keys.length) { - return setImmediate(callback, new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } value = helpers.valueToString(value); var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for (var i = 0; i < keys.length; i += 1) { bulk.find({ _key: keys[i], value: value }).upsert().updateOne({ $set: { score: parseFloat(isArrayOfScores ? scores[i] : scores) } }); } - - bulk.execute(err => callback(err)); + await bulk.execute(); }; - module.sortedSetAddBulk = function (data, callback) { + module.sortedSetAddBulk = async function (data) { if (!Array.isArray(data) || !data.length) { - return setImmediate(callback); + return; } var bulk = db.collection('objects').initializeUnorderedBulkOp(); data.forEach(function (item) { bulk.find({ _key: item[0], value: String(item[2]) }).upsert().updateOne({ $set: { score: parseFloat(item[1]) } }); }); - bulk.execute(err => callback(err)); + await bulk.execute(); }; }; diff --git a/src/database/mongo/sorted/intersect.js b/src/database/mongo/sorted/intersect.js index ddb39e9591..fa8f55589b 100644 --- a/src/database/mongo/sorted/intersect.js +++ b/src/database/mongo/sorted/intersect.js @@ -1,9 +1,9 @@ 'use strict'; module.exports = function (db, module) { - module.sortedSetIntersectCard = function (keys, callback) { + module.sortedSetIntersectCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, 0); + return 0; } var pipeline = [ @@ -13,23 +13,22 @@ module.exports = function (db, module) { { $group: { _id: null, count: { $sum: 1 } } }, ]; - db.collection('objects').aggregate(pipeline).toArray(function (err, data) { - callback(err, Array.isArray(data) && data.length ? data[0].count : 0); - }); + const data = await db.collection('objects').aggregate(pipeline).toArray(); + return Array.isArray(data) && data.length ? data[0].count : 0; }; - module.getSortedSetIntersect = function (params, callback) { + module.getSortedSetIntersect = async function (params) { params.sort = 1; - getSortedSetRevIntersect(params, callback); + return await getSortedSetRevIntersect(params); }; - module.getSortedSetRevIntersect = function (params, callback) { + module.getSortedSetRevIntersect = async function (params) { params.sort = -1; - getSortedSetRevIntersect(params, callback); + return await getSortedSetRevIntersect(params); }; - function getSortedSetRevIntersect(params, callback) { + async function getSortedSetRevIntersect(params) { var sets = params.sets; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = params.hasOwnProperty('stop') ? params.stop : -1; @@ -88,18 +87,11 @@ module.exports = function (db, module) { } pipeline.push({ $project: project }); - db.collection('objects').aggregate(pipeline).toArray(function (err, data) { - if (err || !data) { - return callback(err); - } + let data = await db.collection('objects').aggregate(pipeline).toArray(); - if (!params.withScores) { - data = data.map(function (item) { - return item.value; - }); - } - - callback(null, data); - }); + if (!params.withScores) { + data = data.map(item => item.value); + } + return data; } }; diff --git a/src/database/mongo/sorted/remove.js b/src/database/mongo/sorted/remove.js index d8ca5af2cb..2803f2fcd2 100644 --- a/src/database/mongo/sorted/remove.js +++ b/src/database/mongo/sorted/remove.js @@ -1,45 +1,41 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.mongo; + var helpers = require('../helpers'); - module.sortedSetRemove = function (key, value, callback) { - function done(err) { - callback(err); - } - callback = callback || helpers.noop; + module.sortedSetRemove = async function (key, value) { if (!key) { - return callback(); + return; + } + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; } - if (Array.isArray(value)) { + if (isValueArray) { value = value.map(helpers.valueToString); } else { value = helpers.valueToString(value); } - db.collection('objects').deleteMany({ + await db.collection('objects').deleteMany({ _key: Array.isArray(key) ? { $in: key } : key, - value: Array.isArray(value) ? { $in: value } : value, - }, done); - }; - - module.sortedSetsRemove = function (keys, value, callback) { - callback = callback || helpers.noop; - if (!Array.isArray(keys) || !keys.length) { - return callback(); - } - value = helpers.valueToString(value); - - db.collection('objects').deleteMany({ _key: { $in: keys }, value: value }, function (err) { - callback(err); + value: isValueArray ? { $in: value } : value, }); }; - module.sortedSetsRemoveRangeByScore = function (keys, min, max, callback) { - callback = callback || helpers.noop; + module.sortedSetsRemove = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; + } + value = helpers.valueToString(value); + + await db.collection('objects').deleteMany({ _key: { $in: keys }, value: value }); + }; + + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { + if (!Array.isArray(keys) || !keys.length) { + return; } var query = { _key: { $in: keys } }; @@ -51,8 +47,6 @@ module.exports = function (db, module) { query.score.$lte = parseFloat(max); } - db.collection('objects').deleteMany(query, function (err) { - callback(err); - }); + await db.collection('objects').deleteMany(query); }; }; diff --git a/src/database/mongo/sorted/union.js b/src/database/mongo/sorted/union.js index 5e350c1266..0b4607399d 100644 --- a/src/database/mongo/sorted/union.js +++ b/src/database/mongo/sorted/union.js @@ -1,34 +1,33 @@ 'use strict'; module.exports = function (db, module) { - module.sortedSetUnionCard = function (keys, callback) { + module.sortedSetUnionCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, 0); + return 0; } - db.collection('objects').aggregate([ + const data = await db.collection('objects').aggregate([ { $match: { _key: { $in: keys } } }, { $group: { _id: { value: '$value' } } }, { $group: { _id: null, count: { $sum: 1 } } }, { $project: { _id: 0, count: '$count' } }, - ]).toArray(function (err, data) { - callback(err, Array.isArray(data) && data.length ? data[0].count : 0); - }); + ]).toArray(); + return Array.isArray(data) && data.length ? data[0].count : 0; }; - module.getSortedSetUnion = function (params, callback) { + module.getSortedSetUnion = async function (params) { params.sort = 1; - getSortedSetUnion(params, callback); + return await getSortedSetUnion(params); }; - module.getSortedSetRevUnion = function (params, callback) { + module.getSortedSetRevUnion = async function (params) { params.sort = -1; - getSortedSetUnion(params, callback); + return await getSortedSetUnion(params); }; - function getSortedSetUnion(params, callback) { + async function getSortedSetUnion(params) { if (!Array.isArray(params.sets) || !params.sets.length) { - return callback(); + return; } var limit = params.stop - params.start + 1; if (limit <= 0) { @@ -62,18 +61,10 @@ module.exports = function (db, module) { } pipeline.push({ $project: project }); - db.collection('objects').aggregate(pipeline).toArray(function (err, data) { - if (err || !data) { - return callback(err); - } - - if (!params.withScores) { - data = data.map(function (item) { - return item.value; - }); - } - - callback(null, data); - }); + let data = await db.collection('objects').aggregate(pipeline).toArray(); + if (!params.withScores) { + data = data.map(item => item.value); + } + return data; } }; diff --git a/src/database/postgres.js b/src/database/postgres.js index d7358875ec..d0c2550b50 100644 --- a/src/database/postgres.js +++ b/src/database/postgres.js @@ -6,8 +6,6 @@ var nconf = require('nconf'); var session = require('express-session'); var _ = require('lodash'); var semver = require('semver'); -var dbNamespace = require('continuation-local-storage').createNamespace('postgres'); - var postgresModule = module.exports; @@ -41,9 +39,6 @@ postgresModule.questions = [ }, ]; -postgresModule.helpers = postgresModule.helpers || {}; -postgresModule.helpers.postgres = require('./postgres/helpers'); - postgresModule.getConnectionOptions = function (postgres) { postgres = postgres || nconf.get('postgres'); // Sensible defaults for PostgreSQL, if not set @@ -79,17 +74,6 @@ postgresModule.init = function (callback) { const db = new Pool(connOptions); - db.on('connect', function (client) { - var realQuery = client.query; - client.query = function () { - var args = Array.prototype.slice.call(arguments, 0); - if (dbNamespace.active && typeof args[args.length - 1] === 'function') { - args[args.length - 1] = dbNamespace.bind(args[args.length - 1]); - } - return realQuery.apply(client, args); - }; - }); - db.connect(function (err, client, release) { if (err) { winston.error('NodeBB could not connect to your PostgreSQL database. PostgreSQL returned the following error: ' + err.message); @@ -99,7 +83,7 @@ postgresModule.init = function (callback) { postgresModule.pool = db; Object.defineProperty(postgresModule, 'client', { get: function () { - return (dbNamespace.active && dbNamespace.get('db')) || db; + return db; }, configurable: true, }); @@ -124,10 +108,9 @@ postgresModule.init = function (callback) { require('./postgres/sets')(wrappedDB, postgresModule); require('./postgres/sorted')(wrappedDB, postgresModule); require('./postgres/list')(wrappedDB, postgresModule); - require('./postgres/transaction')(db, dbNamespace, postgresModule); - - postgresModule.async = require('../promisify')(postgresModule, ['client', 'sessionStore', 'pool']); + require('./postgres/transaction')(db, postgresModule); + postgresModule.async = require('../promisify')(postgresModule, ['client', 'sessionStore', 'pool', 'transaction']); callback(); }); }); @@ -140,17 +123,6 @@ postgresModule.connect = function (options, callback) { const db = new Pool(connOptions); - db.on('connect', function (client) { - var realQuery = client.query; - client.query = function () { - var args = Array.prototype.slice.call(arguments, 0); - if (dbNamespace.active && typeof args[args.length - 1] === 'function') { - args[args.length - 1] = dbNamespace.bind(args[args.length - 1]); - } - return realQuery.apply(client, args); - }; - }); - db.connect(function (err) { callback(err, db); }); diff --git a/src/database/postgres/hash.js b/src/database/postgres/hash.js index 969104c84b..1a6c4aecaa 100644 --- a/src/database/postgres/hash.js +++ b/src/database/postgres/hash.js @@ -1,74 +1,59 @@ 'use strict'; -var async = require('async'); - module.exports = function (db, module) { - var helpers = module.helpers.postgres; - - module.setObject = function (key, data, callback) { - callback = callback || helpers.noop; + var helpers = require('./helpers'); + module.setObject = async function (key, data) { if (!key || !data) { - return callback(); + return; } if (data.hasOwnProperty('')) { delete data['']; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); + await module.transaction(async function (client) { + var query = client.query.bind(client); - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'hash'), - async.apply(query, { - name: 'setObject', - text: ` + await helpers.ensureLegacyObjectType(client, key, 'hash'); + await query({ + name: 'setObject', + text: ` INSERT INTO "legacy_hash" ("_key", "data") VALUES ($1::TEXT, $2::TEXT::JSONB) - ON CONFLICT ("_key") - DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`, - values: [key, JSON.stringify(data)], - }), - ], function (err) { - done(err); +ON CONFLICT ("_key") +DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`, + values: [key, JSON.stringify(data)], }); - }, callback); + }); }; - module.setObjectField = function (key, field, value, callback) { - callback = callback || helpers.noop; - + module.setObjectField = async function (key, field, value) { if (!field) { - return callback(); + return; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'hash'), - async.apply(query, { - name: 'setObjectField', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'hash'); + await query({ + name: 'setObjectField', + text: ` INSERT INTO "legacy_hash" ("_key", "data") VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::TEXT::JSONB)) - ON CONFLICT ("_key") - DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], $3::TEXT::JSONB)`, - values: [key, field, JSON.stringify(value)], - }), - ], function (err) { - done(err); +ON CONFLICT ("_key") +DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], $3::TEXT::JSONB)`, + values: [key, field, JSON.stringify(value)], }); - }, callback); + }); }; - module.getObject = function (key, callback) { + module.getObject = async function (key) { if (!key) { - return callback(null, null); + return null; } - db.query({ + const res = await db.query({ name: 'getObject', text: ` SELECT h."data" @@ -79,25 +64,17 @@ SELECT h."data" WHERE o."_key" = $1::TEXT LIMIT 1`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].data); - } - - callback(null, null); }); + + return res.rows.length ? res.rows[0].data : null; }; - module.getObjects = function (keys, callback) { + module.getObjects = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - db.query({ + const res = await db.query({ name: 'getObjects', text: ` SELECT h."data" @@ -109,23 +86,17 @@ SELECT h."data" AND o."type" = h."type" ORDER BY k.i ASC`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows.map(function (row) { - return row.data; - })); }); + + return res.rows.map(row => row.data); }; - module.getObjectField = function (key, field, callback) { + module.getObjectField = async function (key, field) { if (!key) { - return setImmediate(callback, null, null); + return null; } - db.query({ + const res = await db.query({ name: 'getObjectField', text: ` SELECT h."data"->>$2::TEXT f @@ -136,25 +107,17 @@ SELECT h."data"->>$2::TEXT f WHERE o."_key" = $1::TEXT LIMIT 1`, values: [key, field], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].f); - } - - callback(null, null); }); + + return res.rows.length ? res.rows[0].f : null; }; - module.getObjectFields = function (key, fields, callback) { + module.getObjectFields = async function (key, fields) { if (!key) { - return setImmediate(callback, null, null); + return null; } - db.query({ + const res = await db.query({ name: 'getObjectFields', text: ` SELECT (SELECT jsonb_object_agg(f, d."value") @@ -167,30 +130,26 @@ SELECT (SELECT jsonb_object_agg(f, d."value") AND o."type" = h."type" WHERE o."_key" = $1::TEXT`, values: [key, fields], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].d); - } - - var obj = {}; - fields.forEach(function (f) { - obj[f] = null; - }); - - callback(null, obj); }); - }; - module.getObjectsFields = function (keys, fields, callback) { - if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + if (res.rows.length) { + return res.rows[0].d; } - db.query({ + var obj = {}; + fields.forEach(function (f) { + obj[f] = null; + }); + + return obj; + }; + + module.getObjectsFields = async function (keys, fields) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + const res = await db.query({ name: 'getObjectsFields', text: ` SELECT (SELECT jsonb_object_agg(f, d."value") @@ -205,23 +164,17 @@ SELECT (SELECT jsonb_object_agg(f, d."value") AND o."type" = h."type" ORDER BY k.i ASC`, values: [keys, fields], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows.map(function (row) { - return row.d; - })); }); + + return res.rows.map(row => row.d); }; - module.getObjectKeys = function (key, callback) { + module.getObjectKeys = async function (key) { if (!key) { - return callback(); + return; } - db.query({ + const res = await db.query({ name: 'getObjectKeys', text: ` SELECT ARRAY(SELECT jsonb_object_keys(h."data")) k @@ -232,45 +185,22 @@ SELECT ARRAY(SELECT jsonb_object_keys(h."data")) k WHERE o."_key" = $1::TEXT LIMIT 1`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].k); - } - - callback(null, []); }); + + return res.rows.length ? res.rows[0].k : []; }; - module.getObjectValues = function (key, callback) { - module.getObject(key, function (err, data) { - if (err) { - return callback(err); - } - - var values = []; - - if (data) { - for (var key in data) { - if (data.hasOwnProperty(key)) { - values.push(data[key]); - } - } - } - - callback(null, values); - }); + module.getObjectValues = async function (key) { + const data = await module.getObject(key); + return data ? Object.values(data) : []; }; - module.isObjectField = function (key, field, callback) { + module.isObjectField = async function (key, field) { if (!key) { - return callback(); + return; } - db.query({ + const res = await db.query({ name: 'isObjectField', text: ` SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b @@ -281,52 +211,33 @@ SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b WHERE o."_key" = $1::TEXT LIMIT 1`, values: [key, field], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].b); - } - - callback(null, false); }); + + return res.rows.length ? res.rows[0].b : false; }; - module.isObjectFields = function (key, fields, callback) { + module.isObjectFields = async function (key, fields) { if (!key) { - return callback(); + return; } - module.getObjectFields(key, fields, function (err, data) { - if (err) { - return callback(err); - } - - if (!data) { - return callback(null, fields.map(function () { - return false; - })); - } - - callback(null, fields.map(function (field) { - return data.hasOwnProperty(field) && data[field] !== null; - })); - }); + const data = await module.getObjectFields(key, fields); + if (!data) { + return fields.map(() => false); + } + return fields.map(field => data.hasOwnProperty(field) && data[field] !== null); }; - module.deleteObjectField = function (key, field, callback) { - module.deleteObjectFields(key, [field], callback); + module.deleteObjectField = async function (key, field) { + await module.deleteObjectFields(key, [field]); }; - module.deleteObjectFields = function (key, fields, callback) { - callback = callback || helpers.noop; + module.deleteObjectFields = async function (key, fields) { if (!key || !Array.isArray(fields) || !fields.length) { - return callback(); + return; } - db.query({ + await db.query({ name: 'deleteObjectFields', text: ` UPDATE "legacy_hash" @@ -335,57 +246,52 @@ UPDATE "legacy_hash" WHERE "key" <> ALL ($2::TEXT[])), '{}') WHERE "_key" = $1::TEXT`, values: [key, fields], - }, function (err) { - callback(err); }); }; - module.incrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, 1, callback); + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); }; - module.decrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, -1, callback); + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); }; - module.incrObjectFieldBy = function (key, field, value, callback) { - callback = callback || helpers.noop; + module.incrObjectFieldBy = async function (key, field, value) { value = parseInt(value, 10); if (!key || isNaN(value)) { - return callback(null, null); + return null; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); + return await module.transaction(async function (client) { + var query = client.query.bind(client); + if (Array.isArray(key)) { + await helpers.ensureLegacyObjectsType(client, key, 'hash'); + } else { + await helpers.ensureLegacyObjectType(client, key, 'hash'); + } - async.waterfall([ - async.apply(Array.isArray(key) ? helpers.ensureLegacyObjectsType : helpers.ensureLegacyObjectType, tx.client, key, 'hash'), - async.apply(query, Array.isArray(key) ? { - name: 'incrObjectFieldByMulti', - text: ` + const res = await query(Array.isArray(key) ? { + name: 'incrObjectFieldByMulti', + text: ` INSERT INTO "legacy_hash" ("_key", "data") SELECT UNNEST($1::TEXT[]), jsonb_build_object($2::TEXT, $3::NUMERIC) - ON CONFLICT ("_key") - DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) +ON CONFLICT ("_key") +DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) RETURNING ("data"->>$2::TEXT)::NUMERIC v`, - values: [key, field, value], - } : { - name: 'incrObjectFieldBy', - text: ` + values: [key, field, value], + } : { + name: 'incrObjectFieldBy', + text: ` INSERT INTO "legacy_hash" ("_key", "data") VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::NUMERIC)) - ON CONFLICT ("_key") - DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) +ON CONFLICT ("_key") +DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) RETURNING ("data"->>$2::TEXT)::NUMERIC v`, - values: [key, field, value], - }), - function (res, next) { - next(null, Array.isArray(key) ? res.rows.map(function (r) { - return parseFloat(r.v); - }) : parseFloat(res.rows[0].v)); - }, - ], done); - }, callback); + values: [key, field, value], + }); + return Array.isArray(key) ? res.rows.map(r => parseFloat(r.v)) : parseFloat(res.rows[0].v); + }); }; }; diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 61965ba588..3a6e583915 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -1,6 +1,6 @@ 'use strict'; -var helpers = {}; +var helpers = module.exports; helpers.valueToString = function (value) { return String(value); @@ -19,117 +19,82 @@ helpers.removeDuplicateValues = function (values) { } }; -helpers.ensureLegacyObjectType = function (db, key, type, callback) { - db.query({ +helpers.ensureLegacyObjectType = async function (db, key, type) { + await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` DELETE FROM "legacy_object" WHERE "expireAt" IS NOT NULL AND "expireAt" <= CURRENT_TIMESTAMP`, - }, function (err) { - if (err) { - return callback(err); - } + }); - db.query({ - name: 'ensureLegacyObjectType1', - text: ` + await db.query({ + name: 'ensureLegacyObjectType1', + text: ` INSERT INTO "legacy_object" ("_key", "type") VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) ON CONFLICT DO NOTHING`, - values: [key, type], - }, function (err) { - if (err) { - return callback(err); - } + values: [key, type], + }); - db.query({ - name: 'ensureLegacyObjectType2', - text: ` + const res = await db.query({ + name: 'ensureLegacyObjectType2', + text: ` SELECT "type" FROM "legacy_object_live" WHERE "_key" = $1::TEXT`, - values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows[0].type !== type) { - return callback(new Error('database: cannot insert ' + JSON.stringify(key) + ' as ' + type + ' because it already exists as ' + res.rows[0].type)); - } - - callback(null); - }); - }); + values: [key], }); + + if (res.rows[0].type !== type) { + throw new Error('database: cannot insert ' + JSON.stringify(key) + ' as ' + type + ' because it already exists as ' + res.rows[0].type); + } }; -helpers.ensureLegacyObjectsType = function (db, keys, type, callback) { - db.query({ +helpers.ensureLegacyObjectsType = async function (db, keys, type) { + await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` DELETE FROM "legacy_object" WHERE "expireAt" IS NOT NULL AND "expireAt" <= CURRENT_TIMESTAMP`, - }, function (err) { - if (err) { - return callback(err); - } + }); - db.query({ - name: 'ensureLegacyObjectsType1', - text: ` + await db.query({ + name: 'ensureLegacyObjectsType1', + text: ` INSERT INTO "legacy_object" ("_key", "type") SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE FROM UNNEST($1::TEXT[]) k ON CONFLICT DO NOTHING`, - values: [keys, type], - }, function (err) { - if (err) { - return callback(err); - } + values: [keys, type], + }); - db.query({ - name: 'ensureLegacyObjectsType2', - text: ` + const res = await db.query({ + name: 'ensureLegacyObjectsType2', + text: ` SELECT "_key", "type" FROM "legacy_object_live" WHERE "_key" = ANY($1::TEXT[])`, - values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } - - var invalid = res.rows.filter(function (r) { - return r.type !== type; - }); - - if (invalid.length) { - return callback(new Error('database: cannot insert multiple objects as ' + type + ' because they already exist: ' + invalid.map(function (r) { - return JSON.stringify(r._key) + ' is ' + r.type; - }).join(', '))); - } - - var missing = keys.filter(function (k) { - return !res.rows.some(function (r) { - return r._key === k; - }); - }); - - if (missing.length) { - return callback(new Error('database: failed to insert keys for objects: ' + JSON.stringify(missing))); - } - - callback(null); - }); - }); + values: [keys], }); + + var invalid = res.rows.filter(r => r.type !== type); + + if (invalid.length) { + const parts = invalid.map(r => JSON.stringify(r._key) + ' is ' + r.type); + throw new Error('database: cannot insert multiple objects as ' + type + ' because they already exist: ' + parts.join(', ')); + } + + var missing = keys.filter(function (k) { + return !res.rows.some(r => r._key === k); + }); + + if (missing.length) { + throw new Error('database: failed to insert keys for objects: ' + JSON.stringify(missing)); + } }; helpers.noop = function () {}; - -module.exports = helpers; diff --git a/src/database/postgres/list.js b/src/database/postgres/list.js index 9d11e16dff..4b0bc8bba2 100644 --- a/src/database/postgres/list.js +++ b/src/database/postgres/list.js @@ -1,72 +1,54 @@ 'use strict'; -var async = require('async'); - module.exports = function (db, module) { - var helpers = module.helpers.postgres; - - module.listPrepend = function (key, value, callback) { - callback = callback || helpers.noop; + var helpers = require('./helpers'); + module.listPrepend = async function (key, value) { if (!key) { - return callback(); + return; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'list'), - async.apply(query, { - name: 'listPrepend', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'list'); + await query({ + name: 'listPrepend', + text: ` INSERT INTO "legacy_list" ("_key", "array") VALUES ($1::TEXT, ARRAY[$2::TEXT]) - ON CONFLICT ("_key") - DO UPDATE SET "array" = ARRAY[$2::TEXT] || "legacy_list"."array"`, - values: [key, value], - }), - ], function (err) { - done(err); +ON CONFLICT ("_key") +DO UPDATE SET "array" = ARRAY[$2::TEXT] || "legacy_list"."array"`, + values: [key, value], }); - }, callback); + }); }; - module.listAppend = function (key, value, callback) { - callback = callback || helpers.noop; - + module.listAppend = async function (key, value) { if (!key) { - return callback(); + return; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'list'), - async.apply(query, { - name: 'listAppend', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'list'); + await query({ + name: 'listAppend', + text: ` INSERT INTO "legacy_list" ("_key", "array") VALUES ($1::TEXT, ARRAY[$2::TEXT]) - ON CONFLICT ("_key") - DO UPDATE SET "array" = "legacy_list"."array" || ARRAY[$2::TEXT]`, - values: [key, value], - }), - ], function (err) { - done(err); +ON CONFLICT ("_key") +DO UPDATE SET "array" = "legacy_list"."array" || ARRAY[$2::TEXT]`, + values: [key, value], }); - }, callback || helpers.noop); + }); }; - module.listRemoveLast = function (key, callback) { - callback = callback || helpers.noop; - + module.listRemoveLast = async function (key) { if (!key) { - return callback(); + return; } - db.query({ + const res = await db.query({ name: 'listRemoveLast', text: ` WITH A AS ( @@ -83,27 +65,17 @@ UPDATE "legacy_list" l WHERE A."_key" = l."_key" RETURNING A."array"[array_length(A."array", 1)] v`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].v); - } - - callback(null, null); }); + + return res.rows.length ? res.rows[0].v : null; }; - module.listRemoveAll = function (key, value, callback) { - callback = callback || helpers.noop; - + module.listRemoveAll = async function (key, value) { if (!key) { - return callback(); + return; } - db.query({ + await db.query({ name: 'listRemoveAll', text: ` UPDATE "legacy_list" l @@ -113,21 +85,17 @@ UPDATE "legacy_list" l AND o."type" = l."type" AND o."_key" = $1::TEXT`, values: [key, value], - }, function (err) { - callback(err); }); }; - module.listTrim = function (key, start, stop, callback) { - callback = callback || helpers.noop; - + module.listTrim = async function (key, start, stop) { if (!key) { - return callback(); + return; } stop += 1; - db.query(stop > 0 ? { + await db.query(stop > 0 ? { name: 'listTrim', text: ` UPDATE "legacy_list" l @@ -155,19 +123,17 @@ UPDATE "legacy_list" l AND o."type" = l."type" AND o."_key" = $1::TEXT`, values: [key, start, stop], - }, function (err) { - callback(err); }); }; - module.getListRange = function (key, start, stop, callback) { + module.getListRange = async function (key, start, stop) { if (!key) { - return callback(); + return; } stop += 1; - db.query(stop > 0 ? { + const res = await db.query(stop > 0 ? { name: 'getListRange', text: ` SELECT ARRAY(SELECT m.m @@ -195,21 +161,13 @@ SELECT ARRAY(SELECT m.m AND o."type" = l."type" WHERE o."_key" = $1::TEXT`, values: [key, start, stop], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].l); - } - - callback(null, []); }); + + return res.rows.length ? res.rows[0].l : []; }; - module.listLength = function (key, callback) { - db.query({ + module.listLength = async function (key) { + const res = await db.query({ name: 'listLength', text: ` SELECT array_length(l."array", 1) l @@ -219,16 +177,8 @@ SELECT array_length(l."array", 1) l AND o."type" = l."type" WHERE o."_key" = $1::TEXT`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].l); - } - - callback(null, 0); }); + + return res.rows.length ? res.rows[0].l : 0; }; }; diff --git a/src/database/postgres/main.js b/src/database/postgres/main.js index cfa724ed1d..f34aa1ad32 100644 --- a/src/database/postgres/main.js +++ b/src/database/postgres/main.js @@ -1,114 +1,83 @@ 'use strict'; -var async = require('async'); - module.exports = function (db, module) { - var helpers = module.helpers.postgres; + var helpers = require('./helpers'); var query = db.query.bind(db); - module.flushdb = function (callback) { - callback = callback || helpers.noop; - - async.series([ - async.apply(query, `DROP SCHEMA "public" CASCADE`), - async.apply(query, `CREATE SCHEMA "public"`), - ], function (err) { - callback(err); - }); + module.flushdb = async function () { + await query(`DROP SCHEMA "public" CASCADE`); + await query(`CREATE SCHEMA "public"`); }; - module.emptydb = function (callback) { - callback = callback || helpers.noop; - query(`DELETE FROM "legacy_object"`, function (err) { - callback(err); - }); + module.emptydb = async function () { + await query(`DELETE FROM "legacy_object"`); }; - module.exists = function (key, callback) { + module.exists = async function (key) { if (!key) { - return callback(); + return; } if (Array.isArray(key)) { - query({ + const res = await query({ name: 'existsArray', text: ` SELECT o."_key" k FROM "legacy_object_live" o WHERE o."_key" = ANY($1::TEXT[])`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, key.map(function (k) { - return res.rows.some(function (r) { - return r.k === k; - }); - })); }); - } else { - query({ - name: 'exists', - text: ` - SELECT EXISTS(SELECT * + return key.map(function (k) { + return res.rows.some(r => r.k === k); + }); + } + const res = await query({ + name: 'exists', + text: ` + SELECT EXISTS(SELECT * FROM "legacy_object_live" WHERE "_key" = $1::TEXT LIMIT 1) e`, - values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows[0].e); - }); - } + values: [key], + }); + return res.rows[0].e; }; - module.delete = function (key, callback) { - callback = callback || helpers.noop; + module.delete = async function (key) { if (!key) { - return callback(); + return; } - query({ + await query({ name: 'delete', text: ` DELETE FROM "legacy_object" WHERE "_key" = $1::TEXT`, values: [key], - }, function (err) { - callback(err); }); }; - module.deleteAll = function (keys, callback) { - callback = callback || helpers.noop; - + module.deleteAll = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } - query({ + await query({ name: 'deleteAll', text: ` DELETE FROM "legacy_object" WHERE "_key" = ANY($1::TEXT[])`, values: [keys], - }, function (err) { - callback(err); }); }; - module.get = function (key, callback) { + module.get = async function (key) { if (!key) { - return callback(); + return; } - query({ + const res = await query({ name: 'get', text: ` SELECT s."data" t @@ -119,94 +88,76 @@ SELECT s."data" t WHERE o."_key" = $1::TEXT LIMIT 1`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - if (res.rows.length) { - return callback(null, res.rows[0].t); - } + return res.rows.length ? res.rows[0].t : null; + }; - callback(null, null); + module.set = async function (key, value) { + if (!key) { + return; + } + + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'string'); + await query({ + name: 'set', + text: ` +INSERT INTO "legacy_string" ("_key", "data") +VALUES ($1::TEXT, $2::TEXT) +ON CONFLICT ("_key") +DO UPDATE SET "data" = $2::TEXT`, + values: [key, value], + }); }); }; - module.set = function (key, value, callback) { - callback = callback || helpers.noop; - + module.increment = async function (key) { if (!key) { - return callback(); + return; } - module.transaction(function (tx, done) { - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'string'), - async.apply(tx.client.query.bind(tx.client), { - name: 'set', - text: ` -INSERT INTO "legacy_string" ("_key", "data") -VALUES ($1::TEXT, $2::TEXT) - ON CONFLICT ("_key") - DO UPDATE SET "data" = $2::TEXT`, - values: [key, value], - }), - ], function (err) { - done(err); - }); - }, callback); - }; - - module.increment = function (key, callback) { - callback = callback || helpers.noop; - - if (!key) { - return callback(); - } - - module.transaction(function (tx, done) { - async.waterfall([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'string'), - async.apply(tx.client.query.bind(tx.client), { - name: 'increment', - text: ` + return await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'string'); + const res = await query({ + name: 'increment', + text: ` INSERT INTO "legacy_string" ("_key", "data") VALUES ($1::TEXT, '1') - ON CONFLICT ("_key") - DO UPDATE SET "data" = ("legacy_string"."data"::NUMERIC + 1)::TEXT +ON CONFLICT ("_key") +DO UPDATE SET "data" = ("legacy_string"."data"::NUMERIC + 1)::TEXT RETURNING "data" d`, - values: [key], - }), - ], function (err, res) { - if (err) { - return done(err); - } - - done(null, parseFloat(res.rows[0].d)); + values: [key], }); - }, callback); + return parseFloat(res.rows[0].d); + }); }; - module.rename = function (oldKey, newKey, callback) { - module.transaction(function (tx, done) { - async.series([ - async.apply(tx.delete, newKey), - async.apply(tx.client.query.bind(tx.client), { - name: 'rename', - text: ` + module.rename = async function (oldKey, newKey) { + await module.transaction(async function (client) { + var query = client.query.bind(client); + await query({ + name: 'deleteRename', + text: ` + DELETE FROM "legacy_object" + WHERE "_key" = $1::TEXT`, + values: [newKey], + }); + await query({ + name: 'rename', + text: ` UPDATE "legacy_object" - SET "_key" = $2::TEXT - WHERE "_key" = $1::TEXT`, - values: [oldKey, newKey], - }), - ], function (err) { - done(err); +SET "_key" = $2::TEXT +WHERE "_key" = $1::TEXT`, + values: [oldKey, newKey], }); - }, callback || helpers.noop); + }); }; - module.type = function (key, callback) { - query({ + module.type = async function (key) { + const res = await query({ name: 'type', text: ` SELECT "type"::TEXT t @@ -214,47 +165,35 @@ SELECT "type"::TEXT t WHERE "_key" = $1::TEXT LIMIT 1`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].t); - } - - callback(null, null); }); + + return res.rows.length ? res.rows[0].t : null; }; - function doExpire(key, date, callback) { - query({ + async function doExpire(key, date) { + await query({ name: 'expire', text: ` UPDATE "legacy_object" SET "expireAt" = $2::TIMESTAMPTZ WHERE "_key" = $1::TEXT`, values: [key, date], - }, function (err) { - if (callback) { - callback(err); - } }); } - module.expire = function (key, seconds, callback) { - doExpire(key, new Date(((Date.now() / 1000) + seconds) * 1000), callback); + module.expire = async function (key, seconds) { + await doExpire(key, new Date(((Date.now() / 1000) + seconds) * 1000)); }; - module.expireAt = function (key, timestamp, callback) { - doExpire(key, new Date(timestamp * 1000), callback); + module.expireAt = async function (key, timestamp) { + await doExpire(key, new Date(timestamp * 1000)); }; - module.pexpire = function (key, ms, callback) { - doExpire(key, new Date(Date.now() + parseInt(ms, 10)), callback); + module.pexpire = async function (key, ms) { + await doExpire(key, new Date(Date.now() + parseInt(ms, 10))); }; - module.pexpireAt = function (key, timestamp, callback) { - doExpire(key, new Date(timestamp), callback); + module.pexpireAt = async function (key, timestamp) { + await doExpire(key, new Date(timestamp)); }; }; diff --git a/src/database/postgres/sets.js b/src/database/postgres/sets.js index d0c41e6233..4e34e57cc0 100644 --- a/src/database/postgres/sets.js +++ b/src/database/postgres/sets.js @@ -1,44 +1,34 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); module.exports = function (db, module) { - var helpers = module.helpers.postgres; - - module.setAdd = function (key, value, callback) { - callback = callback || helpers.noop; + var helpers = require('./helpers'); + module.setAdd = async function (key, value) { if (!Array.isArray(value)) { value = [value]; } - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'set'), - async.apply(query, { - name: 'setAdd', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'set'); + await query({ + name: 'setAdd', + text: ` INSERT INTO "legacy_set" ("_key", "member") SELECT $1::TEXT, m - FROM UNNEST($2::TEXT[]) m - ON CONFLICT ("_key", "member") - DO NOTHING`, - values: [key, value], - }), - ], function (err) { - done(err); +FROM UNNEST($2::TEXT[]) m +ON CONFLICT ("_key", "member") +DO NOTHING`, + values: [key, value], }); - }, callback); + }); }; - module.setsAdd = function (keys, value, callback) { - callback = callback || helpers.noop; - + module.setsAdd = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } if (!Array.isArray(value)) { @@ -47,31 +37,24 @@ SELECT $1::TEXT, m keys = _.uniq(keys); - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectsType, tx.client, keys, 'set'), - async.apply(query, { - name: 'setsAdd', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectsType(client, keys, 'set'); + await query({ + name: 'setsAdd', + text: ` INSERT INTO "legacy_set" ("_key", "member") SELECT k, m - FROM UNNEST($1::TEXT[]) k - CROSS JOIN UNNEST($2::TEXT[]) m - ON CONFLICT ("_key", "member") - DO NOTHING`, - values: [keys, value], - }), - ], function (err) { - done(err); +FROM UNNEST($1::TEXT[]) k +CROSS JOIN UNNEST($2::TEXT[]) m +ON CONFLICT ("_key", "member") +DO NOTHING`, + values: [keys, value], }); - }, callback); + }); }; - module.setRemove = function (key, value, callback) { - callback = callback || helpers.noop; - + module.setRemove = async function (key, value) { if (!Array.isArray(key)) { key = [key]; } @@ -80,43 +63,37 @@ SELECT k, m value = [value]; } - db.query({ + await db.query({ name: 'setRemove', text: ` DELETE FROM "legacy_set" WHERE "_key" = ANY($1::TEXT[]) AND "member" = ANY($2::TEXT[])`, values: [key, value], - }, function (err) { - callback(err); }); }; - module.setsRemove = function (keys, value, callback) { - callback = callback || helpers.noop; - + module.setsRemove = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } - db.query({ + await db.query({ name: 'setsRemove', text: ` DELETE FROM "legacy_set" WHERE "_key" = ANY($1::TEXT[]) AND "member" = $2::TEXT`, values: [keys, value], - }, function (err) { - callback(err); }); }; - module.isSetMember = function (key, value, callback) { + module.isSetMember = async function (key, value) { if (!key) { - return callback(null, false); + return false; } - db.query({ + const res = await db.query({ name: 'isSetMember', text: ` SELECT 1 @@ -127,23 +104,19 @@ SELECT 1 WHERE o."_key" = $1::TEXT AND s."member" = $2::TEXT`, values: [key, value], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, !!res.rows.length); }); + + return !!res.rows.length; }; - module.isSetMembers = function (key, values, callback) { + module.isSetMembers = async function (key, values) { if (!key || !Array.isArray(values) || !values.length) { - return callback(null, []); + return []; } values = values.map(helpers.valueToString); - db.query({ + const res = await db.query({ name: 'isSetMembers', text: ` SELECT s."member" m @@ -154,27 +127,21 @@ SELECT s."member" m WHERE o."_key" = $1::TEXT AND s."member" = ANY($2::TEXT[])`, values: [key, values], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, values.map(function (v) { - return res.rows.some(function (r) { - return r.m === v; - }); - })); + return values.map(function (v) { + return res.rows.some(r => r.m === v); }); }; - module.isMemberOfSets = function (sets, value, callback) { + module.isMemberOfSets = async function (sets, value) { if (!Array.isArray(sets) || !sets.length) { - return callback(null, []); + return []; } value = helpers.valueToString(value); - db.query({ + const res = await db.query({ name: 'isMemberOfSets', text: ` SELECT o."_key" k @@ -185,25 +152,19 @@ SELECT o."_key" k WHERE o."_key" = ANY($1::TEXT[]) AND s."member" = $2::TEXT`, values: [sets, value], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, sets.map(function (s) { - return res.rows.some(function (r) { - return r.k === s; - }); - })); + return sets.map(function (s) { + return res.rows.some(r => r.k === s); }); }; - module.getSetMembers = function (key, callback) { + module.getSetMembers = async function (key) { if (!key) { - return callback(null, []); + return []; } - db.query({ + const res = await db.query({ name: 'getSetMembers', text: ` SELECT s."member" m @@ -213,23 +174,17 @@ SELECT s."member" m AND o."type" = s."type" WHERE o."_key" = $1::TEXT`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows.map(function (r) { - return r.m; - })); }); + + return res.rows.map(r => r.m); }; - module.getSetsMembers = function (keys, callback) { + module.getSetsMembers = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - db.query({ + const res = await db.query({ name: 'getSetsMembers', text: ` SELECT o."_key" k, @@ -241,25 +196,19 @@ SELECT o."_key" k, WHERE o."_key" = ANY($1::TEXT[]) GROUP BY o."_key"`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - return (res.rows.find(function (r) { - return r.k === k; - }) || { m: [] }).m; - })); + return keys.map(function (k) { + return (res.rows.find(r => r.k === k) || { m: [] }).m; }); }; - module.setCount = function (key, callback) { + module.setCount = async function (key) { if (!key) { - return callback(null, 0); + return 0; } - db.query({ + const res = await db.query({ name: 'setCount', text: ` SELECT COUNT(*) c @@ -269,17 +218,13 @@ SELECT COUNT(*) c AND o."type" = s."type" WHERE o."_key" = $1::TEXT`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + + return parseInt(res.rows[0].c, 10); }; - module.setsCount = function (keys, callback) { - db.query({ + module.setsCount = async function (keys) { + const res = await db.query({ name: 'setsCount', text: ` SELECT o."_key" k, @@ -291,23 +236,15 @@ SELECT o."_key" k, WHERE o."_key" = ANY($1::TEXT[]) GROUP BY o."_key"`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - return (res.rows.find(function (r) { - return r.k === k; - }) || { c: 0 }).c; - })); + return keys.map(function (k) { + return (res.rows.find(r => r.k === k) || { c: 0 }).c; }); }; - module.setRemoveRandom = function (key, callback) { - callback = callback || helpers.noop; - - db.query({ + module.setRemoveRandom = async function (key) { + const res = await db.query({ name: 'setRemoveRandom', text: ` WITH A AS ( @@ -326,16 +263,7 @@ DELETE FROM "legacy_set" s AND s."member" = A."member" RETURNING A."member" m`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, res.rows[0].m); - } - - callback(null, null); }); + return res.rows.length ? res.rows[0].m : null; }; }; diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 9f06248298..29d193e68b 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -1,9 +1,11 @@ 'use strict'; -var async = require('async'); - module.exports = function (db, module) { - var helpers = module.helpers.postgres; + var helpers = require('./helpers'); + const util = require('util'); + var Cursor = require('pg-cursor'); + Cursor.prototype.readAsync = util.promisify(Cursor.prototype.read); + const sleep = util.promisify(setTimeout); var query = db.query.bind(db); @@ -12,25 +14,25 @@ module.exports = function (db, module) { require('./sorted/union')(db, module); require('./sorted/intersect')(db, module); - module.getSortedSetRange = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, 1, false, callback); + module.getSortedSetRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, 1, false); }; - module.getSortedSetRevRange = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, -1, false, callback); + module.getSortedSetRevRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, -1, false); }; - module.getSortedSetRangeWithScores = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, 1, true, callback); + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, 1, true); }; - module.getSortedSetRevRangeWithScores = function (key, start, stop, callback) { - getSortedSetRange(key, start, stop, -1, true, callback); + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, -1, true); }; - function getSortedSetRange(key, start, stop, sort, withScores, callback) { + async function getSortedSetRange(key, start, stop, sort, withScores) { if (!key) { - return callback(); + return; } if (!Array.isArray(key)) { @@ -38,7 +40,7 @@ module.exports = function (db, module) { } if (start < 0 && start > stop) { - return callback(null, []); + return []; } var reverse = false; @@ -58,7 +60,7 @@ module.exports = function (db, module) { limit = null; } - query({ + const res = await query({ name: 'getSortedSetRangeWithScores' + (sort > 0 ? 'Asc' : 'Desc'), text: ` SELECT z."value", @@ -72,51 +74,40 @@ SELECT z."value", LIMIT $3::INTEGER OFFSET $2::INTEGER`, values: [key, start, limit], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (reverse) { - res.rows.reverse(); - } - - if (withScores) { - res.rows = res.rows.map(function (r) { - return { - value: r.value, - score: parseFloat(r.score), - }; - }); - } else { - res.rows = res.rows.map(function (r) { - return r.value; - }); - } - - callback(null, res.rows); }); + + if (reverse) { + res.rows.reverse(); + } + + if (withScores) { + res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; } - module.getSortedSetRangeByScore = function (key, start, count, min, max, callback) { - getSortedSetRangeByScore(key, start, count, min, max, 1, false, callback); + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, false); }; - module.getSortedSetRevRangeByScore = function (key, start, count, max, min, callback) { - getSortedSetRangeByScore(key, start, count, min, max, -1, false, callback); + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, false); }; - module.getSortedSetRangeByScoreWithScores = function (key, start, count, min, max, callback) { - getSortedSetRangeByScore(key, start, count, min, max, 1, true, callback); + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, true); }; - module.getSortedSetRevRangeByScoreWithScores = function (key, start, count, max, min, callback) { - getSortedSetRangeByScore(key, start, count, min, max, -1, true, callback); + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, true); }; - function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores, callback) { + async function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores) { if (!key) { - return callback(); + return; } if (!Array.isArray(key)) { @@ -134,7 +125,7 @@ OFFSET $2::INTEGER`, max = null; } - query({ + const res = await query({ name: 'getSortedSetRangeByScoreWithScores' + (sort > 0 ? 'Asc' : 'Desc'), text: ` SELECT z."value", @@ -150,31 +141,20 @@ SELECT z."value", LIMIT $3::INTEGER OFFSET $2::INTEGER`, values: [key, start, count, min, max], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (withScores) { - res.rows = res.rows.map(function (r) { - return { - value: r.value, - score: parseFloat(r.score), - }; - }); - } else { - res.rows = res.rows.map(function (r) { - return r.value; - }); - } - - return callback(null, res.rows); }); + + if (withScores) { + res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; } - module.sortedSetCount = function (key, min, max, callback) { + module.sortedSetCount = async function (key, min, max) { if (!key) { - return callback(); + return; } if (min === '-inf') { @@ -184,7 +164,7 @@ OFFSET $2::INTEGER`, max = null; } - query({ + const res = await query({ name: 'sortedSetCount', text: ` SELECT COUNT(*) c @@ -196,21 +176,17 @@ SELECT COUNT(*) c AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL) AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)`, values: [key, min, max], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + + return parseInt(res.rows[0].c, 10); }; - module.sortedSetCard = function (key, callback) { + module.sortedSetCard = async function (key) { if (!key) { - return callback(null, 0); + return 0; } - query({ + const res = await query({ name: 'sortedSetCard', text: ` SELECT COUNT(*) c @@ -220,21 +196,17 @@ SELECT COUNT(*) c AND o."type" = z."type" WHERE o."_key" = $1::TEXT`, values: [key], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + + return parseInt(res.rows[0].c, 10); }; - module.sortedSetsCard = function (keys, callback) { + module.sortedSetsCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - query({ + const res = await query({ name: 'sortedSetsCard', text: ` SELECT o."_key" k, @@ -246,50 +218,38 @@ SELECT o."_key" k, WHERE o."_key" = ANY($1::TEXT[]) GROUP BY o."_key"`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - return parseInt((res.rows.find(function (r) { - return r.k === k; - }) || { c: 0 }).c, 10); - })); + return keys.map(function (k) { + return parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10); }); }; - module.sortedSetsCardSum = function (keys, callback) { + module.sortedSetsCardSum = async function (keys) { if (!keys || (Array.isArray(keys) && !keys.length)) { - return callback(null, 0); + return 0; } if (!Array.isArray(keys)) { keys = [keys]; } - module.sortedSetsCard(keys, function (err, counts) { - if (err) { - return callback(err); - } - const sum = counts.reduce(function (acc, val) { return acc + val; }, 0); - callback(null, sum); - }); + const counts = await module.sortedSetsCard(keys); + const sum = counts.reduce((acc, val) => acc + val, 0); + return sum; }; - module.sortedSetRank = function (key, value, callback) { - getSortedSetRank('ASC', [key], [value], function (err, result) { - callback(err, result ? result[0] : null); - }); + module.sortedSetRank = async function (key, value) { + const result = await getSortedSetRank('ASC', [key], [value]); + return result ? result[0] : null; }; - module.sortedSetRevRank = function (key, value, callback) { - getSortedSetRank('DESC', [key], [value], function (err, result) { - callback(err, result ? result[0] : null); - }); + module.sortedSetRevRank = async function (key, value) { + const result = await getSortedSetRank('DESC', [key], [value]); + return result ? result[0] : null; }; - function getSortedSetRank(sort, keys, values, callback) { + async function getSortedSetRank(sort, keys, values) { values = values.map(helpers.valueToString); - query({ + const res = await query({ name: 'getSortedSetRank' + sort, text: ` SELECT (SELECT r @@ -306,55 +266,51 @@ SELECT (SELECT r FROM UNNEST($1::TEXT[], $2::TEXT[]) WITH ORDINALITY kvi(k, v, i) ORDER BY kvi.i ASC`, values: [keys, values], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows.map(function (r) { return r.r === null ? null : parseFloat(r.r); })); }); + + return res.rows.map(r => (r.r === null ? null : parseFloat(r.r))); } - module.sortedSetsRanks = function (keys, values, callback) { + module.sortedSetsRanks = async function (keys, values) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - getSortedSetRank('ASC', keys, values, callback); + return await getSortedSetRank('ASC', keys, values); }; - module.sortedSetsRevRanks = function (keys, values, callback) { + module.sortedSetsRevRanks = async function (keys, values) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - getSortedSetRank('DESC', keys, values, callback); + return await getSortedSetRank('DESC', keys, values); }; - module.sortedSetRanks = function (key, values, callback) { + module.sortedSetRanks = async function (key, values) { if (!Array.isArray(values) || !values.length) { - return callback(null, []); + return []; } - getSortedSetRank('ASC', new Array(values.length).fill(key), values, callback); + return await getSortedSetRank('ASC', new Array(values.length).fill(key), values); }; - module.sortedSetRevRanks = function (key, values, callback) { + module.sortedSetRevRanks = async function (key, values) { if (!Array.isArray(values) || !values.length) { - return callback(null, []); + return []; } - getSortedSetRank('DESC', new Array(values.length).fill(key), values, callback); + return await getSortedSetRank('DESC', new Array(values.length).fill(key), values); }; - module.sortedSetScore = function (key, value, callback) { + module.sortedSetScore = async function (key, value) { if (!key) { - return callback(null, null); + return null; } value = helpers.valueToString(value); - query({ + const res = await query({ name: 'sortedSetScore', text: ` SELECT z."score" s @@ -365,27 +321,21 @@ SELECT z."score" s WHERE o."_key" = $1::TEXT AND z."value" = $2::TEXT`, values: [key, value], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (res.rows.length) { - return callback(null, parseFloat(res.rows[0].s)); - } - - callback(null, null); }); + if (res.rows.length) { + return parseFloat(res.rows[0].s); + } + return null; }; - module.sortedSetsScore = function (keys, value, callback) { + module.sortedSetsScore = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } value = helpers.valueToString(value); - query({ + const res = await query({ name: 'sortedSetsScore', text: ` SELECT o."_key" k, @@ -397,31 +347,24 @@ SELECT o."_key" k, WHERE o."_key" = ANY($1::TEXT[]) AND z."value" = $2::TEXT`, values: [keys, value], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - var s = res.rows.find(function (r) { - return r.k === k; - }); - - return s ? parseFloat(s.s) : null; - })); + return keys.map(function (k) { + var s = res.rows.find(r => r.k === k); + return s ? parseFloat(s.s) : null; }); }; - module.sortedSetScores = function (key, values, callback) { + module.sortedSetScores = async function (key, values) { if (!key) { - return setImmediate(callback, null, null); + return null; } if (!values.length) { - return setImmediate(callback, null, []); + return []; } values = values.map(helpers.valueToString); - query({ + const res = await query({ name: 'sortedSetScores', text: ` SELECT z."value" v, @@ -433,29 +376,22 @@ SELECT z."value" v, WHERE o."_key" = $1::TEXT AND z."value" = ANY($2::TEXT[])`, values: [key, values], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, values.map(function (v) { - var s = res.rows.find(function (r) { - return r.v === v; - }); - - return s ? parseFloat(s.s) : null; - })); + return values.map(function (v) { + var s = res.rows.find(r => r.v === v); + return s ? parseFloat(s.s) : null; }); }; - module.isSortedSetMember = function (key, value, callback) { + module.isSortedSetMember = async function (key, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); - query({ + const res = await query({ name: 'isSortedSetMember', text: ` SELECT 1 @@ -466,23 +402,19 @@ SELECT 1 WHERE o."_key" = $1::TEXT AND z."value" = $2::TEXT`, values: [key, value], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, !!res.rows.length); }); + + return !!res.rows.length; }; - module.isSortedSetMembers = function (key, values, callback) { + module.isSortedSetMembers = async function (key, values) { if (!key) { - return callback(); + return; } values = values.map(helpers.valueToString); - query({ + const res = await query({ name: 'isSortedSetMembers', text: ` SELECT z."value" v @@ -493,27 +425,21 @@ SELECT z."value" v WHERE o."_key" = $1::TEXT AND z."value" = ANY($2::TEXT[])`, values: [key, values], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, values.map(function (v) { - return res.rows.some(function (r) { - return r.v === v; - }); - })); + return values.map(function (v) { + return res.rows.some(r => r.v === v); }); }; - module.isMemberOfSortedSets = function (keys, value, callback) { + module.isMemberOfSortedSets = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } value = helpers.valueToString(value); - query({ + const res = await query({ name: 'isMemberOfSortedSets', text: ` SELECT o."_key" k @@ -524,25 +450,19 @@ SELECT o."_key" k WHERE o."_key" = ANY($1::TEXT[]) AND z."value" = $2::TEXT`, values: [keys, value], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - return res.rows.some(function (r) { - return r.k === k; - }); - })); + return keys.map(function (k) { + return res.rows.some(r => r.k === k); }); }; - module.getSortedSetsMembers = function (keys, callback) { + module.getSortedSetsMembers = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - query({ + const res = await query({ name: 'getSortedSetsMembers', text: ` SELECT o."_key" k, @@ -554,61 +474,50 @@ SELECT o."_key" k, WHERE o."_key" = ANY($1::TEXT[]) GROUP BY o."_key"`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } + }); - callback(null, keys.map(function (k) { - return (res.rows.find(function (r) { - return r.k === k; - }) || { m: [] }).m; - })); + return keys.map(function (k) { + return (res.rows.find(r => r.k === k) || { m: [] }).m; }); }; - module.sortedSetIncrBy = function (key, increment, value, callback) { - callback = callback || helpers.noop; - + module.sortedSetIncrBy = async function (key, increment, value) { if (!key) { - return callback(); + return; } value = helpers.valueToString(value); increment = parseFloat(increment); - module.transaction(function (tx, done) { - async.waterfall([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'zset'), - async.apply(tx.client.query.bind(tx.client), { - name: 'sortedSetIncrBy', - text: ` + return await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'zset'); + const res = await query({ + name: 'sortedSetIncrBy', + text: ` INSERT INTO "legacy_zset" ("_key", "value", "score") VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC) - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = "legacy_zset"."score" + $3::NUMERIC +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = "legacy_zset"."score" + $3::NUMERIC RETURNING "score" s`, - values: [key, value, increment], - }), - function (res, next) { - next(null, parseFloat(res.rows[0].s)); - }, - ], done); - }, callback); + values: [key, value, increment], + }); + return parseFloat(res.rows[0].s); + }); }; - module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { - sortedSetLex(key, min, max, 1, start, count, callback); + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex(key, min, max, 1, start, count); }; - module.getSortedSetRevRangeByLex = function (key, max, min, start, count, callback) { - sortedSetLex(key, min, max, -1, start, count, callback); + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex(key, min, max, -1, start, count); }; - module.sortedSetLexCount = function (key, min, max, callback) { + module.sortedSetLexCount = async function (key, min, max) { var q = buildLexQuery(key, min, max); - query({ + const res = await query({ name: 'sortedSetLexCount' + q.suffix, text: ` SELECT COUNT(*) c @@ -618,26 +527,19 @@ SELECT COUNT(*) c AND o."type" = z."type" WHERE ` + q.where, values: q.values, - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + + return parseInt(res.rows[0].c, 10); }; - function sortedSetLex(key, min, max, sort, start, count, callback) { - if (!callback) { - callback = start; - start = 0; - count = 0; - } + async function sortedSetLex(key, min, max, sort, start, count) { + start = start !== undefined ? start : 0; + count = count !== undefined ? count : 0; var q = buildLexQuery(key, min, max); q.values.push(start); q.values.push(count <= 0 ? null : count); - query({ + const res = await query({ name: 'sortedSetLex' + (sort > 0 ? 'Asc' : 'Desc') + q.suffix, text: ` SELECT z."value" v @@ -650,22 +552,14 @@ SELECT z."value" v LIMIT $` + q.values.length + `::INTEGER OFFSET $` + (q.values.length - 1) + `::INTEGER`, values: q.values, - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, res.rows.map(function (r) { - return r.v; - })); }); + + return res.rows.map(r => r.v); } - module.sortedSetRemoveRangeByLex = function (key, min, max, callback) { - callback = callback || helpers.noop; - + module.sortedSetRemoveRangeByLex = async function (key, min, max) { var q = buildLexQuery(key, min, max); - query({ + await query({ name: 'sortedSetRemoveRangeByLex' + q.suffix, text: ` DELETE FROM "legacy_zset" z @@ -674,8 +568,6 @@ DELETE FROM "legacy_zset" z AND o."type" = z."type" AND ` + q.where, values: q.values, - }, function (err) { - callback(err); }); }; @@ -721,16 +613,10 @@ DELETE FROM "legacy_zset" z return q; } - module.processSortedSet = function (setKey, process, options, callback) { - var Cursor = require('pg-cursor'); - - db.connect(function (err, client, done) { - if (err) { - return callback(err); - } - - var batchSize = (options || {}).batch || 100; - var query = client.query(new Cursor(` + module.processSortedSet = async function (setKey, process, options) { + const client = await db.connect(); + var batchSize = (options || {}).batch || 100; + var cursor = client.query(new Cursor(` SELECT z."value", z."score" FROM "legacy_object_live" o INNER JOIN "legacy_zset" z @@ -739,43 +625,32 @@ SELECT z."value", z."score" WHERE o."_key" = $1::TEXT ORDER BY z."score" ASC, z."value" ASC`, [setKey])); - var isDone = false; + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); + } - async.whilst(function (next) { - next(null, !isDone); - }, function (next) { - query.read(batchSize, function (err, rows) { - if (err) { - return next(err); - } + while (true) { + /* eslint-disable no-await-in-loop */ + let rows = await cursor.readAsync(batchSize); + if (!rows.length) { + client.release(); + return; + } - if (!rows.length) { - isDone = true; - return next(); - } - - rows = rows.map(function (row) { - return options.withScores ? row : row.value; - }); - - process(rows, function (err) { - if (err) { - return query.close(function () { - next(err); - }); - } - - if (options.interval) { - setTimeout(next, options.interval); - } else { - next(); - } - }); - }); - }, function (err) { - done(); - callback(err); - }); - }); + if (options.withScores) { + rows = rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + rows = rows.map(r => r.value); + } + try { + await process(rows); + } catch (err) { + await query.close(); + throw err; + } + if (options.interval) { + await sleep(options.interval); + } + } }; }; diff --git a/src/database/postgres/sorted/add.js b/src/database/postgres/sorted/add.js index 6fc70fe378..1d1417fd9b 100644 --- a/src/database/postgres/sorted/add.js +++ b/src/database/postgres/sorted/add.js @@ -1,134 +1,112 @@ 'use strict'; -var async = require('async'); - module.exports = function (db, module) { - var helpers = module.helpers.postgres; + var helpers = require('../helpers'); var utils = require('../../../utils'); - module.sortedSetAdd = function (key, score, value, callback) { - callback = callback || helpers.noop; - + module.sortedSetAdd = async function (key, score, value) { if (!key) { - return callback(); + return; } if (Array.isArray(score) && Array.isArray(value)) { - return sortedSetAddBulk(key, score, value, callback); + return await sortedSetAddBulk(key, score, value); } if (!utils.isNumber(score)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + score + ']]')); + throw new Error('[[error:invalid-score, ' + score + ']]'); } value = helpers.valueToString(value); score = parseFloat(score); - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); + await module.transaction(async function (client) { + var query = client.query.bind(client); - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'zset'), - async.apply(query, { - name: 'sortedSetAdd', - text: ` -INSERT INTO "legacy_zset" ("_key", "value", "score") -VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC) - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = $3::NUMERIC`, - values: [key, value, score], - }), - ], function (err) { - done(err); + await helpers.ensureLegacyObjectType(client, key, 'zset'); + await query({ + name: 'sortedSetAdd', + text: ` + INSERT INTO "legacy_zset" ("_key", "value", "score") + VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC) + ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = $3::NUMERIC`, + values: [key, value, score], }); - }, callback); + }); }; - function sortedSetAddBulk(key, scores, values, callback) { + async function sortedSetAddBulk(key, scores, values) { if (!scores.length || !values.length) { - return callback(); + return; } if (scores.length !== values.length) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } for (let i = 0; i < scores.length; i += 1) { if (!utils.isNumber(scores[i])) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores[i] + ']]')); + throw new Error('[[error:invalid-score, ' + scores[i] + ']]'); } } values = values.map(helpers.valueToString); - scores = scores.map(function (score) { - return parseFloat(score); - }); + scores = scores.map(score => parseFloat(score)); helpers.removeDuplicateValues(values, scores); - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectType, tx.client, key, 'zset'), - async.apply(query, { - name: 'sortedSetAddBulk', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectType(client, key, 'zset'); + await query({ + name: 'sortedSetAddBulk', + text: ` INSERT INTO "legacy_zset" ("_key", "value", "score") SELECT $1::TEXT, v, s - FROM UNNEST($2::TEXT[], $3::NUMERIC[]) vs(v, s) - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = EXCLUDED."score"`, - values: [key, values, scores], - }), - ], function (err) { - done(err); +FROM UNNEST($2::TEXT[], $3::NUMERIC[]) vs(v, s) +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = EXCLUDED."score"`, + values: [key, values, scores], }); - }, callback); + }); } - module.sortedSetsAdd = function (keys, scores, value, callback) { - callback = callback || helpers.noop; - + module.sortedSetsAdd = async function (keys, scores, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } const isArrayOfScores = Array.isArray(scores); if (!isArrayOfScores && !utils.isNumber(scores)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores + ']]')); + throw new Error('[[error:invalid-score, ' + scores + ']]'); } if (isArrayOfScores && scores.length !== keys.length) { - return setImmediate(callback, new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } value = helpers.valueToString(value); scores = isArrayOfScores ? scores.map(score => parseFloat(score)) : parseFloat(scores); - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectsType, tx.client, keys, 'zset'), - async.apply(query, { - name: isArrayOfScores ? 'sortedSetsAddScores' : 'sortedSetsAdd', - text: isArrayOfScores ? ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectsType(client, keys, 'zset'); + await query({ + name: isArrayOfScores ? 'sortedSetsAddScores' : 'sortedSetsAdd', + text: isArrayOfScores ? ` INSERT INTO "legacy_zset" ("_key", "value", "score") SELECT k, $2::TEXT, s - FROM UNNEST($1::TEXT[], $3::NUMERIC[]) vs(k, s) - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = EXCLUDED."score"` : ` +FROM UNNEST($1::TEXT[], $3::NUMERIC[]) vs(k, s) +ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = EXCLUDED."score"` : ` INSERT INTO "legacy_zset" ("_key", "value", "score") - SELECT k, $2::TEXT, $3::NUMERIC - FROM UNNEST($1::TEXT[]) k - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = $3::NUMERIC`, - values: [keys, value, scores], - }), - ], function (err) { - done(err); + SELECT k, $2::TEXT, $3::NUMERIC + FROM UNNEST($1::TEXT[]) k + ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = $3::NUMERIC`, + values: [keys, value, scores], }); - }, callback); + }); }; - module.sortedSetAddBulk = function (data, callback) { + module.sortedSetAddBulk = async function (data) { if (!Array.isArray(data) || !data.length) { - return setImmediate(callback); + return; } const keys = []; const values = []; @@ -138,24 +116,19 @@ INSERT INTO "legacy_zset" ("_key", "value", "score") scores.push(item[1]); values.push(item[2]); }); - module.transaction(function (tx, done) { - var query = tx.client.query.bind(tx.client); - - async.series([ - async.apply(helpers.ensureLegacyObjectsType, tx.client, keys, 'zset'), - async.apply(query, { - name: 'sortedSetAddBulk2', - text: ` + await module.transaction(async function (client) { + var query = client.query.bind(client); + await helpers.ensureLegacyObjectsType(client, keys, 'zset'); + await query({ + name: 'sortedSetAddBulk2', + text: ` INSERT INTO "legacy_zset" ("_key", "value", "score") SELECT k, v, s - FROM UNNEST($1::TEXT[], $2::TEXT[], $3::NUMERIC[]) vs(k, v, s) - ON CONFLICT ("_key", "value") - DO UPDATE SET "score" = EXCLUDED."score"`, - values: [keys, values, scores], - }), - ], function (err) { - done(err); +FROM UNNEST($1::TEXT[], $2::TEXT[], $3::NUMERIC[]) vs(k, v, s) +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = EXCLUDED."score"`, + values: [keys, values, scores], }); - }, callback); + }); }; }; diff --git a/src/database/postgres/sorted/intersect.js b/src/database/postgres/sorted/intersect.js index 47fe5b9b16..f17267556e 100644 --- a/src/database/postgres/sorted/intersect.js +++ b/src/database/postgres/sorted/intersect.js @@ -1,12 +1,12 @@ 'use strict'; module.exports = function (db, module) { - module.sortedSetIntersectCard = function (keys, callback) { + module.sortedSetIntersectCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, 0); + return 0; } - db.query({ + const res = await db.query({ name: 'sortedSetIntersectCard', text: ` WITH A AS (SELECT z."value" v, @@ -21,27 +21,22 @@ SELECT COUNT(*) c FROM A WHERE A.c = array_length($1::TEXT[], 1)`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + + return parseInt(res.rows[0].c, 10); }; - - module.getSortedSetIntersect = function (params, callback) { + module.getSortedSetIntersect = async function (params) { params.sort = 1; - getSortedSetIntersect(params, callback); + return await getSortedSetIntersect(params); }; - module.getSortedSetRevIntersect = function (params, callback) { + module.getSortedSetRevIntersect = async function (params) { params.sort = -1; - getSortedSetIntersect(params, callback); + return await getSortedSetIntersect(params); }; - function getSortedSetIntersect(params, callback) { + async function getSortedSetIntersect(params) { var sets = params.sets; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = params.hasOwnProperty('stop') ? params.stop : -1; @@ -60,7 +55,7 @@ SELECT COUNT(*) c limit = null; } - db.query({ + const res = await db.query({ name: 'getSortedSetIntersect' + aggregate + (params.sort > 0 ? 'Asc' : 'Desc') + 'WithScores', text: ` WITH A AS (SELECT z."value", @@ -81,25 +76,19 @@ SELECT A."value", LIMIT $4::INTEGER OFFSET $3::INTEGER`, values: [sets, weights, start, limit], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (params.withScores) { - res.rows = res.rows.map(function (r) { - return { - value: r.value, - score: parseFloat(r.score), - }; - }); - } else { - res.rows = res.rows.map(function (r) { - return r.value; - }); - } - - callback(null, res.rows); }); + + if (params.withScores) { + res.rows = res.rows.map(function (r) { + return { + value: r.value, + score: parseFloat(r.score), + }; + }); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; } }; diff --git a/src/database/postgres/sorted/remove.js b/src/database/postgres/sorted/remove.js index 6118b22981..bf5e9fd69d 100644 --- a/src/database/postgres/sorted/remove.js +++ b/src/database/postgres/sorted/remove.js @@ -1,64 +1,55 @@ 'use strict'; module.exports = function (db, module) { - var helpers = module.helpers.postgres; - - module.sortedSetRemove = function (key, value, callback) { - function done(err) { - if (callback) { - callback(err); - } - } + var helpers = require('../helpers'); + module.sortedSetRemove = async function (key, value) { if (!key) { - return done(); + return; + } + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; } if (!Array.isArray(key)) { key = [key]; } - if (!Array.isArray(value)) { + if (!isValueArray) { value = [value]; } value = value.map(helpers.valueToString); - - db.query({ + await db.query({ name: 'sortedSetRemove', text: ` DELETE FROM "legacy_zset" WHERE "_key" = ANY($1::TEXT[]) AND "value" = ANY($2::TEXT[])`, values: [key, value], - }, done); + }); }; - module.sortedSetsRemove = function (keys, value, callback) { - callback = callback || helpers.noop; - + module.sortedSetsRemove = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } value = helpers.valueToString(value); - db.query({ + await db.query({ name: 'sortedSetsRemove', text: ` DELETE FROM "legacy_zset" WHERE "_key" = ANY($1::TEXT[]) AND "value" = $2::TEXT`, values: [keys, value], - }, function (err) { - callback(err); }); }; - module.sortedSetsRemoveRangeByScore = function (keys, min, max, callback) { - callback = callback || helpers.noop; - + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { if (!Array.isArray(keys) || !keys.length) { - return callback(); + return; } if (min === '-inf') { @@ -68,7 +59,7 @@ DELETE FROM "legacy_zset" max = null; } - db.query({ + await db.query({ name: 'sortedSetsRemoveRangeByScore', text: ` DELETE FROM "legacy_zset" @@ -76,8 +67,6 @@ DELETE FROM "legacy_zset" AND ("score" >= $2::NUMERIC OR $2::NUMERIC IS NULL) AND ("score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)`, values: [keys, min, max], - }, function (err) { - callback(err); }); }; }; diff --git a/src/database/postgres/sorted/union.js b/src/database/postgres/sorted/union.js index 2f991dc761..891e7a407f 100644 --- a/src/database/postgres/sorted/union.js +++ b/src/database/postgres/sorted/union.js @@ -1,12 +1,12 @@ 'use strict'; module.exports = function (db, module) { - module.sortedSetUnionCard = function (keys, callback) { + module.sortedSetUnionCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, 0); + return 0; } - db.query({ + const res = await db.query({ name: 'sortedSetUnionCard', text: ` SELECT COUNT(DISTINCT z."value") c @@ -16,26 +16,21 @@ SELECT COUNT(DISTINCT z."value") c AND o."type" = z."type" WHERE o."_key" = ANY($1::TEXT[])`, values: [keys], - }, function (err, res) { - if (err) { - return callback(err); - } - - callback(null, parseInt(res.rows[0].c, 10)); }); + return res.rows[0].c; }; - module.getSortedSetUnion = function (params, callback) { + module.getSortedSetUnion = async function (params) { params.sort = 1; - getSortedSetUnion(params, callback); + return await getSortedSetUnion(params); }; - module.getSortedSetRevUnion = function (params, callback) { + module.getSortedSetRevUnion = async function (params) { params.sort = -1; - getSortedSetUnion(params, callback); + return await getSortedSetUnion(params); }; - function getSortedSetUnion(params, callback) { + async function getSortedSetUnion(params) { var sets = params.sets; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = params.hasOwnProperty('stop') ? params.stop : -1; @@ -54,7 +49,7 @@ SELECT COUNT(DISTINCT z."value") c limit = null; } - db.query({ + const res = await db.query({ name: 'getSortedSetUnion' + aggregate + (params.sort > 0 ? 'Asc' : 'Desc') + 'WithScores', text: ` WITH A AS (SELECT z."value", @@ -73,25 +68,18 @@ SELECT A."value", LIMIT $4::INTEGER OFFSET $3::INTEGER`, values: [sets, weights, start, limit], - }, function (err, res) { - if (err) { - return callback(err); - } - - if (params.withScores) { - res.rows = res.rows.map(function (r) { - return { - value: r.value, - score: parseFloat(r.score), - }; - }); - } else { - res.rows = res.rows.map(function (r) { - return r.value; - }); - } - - callback(null, res.rows); }); + + if (params.withScores) { + res.rows = res.rows.map(function (r) { + return { + value: r.value, + score: parseFloat(r.score), + }; + }); + } else { + res.rows = res.rows.map(r => r.value); + } + return res.rows; } }; diff --git a/src/database/postgres/transaction.js b/src/database/postgres/transaction.js index ed13d7a537..a12e7995cd 100644 --- a/src/database/postgres/transaction.js +++ b/src/database/postgres/transaction.js @@ -1,50 +1,32 @@ 'use strict'; -module.exports = function (db, dbNamespace, module) { - module.transaction = function (perform, callback) { - if (dbNamespace.active && dbNamespace.get('db')) { - var client = dbNamespace.get('db'); - return client.query(`SAVEPOINT nodebb_subtx`, function (err) { - if (err) { - return callback(err); - } - - perform(module, function (err) { - var args = Array.prototype.slice.call(arguments, 1); - - client.query(err ? `ROLLBACK TO SAVEPOINT nodebb_subtx` : `RELEASE SAVEPOINT nodebb_subtx`, function (err1) { - callback.apply(this, [err || err1].concat(args)); - }); - }); - }); - } - - db.connect(function (err, client, done) { - if (err) { - return callback(err); +module.exports = function (db, module) { + module.transaction = async function (perform, txClient) { + let res; + if (txClient) { + await txClient.query(`SAVEPOINT nodebb_subtx`); + try { + res = await perform(txClient); + } catch (err) { + await txClient.query(`ROLLBACK TO SAVEPOINT nodebb_subtx`); + throw err; } + await txClient.query(`RELEASE SAVEPOINT nodebb_subtx`); + return res; + } + // see https://node-postgres.com/features/transactions#a-pooled-client-with-async-await + const client = await db.connect(); - dbNamespace.run(function () { - dbNamespace.set('db', client); - - client.query(`BEGIN`, function (err) { - if (err) { - done(); - dbNamespace.set('db', null); - return callback(err); - } - - perform(module, function (err) { - var args = Array.prototype.slice.call(arguments, 1); - - client.query(err ? `ROLLBACK` : `COMMIT`, function (err1) { - done(); - dbNamespace.set('db', null); - callback.apply(this, [err || err1].concat(args)); - }); - }); - }); - }); - }); + try { + await client.query('BEGIN'); + res = await perform(client); + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } + return res; }; }; diff --git a/src/database/redis.js b/src/database/redis.js index 58a427d8c8..141d4c6923 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -56,6 +56,8 @@ redisModule.init = function (callback) { } redisModule.client = redisClient; + require('./redis/promisify')(redisClient); + require('./redis/main')(redisClient, redisModule); require('./redis/hash')(redisClient, redisModule); require('./redis/sets')(redisClient, redisModule); @@ -63,8 +65,7 @@ redisModule.init = function (callback) { require('./redis/list')(redisClient, redisModule); require('./redis/transaction')(redisClient, redisModule); - redisModule.async = require('../promisify')(redisModule, ['client', 'sessionStore']); - + redisModule.async = require('../promisify')(redisModule, ['client', 'sessionStore', 'connect']); callback(); }); }; @@ -220,6 +221,3 @@ redisModule.socketAdapter = function () { subClient: sub, }); }; - -redisModule.helpers = redisModule.helpers || {}; -redisModule.helpers.redis = require('./redis/helpers'); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index 355393f05c..873547f80b 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -1,19 +1,17 @@ 'use strict'; module.exports = function (redisClient, module) { - var helpers = module.helpers.redis; + var helpers = require('./helpers'); - const async = require('async'); const _ = require('lodash'); const cache = require('../cache').create('redis'); module.objectCache = cache; - module.setObject = function (key, data, callback) { - callback = callback || function () {}; + module.setObject = async function (key, data) { if (!key || !data) { - return callback(); + return; } if (data.hasOwnProperty('')) { @@ -27,176 +25,147 @@ module.exports = function (redisClient, module) { }); if (!Object.keys(data).length) { - return callback(); + return; } - redisClient.hmset(key, data, function (err) { - if (err) { - return callback(err); - } - cache.delObjectCache(key); - callback(); - }); + await redisClient.async.hmset(key, data); + cache.delObjectCache(key); }; - module.setObjectField = function (key, field, value, callback) { - callback = callback || function () {}; + module.setObjectField = async function (key, field, value) { if (!field) { - return callback(); + return; } - redisClient.hset(key, field, value, function (err) { - if (err) { - return callback(err); - } - cache.delObjectCache(key); - callback(); - }); + await redisClient.async.hset(key, field, value); + cache.delObjectCache(key); }; - module.getObject = function (key, callback) { + module.getObject = async function (key) { if (!key) { - return setImmediate(callback, null, null); + return null; } - module.getObjectsFields([key], [], function (err, data) { - callback(err, data && data.length ? data[0] : null); - }); + const data = await module.getObjectsFields([key], []); + return data && data.length ? data[0] : null; }; - module.getObjects = function (keys, callback) { - module.getObjectsFields(keys, [], callback); + module.getObjects = async function (keys) { + return await module.getObjectsFields(keys, []); }; - module.getObjectField = function (key, field, callback) { + module.getObjectField = async function (key, field) { if (!key) { - return setImmediate(callback, null, null); + return null; } const cachedData = {}; cache.getUnCachedKeys([key], cachedData); if (cachedData[key]) { - return setImmediate(callback, null, cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null); + return cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null; } - redisClient.hget(key, field, callback); + return await redisClient.async.hget(key, field); }; - module.getObjectFields = function (key, fields, callback) { + module.getObjectFields = async function (key, fields) { if (!key) { - return setImmediate(callback, null, null); + return null; } - module.getObjectsFields([key], fields, function (err, results) { - callback(err, results ? results[0] : null); - }); + const results = await module.getObjectsFields([key], fields); + return results ? results[0] : null; }; - module.getObjectsFields = function (keys, fields, callback) { + module.getObjectsFields = async function (keys, fields) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } if (!Array.isArray(fields)) { - return callback(null, keys.map(function () { return {}; })); + return keys.map(function () { return {}; }); } const cachedData = {}; const unCachedKeys = cache.getUnCachedKeys(keys, cachedData); - async.waterfall([ - function (next) { - if (unCachedKeys.length > 1) { - helpers.execKeys(redisClient, 'batch', 'hgetall', unCachedKeys, next); - } else if (unCachedKeys.length === 1) { - redisClient.hgetall(unCachedKeys[0], (err, data) => next(err, [data])); - } else { - next(null, []); - } - }, - function (data, next) { - unCachedKeys.forEach(function (key, i) { - cachedData[key] = data[i] || null; - cache.set(key, cachedData[key]); - }); - - var mapped = keys.map(function (key) { - if (!fields.length) { - return _.clone(cachedData[key]); - } - - const item = cachedData[key] || {}; - const result = {}; - fields.forEach((field) => { - result[field] = item[field] !== undefined ? item[field] : null; - }); - return result; - }); - next(null, mapped); - }, - ], callback); - }; - - module.getObjectKeys = function (key, callback) { - redisClient.hkeys(key, callback); - }; - - module.getObjectValues = function (key, callback) { - redisClient.hvals(key, callback); - }; - - module.isObjectField = function (key, field, callback) { - redisClient.hexists(key, field, function (err, exists) { - callback(err, exists === 1); - }); - }; - - module.isObjectFields = function (key, fields, callback) { - helpers.execKeyValues(redisClient, 'batch', 'hexists', key, fields, function (err, results) { - callback(err, Array.isArray(results) ? helpers.resultsToBool(results) : null); - }); - }; - - module.deleteObjectField = function (key, field, callback) { - callback = callback || function () {}; - if (key === undefined || key === null || field === undefined || field === null) { - return setImmediate(callback); + let data = []; + if (unCachedKeys.length > 1) { + const batch = redisClient.batch(); + unCachedKeys.forEach(k => batch.hgetall(k)); + data = await helpers.execBatch(batch); + } else if (unCachedKeys.length === 1) { + data = [await redisClient.async.hgetall(unCachedKeys[0])]; } - redisClient.hdel(key, field, function (err) { - cache.delObjectCache(key); - callback(err); + + unCachedKeys.forEach(function (key, i) { + cachedData[key] = data[i] || null; + cache.set(key, cachedData[key]); }); - }; - module.deleteObjectFields = function (key, fields, callback) { - helpers.execKeyValues(redisClient, 'batch', 'hdel', key, fields, function (err) { - cache.delObjectCache(key); - callback(err); - }); - }; - - module.incrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, 1, callback); - }; - - module.decrObjectField = function (key, field, callback) { - module.incrObjectFieldBy(key, field, -1, callback); - }; - - module.incrObjectFieldBy = function (key, field, value, callback) { - callback = callback || helpers.noop; - function done(err, result) { - if (err) { - return callback(err); + const mapped = keys.map(function (key) { + if (!fields.length) { + return _.clone(cachedData[key]); } - cache.delObjectCache(key); - callback(null, Array.isArray(result) ? result.map(value => parseInt(value, 10)) : parseInt(result, 10)); + + const item = cachedData[key] || {}; + const result = {}; + fields.forEach((field) => { + result[field] = item[field] !== undefined ? item[field] : null; + }); + return result; + }); + return mapped; + }; + + module.getObjectKeys = async function (key) { + return await redisClient.async.hkeys(key); + }; + + module.getObjectValues = async function (key) { + return await redisClient.async.hvals(key); + }; + + module.isObjectField = async function (key, field) { + const exists = await redisClient.async.hexists(key, field); + return exists === 1; + }; + + module.isObjectFields = async function (key, fields) { + const batch = redisClient.batch(); + fields.forEach(f => batch.hexists(String(key), String(f))); + const results = await helpers.execBatch(batch); + return Array.isArray(results) ? helpers.resultsToBool(results) : null; + }; + + module.deleteObjectField = async function (key, field) { + if (key === undefined || key === null || field === undefined || field === null) { + return; } + await redisClient.async.hdel(key, field); + cache.delObjectCache(key); + }; + + module.deleteObjectFields = async function (key, fields) { + await redisClient.async.hdel(key, fields); + cache.delObjectCache(key); + }; + + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); + }; + + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); + }; + + module.incrObjectFieldBy = async function (key, field, value) { value = parseInt(value, 10); if (!key || isNaN(value)) { - return callback(null, null); + return null; } + let result; if (Array.isArray(key)) { var batch = redisClient.batch(); - key.forEach(function (key) { - batch.hincrby(key, field, value); - }); - batch.exec(done); + key.forEach(k => batch.hincrby(k, field, value)); + result = await helpers.execBatch(batch); } else { - redisClient.hincrby(key, field, value, done); + result = await redisClient.async.hincrby(key, field, value); } + cache.delObjectCache(key); + return Array.isArray(result) ? result.map(value => parseInt(value, 10)) : parseInt(result, 10); }; }; diff --git a/src/database/redis/helpers.js b/src/database/redis/helpers.js index fb1b46909e..dd7bf1b6af 100644 --- a/src/database/redis/helpers.js +++ b/src/database/redis/helpers.js @@ -1,9 +1,16 @@ 'use strict'; +const util = require('util'); + var helpers = module.exports; helpers.noop = function () {}; +helpers.execBatch = async function (batch) { + const proFn = util.promisify(batch.exec).bind(batch); + return await proFn(); +}; + helpers.execKeys = function (redisClient, type, command, keys, callback) { callback = callback || function () {}; var queue = redisClient[type](); diff --git a/src/database/redis/list.js b/src/database/redis/list.js index ba127d3100..e3a8e7bc8a 100644 --- a/src/database/redis/list.js +++ b/src/database/redis/list.js @@ -1,63 +1,49 @@ 'use strict'; module.exports = function (redisClient, module) { - module.listPrepend = function (key, value, callback) { - callback = callback || function () {}; + module.listPrepend = async function (key, value) { if (!key) { - return callback(); + return; } - redisClient.lpush(key, value, function (err) { - callback(err); - }); + await redisClient.async.lpush(key, value); }; - module.listAppend = function (key, value, callback) { - callback = callback || function () {}; + module.listAppend = async function (key, value) { if (!key) { - return callback(); + return; } - redisClient.rpush(key, value, function (err) { - callback(err); - }); + await redisClient.async.rpush(key, value); }; - module.listRemoveLast = function (key, callback) { - callback = callback || function () {}; + module.listRemoveLast = async function (key) { if (!key) { - return callback(); + return; } - redisClient.rpop(key, callback); + return await redisClient.async.rpop(key); }; - module.listRemoveAll = function (key, value, callback) { - callback = callback || function () {}; + module.listRemoveAll = async function (key, value) { if (!key) { - return callback(); + return; } - redisClient.lrem(key, 0, value, function (err) { - callback(err); - }); + await redisClient.async.lrem(key, 0, value); }; - module.listTrim = function (key, start, stop, callback) { - callback = callback || function () {}; + module.listTrim = async function (key, start, stop) { if (!key) { - return callback(); + return; } - redisClient.ltrim(key, start, stop, function (err) { - callback(err); - }); + await redisClient.async.ltrim(key, start, stop); }; - module.getListRange = function (key, start, stop, callback) { - callback = callback || function () {}; + module.getListRange = async function (key, start, stop) { if (!key) { - return callback(); + return; } - redisClient.lrange(key, start, stop, callback); + return await redisClient.async.lrange(key, start, stop); }; - module.listLength = function (key, callback) { - redisClient.llen(key, callback); + module.listLength = async function (key) { + return await redisClient.async.llen(key); }; }; diff --git a/src/database/redis/main.js b/src/database/redis/main.js index c81f3aa73e..77945ff91a 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -1,117 +1,83 @@ 'use strict'; module.exports = function (redisClient, module) { - var helpers = module.helpers.redis; + var helpers = require('./helpers'); - module.flushdb = function (callback) { - redisClient.send_command('flushdb', [], function (err) { - if (typeof callback === 'function') { - callback(err); - } - }); + module.flushdb = async function () { + await redisClient.async.send_command('flushdb', []); }; - module.emptydb = function (callback) { - module.flushdb(function (err) { - if (err) { - return callback(err); - } - module.objectCache.resetObjectCache(); - callback(); - }); + module.emptydb = async function () { + await module.flushdb(); + module.objectCache.resetObjectCache(); }; - module.exists = function (key, callback) { + module.exists = async function (key) { if (Array.isArray(key)) { - helpers.execKeys(redisClient, 'batch', 'exists', key, function (err, data) { - callback(err, data && data.map(exists => exists === 1)); - }); - } else { - redisClient.exists(key, function (err, exists) { - callback(err, exists === 1); - }); + const batch = redisClient.batch(); + key.forEach(key => batch.exists(key)); + const data = await helpers.execBatch(batch); + return data.map(exists => exists === 1); } + const exists = await redisClient.async.exists(key); + return exists === 1; }; - module.delete = function (key, callback) { - callback = callback || function () {}; - redisClient.del(key, function (err) { - module.objectCache.delObjectCache(key); - callback(err); - }); + module.delete = async function (key) { + await redisClient.async.del(key); + module.objectCache.delObjectCache(key); }; - module.deleteAll = function (keys, callback) { - callback = callback || function () {}; - var batch = redisClient.batch(); - for (var i = 0; i < keys.length; i += 1) { - batch.del(keys[i]); + module.deleteAll = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return; } - batch.exec(function (err) { - module.objectCache.delObjectCache(keys); - callback(err); - }); + await redisClient.async.del(keys); + module.objectCache.delObjectCache(keys); }; - module.get = function (key, callback) { - redisClient.get(key, callback); + module.get = async function (key) { + return await redisClient.async.get(key); }; - module.set = function (key, value, callback) { - callback = callback || function () {}; - redisClient.set(key, value, function (err) { - callback(err); - }); + module.set = async function (key, value) { + await redisClient.async.set(key, value); }; - module.increment = function (key, callback) { - callback = callback || function () {}; - redisClient.incr(key, callback); + module.increment = async function (key) { + return await redisClient.async.incr(key); }; - module.rename = function (oldKey, newKey, callback) { - callback = callback || function () {}; - redisClient.rename(oldKey, newKey, function (err) { + module.rename = async function (oldKey, newKey) { + try { + await redisClient.async.rename(oldKey, newKey); + } catch (err) { if (err && err.message !== 'ERR no such key') { - return callback(err); + throw err; } - module.objectCache.delObjectCache(oldKey); - module.objectCache.delObjectCache(newKey); - callback(); - }); + } + + module.objectCache.delObjectCache([oldKey, newKey]); }; - module.type = function (key, callback) { - redisClient.type(key, function (err, type) { - callback(err, type !== 'none' ? type : null); - }); + module.type = async function (key) { + const type = await redisClient.async.type(key); + return type !== 'none' ? type : null; }; - module.expire = function (key, seconds, callback) { - callback = callback || function () {}; - redisClient.expire(key, seconds, function (err) { - callback(err); - }); + module.expire = async function (key, seconds) { + await redisClient.async.expire(key, seconds); }; - module.expireAt = function (key, timestamp, callback) { - callback = callback || function () {}; - redisClient.expireat(key, timestamp, function (err) { - callback(err); - }); + module.expireAt = async function (key, timestamp) { + await redisClient.async.expireat(key, timestamp); }; - module.pexpire = function (key, ms, callback) { - callback = callback || function () {}; - redisClient.pexpire(key, ms, function (err) { - callback(err); - }); + module.pexpire = async function (key, ms) { + await redisClient.async.pexpire(key, ms); }; - module.pexpireAt = function (key, timestamp, callback) { - callback = callback || function () {}; - redisClient.pexpireat(key, timestamp, function (err) { - callback(err); - }); + module.pexpireAt = async function (key, timestamp) { + await redisClient.async.pexpireat(key, timestamp); }; }; diff --git a/src/database/redis/promisify.js b/src/database/redis/promisify.js new file mode 100644 index 0000000000..b6a59e1cb8 --- /dev/null +++ b/src/database/redis/promisify.js @@ -0,0 +1,66 @@ +'use strict'; + +const util = require('util'); + +module.exports = function (redisClient) { + redisClient.async = { + send_command: util.promisify(redisClient.send_command).bind(redisClient), + + exists: util.promisify(redisClient.exists).bind(redisClient), + + del: util.promisify(redisClient.del).bind(redisClient), + get: util.promisify(redisClient.get).bind(redisClient), + set: util.promisify(redisClient.set).bind(redisClient), + incr: util.promisify(redisClient.incr).bind(redisClient), + rename: util.promisify(redisClient.rename).bind(redisClient), + type: util.promisify(redisClient.type).bind(redisClient), + expire: util.promisify(redisClient.expire).bind(redisClient), + expireat: util.promisify(redisClient.expireat).bind(redisClient), + pexpire: util.promisify(redisClient.pexpire).bind(redisClient), + pexpireat: util.promisify(redisClient.pexpireat).bind(redisClient), + + hmset: util.promisify(redisClient.hmset).bind(redisClient), + hset: util.promisify(redisClient.hset).bind(redisClient), + hget: util.promisify(redisClient.hget).bind(redisClient), + hdel: util.promisify(redisClient.hdel).bind(redisClient), + hgetall: util.promisify(redisClient.hgetall).bind(redisClient), + hkeys: util.promisify(redisClient.hkeys).bind(redisClient), + hvals: util.promisify(redisClient.hvals).bind(redisClient), + hexists: util.promisify(redisClient.hexists).bind(redisClient), + hincrby: util.promisify(redisClient.hincrby).bind(redisClient), + + sadd: util.promisify(redisClient.sadd).bind(redisClient), + srem: util.promisify(redisClient.srem).bind(redisClient), + sismember: util.promisify(redisClient.sismember).bind(redisClient), + smembers: util.promisify(redisClient.smembers).bind(redisClient), + scard: util.promisify(redisClient.scard).bind(redisClient), + spop: util.promisify(redisClient.spop).bind(redisClient), + + zadd: util.promisify(redisClient.zadd).bind(redisClient), + zrem: util.promisify(redisClient.zrem).bind(redisClient), + zrange: util.promisify(redisClient.zrange).bind(redisClient), + zrevrange: util.promisify(redisClient.zrevrange).bind(redisClient), + zrangebyscore: util.promisify(redisClient.zrangebyscore).bind(redisClient), + zrevrangebyscore: util.promisify(redisClient.zrevrangebyscore).bind(redisClient), + zscore: util.promisify(redisClient.zscore).bind(redisClient), + zcount: util.promisify(redisClient.zcount).bind(redisClient), + zcard: util.promisify(redisClient.zcard).bind(redisClient), + zrank: util.promisify(redisClient.zrank).bind(redisClient), + zrevrank: util.promisify(redisClient.zrevrank).bind(redisClient), + zincrby: util.promisify(redisClient.zincrby).bind(redisClient), + + zrangebylex: util.promisify(redisClient.zrangebylex).bind(redisClient), + zrevrangebylex: util.promisify(redisClient.zrevrangebylex).bind(redisClient), + zremrangebylex: util.promisify(redisClient.zremrangebylex).bind(redisClient), + zlexcount: util.promisify(redisClient.zlexcount).bind(redisClient), + + lpush: util.promisify(redisClient.lpush).bind(redisClient), + rpush: util.promisify(redisClient.rpush).bind(redisClient), + rpop: util.promisify(redisClient.rpop).bind(redisClient), + lrem: util.promisify(redisClient.lrem).bind(redisClient), + ltrim: util.promisify(redisClient.ltrim).bind(redisClient), + lrange: util.promisify(redisClient.lrange).bind(redisClient), + llen: util.promisify(redisClient.llen).bind(redisClient), + + }; +}; diff --git a/src/database/redis/sets.js b/src/database/redis/sets.js index 9cb93effae..a76df60236 100644 --- a/src/database/redis/sets.js +++ b/src/database/redis/sets.js @@ -1,33 +1,28 @@ 'use strict'; module.exports = function (redisClient, module) { - var helpers = module.helpers.redis; + var helpers = require('./helpers'); - module.setAdd = function (key, value, callback) { - callback = callback || function () {}; + module.setAdd = async function (key, value) { if (!Array.isArray(value)) { value = [value]; } if (!value.length) { - return callback(); + return; } - redisClient.sadd(key, value, function (err) { - callback(err); - }); + await redisClient.async.sadd(key, value); }; - module.setsAdd = function (keys, value, callback) { - callback = callback || function () {}; + module.setsAdd = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback); + return; } - helpers.execKeysValue(redisClient, 'batch', 'sadd', keys, value, function (err) { - callback(err); - }); + const batch = redisClient.batch(); + keys.forEach(k => batch.sadd(String(k), String(value))); + await helpers.execBatch(batch); }; - module.setRemove = function (key, value, callback) { - callback = callback || function () {}; + module.setRemove = async function (key, value) { if (!Array.isArray(value)) { value = [value]; } @@ -36,58 +31,57 @@ module.exports = function (redisClient, module) { } var batch = redisClient.batch(); - key.forEach(function (key) { - batch.srem(key, value); - }); - batch.exec(function (err) { - callback(err); - }); + key.forEach(k => batch.srem(String(k), value)); + await helpers.execBatch(batch); }; - module.setsRemove = function (keys, value, callback) { - callback = callback || function () {}; - helpers.execKeysValue(redisClient, 'batch', 'srem', keys, value, function (err) { - callback(err); - }); + module.setsRemove = async function (keys, value) { + var batch = redisClient.batch(); + keys.forEach(k => batch.srem(String(k), value)); + await helpers.execBatch(batch); }; - module.isSetMember = function (key, value, callback) { - redisClient.sismember(key, value, function (err, result) { - callback(err, result === 1); - }); + module.isSetMember = async function (key, value) { + const result = await redisClient.async.sismember(key, value); + return result === 1; }; - module.isSetMembers = function (key, values, callback) { - helpers.execKeyValues(redisClient, 'batch', 'sismember', key, values, function (err, results) { - callback(err, results ? helpers.resultsToBool(results) : null); - }); + module.isSetMembers = async function (key, values) { + const batch = redisClient.batch(); + values.forEach(v => batch.sismember(String(key), String(v))); + const results = await helpers.execBatch(batch); + return results ? helpers.resultsToBool(results) : null; }; - module.isMemberOfSets = function (sets, value, callback) { - helpers.execKeysValue(redisClient, 'batch', 'sismember', sets, value, function (err, results) { - callback(err, results ? helpers.resultsToBool(results) : null); - }); + module.isMemberOfSets = async function (sets, value) { + const batch = redisClient.batch(); + sets.forEach(s => batch.sismember(String(s), String(value))); + const results = await helpers.execBatch(batch); + return results ? helpers.resultsToBool(results) : null; }; - module.getSetMembers = function (key, callback) { - redisClient.smembers(key, callback); + module.getSetMembers = async function (key) { + return await redisClient.async.smembers(key); }; - module.getSetsMembers = function (keys, callback) { - helpers.execKeys(redisClient, 'batch', 'smembers', keys, callback); + module.getSetsMembers = async function (keys) { + const batch = redisClient.batch(); + keys.forEach(k => batch.smembers(String(k))); + return await helpers.execBatch(batch); }; - module.setCount = function (key, callback) { - redisClient.scard(key, callback); + module.setCount = async function (key) { + return await redisClient.async.scard(key); }; - module.setsCount = function (keys, callback) { - helpers.execKeys(redisClient, 'batch', 'scard', keys, callback); + module.setsCount = async function (keys) { + const batch = redisClient.batch(); + keys.forEach(k => batch.scard(String(k))); + return await helpers.execBatch(batch); }; - module.setRemoveRandom = function (key, callback) { - callback = callback || function () {}; - redisClient.spop(key, callback); + module.setRemoveRandom = async function (key) { + return await redisClient.async.spop(key); }; return module; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index ebed45130a..6a9d6df882 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -3,62 +3,55 @@ module.exports = function (redisClient, module) { var _ = require('lodash'); var utils = require('../../utils'); - - var helpers = module.helpers.redis; + var helpers = require('./helpers'); require('./sorted/add')(redisClient, module); require('./sorted/remove')(redisClient, module); require('./sorted/union')(redisClient, module); require('./sorted/intersect')(redisClient, module); - module.getSortedSetRange = function (key, start, stop, callback) { - sortedSetRange('zrange', key, start, stop, false, callback); + module.getSortedSetRange = async function (key, start, stop) { + return await sortedSetRange('zrange', key, start, stop, false); }; - module.getSortedSetRevRange = function (key, start, stop, callback) { - sortedSetRange('zrevrange', key, start, stop, false, callback); + module.getSortedSetRevRange = async function (key, start, stop) { + return await sortedSetRange('zrevrange', key, start, stop, false); }; - module.getSortedSetRangeWithScores = function (key, start, stop, callback) { - sortedSetRange('zrange', key, start, stop, true, callback); + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await sortedSetRange('zrange', key, start, stop, true); }; - module.getSortedSetRevRangeWithScores = function (key, start, stop, callback) { - sortedSetRange('zrevrange', key, start, stop, true, callback); + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await sortedSetRange('zrevrange', key, start, stop, true); }; - function sortedSetRange(method, key, start, stop, withScores, callback) { + async function sortedSetRange(method, key, start, stop, withScores) { if (Array.isArray(key)) { if (!key.length) { - return setImmediate(callback, null, []); + return []; } const batch = redisClient.batch(); key.forEach((key) => { batch[method]([key, start, stop, 'WITHSCORES']); }); - batch.exec(function (err, data) { - if (err) { - return callback(err); - } - data = _.flatten(data); - var objects = []; - for (var i = 0; i < data.length; i += 2) { - objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); - } + let data = await helpers.execBatch(batch); + data = _.flatten(data); + let objects = []; + for (let i = 0; i < data.length; i += 2) { + objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); + } - objects.sort((a, b) => { - if (method === 'zrange') { - return a.score - b.score; - } - return b.score - a.score; - }); - if (withScores) { - return callback(null, objects); + objects.sort((a, b) => { + if (method === 'zrange') { + return a.score - b.score; } - objects = objects.map(item => item.value); - callback(null, objects); + return b.score - a.score; }); - return; + if (!withScores) { + objects = objects.map(item => item.value); + } + return objects; } var params = [key, start, stop]; @@ -66,238 +59,193 @@ module.exports = function (redisClient, module) { params.push('WITHSCORES'); } - redisClient[method](params, function (err, data) { - if (err) { - return callback(err); - } - if (!withScores) { - return callback(null, data); - } - var objects = []; - for (var i = 0; i < data.length; i += 2) { - objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); - } - callback(null, objects); - }); + const data = await redisClient.async[method](params); + if (!withScores) { + return data; + } + const objects = []; + for (var i = 0; i < data.length; i += 2) { + objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); + } + return objects; } - module.getSortedSetRangeByScore = function (key, start, count, min, max, callback) { - redisClient.zrangebyscore([key, min, max, 'LIMIT', start, count], callback); + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await redisClient.async.zrangebyscore([key, min, max, 'LIMIT', start, count]); }; - module.getSortedSetRevRangeByScore = function (key, start, count, max, min, callback) { - redisClient.zrevrangebyscore([key, max, min, 'LIMIT', start, count], callback); + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await redisClient.async.zrevrangebyscore([key, max, min, 'LIMIT', start, count]); }; - module.getSortedSetRangeByScoreWithScores = function (key, start, count, min, max, callback) { - sortedSetRangeByScoreWithScores('zrangebyscore', key, start, count, min, max, callback); + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await sortedSetRangeByScoreWithScores('zrangebyscore', key, start, count, min, max); }; - module.getSortedSetRevRangeByScoreWithScores = function (key, start, count, max, min, callback) { - sortedSetRangeByScoreWithScores('zrevrangebyscore', key, start, count, max, min, callback); + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await sortedSetRangeByScoreWithScores('zrevrangebyscore', key, start, count, max, min); }; - function sortedSetRangeByScoreWithScores(method, key, start, count, min, max, callback) { - redisClient[method]([key, min, max, 'WITHSCORES', 'LIMIT', start, count], function (err, data) { - if (err) { - return callback(err); - } - var objects = []; - for (var i = 0; i < data.length; i += 2) { - objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); - } - callback(null, objects); - }); + async function sortedSetRangeByScoreWithScores(method, key, start, count, min, max) { + const data = await redisClient.async[method]([key, min, max, 'WITHSCORES', 'LIMIT', start, count]); + const objects = []; + for (var i = 0; i < data.length; i += 2) { + objects.push({ value: data[i], score: parseFloat(data[i + 1]) }); + } + return objects; } - module.sortedSetCount = function (key, min, max, callback) { - redisClient.zcount(key, min, max, callback); + module.sortedSetCount = async function (key, min, max) { + return await redisClient.async.zcount(key, min, max); }; - module.sortedSetCard = function (key, callback) { - redisClient.zcard(key, callback); + module.sortedSetCard = async function (key) { + return await redisClient.async.zcard(key); }; - module.sortedSetsCard = function (keys, callback) { + module.sortedSetsCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } var batch = redisClient.batch(); - for (var i = 0; i < keys.length; i += 1) { - batch.zcard(keys[i]); - } - batch.exec(callback); + keys.forEach(k => batch.zcard(String(k))); + return await helpers.execBatch(batch); }; - module.sortedSetsCardSum = function (keys, callback) { + module.sortedSetsCardSum = async function (keys) { if (!keys || (Array.isArray(keys) && !keys.length)) { - return callback(null, 0); + return 0; } if (!Array.isArray(keys)) { keys = [keys]; } - module.sortedSetsCard(keys, function (err, counts) { - if (err) { - return callback(err); - } - const sum = counts.reduce(function (acc, val) { return acc + val; }, 0); - callback(null, sum); - }); + const counts = await module.sortedSetsCard(keys); + const sum = counts.reduce((acc, val) => acc + val, 0); + return sum; }; - module.sortedSetRank = function (key, value, callback) { - redisClient.zrank(key, value, callback); + module.sortedSetRank = async function (key, value) { + return await redisClient.async.zrank(key, value); }; - module.sortedSetRevRank = function (key, value, callback) { - redisClient.zrevrank(key, value, callback); + module.sortedSetRevRank = async function (key, value) { + return await redisClient.async.zrevrank(key, value); }; - module.sortedSetsRanks = function (keys, values, callback) { - var batch = redisClient.batch(); + module.sortedSetsRanks = async function (keys, values) { + const batch = redisClient.batch(); for (var i = 0; i < values.length; i += 1) { - batch.zrank(keys[i], values[i]); + batch.zrank(keys[i], String(values[i])); } - batch.exec(callback); + return await helpers.execBatch(batch); }; - module.sortedSetsRevRanks = function (keys, values, callback) { - var batch = redisClient.batch(); + module.sortedSetsRevRanks = async function (keys, values) { + const batch = redisClient.batch(); for (var i = 0; i < values.length; i += 1) { - batch.zrevrank(keys[i], values[i]); + batch.zrevrank(keys[i], String(values[i])); } - batch.exec(callback); + return await helpers.execBatch(batch); }; - module.sortedSetRanks = function (key, values, callback) { - var batch = redisClient.batch(); + module.sortedSetRanks = async function (key, values) { + const batch = redisClient.batch(); for (var i = 0; i < values.length; i += 1) { - batch.zrank(key, values[i]); + batch.zrank(key, String(values[i])); } - batch.exec(callback); + return await helpers.execBatch(batch); }; - module.sortedSetRevRanks = function (key, values, callback) { - var batch = redisClient.batch(); + module.sortedSetRevRanks = async function (key, values) { + const batch = redisClient.batch(); for (var i = 0; i < values.length; i += 1) { - batch.zrevrank(key, values[i]); + batch.zrevrank(key, String(values[i])); } - batch.exec(callback); + return await helpers.execBatch(batch); }; - module.sortedSetScore = function (key, value, callback) { + module.sortedSetScore = async function (key, value) { if (!key || value === undefined) { - return callback(null, null); + return null; } - redisClient.zscore(key, value, function (err, score) { - if (err) { - return callback(err); - } - if (score === null) { - return callback(null, score); - } - callback(null, parseFloat(score)); - }); + const score = await redisClient.async.zscore(key, value); + return score === null ? score : parseFloat(score); }; - module.sortedSetsScore = function (keys, value, callback) { + module.sortedSetsScore = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, []); + return []; } - helpers.execKeysValue(redisClient, 'batch', 'zscore', keys, value, function (err, scores) { - if (err) { - return callback(err); - } - scores = scores.map(function (d) { - return d === null ? d : parseFloat(d); - }); - callback(null, scores); - }); + const batch = redisClient.batch(); + keys.forEach(key => batch.zscore(String(key), String(value))); + const scores = await helpers.execBatch(batch); + return scores.map(d => (d === null ? d : parseFloat(d))); }; - module.sortedSetScores = function (key, values, callback) { + module.sortedSetScores = async function (key, values) { if (!values.length) { - return setImmediate(callback, null, []); + return []; } - helpers.execKeyValues(redisClient, 'batch', 'zscore', key, values, function (err, scores) { - if (err) { - return callback(err); - } - scores = scores.map(function (d) { - return d === null ? d : parseFloat(d); - }); - callback(null, scores); - }); + const batch = redisClient.batch(); + values.forEach(value => batch.zscore(String(key), String(value))); + const scores = await helpers.execBatch(batch); + return scores.map(d => (d === null ? d : parseFloat(d))); }; - module.isSortedSetMember = function (key, value, callback) { - module.sortedSetScore(key, value, function (err, score) { - callback(err, utils.isNumber(score)); - }); + module.isSortedSetMember = async function (key, value) { + const score = await module.sortedSetScore(key, value); + return utils.isNumber(score); }; - module.isSortedSetMembers = function (key, values, callback) { - helpers.execKeyValues(redisClient, 'batch', 'zscore', key, values, function (err, results) { - if (err) { - return callback(err); - } - callback(null, results.map(Boolean)); - }); + module.isSortedSetMembers = async function (key, values) { + const batch = redisClient.batch(); + values.forEach(v => batch.zscore(key, String(v))); + const results = await helpers.execBatch(batch); + return results.map(utils.isNumber); }; - module.isMemberOfSortedSets = function (keys, value, callback) { + module.isMemberOfSortedSets = async function (keys, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } - helpers.execKeysValue(redisClient, 'batch', 'zscore', keys, value, function (err, results) { - if (err) { - return callback(err); - } - callback(null, results.map(Boolean)); - }); + const batch = redisClient.batch(); + keys.forEach(k => batch.zscore(k, String(value))); + const results = await helpers.execBatch(batch); + return results.map(utils.isNumber); }; - module.getSortedSetsMembers = function (keys, callback) { + module.getSortedSetsMembers = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback, null, []); + return []; } var batch = redisClient.batch(); - for (var i = 0; i < keys.length; i += 1) { - batch.zrange(keys[i], 0, -1); - } - batch.exec(callback); + keys.forEach(k => batch.zrange(k, 0, -1)); + return await helpers.execBatch(batch); }; - module.sortedSetIncrBy = function (key, increment, value, callback) { - callback = callback || helpers.noop; - redisClient.zincrby(key, increment, value, function (err, newValue) { - callback(err, !err ? parseFloat(newValue) : undefined); - }); + module.sortedSetIncrBy = async function (key, increment, value) { + const newValue = await redisClient.async.zincrby(key, increment, value); + return parseFloat(newValue); }; - module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { - sortedSetLex('zrangebylex', false, key, min, max, start, count, callback); + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex('zrangebylex', false, key, min, max, start, count); }; - module.getSortedSetRevRangeByLex = function (key, max, min, start, count, callback) { - sortedSetLex('zrevrangebylex', true, key, max, min, start, count, callback); + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex('zrevrangebylex', true, key, max, min, start, count); }; - module.sortedSetRemoveRangeByLex = function (key, min, max, callback) { - callback = callback || helpers.noop; - sortedSetLex('zremrangebylex', false, key, min, max, function (err) { - callback(err); - }); + module.sortedSetRemoveRangeByLex = async function (key, min, max) { + await sortedSetLex('zremrangebylex', false, key, min, max); }; - module.sortedSetLexCount = function (key, min, max, callback) { - sortedSetLex('zlexcount', false, key, min, max, callback); + module.sortedSetLexCount = async function (key, min, max) { + return await sortedSetLex('zlexcount', false, key, min, max); }; - function sortedSetLex(method, reverse, key, min, max, start, count, callback) { - callback = callback || start; - + async function sortedSetLex(method, reverse, key, min, max, start, count) { var minmin; var maxmax; if (reverse) { @@ -314,11 +262,10 @@ module.exports = function (redisClient, module) { if (max !== maxmax && !max.match(/^[[(]/)) { max = '[' + max; } - + const args = [key, min, max]; if (count) { - redisClient[method]([key, min, max, 'LIMIT', start, count], callback); - } else { - redisClient[method]([key, min, max], callback); + args.push('LIMIT', start, count); } + return await redisClient.async[method](args); } }; diff --git a/src/database/redis/sorted/add.js b/src/database/redis/sorted/add.js index 6cd6cc46a8..09ff7bf7f0 100644 --- a/src/database/redis/sorted/add.js +++ b/src/database/redis/sorted/add.js @@ -1,83 +1,72 @@ 'use strict'; module.exports = function (redisClient, module) { + const helpers = require('../helpers'); const utils = require('../../../utils'); - module.sortedSetAdd = function (key, score, value, callback) { - callback = callback || function () {}; + module.sortedSetAdd = async function (key, score, value) { if (!key) { - return setImmediate(callback); + return; } if (Array.isArray(score) && Array.isArray(value)) { - return sortedSetAddMulti(key, score, value, callback); + return await sortedSetAddMulti(key, score, value); } if (!utils.isNumber(score)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + score + ']]')); + throw new Error('[[error:invalid-score, ' + score + ']]'); } - redisClient.zadd(key, score, String(value), function (err) { - callback(err); - }); + await redisClient.async.zadd(key, score, String(value)); }; - function sortedSetAddMulti(key, scores, values, callback) { + async function sortedSetAddMulti(key, scores, values) { if (!scores.length || !values.length) { - return callback(); + return; } if (scores.length !== values.length) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } for (let i = 0; i < scores.length; i += 1) { if (!utils.isNumber(scores[i])) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores[i] + ']]')); + throw new Error('[[error:invalid-score, ' + scores[i] + ']]'); } } var args = [key]; - for (var i = 0; i < scores.length; i += 1) { args.push(scores[i], String(values[i])); } - - redisClient.zadd(args, function (err) { - callback(err); - }); + await redisClient.async.zadd(args); } - module.sortedSetsAdd = function (keys, scores, value, callback) { - callback = callback || function () {}; + module.sortedSetsAdd = async function (keys, scores, value) { if (!Array.isArray(keys) || !keys.length) { - return setImmediate(callback); + return; } const isArrayOfScores = Array.isArray(scores); if (!isArrayOfScores && !utils.isNumber(scores)) { - return setImmediate(callback, new Error('[[error:invalid-score, ' + scores + ']]')); + throw new Error('[[error:invalid-score, ' + scores + ']]'); } if (isArrayOfScores && scores.length !== keys.length) { - return setImmediate(callback, new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } var batch = redisClient.batch(); - for (var i = 0; i < keys.length; i += 1) { if (keys[i]) { batch.zadd(keys[i], isArrayOfScores ? scores[i] : scores, String(value)); } } - - batch.exec(function (err) { - callback(err); - }); + await helpers.execBatch(batch); }; - module.sortedSetAddBulk = function (data, callback) { + module.sortedSetAddBulk = async function (data) { if (!Array.isArray(data) || !data.length) { - return setImmediate(callback); + return; } var batch = redisClient.batch(); data.forEach(function (item) { batch.zadd(item[0], item[1], item[2]); }); - batch.exec(err => callback(err)); + await helpers.execBatch(batch); }; }; diff --git a/src/database/redis/sorted/intersect.js b/src/database/redis/sorted/intersect.js index 86240a7a34..188a7bee0c 100644 --- a/src/database/redis/sorted/intersect.js +++ b/src/database/redis/sorted/intersect.js @@ -2,9 +2,10 @@ 'use strict'; module.exports = function (redisClient, module) { - module.sortedSetIntersectCard = function (keys, callback) { + const helpers = require('../helpers'); + module.sortedSetIntersectCard = async function (keys) { if (!Array.isArray(keys) || !keys.length) { - return callback(null, 0); + return 0; } var tempSetName = 'temp_' + Date.now(); @@ -14,26 +15,21 @@ module.exports = function (redisClient, module) { multi.zinterstore(interParams); multi.zcard(tempSetName); multi.del(tempSetName); - multi.exec(function (err, results) { - if (err) { - return callback(err); - } - - callback(null, results[1] || 0); - }); + const results = await helpers.execBatch(multi); + return results[1] || 0; }; - module.getSortedSetIntersect = function (params, callback) { + module.getSortedSetIntersect = async function (params) { params.method = 'zrange'; - getSortedSetRevIntersect(params, callback); + return await getSortedSetRevIntersect(params); }; - module.getSortedSetRevIntersect = function (params, callback) { + module.getSortedSetRevIntersect = async function (params) { params.method = 'zrevrange'; - getSortedSetRevIntersect(params, callback); + return await getSortedSetRevIntersect(params); }; - function getSortedSetRevIntersect(params, callback) { + async function getSortedSetRevIntersect(params) { var sets = params.sets; var start = params.hasOwnProperty('start') ? params.start : 0; var stop = params.hasOwnProperty('stop') ? params.stop : -1; @@ -59,20 +55,16 @@ module.exports = function (redisClient, module) { multi.zinterstore(interParams); multi[params.method](rangeParams); multi.del(tempSetName); - multi.exec(function (err, results) { - if (err) { - return callback(err); - } + let results = await helpers.execBatch(multi); - if (!params.withScores) { - return callback(null, results ? results[1] : null); - } - results = results[1] || []; - var objects = []; - for (var i = 0; i < results.length; i += 2) { - objects.push({ value: results[i], score: parseFloat(results[i + 1]) }); - } - callback(null, objects); - }); + if (!params.withScores) { + return results ? results[1] : null; + } + results = results[1] || []; + var objects = []; + for (var i = 0; i < results.length; i += 2) { + objects.push({ value: results[i], score: parseFloat(results[i + 1]) }); + } + return objects; } }; diff --git a/src/database/redis/sorted/remove.js b/src/database/redis/sorted/remove.js index cbe2247b3e..d8f5000964 100644 --- a/src/database/redis/sorted/remove.js +++ b/src/database/redis/sorted/remove.js @@ -2,46 +2,36 @@ 'use strict'; module.exports = function (redisClient, module) { - var helpers = module.helpers.redis; + var helpers = require('../helpers'); - module.sortedSetRemove = function (key, value, callback) { - callback = callback || function () {}; - if (!value) { - return callback(); + module.sortedSetRemove = async function (key, value) { + if (!key) { + return; } - if (!Array.isArray(value)) { + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; + } + if (!isValueArray) { value = [value]; } if (Array.isArray(key)) { - var batch = redisClient.batch(); - key.forEach(function (key) { - batch.zrem(key, value); - }); - batch.exec(function (err) { - callback(err); - }); + const batch = redisClient.batch(); + key.forEach(k => batch.zrem(k, value)); + await helpers.execBatch(batch); } else { - helpers.execKeyValues(redisClient, 'batch', 'zrem', key, value, function (err) { - callback(err); - }); + await redisClient.async.zrem(key, value); } }; - module.sortedSetsRemove = function (keys, value, callback) { - helpers.execKeysValue(redisClient, 'batch', 'zrem', keys, value, function (err) { - callback(err); - }); + module.sortedSetsRemove = async function (keys, value) { + await module.sortedSetRemove(keys, value); }; - module.sortedSetsRemoveRangeByScore = function (keys, min, max, callback) { - callback = callback || function () {}; + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { var batch = redisClient.batch(); - for (var i = 0; i < keys.length; i += 1) { - batch.zremrangebyscore(keys[i], min, max); - } - batch.exec(function (err) { - callback(err); - }); + keys.forEach(k => batch.zremrangebyscore(k, min, max)); + await helpers.execBatch(batch); }; }; diff --git a/src/database/redis/sorted/union.js b/src/database/redis/sorted/union.js index c12f8b465b..e89fc09a1d 100644 --- a/src/database/redis/sorted/union.js +++ b/src/database/redis/sorted/union.js @@ -2,37 +2,33 @@ 'use strict'; module.exports = function (redisClient, module) { - module.sortedSetUnionCard = function (keys, callback) { + const helpers = require('../helpers'); + module.sortedSetUnionCard = async function (keys) { var tempSetName = 'temp_' + Date.now(); if (!keys.length) { - return setImmediate(callback, null, 0); + return 0; } var multi = redisClient.multi(); multi.zunionstore([tempSetName, keys.length].concat(keys)); multi.zcard(tempSetName); multi.del(tempSetName); - multi.exec(function (err, results) { - if (err) { - return callback(err); - } - - callback(null, Array.isArray(results) && results.length ? results[1] : 0); - }); + const results = await helpers.execBatch(multi); + return Array.isArray(results) && results.length ? results[1] : 0; }; - module.getSortedSetUnion = function (params, callback) { + module.getSortedSetUnion = async function (params) { params.method = 'zrange'; - module.sortedSetUnion(params, callback); + return await module.sortedSetUnion(params); }; - module.getSortedSetRevUnion = function (params, callback) { + module.getSortedSetRevUnion = async function (params) { params.method = 'zrevrange'; - module.sortedSetUnion(params, callback); + return await module.sortedSetUnion(params); }; - module.sortedSetUnion = function (params, callback) { + module.sortedSetUnion = async function (params) { if (!params.sets.length) { - return setImmediate(callback, null, []); + return []; } var tempSetName = 'temp_' + Date.now(); @@ -46,19 +42,15 @@ module.exports = function (redisClient, module) { multi.zunionstore([tempSetName, params.sets.length].concat(params.sets)); multi[params.method](rangeParams); multi.del(tempSetName); - multi.exec(function (err, results) { - if (err) { - return callback(err); - } - if (!params.withScores) { - return callback(null, results ? results[1] : null); - } - results = results[1] || []; - var objects = []; - for (var i = 0; i < results.length; i += 2) { - objects.push({ value: results[i], score: parseFloat(results[i + 1]) }); - } - callback(null, objects); - }); + let results = await helpers.execBatch(multi); + if (!params.withScores) { + return results ? results[1] : null; + } + results = results[1] || []; + var objects = []; + for (var i = 0; i < results.length; i += 2) { + objects.push({ value: results[i], score: parseFloat(results[i + 1]) }); + } + return objects; }; }; diff --git a/src/file.js b/src/file.js index 1451179c1a..08024b883d 100644 --- a/src/file.js +++ b/src/file.js @@ -98,19 +98,15 @@ file.base64ToLocal = function (imageData, uploadPath, callback) { }); }; -file.isFileTypeAllowed = function (path, callback) { +file.isFileTypeAllowed = async function (path) { var plugins = require('./plugins'); if (plugins.hasListeners('filter:file.isFileTypeAllowed')) { - return plugins.fireHook('filter:file.isFileTypeAllowed', path, function (err) { - callback(err); - }); + return await plugins.fireHook('filter:file.isFileTypeAllowed', path); } - - require('sharp')(path, { + const sharp = require('sharp'); + await sharp(path, { failOnError: true, - }).metadata(function (err) { - callback(err); - }); + }).metadata(); }; // https://stackoverflow.com/a/31205878/583363 @@ -263,3 +259,5 @@ file.walk = function (dir, done) { }); }); }; + +require('./promisify')(file); diff --git a/src/image.js b/src/image.js index 791c37c94a..803ce0960b 100644 --- a/src/image.js +++ b/src/image.js @@ -6,6 +6,8 @@ var path = require('path'); var crypto = require('crypto'); var async = require('async'); var winston = require('winston'); +const util = require('util'); +const readFileAsync = util.promisify(fs.readFile); var file = require('./file'); var plugins = require('./plugins'); @@ -22,43 +24,31 @@ function requireSharp() { return sharp; } -image.resizeImage = function (data, callback) { +image.resizeImage = async function (data) { if (plugins.hasListeners('filter:image.resize')) { - plugins.fireHook('filter:image.resize', { + await plugins.fireHook('filter:image.resize', { path: data.path, target: data.target, width: data.width, height: data.height, quality: data.quality, - }, function (err) { - callback(err); }); } else { - var sharpImage; - async.waterfall([ - function (next) { - fs.readFile(data.path, next); - }, - function (buffer, next) { - var sharp = requireSharp(); - sharpImage = sharp(buffer, { - failOnError: true, - }); - sharpImage.metadata(next); - }, - function (metadata, next) { - sharpImage.rotate(); // auto-orients based on exif data - sharpImage.resize(data.hasOwnProperty('width') ? data.width : null, data.hasOwnProperty('height') ? data.height : null); - - if (data.quality && metadata.format === 'jpeg') { - sharpImage.jpeg({ quality: data.quality }); - } - - sharpImage.toFile(data.target || data.path, next); - }, - ], function (err) { - callback(err); + const sharp = requireSharp(); + const buffer = await readFileAsync(data.path); + const sharpImage = sharp(buffer, { + failOnError: true, }); + const metadata = await sharpImage.metadata(); + + sharpImage.rotate(); // auto-orients based on exif data + sharpImage.resize(data.hasOwnProperty('width') ? data.width : null, data.hasOwnProperty('height') ? data.height : null); + + if (data.quality && metadata.format === 'jpeg') { + sharpImage.jpeg({ quality: data.quality }); + } + + await sharpImage.toFile(data.target || data.path); } }; @@ -189,3 +179,5 @@ image.uploadImage = function (filename, folder, image, callback) { }, ], callback); }; + +require('./promisify')(image); diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index eb055a120d..de44274169 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -186,16 +186,14 @@ module.exports = function (Messaging) { Messaging.leaveRooms = function (uid, roomIds, callback) { async.waterfall([ function (next) { - var roomKeys = roomIds.map(function (roomId) { - return 'chat:room:' + roomId + ':uids'; - }); + const roomKeys = roomIds.map(roomId => 'chat:room:' + roomId + ':uids'); db.sortedSetsRemove(roomKeys, uid, next); }, function (next) { - db.sortedSetRemove('uid:' + uid + ':chat:rooms', roomIds, next); - }, - function (next) { - db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomIds, next); + db.sortedSetRemove([ + 'uid:' + uid + ':chat:rooms', + 'uid:' + uid + ':chat:rooms:unread', + ], roomIds, next); }, function (next) { async.eachSeries(roomIds, updateOwner, next); diff --git a/src/posts/uploads.js b/src/posts/uploads.js index 83aefb45e2..ae50b73e55 100644 --- a/src/posts/uploads.js +++ b/src/posts/uploads.js @@ -57,8 +57,8 @@ module.exports = function (Posts) { }; Posts.uploads.listWithSizes = async function (pid) { - const paths = await Posts.async.uploads.list(pid); - const sizes = await db.async.getObjects(paths.map(path => 'upload:' + md5(path))) || []; + const paths = await Posts.uploads.list(pid); + const sizes = await db.getObjects(paths.map(path => 'upload:' + md5(path))) || []; return sizes.map((sizeObj, idx) => ({ ...sizeObj, diff --git a/src/promisify.js b/src/promisify.js index 7b6721f8ec..6830adc40e 100644 --- a/src/promisify.js +++ b/src/promisify.js @@ -12,23 +12,93 @@ module.exports = function (theModule, ignoreKeys) { var str = func.toString().split('\n')[0]; return str.includes('callback)'); } - function promisifyRecursive(module) { + + function isAsyncFunction(fn) { + return fn && fn.constructor && fn.constructor.name === 'AsyncFunction'; + } + + var parts = []; + function promisifyRecursive(module, key) { if (!module) { return; } + if (key) { + parts.push(key); + } var keys = Object.keys(module); keys.forEach(function (key) { if (ignoreKeys.includes(key)) { return; } - if (isCallbackedFunction(module[key])) { - module[key] = util.promisify(module[key]); + if (isAsyncFunction(module[key])) { + module[key] = wrapIt(module[key], util.callbackify(module[key])); + } else if (isCallbackedFunction(module[key])) { + module[key] = wrapTwo(module[key], util.promisify(module[key])); } else if (typeof module[key] === 'object') { - promisifyRecursive(module[key]); + promisifyRecursive(module[key], key); } }); + parts.pop(); } + + function wrapTwo(origFn, promiseFn) { + return function wrapper2(...args) { + if (arguments.length && typeof arguments[arguments.length - 1] === 'function') { + return origFn.apply(null, args); + } + + return promiseFn.apply(null, arguments); + }; + } + + function wrapIt(origFn, callbackFn) { + return async function wrapper(...args) { + if (arguments.length && typeof arguments[arguments.length - 1] === 'function') { + const cb = args.pop(); + args.push(function (err, res) { + if (err) { + return cb(err); + } + + // fixes callbackified functions used in async.waterfall + if (res !== undefined) { + return cb(err, res); + } + return cb(err); + }); + return callbackFn.apply(null, args); + } + return origFn.apply(null, arguments); + }; + } + + function deprecateRecursive(module, key) { + if (!module) { + return; + } + if (key) { + parts.push(key); + } + var keys = Object.keys(module); + keys.forEach(function (key) { + if (ignoreKeys.includes(key)) { + return; + } + + if (typeof module[key] === 'object') { + deprecateRecursive(module[key], key); + } + + if (typeof module[key] === 'function') { + module[key] = require('util').deprecate(module[key], '.async.' + (parts.concat([key]).join('.')) + ' usage is deprecated use .' + (parts.concat([key]).join('.')) + ' directly!'); + } + }); + parts.pop(); + } + + promisifyRecursive(theModule); const asyncModule = _.cloneDeep(theModule); - promisifyRecursive(asyncModule); + deprecateRecursive(asyncModule); + return asyncModule; }; diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 8cf4e38c90..b3bf038b8b 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -5,8 +5,19 @@ var helpers = module.exports; helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) { middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares); - router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller); - router.get('/api' + name, middlewares, controller); + async function tryRoute(req, res, next) { + if (controller && controller.constructor && controller.constructor.name === 'AsyncFunction') { + try { + return await controller(req, res, next); + } catch (err) { + return next(err); + } + } + controller(req, res, next); + } + + router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, tryRoute); + router.get('/api' + name, middlewares, tryRoute); }; helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, controller) { diff --git a/src/search.js b/src/search.js index e004d4cfef..cfec49f386 100644 --- a/src/search.js +++ b/src/search.js @@ -369,3 +369,5 @@ function getSearchUids(data, callback) { setImmediate(callback, null, []); } } + +search.async = require('./promisify')(search); diff --git a/src/social.js b/src/social.js index ca177ce7ad..78ad13f30b 100644 --- a/src/social.js +++ b/src/social.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var plugins = require('./plugins'); var db = require('./database'); @@ -8,9 +7,9 @@ var social = module.exports; social.postSharing = null; -social.getPostSharing = function (callback) { +social.getPostSharing = async function () { if (social.postSharing) { - return setImmediate(callback, null, social.postSharing); + return social.postSharing; } var networks = [ @@ -25,51 +24,28 @@ social.getPostSharing = function (callback) { class: 'fa-twitter', }, ]; + networks = await plugins.fireHook('filter:social.posts', networks); + const activated = await db.getSetMembers('social:posts.activated'); + networks.forEach(function (network) { + network.activated = activated.includes(network.id); + }); - async.waterfall([ - function (next) { - plugins.fireHook('filter:social.posts', networks, next); - }, - function (networks, next) { - db.getSetMembers('social:posts.activated', next); - }, - function (activated, next) { - networks.forEach(function (network) { - network.activated = activated.includes(network.id); - }); - - social.postSharing = networks; - next(null, networks); - }, - ], callback); + social.postSharing = networks; + return networks; }; -social.getActivePostSharing = function (callback) { - async.waterfall([ - function (next) { - social.getPostSharing(next); - }, - function (networks, next) { - networks = networks.filter(network => network && network.activated); - next(null, networks); - }, - ], callback); +social.getActivePostSharing = async function () { + const networks = await social.getPostSharing(); + return networks.filter(network => network && network.activated); }; -social.setActivePostSharingNetworks = function (networkIDs, callback) { - async.waterfall([ - function (next) { - db.delete('social:posts.activated', next); - }, - function (next) { - if (!networkIDs.length) { - return next(); - } - db.setAdd('social:posts.activated', networkIDs, next); - }, - function (next) { - social.postSharing = null; - next(); - }, - ], callback); +social.setActivePostSharingNetworks = async function (networkIDs) { + await db.delete('social:posts.activated'); + if (!networkIDs.length) { + return; + } + await db.setAdd('social:posts.activated', networkIDs); + social.postSharing = null; }; + +require('./promisify')(social); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index b3b9ef8492..959dcc7fb5 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -252,7 +252,7 @@ SocketUser.getUnreadCount = function (socket, data, callback) { if (!socket.uid) { return callback(null, 0); } - topics.getTotalUnread(socket.uid, callback); + topics.getTotalUnread(socket.uid, '', callback); }; SocketUser.getUnreadChatCount = function (socket, data, callback) { diff --git a/src/topics/bookmarks.js b/src/topics/bookmarks.js index ff62ef289d..c9188791be 100644 --- a/src/topics/bookmarks.js +++ b/src/topics/bookmarks.js @@ -7,92 +7,60 @@ var db = require('../database'); var user = require('../user'); module.exports = function (Topics) { - Topics.getUserBookmark = function (tid, uid, callback) { + Topics.getUserBookmark = async function (tid, uid) { if (parseInt(uid, 10) <= 0) { - return callback(null, null); + return null; } - db.sortedSetScore('tid:' + tid + ':bookmarks', uid, callback); + return await db.sortedSetScore('tid:' + tid + ':bookmarks', uid); }; - Topics.getUserBookmarks = function (tids, uid, callback) { + Topics.getUserBookmarks = async function (tids, uid) { if (parseInt(uid, 10) <= 0) { - return callback(null, tids.map(() => null)); + return tids.map(() => null); } - db.sortedSetsScore(tids.map(tid => 'tid:' + tid + ':bookmarks'), uid, callback); + return await db.sortedSetsScore(tids.map(tid => 'tid:' + tid + ':bookmarks'), uid); }; - Topics.setUserBookmark = function (tid, uid, index, callback) { - db.sortedSetAdd('tid:' + tid + ':bookmarks', index, uid, callback); + Topics.setUserBookmark = async function (tid, uid, index) { + await db.sortedSetAdd('tid:' + tid + ':bookmarks', index, uid); }; - Topics.getTopicBookmarks = function (tid, callback) { - db.getSortedSetRangeWithScores('tid:' + tid + ':bookmarks', 0, -1, callback); + Topics.getTopicBookmarks = async function (tid) { + return await db.getSortedSetRangeWithScores('tid:' + tid + ':bookmarks', 0, -1); }; - Topics.updateTopicBookmarks = function (tid, pids, callback) { - var minIndex; - var maxIndex; - var postIndices; + Topics.updateTopicBookmarks = async function (tid, pids) { + const maxIndex = await Topics.getPostCount(tid); + const indices = await db.sortedSetRanks('tid:' + tid + ':posts', pids); + const postIndices = indices.map(i => (i === null ? 0 : i + 1)); + const minIndex = Math.min.apply(Math, postIndices); - async.waterfall([ - function (next) { - Topics.getPostCount(tid, next); - }, - function (postcount, next) { - maxIndex = postcount; + const bookmarks = await Topics.getTopicBookmarks(tid); - db.sortedSetRanks('tid:' + tid + ':posts', pids, next); - }, - function (indices, next) { - postIndices = indices.map(function (i) { - return i === null ? 0 : i + 1; - }); - minIndex = Math.min.apply(Math, postIndices); + var uidData = bookmarks.map(b => ({ uid: b.value, bookmark: parseInt(b.score, 10) })) + .filter(data => data.bookmark >= minIndex); - Topics.getTopicBookmarks(tid, next); - }, - function (bookmarks, next) { - var uidData = bookmarks.map(function (bookmark) { - return { - uid: bookmark.value, - bookmark: parseInt(bookmark.score, 10), - }; - }).filter(function (data) { - return data.bookmark >= minIndex; - }); + await async.eachLimit(uidData, 50, async function (data) { + var bookmark = Math.min(data.bookmark, maxIndex); - async.eachLimit(uidData, 50, function (data, next) { - var bookmark = data.bookmark; - bookmark = Math.min(bookmark, maxIndex); + postIndices.forEach(function (i) { + if (i < data.bookmark) { + bookmark -= 1; + } + }); - postIndices.forEach(function (i) { - if (i < data.bookmark) { - bookmark -= 1; - } - }); + // make sure the bookmark is valid if we removed the last post + bookmark = Math.min(bookmark, maxIndex - pids.length); + if (bookmark === data.bookmark) { + return; + } - // make sure the bookmark is valid if we removed the last post - bookmark = Math.min(bookmark, maxIndex - pids.length); + const settings = await user.getSettings(data.uid); + if (settings.topicPostSort === 'most_votes') { + return; + } - if (bookmark === data.bookmark) { - return next(); - } - - user.getSettings(data.uid, function (err, settings) { - if (err) { - return next(err); - } - - if (settings.topicPostSort === 'most_votes') { - return next(); - } - - Topics.setUserBookmark(tid, data.uid, bookmark, next); - }); - }, next); - }, - ], function (err) { - callback(err); + await Topics.setUserBookmark(tid, data.uid, bookmark); }); }; }; diff --git a/src/topics/create.js b/src/topics/create.js index 60c2ea78e4..21ed17fe79 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var validator = require('validator'); @@ -16,350 +15,236 @@ var privileges = require('../privileges'); var categories = require('../categories'); module.exports = function (Topics) { - Topics.create = function (data, callback) { + Topics.create = async function (data) { // This is an internal method, consider using Topics.post instead var timestamp = data.timestamp || Date.now(); - var topicData; + await Topics.resizeAndUploadThumb(data); - async.waterfall([ - function (next) { - Topics.resizeAndUploadThumb(data, next); - }, - function (next) { - db.incrObjectField('global', 'nextTid', next); - }, - function (tid, next) { - topicData = { - tid: tid, - uid: data.uid, - cid: data.cid, - mainPid: 0, - title: data.title, - slug: tid + '/' + (utils.slugify(data.title) || 'topic'), - timestamp: timestamp, - lastposttime: 0, - postcount: 0, - viewcount: 0, - locked: 0, - deleted: 0, - pinned: 0, - }; + const tid = await db.incrObjectField('global', 'nextTid'); - if (data.thumb) { - topicData.thumb = data.thumb; - } + let topicData = { + tid: tid, + uid: data.uid, + cid: data.cid, + mainPid: 0, + title: data.title, + slug: tid + '/' + (utils.slugify(data.title) || 'topic'), + timestamp: timestamp, + lastposttime: 0, + postcount: 0, + viewcount: 0, + locked: 0, + deleted: 0, + pinned: 0, + }; + if (data.thumb) { + topicData.thumb = data.thumb; + } + const result = await plugins.fireHook('filter:topic.create', { topic: topicData, data: data }); + topicData = result.topic; + await db.setObject('topic:' + topicData.tid, topicData); - plugins.fireHook('filter:topic.create', { topic: topicData, data: data }, next); - }, - function (data, next) { - topicData = data.topic; - db.setObject('topic:' + topicData.tid, topicData, next); - }, - function (next) { - async.parallel([ - function (next) { - db.sortedSetsAdd([ - 'topics:tid', - 'cid:' + topicData.cid + ':tids', - 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', - ], timestamp, topicData.tid, next); - }, - function (next) { - db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', 0, topicData.tid, next); - }, - function (next) { - categories.updateRecentTid(topicData.cid, topicData.tid, next); - }, - function (next) { - user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp, next); - }, - function (next) { - db.incrObjectField('category:' + topicData.cid, 'topic_count', next); - }, - function (next) { - db.incrObjectField('global', 'topicCount', next); - }, - function (next) { - Topics.createTags(data.tags, topicData.tid, timestamp, next); - }, - ], next); - }, - function (results, next) { - plugins.fireHook('action:topic.save', { topic: _.clone(topicData), data: data }); - next(null, topicData.tid); - }, - ], callback); + await Promise.all([ + db.sortedSetsAdd([ + 'topics:tid', + 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', + ], timestamp, topicData.tid), + db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', 0, topicData.tid), + categories.updateRecentTid(topicData.cid, topicData.tid), + user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp), + db.incrObjectField('category:' + topicData.cid, 'topic_count'), + db.incrObjectField('global', 'topicCount'), + Topics.createTags(data.tags, topicData.tid, timestamp), + ]); + + plugins.fireHook('action:topic.save', { topic: _.clone(topicData), data: data }); + return topicData.tid; }; - Topics.post = function (data, callback) { + Topics.post = async function (data) { var uid = data.uid; data.title = String(data.title).trim(); data.tags = data.tags || []; + if (data.content) { + data.content = utils.rtrim(data.content); + } + check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long'); + check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags'); + check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long'); - async.waterfall([ - function (next) { - check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next); - }, - function (next) { - check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags', next); - }, - function (next) { - if (data.content) { - data.content = utils.rtrim(data.content); - } + const [categoryExists, canCreate, canTag] = await Promise.all([ + categories.exists(data.cid), + privileges.categories.can('topics:create', data.cid, data.uid), + privileges.categories.can('topics:tag', data.cid, data.uid), + ]); - check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); - }, - function (next) { - async.parallel({ - categoryExists: function (next) { - categories.exists(data.cid, next); - }, - canCreate: function (next) { - privileges.categories.can('topics:create', data.cid, data.uid, next); - }, - canTag: function (next) { - if (!data.tags.length) { - return next(null, true); - } - privileges.categories.can('topics:tag', data.cid, data.uid, next); - }, - }, next); - }, - function (results, next) { - if (!results.categoryExists) { - return next(new Error('[[error:no-category]]')); - } + if (!categoryExists) { + throw new Error('[[error:no-category]]'); + } - if (!results.canCreate || !results.canTag) { - return next(new Error('[[error:no-privileges]]')); - } + if (!canCreate || (!canTag && data.tags.length)) { + throw new Error('[[error:no-privileges]]'); + } - guestHandleValid(data, next); - }, - function (next) { - user.isReadyToPost(data.uid, data.cid, next); - }, - function (next) { - plugins.fireHook('filter:topic.post', data, next); - }, - function (filteredData, next) { - data = filteredData; - Topics.create(data, next); - }, - function (tid, next) { - var postData = data; - postData.tid = tid; - postData.ip = data.req ? data.req.ip : null; - postData.isMain = true; - posts.create(postData, next); - }, - function (postData, next) { - onNewPost(postData, data, next); - }, - function (postData, next) { - async.parallel({ - postData: function (next) { - next(null, postData); - }, - settings: function (next) { - user.getSettings(uid, function (err, settings) { - if (err) { - return next(err); - } - if (settings.followTopicsOnCreate) { - Topics.follow(postData.tid, uid, next); - } else { - next(); - } - }); - }, - topicData: function (next) { - Topics.getTopicsByTids([postData.tid], uid, next); - }, - }, next); - }, - function (result, next) { - if (!Array.isArray(result.topicData) || !result.topicData.length) { - return next(new Error('[[error:no-topic]]')); - } + await guestHandleValid(data); + await user.isReadyToPost(data.uid, data.cid); + const filteredData = await plugins.fireHook('filter:topic.post', data); + data = filteredData; + const tid = await Topics.create(data); - result.topicData = result.topicData[0]; - result.topicData.unreplied = 1; - result.topicData.mainPost = result.postData; - result.postData.index = 0; + let postData = data; + postData.tid = tid; + postData.ip = data.req ? data.req.ip : null; + postData.isMain = true; + postData = await posts.create(postData); + postData = await onNewPost(postData, data); - analytics.increment(['topics', 'topics:byCid:' + result.topicData.cid]); - plugins.fireHook('action:topic.post', { topic: result.topicData, post: result.postData, data: data }); + const [settings, topics] = await Promise.all([ + user.getSettings(uid), + Topics.getTopicsByTids([postData.tid], uid), + ]); - if (parseInt(uid, 10)) { - user.notifications.sendTopicNotificationToFollowers(uid, result.topicData, result.postData); - } + if (!Array.isArray(topics) || !topics.length) { + throw new Error('[[error:no-topic]]'); + } - next(null, { - topicData: result.topicData, - postData: result.postData, - }); - }, - ], callback); + if (settings.followTopicsOnCreate) { + await Topics.follow(postData.tid, uid); + } + const topicData = topics[0]; + topicData.unreplied = 1; + topicData.mainPost = postData; + postData.index = 0; + + analytics.increment(['topics', 'topics:byCid:' + topicData.cid]); + plugins.fireHook('action:topic.post', { topic: topicData, post: postData, data: data }); + + if (parseInt(uid, 10)) { + user.notifications.sendTopicNotificationToFollowers(uid, topicData, postData); + } + + return { + topicData: topicData, + postData: postData, + }; }; - Topics.reply = function (data, callback) { + Topics.reply = async function (data) { var tid = data.tid; var uid = data.uid; - var postData; - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - data.cid = cid; - async.parallel({ - topicData: async.apply(Topics.getTopicData, tid), - canReply: async.apply(privileges.topics.can, 'topics:reply', tid, uid), - isAdminOrMod: async.apply(privileges.categories.isAdminOrMod, data.cid, uid), - }, next); - }, - function (results, next) { - if (!results.topicData) { - return next(new Error('[[error:no-topic]]')); - } + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } - if (results.topicData.locked && !results.isAdminOrMod) { - return next(new Error('[[error:topic-locked]]')); - } + data.cid = topicData.cid; - if (results.topicData.deleted && !results.isAdminOrMod) { - return next(new Error('[[error:topic-deleted]]')); - } + const [canReply, isAdminOrMod] = await Promise.all([ + privileges.topics.can('topics:reply', tid, uid), + privileges.categories.isAdminOrMod(data.cid, uid), + ]); - if (!results.canReply) { - return next(new Error('[[error:no-privileges]]')); - } + if (topicData.locked && !isAdminOrMod) { + throw new Error('[[error:topic-locked]]'); + } - guestHandleValid(data, next); - }, - function (next) { - user.isReadyToPost(uid, data.cid, next); - }, - function (next) { - plugins.fireHook('filter:topic.reply', data, next); - }, - function (filteredData, next) { - if (data.content) { - data.content = utils.rtrim(data.content); - } + if (topicData.deleted && !isAdminOrMod) { + throw new Error('[[error:topic-deleted]]'); + } - check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); - }, - function (next) { - data.ip = data.req ? data.req.ip : null; - posts.create(data, next); - }, - function (_postData, next) { - postData = _postData; - onNewPost(postData, data, next); - }, - function (postData, next) { - user.getSettings(uid, next); - }, - function (settings, next) { - if (settings.followTopicsOnReply) { - Topics.follow(postData.tid, uid); - } + if (!canReply) { + throw new Error('[[error:no-privileges]]'); + } - if (parseInt(uid, 10)) { - user.setUserField(uid, 'lastonline', Date.now()); - } + await guestHandleValid(data); + await user.isReadyToPost(uid, data.cid); + await plugins.fireHook('filter:topic.reply', data); + if (data.content) { + data.content = utils.rtrim(data.content); + } + check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long'); - Topics.notifyFollowers(postData, uid); - analytics.increment(['posts', 'posts:byCid:' + data.cid]); - plugins.fireHook('action:topic.reply', { post: _.clone(postData), data: data }); + data.ip = data.req ? data.req.ip : null; + let postData = await posts.create(data); + postData = await onNewPost(postData, data); - next(null, postData); - }, - ], callback); + const settings = await user.getSettings(uid); + if (settings.followTopicsOnReply) { + await Topics.follow(postData.tid, uid); + } + + if (parseInt(uid, 10)) { + user.setUserField(uid, 'lastonline', Date.now()); + } + + Topics.notifyFollowers(postData, uid); + analytics.increment(['posts', 'posts:byCid:' + data.cid]); + plugins.fireHook('action:topic.reply', { post: _.clone(postData), data: data }); + + return postData; }; - function onNewPost(postData, data, callback) { + async function onNewPost(postData, data) { var tid = postData.tid; var uid = postData.uid; - async.waterfall([ - function (next) { - Topics.markAsUnreadForAll(tid, next); - }, - function (next) { - Topics.markAsRead([tid], uid, next); - }, - function (markedRead, next) { - async.parallel({ - userInfo: function (next) { - posts.getUserInfoForPosts([postData.uid], uid, next); - }, - topicInfo: function (next) { - Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid'], next); - }, - parents: function (next) { - Topics.addParentPosts([postData], next); - }, - content: function (next) { - posts.parsePost(postData, next); - }, - }, next); - }, - function (results, next) { - postData.user = results.userInfo[0]; - postData.topic = results.topicInfo; - postData.index = results.topicInfo.postcount - 1; + await Topics.markAsUnreadForAll(tid); + await Topics.markAsRead([tid], uid); + const [ + userInfo, + topicInfo, + ] = await Promise.all([ + posts.getUserInfoForPosts([postData.uid], uid), + Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid']), + Topics.addParentPosts([postData]), + posts.parsePost(postData), + ]); - // Username override for guests, if enabled - if (meta.config.allowGuestHandles && postData.uid === 0 && data.handle) { - postData.user.username = validator.escape(String(data.handle)); - } + postData.user = userInfo[0]; + postData.topic = topicInfo; + postData.index = topicInfo.postcount - 1; - postData.votes = 0; - postData.bookmarked = false; - postData.display_edit_tools = true; - postData.display_delete_tools = true; - postData.display_moderator_tools = true; - postData.display_move_tools = true; - postData.selfPost = false; - postData.timestampISO = utils.toISOString(postData.timestamp); - postData.topic.title = String(postData.topic.title); + // Username override for guests, if enabled + if (meta.config.allowGuestHandles && postData.uid === 0 && data.handle) { + postData.user.username = validator.escape(String(data.handle)); + } - next(null, postData); - }, - ], callback); + postData.votes = 0; + postData.bookmarked = false; + postData.display_edit_tools = true; + postData.display_delete_tools = true; + postData.display_moderator_tools = true; + postData.display_move_tools = true; + postData.selfPost = false; + postData.timestampISO = utils.toISOString(postData.timestamp); + postData.topic.title = String(postData.topic.title); + + return postData; } - function check(item, min, max, minError, maxError, callback) { + function check(item, min, max, minError, maxError) { // Trim and remove HTML (latter for composers that send in HTML, like redactor) if (typeof item === 'string') { item = utils.stripHTMLTags(item).trim(); } if (item === null || item === undefined || item.length < parseInt(min, 10)) { - return callback(new Error('[[error:' + minError + ', ' + min + ']]')); + throw new Error('[[error:' + minError + ', ' + min + ']]'); } else if (item.length > parseInt(max, 10)) { - return callback(new Error('[[error:' + maxError + ', ' + max + ']]')); + throw new Error('[[error:' + maxError + ', ' + max + ']]'); } - callback(); } - function guestHandleValid(data, callback) { + async function guestHandleValid(data) { if (meta.config.allowGuestHandles && parseInt(data.uid, 10) === 0 && data.handle) { if (data.handle.length > meta.config.maximumUsernameLength) { - return callback(new Error('[[error:guest-handle-invalid]]')); + throw new Error('[[error:guest-handle-invalid]]'); + } + const exists = await user.existsBySlug(utils.slugify(data.handle)); + if (exists) { + throw new Error('[[error:username-taken]]'); } - user.existsBySlug(utils.slugify(data.handle), function (err, exists) { - if (err || exists) { - return callback(err || new Error('[[error:username-taken]]')); - } - callback(); - }); - return; } - callback(); } }; diff --git a/src/topics/data.js b/src/topics/data.js index fca7dc0239..de5677e16b 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var validator = require('validator'); var db = require('../database'); @@ -15,75 +14,54 @@ const intFields = [ ]; module.exports = function (Topics) { - Topics.getTopicsFields = function (tids, fields, callback) { + Topics.getTopicsFields = async function (tids, fields) { if (!Array.isArray(tids) || !tids.length) { - return callback(null, []); + return []; } - - async.waterfall([ - function (next) { - const keys = tids.map(tid => 'topic:' + tid); - if (fields.length) { - db.getObjectsFields(keys, fields, next); - } else { - db.getObjects(keys, next); - } - }, - function (topics, next) { - topics.forEach(topic => modifyTopic(topic, fields)); - next(null, topics); - }, - ], callback); + const keys = tids.map(tid => 'topic:' + tid); + const topics = await (fields.length ? db.getObjectsFields(keys, fields) : db.getObjects(keys)); + topics.forEach(topic => modifyTopic(topic, fields)); + return topics; }; - Topics.getTopicField = function (tid, field, callback) { - Topics.getTopicFields(tid, [field], function (err, topic) { - callback(err, topic ? topic[field] : null); - }); + Topics.getTopicField = async function (tid, field) { + const topic = await Topics.getTopicFields(tid, [field]); + return topic ? topic[field] : null; }; - Topics.getTopicFields = function (tid, fields, callback) { - Topics.getTopicsFields([tid], fields, function (err, topics) { - callback(err, topics ? topics[0] : null); - }); + Topics.getTopicFields = async function (tid, fields) { + const topics = await Topics.getTopicsFields([tid], fields); + return topics ? topics[0] : null; }; - Topics.getTopicData = function (tid, callback) { - Topics.getTopicsFields([tid], [], function (err, topics) { - callback(err, topics && topics.length ? topics[0] : null); - }); + Topics.getTopicData = async function (tid) { + const topics = await Topics.getTopicsFields([tid], []); + return topics && topics.length ? topics[0] : null; }; - Topics.getTopicsData = function (tids, callback) { - Topics.getTopicsFields(tids, [], callback); + Topics.getTopicsData = async function (tids) { + return await Topics.getTopicsFields(tids, []); }; - Topics.getCategoryData = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - categories.getCategoryData(cid, next); - }, - ], callback); + Topics.getCategoryData = async function (tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + return await categories.getCategoryData(cid); }; - Topics.setTopicField = function (tid, field, value, callback) { - db.setObjectField('topic:' + tid, field, value, callback); + Topics.setTopicField = async function (tid, field, value) { + await db.setObjectField('topic:' + tid, field, value); }; - Topics.setTopicFields = function (tid, data, callback) { - callback = callback || function () {}; - db.setObject('topic:' + tid, data, callback); + Topics.setTopicFields = async function (tid, data) { + await db.setObject('topic:' + tid, data); }; - Topics.deleteTopicField = function (tid, field, callback) { - db.deleteObjectField('topic:' + tid, field, callback); + Topics.deleteTopicField = async function (tid, field) { + await db.deleteObjectField('topic:' + tid, field); }; - Topics.deleteTopicFields = function (tid, fields, callback) { - db.deleteObjectFields('topic:' + tid, fields, callback); + Topics.deleteTopicFields = async function (tid, fields) { + await db.deleteObjectFields('topic:' + tid, fields); }; }; diff --git a/src/topics/delete.js b/src/topics/delete.js index eddeabb8c6..c83b11d4b5 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -10,255 +10,144 @@ var batch = require('../batch'); module.exports = function (Topics) { - Topics.delete = function (tid, uid, callback) { - async.parallel([ - function (next) { - Topics.setTopicFields(tid, { - deleted: 1, - deleterUid: uid, - deletedTimestamp: Date.now(), - }, next); - }, - function (next) { - db.sortedSetsRemove([ - 'topics:recent', - 'topics:posts', - 'topics:views', - 'topics:votes', - ], tid, next); - }, - function (next) { - async.waterfall([ - function (next) { - async.parallel({ - cid: function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - pids: function (next) { - Topics.getPids(tid, next); - }, - }, next); - }, - function (results, next) { - db.sortedSetRemove('cid:' + results.cid + ':pids', results.pids, next); - }, - ], next); - }, - ], function (err) { - callback(err); - }); + Topics.delete = async function (tid, uid) { + await Promise.all([ + Topics.setTopicFields(tid, { + deleted: 1, + deleterUid: uid, + deletedTimestamp: Date.now(), + }), + db.sortedSetsRemove([ + 'topics:recent', + 'topics:posts', + 'topics:views', + 'topics:votes', + ], tid), + removeTopicPidsFromCid(tid), + ]); }; - Topics.restore = function (tid, uid, callback) { - var topicData; - async.waterfall([ - function (next) { - Topics.getTopicData(tid, next); - }, - function (_topicData, next) { - topicData = _topicData; - async.parallel([ - function (next) { - Topics.setTopicField(tid, 'deleted', 0, next); - }, - function (next) { - Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp'], next); - }, - function (next) { - Topics.updateRecent(tid, topicData.lastposttime, next); - }, - function (next) { - db.sortedSetAddBulk([ - ['topics:posts', topicData.postcount, tid], - ['topics:views', topicData.viewcount, tid], - ['topics:votes', parseInt(topicData.votes, 10) || 0, tid], - ], next); - }, - function (next) { - async.waterfall([ - function (next) { - Topics.getPids(tid, next); - }, - function (pids, next) { - posts.getPostsFields(pids, ['pid', 'timestamp', 'deleted'], next); - }, - function (postData, next) { - postData = postData.filter(post => post && !post.deleted); - var pidsToAdd = []; - var scores = []; - postData.forEach(function (post) { - pidsToAdd.push(post.pid); - scores.push(post.timestamp); - }); - db.sortedSetAdd('cid:' + topicData.cid + ':pids', scores, pidsToAdd, next); - }, - ], next); - }, - ], function (err) { - next(err); - }); - }, - ], callback); - }; - - Topics.purgePostsAndTopic = function (tid, uid, callback) { - var mainPid; - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'mainPid', next); - }, - function (_mainPid, next) { - mainPid = _mainPid; - batch.processSortedSet('tid:' + tid + ':posts', function (pids, next) { - async.eachSeries(pids, function (pid, next) { - posts.purge(pid, uid, next); - }, next); - }, { alwaysStartAt: 0 }, next); - }, - function (next) { - posts.purge(mainPid, uid, next); - }, - function (next) { - Topics.purge(tid, uid, next); - }, - ], callback); - }; - - Topics.purge = function (tid, uid, callback) { - var deletedTopic; - async.waterfall([ - function (next) { - async.parallel({ - topic: async.apply(Topics.getTopicData, tid), - tags: async.apply(Topics.getTopicTags, tid), - }, next); - }, - function (results, next) { - if (!results.topic) { - return callback(); - } - deletedTopic = results.topic; - deletedTopic.tags = results.tags; - deleteFromFollowersIgnorers(tid, next); - }, - function (next) { - async.parallel([ - function (next) { - db.deleteAll([ - 'tid:' + tid + ':followers', - 'tid:' + tid + ':ignorers', - 'tid:' + tid + ':posts', - 'tid:' + tid + ':posts:votes', - 'tid:' + tid + ':bookmarks', - 'tid:' + tid + ':posters', - ], next); - }, - function (next) { - db.sortedSetsRemove([ - 'topics:tid', - 'topics:recent', - 'topics:posts', - 'topics:views', - 'topics:votes', - ], tid, next); - }, - function (next) { - deleteTopicFromCategoryAndUser(tid, next); - }, - function (next) { - Topics.deleteTopicTags(tid, next); - }, - function (next) { - reduceCounters(tid, next); - }, - ], function (err) { - next(err); - }); - }, - function (next) { - plugins.fireHook('action:topic.purge', { topic: deletedTopic, uid: uid }); - db.delete('topic:' + tid, next); - }, - ], callback); - }; - - function deleteFromFollowersIgnorers(tid, callback) { - async.waterfall([ - function (next) { - async.parallel({ - followers: async.apply(db.getSetMembers, 'tid:' + tid + ':followers'), - ignorers: async.apply(db.getSetMembers, 'tid:' + tid + ':ignorers'), - }, next); - }, - function (results, next) { - var followerKeys = results.followers.map(function (uid) { - return 'uid:' + uid + ':followed_tids'; - }); - var ignorerKeys = results.ignorers.map(function (uid) { - return 'uid:' + uid + 'ignored_tids'; - }); - db.sortedSetsRemove(followerKeys.concat(ignorerKeys), tid, next); - }, - ], callback); + async function removeTopicPidsFromCid(tid) { + const [cid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'cid'), + Topics.getPids(tid), + ]); + await db.sortedSetRemove('cid:' + cid + ':pids', pids); } - function deleteTopicFromCategoryAndUser(tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicFields(tid, ['cid', 'uid'], next); - }, - function (topicData, next) { - async.parallel([ - function (next) { - db.sortedSetsRemove([ - 'cid:' + topicData.cid + ':tids', - 'cid:' + topicData.cid + ':tids:pinned', - 'cid:' + topicData.cid + ':tids:posts', - 'cid:' + topicData.cid + ':tids:lastposttime', - 'cid:' + topicData.cid + ':tids:votes', - 'cid:' + topicData.cid + ':recent_tids', - 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', - 'uid:' + topicData.uid + ':topics', - ], tid, next); - }, - function (next) { - user.decrementUserFieldBy(topicData.uid, 'topiccount', 1, next); - }, - ], next); - }, - ], function (err) { - callback(err); + async function addTopicPidsToCid(tid) { + const [cid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'cid'), + Topics.getPids(tid), + ]); + let postData = await posts.getPostsFields(pids, ['pid', 'timestamp', 'deleted']); + postData = postData.filter(post => post && !post.deleted); + var pidsToAdd = []; + var scores = []; + postData.forEach(function (post) { + pidsToAdd.push(post.pid); + scores.push(post.timestamp); }); + await db.sortedSetAdd('cid:' + cid + ':pids', scores, pidsToAdd); } - function reduceCounters(tid, callback) { + Topics.restore = async function (tid) { + const topicData = await Topics.getTopicData(tid); + await Promise.all([ + Topics.setTopicField(tid, 'deleted', 0), + Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp']), + Topics.updateRecent(tid, topicData.lastposttime), + db.sortedSetAddBulk([ + ['topics:posts', topicData.postcount, tid], + ['topics:views', topicData.viewcount, tid], + ['topics:votes', parseInt(topicData.votes, 10) || 0, tid], + ]), + addTopicPidsToCid(tid), + ]); + }; + + Topics.purgePostsAndTopic = async function (tid, uid) { + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + await batch.processSortedSet('tid:' + tid + ':posts', function (pids, next) { + async.eachSeries(pids, function (pid, next) { + posts.purge(pid, uid, next); + }, next); + }, { alwaysStartAt: 0 }); + await posts.purge(mainPid, uid); + await Topics.purge(tid, uid); + }; + + Topics.purge = async function (tid, uid) { + const [deletedTopic, tags] = await Promise.all([ + Topics.getTopicData(tid), + Topics.getTopicTags(tid), + ]); + if (!deletedTopic) { + return; + } + deletedTopic.tags = tags; + await deleteFromFollowersIgnorers(tid); + + await Promise.all([ + db.deleteAll([ + 'tid:' + tid + ':followers', + 'tid:' + tid + ':ignorers', + 'tid:' + tid + ':posts', + 'tid:' + tid + ':posts:votes', + 'tid:' + tid + ':bookmarks', + 'tid:' + tid + ':posters', + ]), + db.sortedSetsRemove([ + 'topics:tid', + 'topics:recent', + 'topics:posts', + 'topics:views', + 'topics:votes', + ], tid), + deleteTopicFromCategoryAndUser(tid), + Topics.deleteTopicTags(tid), + reduceCounters(tid), + ]); + plugins.fireHook('action:topic.purge', { topic: deletedTopic, uid: uid }); + await db.delete('topic:' + tid); + }; + + async function deleteFromFollowersIgnorers(tid) { + const [followers, ignorers] = await Promise.all([ + db.getSetMembers('tid:' + tid + ':followers'), + db.getSetMembers('tid:' + tid + ':ignorers'), + ]); + const followerKeys = followers.map(uid => 'uid:' + uid + ':followed_tids'); + const ignorerKeys = ignorers.map(uid => 'uid:' + uid + 'ignored_tids'); + await db.sortedSetsRemove(followerKeys.concat(ignorerKeys), tid); + } + + async function deleteTopicFromCategoryAndUser(tid) { + const topicData = await Topics.getTopicFields(tid, ['cid', 'uid']); + await Promise.all([ + db.sortedSetsRemove([ + 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':tids:pinned', + 'cid:' + topicData.cid + ':tids:posts', + 'cid:' + topicData.cid + ':tids:lastposttime', + 'cid:' + topicData.cid + ':tids:votes', + 'cid:' + topicData.cid + ':recent_tids', + 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', + 'uid:' + topicData.uid + ':topics', + ], tid), + user.decrementUserFieldBy(topicData.uid, 'topiccount', 1), + ]); + } + + async function reduceCounters(tid) { var incr = -1; - async.parallel([ - function (next) { - db.incrObjectFieldBy('global', 'topicCount', incr, next); - }, - function (next) { - async.waterfall([ - function (next) { - Topics.getTopicFields(tid, ['cid', 'postcount'], next); - }, - function (topicData, next) { - var postCountChange = incr * topicData.postcount; - - async.parallel([ - function (next) { - db.incrObjectFieldBy('global', 'postCount', postCountChange, next); - }, - function (next) { - db.incrObjectFieldBy('category:' + topicData.cid, 'post_count', postCountChange, next); - }, - function (next) { - db.incrObjectFieldBy('category:' + topicData.cid, 'topic_count', incr, next); - }, - ], next); - }, - ], next); - }, - ], callback); + await db.incrObjectFieldBy('global', 'topicCount', incr); + const topicData = await Topics.getTopicFields(tid, ['cid', 'postcount']); + var postCountChange = incr * topicData.postcount; + await Promise.all([ + db.incrObjectFieldBy('global', 'postCount', postCountChange), + db.incrObjectFieldBy('category:' + topicData.cid, 'post_count', postCountChange), + db.incrObjectFieldBy('category:' + topicData.cid, 'topic_count', incr), + ]); } }; diff --git a/src/topics/follow.js b/src/topics/follow.js index a0e3ac17af..957e97a6de 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -1,8 +1,6 @@ 'use strict'; -var async = require('async'); - var db = require('../database'); var posts = require('../posts'); var notifications = require('../notifications'); @@ -11,258 +9,187 @@ var plugins = require('../plugins'); var utils = require('../utils'); module.exports = function (Topics) { - Topics.toggleFollow = function (tid, uid, callback) { - callback = callback || function () {}; - var isFollowing; - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - Topics.isFollowing([tid], uid, next); - }, - function (_isFollowing, next) { - isFollowing = _isFollowing[0]; - if (isFollowing) { - Topics.unfollow(tid, uid, next); - } else { - Topics.follow(tid, uid, next); - } - }, - function (next) { - next(null, !isFollowing); - }, - ], callback); - }; - - Topics.follow = function (tid, uid, callback) { - setWatching(follow, unignore, 'action:topic.follow', tid, uid, callback); - }; - - Topics.unfollow = function (tid, uid, callback) { - setWatching(unfollow, unignore, 'action:topic.unfollow', tid, uid, callback); - }; - - Topics.ignore = function (tid, uid, callback) { - setWatching(ignore, unfollow, 'action:topic.ignore', tid, uid, callback); - }; - - function setWatching(method1, method2, hook, tid, uid, callback) { - callback = callback || function () {}; - if (parseInt(uid, 10) <= 0) { - return setImmediate(callback); + Topics.toggleFollow = async function (tid, uid) { + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); } - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - method1(tid, uid, next); - }, - function (next) { - method2(tid, uid, next); - }, - function (next) { - plugins.fireHook(hook, { uid: uid, tid: tid }); - next(); - }, - ], callback); - } - - function follow(tid, uid, callback) { - addToSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid, callback); - } - - function unfollow(tid, uid, callback) { - removeFromSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid, callback); - } - - function ignore(tid, uid, callback) { - addToSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid, callback); - } - - function unignore(tid, uid, callback) { - removeFromSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid, callback); - } - - function addToSets(set1, set2, tid, uid, callback) { - async.waterfall([ - function (next) { - db.setAdd(set1, uid, next); - }, - function (next) { - db.sortedSetAdd(set2, Date.now(), tid, next); - }, - ], callback); - } - - function removeFromSets(set1, set2, tid, uid, callback) { - async.waterfall([ - function (next) { - db.setRemove(set1, uid, next); - }, - function (next) { - db.sortedSetRemove(set2, tid, next); - }, - ], callback); - } - - Topics.isFollowing = function (tids, uid, callback) { - isIgnoringOrFollowing('followers', tids, uid, callback); + const isFollowing = await Topics.isFollowing([tid], uid); + if (isFollowing[0]) { + await Topics.unfollow(tid, uid); + } else { + await Topics.follow(tid, uid); + } + return !isFollowing[0]; }; - Topics.isIgnoring = function (tids, uid, callback) { - isIgnoringOrFollowing('ignorers', tids, uid, callback); + Topics.follow = async function (tid, uid) { + await setWatching(follow, unignore, 'action:topic.follow', tid, uid); }; - Topics.getFollowData = function (tids, uid, callback) { + Topics.unfollow = async function (tid, uid) { + await setWatching(unfollow, unignore, 'action:topic.unfollow', tid, uid); + }; + + Topics.ignore = async function (tid, uid) { + await setWatching(ignore, unfollow, 'action:topic.ignore', tid, uid); + }; + + async function setWatching(method1, method2, hook, tid, uid) { + if (parseInt(uid, 10) <= 0) { + return; + } + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + await method1(tid, uid); + await method2(tid, uid); + plugins.fireHook(hook, { uid: uid, tid: tid }); + } + + async function follow(tid, uid) { + await addToSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid); + } + + async function unfollow(tid, uid) { + await removeFromSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid); + } + + async function ignore(tid, uid) { + await addToSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid); + } + + async function unignore(tid, uid) { + await removeFromSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid); + } + + async function addToSets(set1, set2, tid, uid) { + await db.setAdd(set1, uid); + await db.sortedSetAdd(set2, Date.now(), tid); + } + + async function removeFromSets(set1, set2, tid, uid) { + await db.setRemove(set1, uid); + await db.sortedSetRemove(set2, tid); + } + + Topics.isFollowing = async function (tids, uid) { + return await isIgnoringOrFollowing('followers', tids, uid); + }; + + Topics.isIgnoring = async function (tids, uid) { + return await isIgnoringOrFollowing('ignorers', tids, uid); + }; + + Topics.getFollowData = async function (tids, uid) { if (!Array.isArray(tids)) { - return setImmediate(callback); + return; } if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, tids.map(() => ({ following: false, ignoring: false }))); + return tids.map(() => ({ following: false, ignoring: false })); } const keys = []; tids.forEach((tid) => { keys.push('tid:' + tid + ':followers', 'tid:' + tid + ':ignorers'); }); - db.isMemberOfSets(keys, uid, function (err, data) { - if (err) { - return callback(err); - } - const followData = []; - for (let i = 0; i < data.length; i += 2) { - followData.push({ - following: data[i], - ignoring: data[i + 1], - }); - } - callback(null, followData); - }); + const data = await db.isMemberOfSets(keys, uid); + + const followData = []; + for (let i = 0; i < data.length; i += 2) { + followData.push({ + following: data[i], + ignoring: data[i + 1], + }); + } + return followData; }; - function isIgnoringOrFollowing(set, tids, uid, callback) { + async function isIgnoringOrFollowing(set, tids, uid) { if (!Array.isArray(tids)) { - return setImmediate(callback); + return; } if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, tids.map(() => false)); + return tids.map(() => false); } var keys = tids.map(tid => 'tid:' + tid + ':' + set); - db.isMemberOfSets(keys, uid, callback); + return await db.isMemberOfSets(keys, uid); } - Topics.getFollowers = function (tid, callback) { - db.getSetMembers('tid:' + tid + ':followers', callback); + Topics.getFollowers = async function (tid) { + return await db.getSetMembers('tid:' + tid + ':followers'); }; - Topics.getIgnorers = function (tid, callback) { - db.getSetMembers('tid:' + tid + ':ignorers', callback); + Topics.getIgnorers = async function (tid) { + return await db.getSetMembers('tid:' + tid + ':ignorers'); }; - Topics.filterIgnoringUids = function (tid, uids, callback) { - async.waterfall([ - function (next) { - db.isSetMembers('tid:' + tid + ':ignorers', uids, next); - }, - function (isIgnoring, next) { - const readingUids = uids.filter((uid, index) => uid && !isIgnoring[index]); - next(null, readingUids); - }, - ], callback); + Topics.filterIgnoringUids = async function (tid, uids) { + const isIgnoring = await db.isSetMembers('tid:' + tid + ':ignorers', uids); + const readingUids = uids.filter((uid, index) => uid && !isIgnoring[index]); + return readingUids; }; - Topics.filterWatchedTids = function (tids, uid, callback) { + Topics.filterWatchedTids = async function (tids, uid) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, []); + return []; } - async.waterfall([ - function (next) { - db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next); - }, - function (scores, next) { - tids = tids.filter((tid, index) => tid && !!scores[index]); - next(null, tids); - }, - ], callback); + const scores = await db.sortedSetScores('uid:' + uid + ':followed_tids', tids); + tids = tids.filter((tid, index) => tid && !!scores[index]); + return tids; }; - Topics.filterNotIgnoredTids = function (tids, uid, callback) { + Topics.filterNotIgnoredTids = async function (tids, uid) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, tids); + return tids; } - async.waterfall([ - function (next) { - db.sortedSetScores('uid:' + uid + ':ignored_tids', tids, next); - }, - function (scores, next) { - tids = tids.filter((tid, index) => tid && !scores[index]); - next(null, tids); - }, - ], callback); + const scores = await db.sortedSetScores('uid:' + uid + ':ignored_tids', tids); + tids = tids.filter((tid, index) => tid && !scores[index]); + return tids; }; - Topics.notifyFollowers = function (postData, exceptUid, callback) { - callback = callback || function () {}; - var followers; + Topics.notifyFollowers = async function (postData, exceptUid) { var title; var titleEscaped; - async.waterfall([ - function (next) { - Topics.getFollowers(postData.topic.tid, next); - }, - function (followers, next) { - var index = followers.indexOf(exceptUid.toString()); - if (index !== -1) { - followers.splice(index, 1); - } + let followers = await Topics.getFollowers(postData.topic.tid); - privileges.topics.filterUids('topics:read', postData.topic.tid, followers, next); - }, - function (_followers, next) { - followers = _followers; - if (!followers.length) { - return callback(); - } - title = postData.topic.title; + var index = followers.indexOf(exceptUid.toString()); + if (index !== -1) { + followers.splice(index, 1); + } - if (title) { - title = utils.decodeHTMLEntities(title); - titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); - } + followers = await privileges.topics.filterUids('topics:read', postData.topic.tid, followers); - postData.content = posts.relativeToAbsolute(postData.content, posts.urlRegex); - postData.content = posts.relativeToAbsolute(postData.content, posts.imgRegex); + if (!followers.length) { + return; + } + title = postData.topic.title; - notifications.create({ - type: 'new-reply', - subject: title, - bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', - bodyLong: postData.content, - pid: postData.pid, - path: '/post/' + postData.pid, - nid: 'new_post:tid:' + postData.topic.tid + ':pid:' + postData.pid + ':uid:' + exceptUid, - tid: postData.topic.tid, - from: exceptUid, - mergeId: 'notifications:user_posted_to|' + postData.topic.tid, - topicTitle: title, - }, next); - }, - function (notification, next) { - if (notification) { - notifications.push(notification, followers); - } + if (title) { + title = utils.decodeHTMLEntities(title); + titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + } - next(); - }, - ], callback); + postData.content = posts.relativeToAbsolute(postData.content, posts.urlRegex); + postData.content = posts.relativeToAbsolute(postData.content, posts.imgRegex); + + const notification = await notifications.create({ + type: 'new-reply', + subject: title, + bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', + bodyLong: postData.content, + pid: postData.pid, + path: '/post/' + postData.pid, + nid: 'new_post:tid:' + postData.topic.tid + ':pid:' + postData.pid + ':uid:' + exceptUid, + tid: postData.topic.tid, + from: exceptUid, + mergeId: 'notifications:user_posted_to|' + postData.topic.tid, + topicTitle: title, + }); + if (notification) { + notifications.push(notification, followers); + } }; }; diff --git a/src/topics/fork.js b/src/topics/fork.js index ecefbc8eea..70649af9ee 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -10,171 +10,116 @@ var plugins = require('../plugins'); var meta = require('../meta'); module.exports = function (Topics) { - Topics.createTopicFromPosts = function (uid, title, pids, fromTid, callback) { + Topics.createTopicFromPosts = async function (uid, title, pids, fromTid) { if (title) { title = title.trim(); } if (title.length < meta.config.minimumTitleLength) { - return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]')); + throw new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]'); } else if (title.length > meta.config.maximumTitleLength) { - return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]')); + throw new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]'); } if (!pids || !pids.length) { - return callback(new Error('[[error:invalid-pid]]')); + throw new Error('[[error:invalid-pid]]'); } - pids.sort(function (a, b) { - return a - b; - }); - var mainPid = pids[0]; - var cid; - var tid; - async.waterfall([ - function (next) { - posts.getCidByPid(mainPid, next); - }, - function (_cid, next) { - cid = _cid; - async.parallel({ - postData: function (next) { - posts.getPostData(mainPid, next); - }, - isAdminOrMod: function (next) { - privileges.categories.isAdminOrMod(cid, uid, next); - }, - }, next); - }, - function (results, next) { - if (!results.isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } - Topics.create({ uid: results.postData.uid, title: title, cid: cid }, next); - }, - function (tid, next) { - Topics.updateTopicBookmarks(fromTid, pids, function (err) { next(err, tid); }); - }, - function (_tid, next) { - tid = _tid; - async.eachSeries(pids, function (pid, next) { - privileges.posts.canEdit(pid, uid, function (err, canEdit) { - if (err || !canEdit.flag) { - return next(err || new Error(canEdit.message)); - } + pids.sort((a, b) => a - b); - Topics.movePostToTopic(uid, pid, tid, next); - }); - }, next); - }, - function (next) { - Topics.updateLastPostTime(tid, Date.now(), next); - }, - function (next) { - plugins.fireHook('action:topic.fork', { tid: tid, fromTid: fromTid, uid: uid }); - Topics.getTopicData(tid, next); - }, - ], callback); + var mainPid = pids[0]; + var cid = await posts.getCidByPid(mainPid); + + const [postData, isAdminOrMod] = await Promise.all([ + posts.getPostData(mainPid), + privileges.categories.isAdminOrMod(cid, uid), + ]); + + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + const tid = await Topics.create({ uid: postData.uid, title: title, cid: cid }); + await Topics.updateTopicBookmarks(fromTid, pids); + + await async.eachSeries(pids, async function (pid) { + const canEdit = await privileges.posts.canEdit(pid, uid); + if (!canEdit.flag) { + throw new Error(canEdit.message); + } + await Topics.movePostToTopic(uid, pid, tid); + }); + + await Topics.updateLastPostTime(tid, Date.now()); + + plugins.fireHook('action:topic.fork', { tid: tid, fromTid: fromTid, uid: uid }); + + return await Topics.getTopicData(tid); }; - Topics.movePostToTopic = function (callerUid, pid, tid, callback) { + Topics.movePostToTopic = async function (callerUid, pid, tid) { var postData; tid = parseInt(tid, 10); - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - posts.getPostFields(pid, ['tid', 'uid', 'timestamp', 'upvotes', 'downvotes'], next); - }, - function (post, next) { - if (!post || !post.tid) { - return next(new Error('[[error:no-post]]')); - } + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + const post = await posts.getPostFields(pid, ['tid', 'uid', 'timestamp', 'upvotes', 'downvotes']); + if (!post || !post.tid) { + throw new Error('[[error:no-post]]'); + } - if (post.tid === tid) { - return next(new Error('[[error:cant-move-to-same-topic]]')); - } + if (post.tid === tid) { + throw new Error('[[error:cant-move-to-same-topic]]'); + } - postData = post; - postData.pid = pid; + postData = post; + postData.pid = pid; - Topics.removePostFromTopic(postData.tid, postData, next); - }, - function (next) { - async.parallel([ - function (next) { - updateCategory(postData, tid, next); - }, - function (next) { - posts.setPostField(pid, 'tid', tid, next); - }, - function (next) { - Topics.addPostToTopic(tid, postData, next); - }, - ], next); - }, - function (results, next) { - async.parallel([ - async.apply(Topics.updateLastPostTimeFromLastPid, tid), - async.apply(Topics.updateLastPostTimeFromLastPid, postData.tid), - ], function (err) { - next(err); - }); - }, - function (next) { - plugins.fireHook('action:post.move', { uid: callerUid, post: postData, tid: tid }); - next(); - }, - ], callback); + await Topics.removePostFromTopic(postData.tid, postData); + await Promise.all([ + updateCategory(postData, tid), + posts.setPostField(pid, 'tid', tid), + Topics.addPostToTopic(tid, postData), + ]); + + await Promise.all([ + Topics.updateLastPostTimeFromLastPid(tid), + Topics.updateLastPostTimeFromLastPid(postData.tid), + ]); + plugins.fireHook('action:post.move', { uid: callerUid, post: postData, tid: tid }); }; - function updateCategory(postData, toTid, callback) { - var topicData; - async.waterfall([ - function (next) { - Topics.getTopicsFields([postData.tid, toTid], ['cid', 'pinned'], next); - }, - function (_topicData, next) { - topicData = _topicData; - if (!topicData[0].cid || !topicData[1].cid) { - return callback(); - } - var tasks = []; - if (!topicData[0].pinned) { - tasks.push(async.apply(db.sortedSetIncrBy, 'cid:' + topicData[0].cid + ':tids:posts', -1, postData.tid)); - } - if (!topicData[1].pinned) { - tasks.push(async.apply(db.sortedSetIncrBy, 'cid:' + topicData[1].cid + ':tids:posts', 1, toTid)); - } - async.series(tasks, function (err) { - next(err); - }); - }, - function (next) { - if (topicData[0].cid === topicData[1].cid) { - return callback(); - } - const removeFrom = [ - 'cid:' + topicData[0].cid + ':pids', - 'cid:' + topicData[0].cid + ':uid:' + postData.uid + ':pids', - 'cid:' + topicData[0].cid + ':uid:' + postData.uid + ':pids:votes', - ]; - const tasks = [ - async.apply(db.incrObjectFieldBy, 'category:' + topicData[0].cid, 'post_count', -1), - async.apply(db.incrObjectFieldBy, 'category:' + topicData[1].cid, 'post_count', 1), - async.apply(db.sortedSetRemove, removeFrom, postData.pid), - async.apply(db.sortedSetAdd, 'cid:' + topicData[1].cid + ':pids', postData.timestamp, postData.pid), - async.apply(db.sortedSetAdd, 'cid:' + topicData[1].cid + ':uid:' + postData.uid + ':pids', postData.timestamp, postData.pid), - ]; - if (postData.votes > 0) { - tasks.push(async.apply(db.sortedSetAdd, 'cid:' + topicData[1].cid + ':uid:' + postData.uid + ':pids:votes', postData.votes, postData.pid)); - } - async.parallel(tasks, next); - }, - ], callback); + async function updateCategory(postData, toTid) { + const topicData = await Topics.getTopicsFields([postData.tid, toTid], ['cid', 'pinned']); + + if (!topicData[0].cid || !topicData[1].cid) { + return; + } + + if (!topicData[0].pinned) { + await db.sortedSetIncrBy('cid:' + topicData[0].cid + ':tids:posts', -1, postData.tid); + } + if (!topicData[1].pinned) { + await db.sortedSetIncrBy('cid:' + topicData[1].cid + ':tids:posts', 1, toTid); + } + if (topicData[0].cid === topicData[1].cid) { + return; + } + const removeFrom = [ + 'cid:' + topicData[0].cid + ':pids', + 'cid:' + topicData[0].cid + ':uid:' + postData.uid + ':pids', + 'cid:' + topicData[0].cid + ':uid:' + postData.uid + ':pids:votes', + ]; + const tasks = [ + db.incrObjectFieldBy('category:' + topicData[0].cid, 'post_count', -1), + db.incrObjectFieldBy('category:' + topicData[1].cid, 'post_count', 1), + db.sortedSetRemove(removeFrom, postData.pid), + db.sortedSetAdd('cid:' + topicData[1].cid + ':pids', postData.timestamp, postData.pid), + db.sortedSetAdd('cid:' + topicData[1].cid + ':uid:' + postData.uid + ':pids', postData.timestamp, postData.pid), + ]; + if (postData.votes > 0) { + tasks.push(db.sortedSetAdd('cid:' + topicData[1].cid + ':uid:' + postData.uid + ':pids:votes', postData.votes, postData.pid)); + } + await Promise.all(tasks); } }; diff --git a/src/topics/index.js b/src/topics/index.js index accdc9041e..61c1c62908 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var db = require('../database'); @@ -33,315 +32,241 @@ require('./thumb')(Topics); require('./bookmarks')(Topics); require('./merge')(Topics); -Topics.exists = function (tid, callback) { - db.exists('topic:' + tid, callback); +Topics.exists = async function (tid) { + return await db.exists('topic:' + tid); }; -Topics.getTopicsFromSet = function (set, uid, start, stop, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange(set, start, stop, next); - }, - function (tids, next) { - Topics.getTopics(tids, uid, next); - }, - function (topics, next) { - Topics.calculateTopicIndices(topics, start); - next(null, { topics: topics, nextStart: stop + 1 }); - }, - ], callback); +Topics.getTopicsFromSet = async function (set, uid, start, stop) { + const tids = await db.getSortedSetRange(set, start, stop); + const topics = await Topics.getTopics(tids, uid); + Topics.calculateTopicIndices(topics, start); + return { topics: topics, nextStart: stop + 1 }; }; -Topics.getTopics = function (tids, options, callback) { +Topics.getTopics = async function (tids, options) { let uid = options; if (typeof options === 'object') { uid = options.uid; } - async.waterfall([ - function (next) { - privileges.topics.filterTids('topics:read', tids, uid, next); - }, - function (tids, next) { - Topics.getTopicsByTids(tids, options, next); - }, - ], callback); + + tids = await privileges.topics.filterTids('topics:read', tids, uid); + const topics = await Topics.getTopicsByTids(tids, options); + return topics; }; -Topics.getTopicsByTids = function (tids, options, callback) { +Topics.getTopicsByTids = async function (tids, options) { if (!Array.isArray(tids) || !tids.length) { - return callback(null, []); + return []; } let uid = options; if (typeof options === 'object') { uid = options.uid; } - var uids; - var cids; - var topics; - async.waterfall([ - function (next) { - Topics.getTopicsData(tids, next); - }, - function (_topics, next) { - function mapFilter(array, field) { - return array.map(function (topic) { - return topic && topic[field] && topic[field].toString(); - }).filter(value => utils.isNumber(value)); - } + let topics = await Topics.getTopicsData(tids); - topics = _topics; - uids = _.uniq(mapFilter(topics, 'uid')); - cids = _.uniq(mapFilter(topics, 'cid')); + const uids = _.uniq(topics.map(t => t && t.uid && t.uid.toString()).filter(v => utils.isNumber(v))); + const cids = _.uniq(topics.map(t => t && t.cid && t.cid.toString()).filter(v => utils.isNumber(v))); - async.parallel({ - users: function (next) { - user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status'], next); - }, - userSettings: function (next) { - user.getMultipleUserSettings(uids, next); - }, - categories: function (next) { - categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'image', 'imageClass', 'bgColor', 'color', 'disabled'], next); - }, - hasRead: function (next) { - Topics.hasReadTopics(tids, uid, next); - }, - isIgnored: function (next) { - Topics.isIgnoring(tids, uid, next); - }, - bookmarks: function (next) { - Topics.getUserBookmarks(tids, uid, next); - }, - teasers: function (next) { - Topics.getTeasers(topics, options, next); - }, - tags: function (next) { - Topics.getTopicsTagsObjects(tids, next); - }, - }, next); - }, - function (results, next) { - results.users.forEach(function (user, index) { - if (meta.config.hideFullname || !results.userSettings[index].showfullname) { - user.fullname = undefined; - } - }); + const [ + users, + userSettings, + categoriesData, + hasRead, + isIgnored, + bookmarks, + teasers, + tags, + ] = await Promise.all([ + user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status']), + user.getMultipleUserSettings(uids), + categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'image', 'imageClass', 'bgColor', 'color', 'disabled']), + Topics.hasReadTopics(tids, uid), + Topics.isIgnoring(tids, uid), + Topics.getUserBookmarks(tids, uid), + Topics.getTeasers(topics, options), + Topics.getTopicsTagsObjects(tids), + ]); - var users = _.zipObject(uids, results.users); - var categories = _.zipObject(cids, results.categories); + users.forEach(function (user, index) { + if (meta.config.hideFullname || !userSettings[index].showfullname) { + user.fullname = undefined; + } + }); - for (var i = 0; i < topics.length; i += 1) { - if (topics[i]) { - topics[i].category = categories[topics[i].cid]; - topics[i].user = users[topics[i].uid]; - topics[i].teaser = results.teasers[i]; - topics[i].tags = results.tags[i]; + const usersMap = _.zipObject(uids, users); + const categoriesMap = _.zipObject(cids, categoriesData); - topics[i].isOwner = topics[i].uid === parseInt(uid, 10); - topics[i].ignored = results.isIgnored[i]; - topics[i].unread = !results.hasRead[i] && !results.isIgnored[i]; - topics[i].bookmark = results.bookmarks[i]; - topics[i].unreplied = !topics[i].teaser; + for (var i = 0; i < topics.length; i += 1) { + if (topics[i]) { + topics[i].category = categoriesMap[topics[i].cid]; + topics[i].user = usersMap[topics[i].uid]; + topics[i].teaser = teasers[i]; + topics[i].tags = tags[i]; - topics[i].icons = []; - } - } + topics[i].isOwner = topics[i].uid === parseInt(uid, 10); + topics[i].ignored = isIgnored[i]; + topics[i].unread = !hasRead[i] && !isIgnored[i]; + topics[i].bookmark = bookmarks[i]; + topics[i].unreplied = !topics[i].teaser; - topics = topics.filter(topic => topic && topic.category && !topic.category.disabled); + topics[i].icons = []; + } + } - plugins.fireHook('filter:topics.get', { topics: topics, uid: uid }, next); - }, - function (data, next) { - next(null, data.topics); - }, - ], callback); + topics = topics.filter(topic => topic && topic.category && !topic.category.disabled); + + const result = await plugins.fireHook('filter:topics.get', { topics: topics, uid: uid }); + return result.topics; }; -Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse, callback) { - async.waterfall([ - function (next) { - async.parallel({ - posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse), - category: async.apply(categories.getCategoryData, topicData.cid), - tagWhitelist: async.apply(categories.getTagWhitelist, [topicData.cid]), - threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', { topic: topicData, uid: uid, tools: [] }), - followData: async.apply(Topics.getFollowData, [topicData.tid], uid), - bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid), - postSharing: async.apply(social.getActivePostSharing), - deleter: async.apply(getDeleter, topicData), - merger: async.apply(getMerger, topicData), - related: function (next) { - async.waterfall([ - function (next) { - Topics.getTopicTagsObjects(topicData.tid, next); - }, - function (tags, next) { - topicData.tags = tags; - Topics.getRelatedTopics(topicData, uid, next); - }, - ], next); - }, - }, next); - }, - function (results, next) { - topicData.posts = results.posts; - topicData.category = results.category; - topicData.tagWhitelist = results.tagWhitelist[0]; - topicData.thread_tools = results.threadTools.tools; - topicData.isFollowing = results.followData[0].following; - topicData.isNotFollowing = !results.followData[0].following && !results.followData[0].ignoring; - topicData.isIgnoring = results.followData[0].ignoring; - topicData.bookmark = results.bookmark; - topicData.postSharing = results.postSharing; - topicData.deleter = results.deleter; - if (results.deleter) { - topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp); - } - topicData.merger = results.merger; - if (results.merger) { - topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp); - } - topicData.related = results.related || []; - topicData.unreplied = topicData.postcount === 1; - topicData.icons = []; +Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, reverse) { + const [ + posts, + category, + tagWhitelist, + threadTools, + followData, + bookmark, + postSharing, + deleter, + merger, + related, + ] = await Promise.all([ + getMainPostAndReplies(topicData, set, uid, start, stop, reverse), + categories.getCategoryData(topicData.cid), + categories.getTagWhitelist([topicData.cid]), + plugins.fireHook('filter:topic.thread_tools', { topic: topicData, uid: uid, tools: [] }), + Topics.getFollowData([topicData.tid], uid), + Topics.getUserBookmark(topicData.tid, uid), + social.getActivePostSharing(), + getDeleter(topicData), + getMerger(topicData), + getRelated(topicData, uid), + ]); - plugins.fireHook('filter:topic.get', { topic: topicData, uid: uid }, next); - }, - function (data, next) { - next(null, data.topic); - }, - ], callback); + topicData.posts = posts; + topicData.category = category; + topicData.tagWhitelist = tagWhitelist[0]; + topicData.thread_tools = threadTools.tools; + topicData.isFollowing = followData[0].following; + topicData.isNotFollowing = !followData[0].following && !followData[0].ignoring; + topicData.isIgnoring = followData[0].ignoring; + topicData.bookmark = bookmark; + topicData.postSharing = postSharing; + topicData.deleter = deleter; + if (deleter) { + topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp); + } + topicData.merger = merger; + if (merger) { + topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp); + } + topicData.related = related || []; + topicData.unreplied = topicData.postcount === 1; + topicData.icons = []; + + const result = await plugins.fireHook('filter:topic.get', { topic: topicData, uid: uid }); + return result.topic; }; -function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) { - async.waterfall([ - function (next) { - if (stop > 0) { - stop -= 1; - if (start > 0) { - start -= 1; - } - } +async function getMainPostAndReplies(topic, set, uid, start, stop, reverse) { + if (stop > 0) { + stop -= 1; + if (start > 0) { + start -= 1; + } + } + const pids = await posts.getPidsFromSet(set, start, stop, reverse); + if (!pids.length && !topic.mainPid) { + return []; + } - posts.getPidsFromSet(set, start, stop, reverse, next); - }, - function (pids, next) { - if (!pids.length && !topic.mainPid) { - return callback(null, []); - } + if (parseInt(topic.mainPid, 10) && start === 0) { + pids.unshift(topic.mainPid); + } + const postData = await posts.getPostsByPids(pids, uid); + if (!postData.length) { + return []; + } + var replies = postData; + if (topic.mainPid && start === 0) { + postData[0].index = 0; + replies = postData.slice(1); + } - if (parseInt(topic.mainPid, 10) && start === 0) { - pids.unshift(topic.mainPid); - } - posts.getPostsByPids(pids, uid, next); - }, - function (posts, next) { - if (!posts.length) { - return next(null, []); - } - var replies = posts; - if (topic.mainPid && start === 0) { - posts[0].index = 0; - replies = posts.slice(1); - } + Topics.calculatePostIndices(replies, start); - Topics.calculatePostIndices(replies, start); - - Topics.addPostData(posts, uid, next); - }, - ], callback); + return await Topics.addPostData(postData, uid); } -function getDeleter(topicData, callback) { +async function getDeleter(topicData) { if (!parseInt(topicData.deleterUid, 10)) { - return setImmediate(callback, null, null); + return null; } - user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture'], callback); + return await user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture']); } -function getMerger(topicData, callback) { +async function getMerger(topicData) { if (!parseInt(topicData.mergerUid, 10)) { - return setImmediate(callback, null, null); + return null; } - async.waterfall([ - function (next) { - async.parallel({ - merger: function (next) { - user.getUserFields(topicData.mergerUid, ['username', 'userslug', 'picture'], next); - }, - mergedIntoTitle: function (next) { - Topics.getTopicField(topicData.mergeIntoTid, 'title', next); - }, - }, next); - }, - function (results, next) { - results.merger.mergedIntoTitle = results.mergedIntoTitle; - next(null, results.merger); - }, - ], callback); + const [ + merger, + mergedIntoTitle, + ] = await Promise.all([ + user.getUserFields(topicData.mergerUid, ['username', 'userslug', 'picture']), + Topics.getTopicField(topicData.mergeIntoTid, 'title'), + ]); + merger.mergedIntoTitle = mergedIntoTitle; + return merger; } -Topics.getMainPost = function (tid, uid, callback) { - Topics.getMainPosts([tid], uid, function (err, mainPosts) { - callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null); - }); +async function getRelated(topicData, uid) { + const tags = await Topics.getTopicTagsObjects(topicData.tid); + topicData.tags = tags; + return await Topics.getRelatedTopics(topicData, uid); +} + +Topics.getMainPost = async function (tid, uid) { + const mainPosts = await Topics.getMainPosts([tid], uid); + return Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null; }; -Topics.getMainPids = function (tids, callback) { +Topics.getMainPids = async function (tids) { if (!Array.isArray(tids) || !tids.length) { - return callback(null, []); + return []; } - async.waterfall([ - function (next) { - Topics.getTopicsFields(tids, ['mainPid'], next); - }, - function (topicData, next) { - next(null, topicData.map(topic => topic && topic.mainPid)); - }, - ], callback); + const topicData = await Topics.getTopicsFields(tids, ['mainPid']); + return topicData.map(topic => topic && topic.mainPid); }; -Topics.getMainPosts = function (tids, uid, callback) { - async.waterfall([ - function (next) { - Topics.getMainPids(tids, next); - }, - function (mainPids, next) { - getMainPosts(mainPids, uid, next); - }, - ], callback); +Topics.getMainPosts = async function (tids, uid) { + const mainPids = await Topics.getMainPids(tids); + return await getMainPosts(mainPids, uid); }; -function getMainPosts(mainPids, uid, callback) { - async.waterfall([ - function (next) { - posts.getPostsByPids(mainPids, uid, next); - }, - function (postData, next) { - postData.forEach(function (post) { - if (post) { - post.index = 0; - } - }); - Topics.addPostData(postData, uid, next); - }, - ], callback); +async function getMainPosts(mainPids, uid) { + const postData = await posts.getPostsByPids(mainPids, uid); + postData.forEach(function (post) { + if (post) { + post.index = 0; + } + }); + return await Topics.addPostData(postData, uid); } -Topics.isLocked = function (tid, callback) { - Topics.getTopicField(tid, 'locked', function (err, locked) { - callback(err, locked === 1); - }); +Topics.isLocked = async function (tid) { + const locked = await Topics.getTopicField(tid, 'locked'); + return locked === 1; }; -Topics.search = function (tid, term, callback) { - plugins.fireHook('filter:topic.search', { +Topics.search = async function (tid, term) { + const pids = await plugins.fireHook('filter:topic.search', { tid: tid, term: term, - }, function (err, pids) { - callback(err, Array.isArray(pids) ? pids : []); }); + return Array.isArray(pids) ? pids : []; }; Topics.async = require('../promisify')(Topics); diff --git a/src/topics/merge.js b/src/topics/merge.js index 5095606729..fb6a6e59d4 100644 --- a/src/topics/merge.js +++ b/src/topics/merge.js @@ -4,44 +4,27 @@ const async = require('async'); const plugins = require('../plugins'); module.exports = function (Topics) { - Topics.merge = function (tids, uid, callback) { - var mergeIntoTid = findOldestTopic(tids); + Topics.merge = async function (tids, uid) { + const mergeIntoTid = findOldestTopic(tids); - var otherTids = tids.filter(function (tid) { - return tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10); + const otherTids = tids.filter(tid => tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10)); + + await async.eachSeries(otherTids, async function (tid) { + const pids = await Topics.getPids(tid); + await async.eachSeries(pids, function (pid, next) { + Topics.movePostToTopic(uid, pid, mergeIntoTid, next); + }); + + await Topics.setTopicField(tid, 'mainPid', 0); + await Topics.delete(tid, uid); + await Topics.setTopicFields(tid, { + mergeIntoTid: mergeIntoTid, + mergerUid: uid, + mergedTimestamp: Date.now(), + }); }); - async.eachSeries(otherTids, function (tid, next) { - async.waterfall([ - function (next) { - Topics.getPids(tid, next); - }, - function (pids, next) { - async.eachSeries(pids, function (pid, next) { - Topics.movePostToTopic(uid, pid, mergeIntoTid, next); - }, next); - }, - function (next) { - Topics.setTopicField(tid, 'mainPid', 0, next); - }, - function (next) { - Topics.delete(tid, uid, next); - }, - function (next) { - Topics.setTopicFields(tid, { - mergeIntoTid: mergeIntoTid, - mergerUid: uid, - mergedTimestamp: Date.now(), - }, next); - }, - ], next); - }, function (err) { - if (err) { - return callback(err); - } - plugins.fireHook('action:topic.merge', { uid: uid, tids: tids, mergeIntoTid: mergeIntoTid, otherTids: otherTids }); - callback(); - }); + plugins.fireHook('action:topic.merge', { uid: uid, tids: tids, mergeIntoTid: mergeIntoTid, otherTids: otherTids }); }; function findOldestTopic(tids) { diff --git a/src/topics/posts.js b/src/topics/posts.js index c37bd38b88..b626256590 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var validator = require('validator'); @@ -13,114 +12,71 @@ var plugins = require('../plugins'); var utils = require('../../public/src/utils'); module.exports = function (Topics) { - Topics.onNewPostMade = function (postData, callback) { - async.series([ - function (next) { - Topics.updateLastPostTime(postData.tid, postData.timestamp, next); - }, - function (next) { - Topics.addPostToTopic(postData.tid, postData, next); - }, - ], callback); + Topics.onNewPostMade = async function (postData) { + await Topics.updateLastPostTime(postData.tid, postData.timestamp); + await Topics.addPostToTopic(postData.tid, postData); }; - Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) { - async.waterfall([ - function (next) { - posts.getPostsFromSet(set, start, stop, uid, reverse, next); - }, - function (posts, next) { - Topics.calculatePostIndices(posts, start); + Topics.getTopicPosts = async function (tid, set, start, stop, uid, reverse) { + const postData = await posts.getPostsFromSet(set, start, stop, uid, reverse); + Topics.calculatePostIndices(postData, start); - Topics.addPostData(posts, uid, next); - }, - ], callback); + return await Topics.addPostData(postData, uid); }; - Topics.addPostData = function (postData, uid, callback) { + Topics.addPostData = async function (postData, uid) { if (!Array.isArray(postData) || !postData.length) { - return callback(null, []); + return []; } var pids = postData.map(post => post && post.pid); - if (!Array.isArray(pids) || !pids.length) { - return callback(null, []); + async function getPostUserData(field, method) { + const uids = _.uniq(postData.filter(p => p && parseInt(p[field], 10) >= 0).map(p => p[field])); + const userData = await method(uids); + return _.zipObject(uids, userData); } + const [ + bookmarks, + voteData, + userData, + editors, + replies, + ] = await Promise.all([ + posts.hasBookmarked(pids, uid), + posts.getVoteStatusByPostIDs(pids, uid), + getPostUserData('uid', async function (uids) { + return await posts.getUserInfoForPosts(uids, uid); + }), + getPostUserData('editor', async function (uids) { + return await user.getUsersFields(uids, ['uid', 'username', 'userslug']); + }), + getPostReplies(pids, uid), + Topics.addParentPosts(postData), + ]); - function getPostUserData(field, method, callback) { - var uidsMap = {}; + postData.forEach(function (postObj, i) { + if (postObj) { + postObj.user = postObj.uid ? userData[postObj.uid] : _.clone(userData[postObj.uid]); + postObj.editor = postObj.editor ? editors[postObj.editor] : null; + postObj.bookmarked = bookmarks[i]; + postObj.upvoted = voteData.upvotes[i]; + postObj.downvoted = voteData.downvotes[i]; + postObj.votes = postObj.votes || 0; + postObj.replies = replies[i]; + postObj.selfPost = parseInt(uid, 10) > 0 && parseInt(uid, 10) === postObj.uid; - postData.forEach((post) => { - if (post && parseInt(post[field], 10) >= 0) { - uidsMap[post[field]] = 1; + // Username override for guests, if enabled + if (meta.config.allowGuestHandles && postObj.uid === 0 && postObj.handle) { + postObj.user.username = validator.escape(String(postObj.handle)); } - }); - const uids = Object.keys(uidsMap); + } + }); - async.waterfall([ - function (next) { - method(uids, next); - }, - function (users, next) { - next(null, _.zipObject(uids, users)); - }, - ], callback); - } - - async.waterfall([ - function (next) { - async.parallel({ - bookmarks: function (next) { - posts.hasBookmarked(pids, uid, next); - }, - voteData: function (next) { - posts.getVoteStatusByPostIDs(pids, uid, next); - }, - userData: function (next) { - getPostUserData('uid', function (uids, next) { - posts.getUserInfoForPosts(uids, uid, next); - }, next); - }, - editors: function (next) { - getPostUserData('editor', function (uids, next) { - user.getUsersFields(uids, ['uid', 'username', 'userslug'], next); - }, next); - }, - parents: function (next) { - Topics.addParentPosts(postData, next); - }, - replies: function (next) { - getPostReplies(pids, uid, next); - }, - }, next); - }, - function (results, next) { - postData.forEach(function (postObj, i) { - if (postObj) { - postObj.user = postObj.uid ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]); - postObj.editor = postObj.editor ? results.editors[postObj.editor] : null; - postObj.bookmarked = results.bookmarks[i]; - postObj.upvoted = results.voteData.upvotes[i]; - postObj.downvoted = results.voteData.downvotes[i]; - postObj.votes = postObj.votes || 0; - postObj.replies = results.replies[i]; - postObj.selfPost = parseInt(uid, 10) > 0 && parseInt(uid, 10) === postObj.uid; - - // Username override for guests, if enabled - if (meta.config.allowGuestHandles && postObj.uid === 0 && postObj.handle) { - postObj.user.username = validator.escape(String(postObj.handle)); - } - } - }); - plugins.fireHook('filter:topics.addPostData', { - posts: postData, - uid: uid, - }, next); - }, - function (data, next) { - next(null, data.posts); - }, - ], callback); + const result = await plugins.fireHook('filter:topics.addPostData', { + posts: postData, + uid: uid, + }); + return result.posts; }; Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) { @@ -142,40 +98,31 @@ module.exports = function (Topics) { }); }; - Topics.addParentPosts = function (postData, callback) { + Topics.addParentPosts = async function (postData) { var parentPids = postData.map(function (postObj) { return postObj && postObj.hasOwnProperty('toPid') ? parseInt(postObj.toPid, 10) : null; }).filter(Boolean); if (!parentPids.length) { - return setImmediate(callback); + return; } parentPids = _.uniq(parentPids); - var parentPosts; - async.waterfall([ - async.apply(posts.getPostsFields, parentPids, ['uid']), - function (_parentPosts, next) { - parentPosts = _parentPosts; - var parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid)); + const parentPosts = await posts.getPostsFields(parentPids, ['uid']); + const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid)); + const userData = await user.getUsersFields(parentUids, ['username']); - user.getUsersFields(parentUids, ['username'], next); - }, - function (userData, next) { - var usersMap = {}; - userData.forEach(function (user) { - usersMap[user.uid] = user.username; - }); - var parents = {}; - parentPosts.forEach(function (post, i) { - parents[parentPids[i]] = { username: usersMap[post.uid] }; - }); + var usersMap = {}; + userData.forEach(function (user) { + usersMap[user.uid] = user.username; + }); + var parents = {}; + parentPosts.forEach(function (post, i) { + parents[parentPids[i]] = { username: usersMap[post.uid] }; + }); - postData.forEach(function (post) { - post.parent = parents[post.toPid]; - }); - next(); - }, - ], callback); + postData.forEach(function (post) { + post.parent = parents[post.toPid]; + }); }; Topics.calculatePostIndices = function (posts, start) { @@ -186,251 +133,151 @@ module.exports = function (Topics) { }); }; - Topics.getLatestUndeletedPid = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getLatestUndeletedReply(tid, next); - }, - function (pid, next) { - if (pid) { - return callback(null, pid); - } - Topics.getTopicField(tid, 'mainPid', next); - }, - function (mainPid, next) { - posts.getPostFields(mainPid, ['pid', 'deleted'], next); - }, - function (mainPost, next) { - next(null, mainPost.pid && !mainPost.deleted ? mainPost.pid : null); - }, - ], callback); + Topics.getLatestUndeletedPid = async function (tid) { + const pid = await Topics.getLatestUndeletedReply(tid); + if (pid) { + return pid; + } + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + const mainPost = await posts.getPostFields(mainPid, ['pid', 'deleted']); + return mainPost.pid && !mainPost.deleted ? mainPost.pid : null; }; - Topics.getLatestUndeletedReply = function (tid, callback) { + Topics.getLatestUndeletedReply = async function (tid) { var isDeleted = false; - var done = false; - var latestPid = null; var index = 0; - var pids; - async.doWhilst( - function (next) { - async.waterfall([ - function (_next) { - db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, _next); - }, - function (_pids, _next) { - pids = _pids; - if (!pids.length) { - done = true; - return next(); - } - - posts.getPostField(pids[0], 'deleted', _next); - }, - function (deleted, _next) { - isDeleted = deleted; - if (!isDeleted) { - latestPid = pids[0]; - } - index += 1; - _next(); - }, - ], next); - }, - function (next) { - next(null, isDeleted && !done); - }, - function (err) { - callback(err, parseInt(latestPid, 10)); + do { + /* eslint-disable no-await-in-loop */ + const pids = await db.getSortedSetRevRange('tid:' + tid + ':posts', index, index); + if (!pids.length) { + return null; } - ); + isDeleted = await posts.getPostField(pids[0], 'deleted'); + if (!isDeleted) { + return parseInt(pids[0], 10); + } + index += 1; + } while (isDeleted); }; - Topics.addPostToTopic = function (tid, postData, callback) { - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'mainPid', next); - }, - function (mainPid, next) { - if (!parseInt(mainPid, 10)) { - Topics.setTopicField(tid, 'mainPid', postData.pid, next); - } else { - const upvotes = parseInt(postData.upvotes, 10) || 0; - const downvotes = parseInt(postData.downvotes, 10) || 0; - const votes = upvotes - downvotes; - db.sortedSetsAdd([ - 'tid:' + tid + ':posts', 'tid:' + tid + ':posts:votes', - ], [postData.timestamp, votes], postData.pid, next); - } - }, - function (next) { - Topics.increasePostCount(tid, next); - }, - function (next) { - db.sortedSetIncrBy('tid:' + tid + ':posters', 1, postData.uid, next); - }, - function (count, next) { - Topics.updateTeaser(tid, next); - }, - ], callback); + Topics.addPostToTopic = async function (tid, postData) { + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + if (!parseInt(mainPid, 10)) { + await Topics.setTopicField(tid, 'mainPid', postData.pid); + } else { + const upvotes = parseInt(postData.upvotes, 10) || 0; + const downvotes = parseInt(postData.downvotes, 10) || 0; + const votes = upvotes - downvotes; + await db.sortedSetsAdd([ + 'tid:' + tid + ':posts', 'tid:' + tid + ':posts:votes', + ], [postData.timestamp, votes], postData.pid); + } + await Topics.increasePostCount(tid); + await db.sortedSetIncrBy('tid:' + tid + ':posters', 1, postData.uid); + await Topics.updateTeaser(tid); }; - Topics.removePostFromTopic = function (tid, postData, callback) { - async.waterfall([ - function (next) { - db.sortedSetsRemove([ - 'tid:' + tid + ':posts', - 'tid:' + tid + ':posts:votes', - ], postData.pid, next); - }, - function (next) { - Topics.decreasePostCount(tid, next); - }, - function (next) { - db.sortedSetIncrBy('tid:' + tid + ':posters', -1, postData.uid, next); - }, - function (count, next) { - Topics.updateTeaser(tid, next); - }, - ], callback); + Topics.removePostFromTopic = async function (tid, postData) { + await db.sortedSetsRemove([ + 'tid:' + tid + ':posts', + 'tid:' + tid + ':posts:votes', + ], postData.pid); + await Topics.decreasePostCount(tid); + await db.sortedSetIncrBy('tid:' + tid + ':posters', -1, postData.uid); + await Topics.updateTeaser(tid); }; - Topics.getPids = function (tid, callback) { - async.waterfall([ - function (next) { - async.parallel({ - mainPid: function (next) { - Topics.getTopicField(tid, 'mainPid', next); - }, - pids: function (next) { - db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next); - }, - }, next); - }, - function (results, next) { - if (parseInt(results.mainPid, 10)) { - results.pids = [results.mainPid].concat(results.pids); - } - next(null, results.pids); - }, - ], callback); + Topics.getPids = async function (tid) { + var [mainPid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'mainPid'), + db.getSortedSetRange('tid:' + tid + ':posts', 0, -1), + ]); + if (parseInt(mainPid, 10)) { + pids = [mainPid].concat(pids); + } + return pids; }; - Topics.increasePostCount = function (tid, callback) { - incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts', callback); + Topics.increasePostCount = async function (tid) { + incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts'); }; - Topics.decreasePostCount = function (tid, callback) { - incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts', callback); + Topics.decreasePostCount = async function (tid) { + incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts'); }; - Topics.increaseViewCount = function (tid, callback) { - incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, 'topics:views', callback); + Topics.increaseViewCount = async function (tid) { + incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, 'topics:views'); }; - function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - db.incrObjectFieldBy('topic:' + tid, field, by, next); - }, - function (value, next) { - db.sortedSetAdd(set, value, tid, next); - }, - ], callback); + async function incrementFieldAndUpdateSortedSet(tid, field, by, set) { + const value = await db.incrObjectFieldBy('topic:' + tid, field, by); + await db.sortedSetAdd(set, value, tid); } - Topics.getTitleByPid = function (pid, callback) { - Topics.getTopicFieldByPid('title', pid, callback); + Topics.getTitleByPid = async function (pid) { + return await Topics.getTopicFieldByPid('title', pid); }; - Topics.getTopicFieldByPid = function (field, pid, callback) { - async.waterfall([ - function (next) { - posts.getPostField(pid, 'tid', next); - }, - function (tid, next) { - Topics.getTopicField(tid, field, next); - }, - ], callback); + Topics.getTopicFieldByPid = async function (field, pid) { + const tid = await posts.getPostField(pid, 'tid'); + return await Topics.getTopicField(tid, field); }; - Topics.getTopicDataByPid = function (pid, callback) { - async.waterfall([ - function (next) { - posts.getPostField(pid, 'tid', next); - }, - function (tid, next) { - Topics.getTopicData(tid, next); - }, - ], callback); + Topics.getTopicDataByPid = async function (pid) { + const tid = await posts.getPostField(pid, 'tid'); + return await Topics.getTopicData(tid); }; - Topics.getPostCount = function (tid, callback) { - db.getObjectField('topic:' + tid, 'postcount', callback); + Topics.getPostCount = async function (tid) { + return await db.getObjectField('topic:' + tid, 'postcount'); }; - function getPostReplies(pids, callerUid, callback) { - var arrayOfReplyPids; - var replyData; - var uniqueUids; - var uniquePids; - async.waterfall([ - function (next) { - const keys = pids.map(pid => 'pid:' + pid + ':replies'); - db.getSortedSetsMembers(keys, next); - }, - function (arrayOfPids, next) { - arrayOfReplyPids = arrayOfPids; + async function getPostReplies(pids, callerUid) { + const keys = pids.map(pid => 'pid:' + pid + ':replies'); + const arrayOfReplyPids = await db.getSortedSetsMembers(keys); - uniquePids = _.uniq(_.flatten(arrayOfPids)); + const uniquePids = _.uniq(_.flatten(arrayOfReplyPids)); - posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp'], next); - }, - function (_replyData, next) { - replyData = _replyData; - const uids = replyData.map(replyData => replyData && replyData.uid); + const replyData = await posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp']); - uniqueUids = _.uniq(uids); + const uids = replyData.map(replyData => replyData && replyData.uid); - user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid, next); - }, - function (userData, next) { - var uidMap = _.zipObject(uniqueUids, userData); - var pidMap = _.zipObject(uniquePids, replyData); + const uniqueUids = _.uniq(uids); - var returnData = arrayOfReplyPids.map(function (replyPids) { - var uidsUsed = {}; - var currentData = { - hasMore: false, - users: [], - text: replyPids.length > 1 ? '[[topic:replies_to_this_post, ' + replyPids.length + ']]' : '[[topic:one_reply_to_this_post]]', - count: replyPids.length, - timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined, - }; + const userData = await user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid); - replyPids.sort(function (a, b) { - return parseInt(a, 10) - parseInt(b, 10); - }); + var uidMap = _.zipObject(uniqueUids, userData); + var pidMap = _.zipObject(uniquePids, replyData); - replyPids.forEach(function (replyPid) { - var replyData = pidMap[replyPid]; - if (!uidsUsed[replyData.uid] && currentData.users.length < 6) { - currentData.users.push(uidMap[replyData.uid]); - uidsUsed[replyData.uid] = true; - } - }); + var returnData = arrayOfReplyPids.map(function (replyPids) { + var uidsUsed = {}; + var currentData = { + hasMore: false, + users: [], + text: replyPids.length > 1 ? '[[topic:replies_to_this_post, ' + replyPids.length + ']]' : '[[topic:one_reply_to_this_post]]', + count: replyPids.length, + timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined, + }; - if (currentData.users.length > 5) { - currentData.users.pop(); - currentData.hasMore = true; - } + replyPids.sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); - return currentData; - }); + replyPids.forEach(function (replyPid) { + var replyData = pidMap[replyPid]; + if (!uidsUsed[replyData.uid] && currentData.users.length < 6) { + currentData.users.push(uidMap[replyData.uid]); + uidsUsed[replyData.uid] = true; + } + }); - next(null, returnData); - }, - ], callback); + if (currentData.users.length > 5) { + currentData.users.pop(); + currentData.hasMore = true; + } + + return currentData; + }); + + return returnData; } }; diff --git a/src/topics/recent.js b/src/topics/recent.js index fee915bd73..333d6a0865 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -2,8 +2,6 @@ 'use strict'; -var async = require('async'); - var db = require('../database'); var plugins = require('../plugins'); var posts = require('../posts'); @@ -16,109 +14,69 @@ module.exports = function (Topics) { year: 31104000000, }; - Topics.getRecentTopics = function (cid, uid, start, stop, filter, callback) { - Topics.getSortedTopics({ + Topics.getRecentTopics = async function (cid, uid, start, stop, filter) { + return await Topics.getSortedTopics({ cids: cid, uid: uid, start: start, stop: stop, filter: filter, sort: 'recent', - }, callback); + }); }; /* not an orphan method, used in widget-essentials */ - Topics.getLatestTopics = function (options, callback) { + Topics.getLatestTopics = async function (options) { // uid, start, stop, term - async.waterfall([ - function (next) { - Topics.getLatestTidsFromSet('topics:recent', options.start, options.stop, options.term, next); - }, - function (tids, next) { - Topics.getTopics(tids, options, next); - }, - function (topics, next) { - next(null, { topics: topics, nextStart: options.stop + 1 }); - }, - ], callback); + const tids = await Topics.getLatestTidsFromSet('topics:recent', options.start, options.stop, options.term); + const topics = await Topics.getTopics(tids, options); + return { topics: topics, nextStart: options.stop + 1 }; }; - Topics.getLatestTidsFromSet = function (set, start, stop, term, callback) { + Topics.getLatestTidsFromSet = async function (set, start, stop, term) { var since = terms.day; if (terms[term]) { since = terms[term]; } var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; - - db.getSortedSetRevRangeByScore(set, start, count, '+inf', Date.now() - since, callback); + return await db.getSortedSetRevRangeByScore(set, start, count, '+inf', Date.now() - since); }; - Topics.updateLastPostTimeFromLastPid = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getLatestUndeletedPid(tid, next); - }, - function (pid, next) { - if (!pid) { - return callback(); - } - posts.getPostField(pid, 'timestamp', next); - }, - function (timestamp, next) { - if (!timestamp) { - return callback(); - } - Topics.updateLastPostTime(tid, timestamp, next); - }, - ], callback); + Topics.updateLastPostTimeFromLastPid = async function (tid) { + const pid = await Topics.getLatestUndeletedPid(tid); + if (!pid) { + return; + } + const timestamp = await posts.getPostField(pid, 'timestamp'); + if (!timestamp) { + return; + } + await Topics.updateLastPostTime(tid, timestamp); }; - Topics.updateLastPostTime = function (tid, lastposttime, callback) { - async.waterfall([ - function (next) { - Topics.setTopicField(tid, 'lastposttime', lastposttime, next); - }, - function (next) { - Topics.getTopicFields(tid, ['cid', 'deleted', 'pinned'], next); - }, - function (topicData, next) { - var tasks = [ - async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:lastposttime', lastposttime, tid), - ]; + Topics.updateLastPostTime = async function (tid, lastposttime) { + await Topics.setTopicField(tid, 'lastposttime', lastposttime); + const topicData = await Topics.getTopicFields(tid, ['cid', 'deleted', 'pinned']); - if (!topicData.deleted) { - tasks.push(async.apply(Topics.updateRecent, tid, lastposttime)); - } + await db.sortedSetAdd('cid:' + topicData.cid + ':tids:lastposttime', lastposttime, tid); - if (!topicData.pinned) { - tasks.push(async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', lastposttime, tid)); - } - async.series(tasks, next); - }, - ], function (err) { - callback(err); - }); + if (!topicData.deleted) { + await Topics.updateRecent(tid, lastposttime); + } + + if (!topicData.pinned) { + await db.sortedSetAdd('cid:' + topicData.cid + ':tids', lastposttime, tid); + } }; - Topics.updateRecent = function (tid, timestamp, callback) { - callback = callback || function () {}; - - async.waterfall([ - function (next) { - if (plugins.hasListeners('filter:topics.updateRecent')) { - plugins.fireHook('filter:topics.updateRecent', { tid: tid, timestamp: timestamp }, next); - } else { - next(null, { tid: tid, timestamp: timestamp }); - } - }, - function (data, next) { - if (data && data.tid && data.timestamp) { - db.sortedSetAdd('topics:recent', data.timestamp, data.tid, next); - } else { - next(); - } - }, - ], callback); + Topics.updateRecent = async function (tid, timestamp) { + let data = { tid: tid, timestamp: timestamp }; + if (plugins.hasListeners('filter:topics.updateRecent')) { + data = await plugins.fireHook('filter:topics.updateRecent', { tid: tid, timestamp: timestamp }); + } + if (data && data.tid && data.timestamp) { + await db.sortedSetAdd('topics:recent', data.timestamp, data.tid); + } }; }; diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 40b7799ab9..463ac72d5f 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -1,7 +1,6 @@ 'use strict'; -const async = require('async'); const _ = require('lodash'); const db = require('../database'); @@ -12,7 +11,7 @@ const meta = require('../meta'); const plugins = require('../plugins'); module.exports = function (Topics) { - Topics.getSortedTopics = function (params, callback) { + Topics.getSortedTopics = async function (params) { var data = { nextStart: 0, topicCount: 0, @@ -25,51 +24,31 @@ module.exports = function (Topics) { if (params.hasOwnProperty('cids') && params.cids && !Array.isArray(params.cids)) { params.cids = [params.cids]; } - - async.waterfall([ - function (next) { - getTids(params, next); - }, - function (tids, next) { - data.topicCount = tids.length; - data.tids = tids; - getTopics(tids, params, next); - }, - function (topicData, next) { - data.topics = topicData; - data.nextStart = params.stop + 1; - next(null, data); - }, - ], callback); + data.tids = await getTids(params); + data.topicCount = data.tids.length; + data.topics = await getTopics(data.tids, params); + data.nextStart = params.stop + 1; + return data; }; - function getTids(params, callback) { - async.waterfall([ - function (next) { - if (params.term === 'alltime') { - if (params.cids) { - getCidTids(params.cids, params.sort, next); - } else { - db.getSortedSetRevRange('topics:' + params.sort, 0, 199, next); - } - } else { - Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term, next); - } - }, - function (tids, next) { - if (params.term !== 'alltime' || (params.cids && params.sort !== 'recent')) { - sortTids(tids, params, next); - } else { - next(null, tids); - } - }, - function (tids, next) { - filterTids(tids, params, next); - }, - ], callback); + async function getTids(params) { + let tids = []; + if (params.term === 'alltime') { + if (params.cids) { + tids = await getCidTids(params.cids, params.sort); + } else { + tids = await db.getSortedSetRevRange('topics:' + params.sort, 0, 199); + } + } else { + tids = await Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term); + } + if (params.term !== 'alltime' || (params.cids && params.sort !== 'recent')) { + tids = await sortTids(tids, params); + } + return await filterTids(tids, params); } - function getCidTids(cids, sort, callback) { + async function getCidTids(cids, sort) { const sets = []; const pinnedSets = []; cids.forEach(function (cid) { @@ -80,35 +59,23 @@ module.exports = function (Topics) { sets.push('cid:' + cid + ':tids' + (sort ? ':' + sort : '')); pinnedSets.push('cid:' + cid + ':tids:pinned'); }); - async.waterfall([ - function (next) { - async.parallel({ - tids: async.apply(db.getSortedSetRevRange, sets, 0, 199), - pinnedTids: async.apply(db.getSortedSetRevRange, pinnedSets, 0, -1), - }, next); - }, - function (results, next) { - next(null, results.pinnedTids.concat(results.tids)); - }, - ], callback); + const [tids, pinnedTids] = await Promise.all([ + db.getSortedSetRevRange(sets, 0, 199), + db.getSortedSetRevRange(pinnedSets, 0, -1), + ]); + return pinnedTids.concat(tids); } - function sortTids(tids, params, callback) { - async.waterfall([ - function (next) { - Topics.getTopicsFields(tids, ['tid', 'lastposttime', 'upvotes', 'downvotes', 'postcount'], next); - }, - function (topicData, next) { - var sortFn = sortRecent; - if (params.sort === 'posts') { - sortFn = sortPopular; - } else if (params.sort === 'votes') { - sortFn = sortVotes; - } - tids = topicData.sort(sortFn).map(topic => topic && topic.tid); - next(null, tids); - }, - ], callback); + async function sortTids(tids, params) { + const topicData = await Topics.getTopicsFields(tids, ['tid', 'lastposttime', 'upvotes', 'downvotes', 'postcount']); + var sortFn = sortRecent; + if (params.sort === 'posts') { + sortFn = sortPopular; + } else if (params.sort === 'votes') { + sortFn = sortVotes; + } + tids = topicData.sort(sortFn).map(topic => topic && topic.tid); + return tids; } function sortRecent(a, b) { @@ -129,71 +96,52 @@ module.exports = function (Topics) { return b.viewcount - a.viewcount; } - function filterTids(tids, params, callback) { + async function filterTids(tids, params) { const filter = params.filter; const uid = params.uid; - let topicData; - let topicCids; - async.waterfall([ - function (next) { - if (filter === 'watched') { - Topics.filterWatchedTids(tids, uid, next); - } else if (filter === 'new') { - Topics.filterNewTids(tids, uid, next); - } else if (filter === 'unreplied') { - Topics.filterUnrepliedTids(tids, next); - } else { - Topics.filterNotIgnoredTids(tids, uid, next); - } - }, - function (tids, next) { - privileges.topics.filterTids('topics:read', tids, uid, next); - }, - function (tids, next) { - Topics.getTopicsFields(tids, ['uid', 'tid', 'cid'], next); - }, - function (_topicData, next) { - topicData = _topicData; - topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); + if (filter === 'watched') { + tids = await Topics.filterWatchedTids(tids, uid); + } else if (filter === 'new') { + tids = await Topics.filterNewTids(tids, uid); + } else if (filter === 'unreplied') { + tids = await Topics.filterUnrepliedTids(tids); + } else { + tids = await Topics.filterNotIgnoredTids(tids, uid); + } - async.parallel({ - ignoredCids: function (next) { - if (filter === 'watched' || meta.config.disableRecentCategoryFilter) { - return next(null, []); - } - categories.isIgnored(topicCids, uid, next); - }, - filtered: async.apply(user.blocks.filter, uid, topicData), - }, next); - }, - function (results, next) { - const isCidIgnored = _.zipObject(topicCids, results.ignoredCids); - topicData = results.filtered; + tids = await privileges.topics.filterTids('topics:read', tids, uid); + let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid']); + const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); - const cids = params.cids && params.cids.map(String); - tids = topicData.filter(function (topic) { - return topic && topic.cid && !isCidIgnored[topic.cid] && (!cids || (cids.length && cids.includes(topic.cid.toString()))); - }).map(topic => topic.tid); - plugins.fireHook('filter:topics.filterSortedTids', { tids: tids, params: params }, next); - }, - function (data, next) { - next(null, data && data.tids); - }, - ], callback); + async function getIgnoredCids() { + if (filter === 'watched' || meta.config.disableRecentCategoryFilter) { + return []; + } + return await categories.isIgnored(topicCids, uid); + } + const [ignoredCids, filtered] = await Promise.all([ + getIgnoredCids(), + user.blocks.filter(uid, topicData), + ]); + + const isCidIgnored = _.zipObject(topicCids, ignoredCids); + topicData = filtered; + + const cids = params.cids && params.cids.map(String); + tids = topicData.filter(function (topic) { + return topic && topic.cid && !isCidIgnored[topic.cid] && (!cids || (cids.length && cids.includes(topic.cid.toString()))); + }).map(topic => topic.tid); + + const result = await plugins.fireHook('filter:topics.filterSortedTids', { tids: tids, params: params }); + return result.tids; } - function getTopics(tids, params, callback) { - async.waterfall([ - function (next) { - tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); - Topics.getTopicsByTids(tids, params, next); - }, - function (topicData, next) { - Topics.calculateTopicIndices(topicData, params.start); - next(null, topicData); - }, - ], callback); + async function getTopics(tids, params) { + tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); + const topicData = await Topics.getTopicsByTids(tids, params); + Topics.calculateTopicIndices(topicData, params.start); + return topicData; } Topics.calculateTopicIndices = function (topicData, start) { diff --git a/src/topics/suggested.js b/src/topics/suggested.js index 2378391e04..d0cd9f1eea 100644 --- a/src/topics/suggested.js +++ b/src/topics/suggested.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); const db = require('../database'); @@ -10,92 +9,52 @@ var privileges = require('../privileges'); var search = require('../search'); module.exports = function (Topics) { - Topics.getSuggestedTopics = function (tid, uid, start, stop, callback) { - var tids; + Topics.getSuggestedTopics = async function (tid, uid, start, stop) { + let tids; tid = parseInt(tid, 10); - async.waterfall([ - function (next) { - async.parallel({ - tagTids: function (next) { - getTidsWithSameTags(tid, next); - }, - searchTids: function (next) { - getSearchTids(tid, uid, next); - }, - }, next); - }, - function (results, next) { - tids = results.tagTids.concat(results.searchTids); - tids = tids.filter(_tid => _tid !== tid); - tids = _.shuffle(_.uniq(tids)); + const [tagTids, searchTids] = await Promise.all([ + getTidsWithSameTags(tid), + getSearchTids(tid, uid), + ]); - if (stop !== -1 && tids.length < stop - start + 1) { - getCategoryTids(tid, next); - } else { - next(null, []); - } - }, - function (categoryTids, next) { - tids = _.uniq(tids.concat(categoryTids)).slice(start, stop !== -1 ? stop + 1 : undefined); - privileges.topics.filterTids('topics:read', tids, uid, next); - }, - function (tids, next) { - Topics.getTopicsByTids(tids, uid, next); - }, - function (topics, next) { - topics = topics.filter(topic => topic && !topic.deleted && topic.tid !== tid); - user.blocks.filter(uid, topics, next); - }, - ], callback); + tids = tagTids.concat(searchTids); + tids = tids.filter(_tid => _tid !== tid); + tids = _.shuffle(_.uniq(tids)); + let categoryTids = []; + if (stop !== -1 && tids.length < stop - start + 1) { + categoryTids = await getCategoryTids(tid); + } + tids = _.uniq(tids.concat(categoryTids)).slice(start, stop !== -1 ? stop + 1 : undefined); + tids = await privileges.topics.filterTids('topics:read', tids, uid); + + let topicData = await Topics.getTopicsByTids(tids, uid); + topicData = topicData.filter(topic => topic && !topic.deleted && topic.tid !== tid); + topicData = await user.blocks.filter(uid, topicData); + return topicData; }; - function getTidsWithSameTags(tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicTags(tid, next); - }, - function (tags, next) { - db.getSortedSetRevRange(tags.map(tag => 'tag:' + tag + ':topics'), 0, -1, next); - }, - function (tids, next) { - next(null, _.uniq(tids).map(Number)); - }, - ], callback); + async function getTidsWithSameTags(tid) { + const tags = await Topics.getTopicTags(tid); + const tids = await db.getSortedSetRevRange(tags.map(tag => 'tag:' + tag + ':topics'), 0, -1); + return _.uniq(tids).map(Number); } - function getSearchTids(tid, uid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicFields(tid, ['title', 'cid'], next); - }, - function (topicData, next) { - search.search({ - query: topicData.title, - searchIn: 'titles', - matchWords: 'any', - categories: [topicData.cid], - uid: uid, - returnIds: true, - }, next); - }, - function (data, next) { - next(null, _.shuffle(data.tids).slice(0, 20).map(Number)); - }, - ], callback); + async function getSearchTids(tid, uid) { + const topicData = await Topics.getTopicFields(tid, ['title', 'cid']); + const data = await search.search({ + query: topicData.title, + searchIn: 'titles', + matchWords: 'any', + categories: [topicData.cid], + uid: uid, + returnIds: true, + }); + return _.shuffle(data.tids).slice(0, 20).map(Number); } - function getCategoryTids(tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - db.getSortedSetRevRange('cid:' + cid + ':tids:lastposttime', 0, 9, next); - }, - function (tids, next) { - tids = tids.map(Number).filter(_tid => _tid !== tid); - next(null, _.shuffle(tids)); - }, - ], callback); + async function getCategoryTids(tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + const tids = await db.getSortedSetRevRange('cid:' + cid + ':tids:lastposttime', 0, 9); + return _.shuffle(tids.map(Number).filter(_tid => _tid !== tid)); } }; diff --git a/src/topics/tags.js b/src/topics/tags.js index 474dfe360f..96dc21ea87 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -13,396 +13,249 @@ var utils = require('../utils'); var batch = require('../batch'); module.exports = function (Topics) { - Topics.createTags = function (tags, tid, timestamp, callback) { - callback = callback || function () {}; - + Topics.createTags = async function (tags, tid, timestamp) { if (!Array.isArray(tags) || !tags.length) { - return callback(); + return; } + const result = await plugins.fireHook('filter:tags.filter', { tags: tags, tid: tid }); + tags = _.uniq(result.tags).slice(0, meta.config.maximumTagsPerTopic || 5) + .map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) + .filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3)); - async.waterfall([ - function (next) { - plugins.fireHook('filter:tags.filter', { tags: tags, tid: tid }, next); - }, - function (data, next) { - tags = _.uniq(data.tags).slice(0, meta.config.maximumTagsPerTopic || 5) - .map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) - .filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3)); + tags = await filterCategoryTags(tags, tid); + await Promise.all([ + db.setAdd('topic:' + tid + ':tags', tags), + db.sortedSetsAdd(tags.map(tag => 'tag:' + tag + ':topics'), timestamp, tid), + ]); - filterCategoryTags(tags, tid, next); - }, - function (_tags, next) { - tags = _tags; - - async.parallel([ - async.apply(db.setAdd, 'topic:' + tid + ':tags', tags), - async.apply(db.sortedSetsAdd, tags.map(tag => 'tag:' + tag + ':topics'), timestamp, tid), - ], function (err) { - next(err); - }); - }, - function (next) { - async.each(tags, updateTagCount, next); - }, - ], callback); + await Promise.all(tags.map(tag => updateTagCount(tag))); }; - function filterCategoryTags(tags, tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - categories.getTagWhitelist([cid], next); - }, - function (tagWhitelist, next) { - if (!Array.isArray(tagWhitelist[0]) || !tagWhitelist[0].length) { - return next(null, tags); - } - const whitelistSet = new Set(tagWhitelist[0]); - tags = tags.filter(tag => whitelistSet.has(tag)); - next(null, tags); - }, - ], callback); + async function filterCategoryTags(tags, tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + const tagWhitelist = await categories.getTagWhitelist([cid]); + if (!Array.isArray(tagWhitelist[0]) || !tagWhitelist[0].length) { + return tags; + } + const whitelistSet = new Set(tagWhitelist[0]); + return tags.filter(tag => whitelistSet.has(tag)); } - Topics.createEmptyTag = function (tag, callback) { + Topics.createEmptyTag = async function (tag) { if (!tag) { - return callback(new Error('[[error:invalid-tag]]')); + throw new Error('[[error:invalid-tag]]'); } tag = utils.cleanUpTag(tag, meta.config.maximumTagLength); if (tag.length < (meta.config.minimumTagLength || 3)) { - return callback(new Error('[[error:tag-too-short]]')); + throw new Error('[[error:tag-too-short]]'); + } + const isMember = await db.isSortedSetMember('tags:topic:count', tag); + if (!isMember) { + await db.sortedSetAdd('tags:topic:count', 0, tag); } - - async.waterfall([ - function (next) { - db.isSortedSetMember('tags:topic:count', tag, next); - }, - function (isMember, next) { - if (isMember) { - return next(); - } - db.sortedSetAdd('tags:topic:count', 0, tag, next); - }, - ], callback); }; - Topics.updateTags = function (data, callback) { - async.eachSeries(data, function (tagData, next) { - db.setObject('tag:' + tagData.value, { + Topics.updateTags = async function (data) { + await async.eachSeries(data, async function (tagData) { + await db.setObject('tag:' + tagData.value, { color: tagData.color, bgColor: tagData.bgColor, - }, next); - }, callback); - }; - - Topics.renameTags = function (data, callback) { - async.eachSeries(data, function (tagData, next) { - renameTag(tagData.value, tagData.newName, next); - }, callback); - }; - - function renameTag(tag, newTagName, callback) { - if (!newTagName || tag === newTagName) { - return setImmediate(callback); - } - async.waterfall([ - function (next) { - Topics.createEmptyTag(newTagName, next); - }, - function (next) { - batch.processSortedSet('tag:' + tag + ':topics', function (tids, next) { - async.waterfall([ - function (next) { - db.sortedSetScores('tag:' + tag + ':topics', tids, next); - }, - function (scores, next) { - db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids, next); - }, - function (next) { - const keys = tids.map(tid => 'topic:' + tid + ':tags'); - - async.series([ - async.apply(db.sortedSetRemove, 'tag:' + tag + ':topics', tids), - async.apply(db.setsRemove, keys, tag), - async.apply(db.setsAdd, keys, newTagName), - ], next); - }, - ], next); - }, next); - }, - function (next) { - Topics.deleteTag(tag, next); - }, - function (next) { - updateTagCount(newTagName, next); - }, - ], callback); - } - - function updateTagCount(tag, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - Topics.getTagTopicCount(tag, next); - }, - function (count, next) { - db.sortedSetAdd('tags:topic:count', count || 0, tag, next); - }, - ], callback); - } - - Topics.getTagTids = function (tag, start, stop, callback) { - db.getSortedSetRevRange('tag:' + tag + ':topics', start, stop, callback); - }; - - Topics.getTagTopicCount = function (tag, callback) { - db.sortedSetCard('tag:' + tag + ':topics', callback); - }; - - Topics.deleteTags = function (tags, callback) { - if (!Array.isArray(tags) || !tags.length) { - return callback(); - } - - async.series([ - function (next) { - removeTagsFromTopics(tags, next); - }, - function (next) { - const keys = tags.map(tag => 'tag:' + tag + ':topics'); - db.deleteAll(keys, next); - }, - function (next) { - db.sortedSetRemove('tags:topic:count', tags, next); - }, - function (next) { - db.deleteAll(tags.map(tag => 'tag:' + tag), next); - }, - ], err => callback(err)); - }; - - function removeTagsFromTopics(tags, callback) { - async.eachLimit(tags, 50, function (tag, next) { - db.getSortedSetRange('tag:' + tag + ':topics', 0, -1, function (err, tids) { - if (err || !tids.length) { - return next(err); - } - const keys = tids.map(tid => 'topic:' + tid + ':tags'); - db.setsRemove(keys, tag, next); }); - }, callback); - } - - Topics.deleteTag = function (tag, callback) { - Topics.deleteTags([tag], callback); - }; - - Topics.getTags = function (start, stop, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop, next); - }, - function (tags, next) { - Topics.getTagData(tags, next); - }, - ], callback); - }; - - Topics.getTagData = function (tags, callback) { - if (!tags.length) { - return setImmediate(callback, null, []); - } - - async.waterfall([ - function (next) { - db.getObjects(tags.map(tag => 'tag:' + tag.value), next); - }, - function (tagData, next) { - tags.forEach(function (tag, index) { - tag.valueEscaped = validator.escape(String(tag.value)); - tag.color = tagData[index] ? tagData[index].color : ''; - tag.bgColor = tagData[index] ? tagData[index].bgColor : ''; - }); - next(null, tags); - }, - ], callback); - }; - - Topics.getTopicTags = function (tid, callback) { - db.getSetMembers('topic:' + tid + ':tags', callback); - }; - - Topics.getTopicsTags = function (tids, callback) { - const keys = tids.map(tid => 'topic:' + tid + ':tags'); - db.getSetsMembers(keys, callback); - }; - - Topics.getTopicTagsObjects = function (tid, callback) { - Topics.getTopicsTagsObjects([tid], function (err, data) { - callback(err, Array.isArray(data) && data.length ? data[0] : []); }); }; - Topics.getTopicsTagsObjects = function (tids, callback) { - var uniqueTopicTags; - var topicTags; - async.waterfall([ - function (next) { - Topics.getTopicsTags(tids, next); - }, - function (_topicTags, next) { - topicTags = _topicTags; - uniqueTopicTags = _.uniq(_.flatten(topicTags)); - - var tags = uniqueTopicTags.map(tag => ({ value: tag })); - - async.parallel({ - tagData: function (next) { - Topics.getTagData(tags, next); - }, - counts: function (next) { - db.sortedSetScores('tags:topic:count', uniqueTopicTags, next); - }, - }, next); - }, - function (results, next) { - results.tagData.forEach(function (tag, index) { - tag.score = results.counts[index] ? results.counts[index] : 0; - }); - - var tagData = _.zipObject(uniqueTopicTags, results.tagData); - - topicTags.forEach(function (tags, index) { - if (Array.isArray(tags)) { - topicTags[index] = tags.map(tag => tagData[tag]); - topicTags[index].sort((tag1, tag2) => tag2.score - tag1.score); - } - }); - - next(null, topicTags); - }, - ], callback); + Topics.renameTags = async function (data) { + await async.eachSeries(data, async function (tagData) { + await renameTag(tagData.value, tagData.newName); + }); }; - Topics.updateTopicTags = function (tid, tags, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - Topics.deleteTopicTags(tid, next); - }, - function (next) { - Topics.getTopicField(tid, 'timestamp', next); - }, - function (timestamp, next) { - Topics.createTags(tags, tid, timestamp, next); - }, - ], callback); - }; - - Topics.deleteTopicTags = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicTags(tid, next); - }, - function (tags, next) { - async.series([ - function (next) { - db.delete('topic:' + tid + ':tags', next); - }, - function (next) { - const sets = tags.map(tag => 'tag:' + tag + ':topics'); - db.sortedSetsRemove(sets, tid, next); - }, - function (next) { - async.each(tags, function (tag, next) { - updateTagCount(tag, next); - }, next); - }, - ], next); - }, - ], err => callback(err)); - }; - - Topics.searchTags = function (data, callback) { - if (!data || !data.query) { - return callback(null, []); + async function renameTag(tag, newTagName) { + if (!newTagName || tag === newTagName) { + return; } - - async.waterfall([ - function (next) { - if (plugins.hasListeners('filter:topics.searchTags')) { - plugins.fireHook('filter:topics.searchTags', { data: data }, next); - } else { - findMatches(data.query, 0, next); - } - }, - function (result, next) { - plugins.fireHook('filter:tags.search', { data: data, matches: result.matches }, next); - }, - function (result, next) { - next(null, result.matches); - }, - ], callback); - }; - - Topics.autocompleteTags = function (data, callback) { - if (!data || !data.query) { - return callback(null, []); - } - - async.waterfall([ - function (next) { - if (plugins.hasListeners('filter:topics.autocompleteTags')) { - plugins.fireHook('filter:topics.autocompleteTags', { data: data }, next); - } else { - findMatches(data.query, data.cid, next); - } - }, - function (result, next) { - next(null, result.matches); - }, - ], callback); - }; - - function findMatches(query, cid, callback) { - async.waterfall([ - function (next) { - if (parseInt(cid, 10)) { - categories.getTagWhitelist([cid], next); - } else { - setImmediate(next, null, []); - } - }, - function (tagWhitelist, next) { - if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) { - setImmediate(next, null, tagWhitelist[0]); - } else { - db.getSortedSetRevRange('tags:topic:count', 0, -1, next); - } - }, - function (tags, next) { - query = query.toLowerCase(); - - var matches = []; - for (var i = 0; i < tags.length; i += 1) { - if (tags[i].toLowerCase().startsWith(query)) { - matches.push(tags[i]); - if (matches.length > 19) { - break; - } - } - } - - matches.sort(); - next(null, { matches: matches }); - }, - ], callback); + await Topics.createEmptyTag(newTagName); + await batch.processSortedSet('tag:' + tag + ':topics', async function (tids) { + const scores = await db.sortedSetScores('tag:' + tag + ':topics', tids); + await db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids); + const keys = tids.map(tid => 'topic:' + tid + ':tags'); + await db.sortedSetRemove('tag:' + tag + ':topics', tids); + await db.setsRemove(keys, tag); + await db.setsAdd(keys, newTagName); + }, {}); + await Topics.deleteTag(tag); + await updateTagCount(newTagName); } - Topics.searchAndLoadTags = function (data, callback) { + async function updateTagCount(tag) { + const count = await Topics.getTagTopicCount(tag); + await db.sortedSetAdd('tags:topic:count', count || 0, tag); + } + + Topics.getTagTids = async function (tag, start, stop) { + return await db.getSortedSetRevRange('tag:' + tag + ':topics', start, stop); + }; + + Topics.getTagTopicCount = async function (tag) { + return await db.sortedSetCard('tag:' + tag + ':topics'); + }; + + Topics.deleteTags = async function (tags) { + if (!Array.isArray(tags) || !tags.length) { + return; + } + await removeTagsFromTopics(tags); + const keys = tags.map(tag => 'tag:' + tag + ':topics'); + await db.deleteAll(keys); + await db.sortedSetRemove('tags:topic:count', tags); + await db.deleteAll(tags.map(tag => 'tag:' + tag)); + }; + + async function removeTagsFromTopics(tags) { + await async.eachLimit(tags, 50, async function (tag) { + const tids = await db.getSortedSetRange('tag:' + tag + ':topics', 0, -1); + if (!tids.length) { + return; + } + const keys = tids.map(tid => 'topic:' + tid + ':tags'); + await db.setsRemove(keys, tag); + }); + } + + Topics.deleteTag = async function (tag) { + await Topics.deleteTags([tag]); + }; + + Topics.getTags = async function (start, stop) { + const tags = await db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop); + return await Topics.getTagData(tags); + }; + + Topics.getTagData = async function (tags) { + if (!tags.length) { + return []; + } + const tagData = await db.getObjects(tags.map(tag => 'tag:' + tag.value)); + tags.forEach(function (tag, index) { + tag.valueEscaped = validator.escape(String(tag.value)); + tag.color = tagData[index] ? tagData[index].color : ''; + tag.bgColor = tagData[index] ? tagData[index].bgColor : ''; + }); + return tags; + }; + + Topics.getTopicTags = async function (tid) { + return await db.getSetMembers('topic:' + tid + ':tags'); + }; + + Topics.getTopicsTags = async function (tids) { + const keys = tids.map(tid => 'topic:' + tid + ':tags'); + return await db.getSetsMembers(keys); + }; + + Topics.getTopicTagsObjects = async function (tid) { + const data = await Topics.getTopicsTagsObjects([tid]); + return Array.isArray(data) && data.length ? data[0] : []; + }; + + Topics.getTopicsTagsObjects = async function (tids) { + const topicTags = await Topics.getTopicsTags(tids); + const uniqueTopicTags = _.uniq(_.flatten(topicTags)); + + var tags = uniqueTopicTags.map(tag => ({ value: tag })); + + const [tagData, counts] = await Promise.all([ + Topics.getTagData(tags), + db.sortedSetScores('tags:topic:count', uniqueTopicTags), + ]); + + tagData.forEach(function (tag, index) { + tag.score = counts[index] ? counts[index] : 0; + }); + + var tagDataMap = _.zipObject(uniqueTopicTags, tagData); + + topicTags.forEach(function (tags, index) { + if (Array.isArray(tags)) { + topicTags[index] = tags.map(tag => tagDataMap[tag]); + topicTags[index].sort((tag1, tag2) => tag2.score - tag1.score); + } + }); + + return topicTags; + }; + + Topics.updateTopicTags = async function (tid, tags) { + await Topics.deleteTopicTags(tid); + const timestamp = await Topics.getTopicField(tid, 'timestamp'); + await Topics.createTags(tags, tid, timestamp); + }; + + Topics.deleteTopicTags = async function (tid) { + const tags = await Topics.getTopicTags(tid); + await db.delete('topic:' + tid + ':tags'); + const sets = tags.map(tag => 'tag:' + tag + ':topics'); + await db.sortedSetsRemove(sets, tid); + await Promise.all(tags.map(tag => updateTagCount(tag))); + }; + + Topics.searchTags = async function (data) { + if (!data || !data.query) { + return []; + } + let result; + if (plugins.hasListeners('filter:topics.searchTags')) { + result = await plugins.fireHook('filter:topics.searchTags', { data: data }); + } else { + result = await findMatches(data.query, 0); + } + result = await plugins.fireHook('filter:tags.search', { data: data, matches: result.matches }); + return result.matches; + }; + + Topics.autocompleteTags = async function (data) { + if (!data || !data.query) { + return []; + } + let result; + if (plugins.hasListeners('filter:topics.autocompleteTags')) { + result = await plugins.fireHook('filter:topics.autocompleteTags', { data: data }); + } else { + result = await findMatches(data.query, data.cid); + } + return result.matches; + }; + + async function findMatches(query, cid) { + let tagWhitelist = []; + if (parseInt(cid, 10)) { + tagWhitelist = await categories.getTagWhitelist([cid]); + } + let tags = []; + if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) { + tags = tagWhitelist[0]; + } else { + tags = await db.getSortedSetRevRange('tags:topic:count', 0, -1); + } + + query = query.toLowerCase(); + + var matches = []; + for (var i = 0; i < tags.length; i += 1) { + if (tags[i].toLowerCase().startsWith(query)) { + matches.push(tags[i]); + if (matches.length > 19) { + break; + } + } + } + + matches.sort(); + return { matches: matches }; + } + + Topics.searchAndLoadTags = async function (data) { var searchResult = { tags: [], matchCount: 0, @@ -410,62 +263,39 @@ module.exports = function (Topics) { }; if (!data || !data.query || !data.query.length) { - return callback(null, searchResult); + return searchResult; } - async.waterfall([ - function (next) { - Topics.searchTags(data, next); - }, - function (tags, next) { - async.parallel({ - counts: function (next) { - db.sortedSetScores('tags:topic:count', tags, next); - }, - tagData: function (next) { - tags = tags.map(tag => ({ value: tag })); - Topics.getTagData(tags, next); - }, - }, next); - }, - function (results, next) { - results.tagData.forEach(function (tag, index) { - tag.score = results.counts[index]; - }); - results.tagData.sort((a, b) => b.score - a.score); - searchResult.tags = results.tagData; - searchResult.matchCount = results.tagData.length; - searchResult.pageCount = 1; - next(null, searchResult); - }, - ], callback); + const tags = await Topics.searchTags(data); + const [counts, tagData] = await Promise.all([ + db.sortedSetScores('tags:topic:count', tags), + Topics.getTagData(tags.map(tag => ({ value: tag }))), + ]); + tagData.forEach(function (tag, index) { + tag.score = counts[index]; + }); + tagData.sort((a, b) => b.score - a.score); + searchResult.tags = tagData; + searchResult.matchCount = tagData.length; + searchResult.pageCount = 1; + return searchResult; }; - Topics.getRelatedTopics = function (topicData, uid, callback) { + Topics.getRelatedTopics = async function (topicData, uid) { if (plugins.hasListeners('filter:topic.getRelatedTopics')) { - return plugins.fireHook('filter:topic.getRelatedTopics', { topic: topicData, uid: uid }, callback); + return await plugins.fireHook('filter:topic.getRelatedTopics', { topic: topicData, uid: uid }); } var maximumTopics = meta.config.maximumRelatedTopics; if (maximumTopics === 0 || !topicData.tags || !topicData.tags.length) { - return callback(null, []); + return []; } maximumTopics = maximumTopics || 5; - - async.waterfall([ - function (next) { - async.map(topicData.tags, function (tag, next) { - Topics.getTagTids(tag.value, 0, 5, next); - }, next); - }, - function (tids, next) { - tids = _.shuffle(_.uniq(_.flatten(tids))).slice(0, maximumTopics); - Topics.getTopics(tids, uid, next); - }, - function (topics, next) { - topics = topics.filter(t => t && !t.deleted && parseInt(t.uid, 10) !== parseInt(uid, 10)); - next(null, topics); - }, - ], callback); + let tids = await async.map(topicData.tags, async function (tag) { + return await Topics.getTagTids(tag.value, 0, 5); + }); + tids = _.shuffle(_.uniq(_.flatten(tids))).slice(0, maximumTopics); + const topics = await Topics.getTopics(tids, uid); + return topics.filter(t => t && !t.deleted && parseInt(t.uid, 10) !== parseInt(uid, 10)); }; }; diff --git a/src/topics/teaser.js b/src/topics/teaser.js index 16e8b7f36e..26c5795a6d 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -14,9 +14,9 @@ var utils = require('../utils'); module.exports = function (Topics) { var stripTeaserTags = utils.stripTags.concat(['img']); - Topics.getTeasers = function (topics, options, callback) { + Topics.getTeasers = async function (topics, options) { if (!Array.isArray(topics) || !topics.length) { - return callback(null, []); + return []; } let uid = options; let teaserPost = meta.config.teaserPost; @@ -27,7 +27,6 @@ module.exports = function (Topics) { var counts = []; var teaserPids = []; - var postData; var tidToPost = {}; topics.forEach(function (topic) { @@ -46,151 +45,112 @@ module.exports = function (Topics) { } }); - async.waterfall([ - function (next) { - posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content'], next); - }, - function (_postData, next) { - _postData = _postData.filter(post => post && post.pid); - handleBlocks(uid, _postData, next); - }, - function (_postData, next) { - postData = _postData.filter(Boolean); - const uids = _.uniq(postData.map(post => post.uid)); + let postData = await posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content']); + postData = postData.filter(post => post && post.pid); + postData = await handleBlocks(uid, postData); + postData = postData.filter(Boolean); + const uids = _.uniq(postData.map(post => post.uid)); - user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next); - }, - function (usersData, next) { - var users = {}; - usersData.forEach(function (user) { - users[user.uid] = user; - }); + const usersData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); - async.each(postData, function (post, next) { - // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. - if (!users.hasOwnProperty(post.uid)) { - post.uid = 0; - } + var users = {}; + usersData.forEach(function (user) { + users[user.uid] = user; + }); + postData.forEach(function (post) { + // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. + if (!users.hasOwnProperty(post.uid)) { + post.uid = 0; + } - post.user = users[post.uid]; - post.timestampISO = utils.toISOString(post.timestamp); - tidToPost[post.tid] = post; - posts.parsePost(post, next); - }, next); - }, - function (next) { - var teasers = topics.map(function (topic, index) { - if (!topic) { - return null; - } - if (tidToPost[topic.tid]) { - tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index]; - if (tidToPost[topic.tid].content) { - tidToPost[topic.tid].content = utils.stripHTMLTags(tidToPost[topic.tid].content, stripTeaserTags); - } - } - return tidToPost[topic.tid]; - }); + post.user = users[post.uid]; + post.timestampISO = utils.toISOString(post.timestamp); + tidToPost[post.tid] = post; + }); + await Promise.all(postData.map(p => posts.parsePost(p))); - plugins.fireHook('filter:teasers.get', { teasers: teasers, uid: uid }, next); - }, - function (data, next) { - next(null, data.teasers); - }, - ], callback); + var teasers = topics.map(function (topic, index) { + if (!topic) { + return null; + } + if (tidToPost[topic.tid]) { + tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index]; + if (tidToPost[topic.tid].content) { + tidToPost[topic.tid].content = utils.stripHTMLTags(tidToPost[topic.tid].content, stripTeaserTags); + } + } + return tidToPost[topic.tid]; + }); + + const result = await plugins.fireHook('filter:teasers.get', { teasers: teasers, uid: uid }); + return result.teasers; }; - function handleBlocks(uid, teasers, callback) { - user.blocks.list(uid, function (err, blockedUids) { - if (err || !blockedUids.length) { - return callback(err, teasers); + async function handleBlocks(uid, teasers) { + const blockedUids = await user.blocks.list(uid); + if (!blockedUids.length) { + return teasers; + } + + return await async.mapSeries(teasers, async function (postData) { + if (blockedUids.includes(parseInt(postData.uid, 10))) { + return await getPreviousNonBlockedPost(postData, blockedUids); } - async.mapSeries(teasers, function (postData, nextPost) { - if (blockedUids.includes(parseInt(postData.uid, 10))) { - getPreviousNonBlockedPost(postData, blockedUids, nextPost); - } else { - setImmediate(nextPost, null, postData); - } - }, callback); + return postData; }); } - function getPreviousNonBlockedPost(postData, blockedUids, callback) { + async function getPreviousNonBlockedPost(postData, blockedUids) { let isBlocked = false; let prevPost = postData; const postsPerIteration = 5; let start = 0; let stop = start + postsPerIteration - 1; let checkedAllReplies = false; - async.doWhilst(function (next) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange('tid:' + postData.tid + ':posts', start, stop, next); - }, - function (pids, next) { - if (pids.length) { - return next(null, pids); - } - checkedAllReplies = true; - Topics.getTopicField(postData.tid, 'mainPid', function (err, mainPid) { - next(err, [mainPid]); - }); - }, - function (pids, next) { - posts.getPostsFields(pids, ['pid', 'uid', 'timestamp', 'tid', 'content'], next); - }, - function (prevPosts, next) { - isBlocked = prevPosts.every(function (post) { - const isPostBlocked = blockedUids.includes(parseInt(post.uid, 10)); - prevPost = !isPostBlocked ? post : prevPost; - return isPostBlocked; - }); - start += postsPerIteration; - stop = start + postsPerIteration - 1; - next(); - }, - ], next); - }, function (next) { - next(null, isBlocked && prevPost && prevPost.pid && !checkedAllReplies); - }, function (err) { - callback(err, prevPost); - }); + function checkBlocked(post) { + const isPostBlocked = blockedUids.includes(parseInt(post.uid, 10)); + prevPost = !isPostBlocked ? post : prevPost; + return isPostBlocked; + } + + do { + /* eslint-disable no-await-in-loop */ + let pids = await db.getSortedSetRevRange('tid:' + postData.tid + ':posts', start, stop); + if (!pids.length) { + checkedAllReplies = true; + const mainPid = await Topics.getTopicField(postData.tid, 'mainPid'); + pids = [mainPid]; + } + const prevPosts = await posts.getPostsFields(pids, ['pid', 'uid', 'timestamp', 'tid', 'content']); + isBlocked = prevPosts.every(checkBlocked); + start += postsPerIteration; + stop = start + postsPerIteration - 1; + } while (isBlocked && prevPost && prevPost.pid && !checkedAllReplies); + + return prevPost; } - Topics.getTeasersByTids = function (tids, uid, callback) { + Topics.getTeasersByTids = async function (tids, uid) { if (!Array.isArray(tids) || !tids.length) { - return callback(null, []); + return []; } - async.waterfall([ - function (next) { - Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid', 'mainPid'], next); - }, - function (topics, next) { - Topics.getTeasers(topics, uid, next); - }, - ], callback); + const topics = await Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid', 'mainPid']); + return await Topics.getTeasers(topics, uid); }; - Topics.getTeaser = function (tid, uid, callback) { - Topics.getTeasersByTids([tid], uid, function (err, teasers) { - callback(err, Array.isArray(teasers) && teasers.length ? teasers[0] : null); - }); + Topics.getTeaser = async function (tid, uid) { + const teasers = await Topics.getTeasersByTids([tid], uid); + return Array.isArray(teasers) && teasers.length ? teasers[0] : null; }; - Topics.updateTeaser = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getLatestUndeletedReply(tid, next); - }, - function (pid, next) { - pid = pid || null; - if (pid) { - Topics.setTopicField(tid, 'teaserPid', pid, next); - } else { - Topics.deleteTopicField(tid, 'teaserPid', next); - } - }, - ], callback); + Topics.updateTeaser = async function (tid) { + let pid = await Topics.getLatestUndeletedReply(tid); + pid = pid || null; + if (pid) { + await Topics.setTopicField(tid, 'teaserPid', pid); + } else { + await Topics.deleteTopicField(tid, 'teaserPid'); + } }; }; diff --git a/src/topics/thumb.js b/src/topics/thumb.js index 0050f5670e..2d9be49856 100644 --- a/src/topics/thumb.js +++ b/src/topics/thumb.js @@ -1,13 +1,13 @@ 'use strict'; -var async = require('async'); var nconf = require('nconf'); var path = require('path'); var fs = require('fs'); var request = require('request'); var mime = require('mime'); var validator = require('validator'); +var util = require('util'); var meta = require('../meta'); var image = require('../image'); @@ -15,61 +15,54 @@ var file = require('../file'); var plugins = require('../plugins'); module.exports = function (Topics) { - Topics.resizeAndUploadThumb = function (data, callback) { + const getHead = util.promisify(request.head); + + function pipeToFile(source, destination, callback) { + request(source).pipe(fs.createWriteStream(destination)).on('close', callback); + } + const pipeToFileAsync = util.promisify(pipeToFile); + + Topics.resizeAndUploadThumb = async function (data) { if (!data.thumb || !validator.isURL(data.thumb)) { - return callback(); + return; } - var pathToUpload; - var filename; + const res = await getHead(data.thumb); - async.waterfall([ - function (next) { - request.head(data.thumb, next); - }, - function (res, body, next) { - var type = res.headers['content-type']; - if (!type.match(/image./)) { - return next(new Error('[[error:invalid-file]]')); - } - - var extension = path.extname(data.thumb); - if (!extension) { - extension = '.' + mime.getExtension(type); - } - filename = Date.now() + '-topic-thumb' + extension; - pathToUpload = path.join(nconf.get('upload_path'), 'files', filename); - - request(data.thumb).pipe(fs.createWriteStream(pathToUpload)).on('close', next); - }, - function (next) { - file.isFileTypeAllowed(pathToUpload, next); - }, - function (next) { - image.resizeImage({ - path: pathToUpload, - width: meta.config.topicThumbSize, - height: meta.config.topicThumbSize, - }, next); - }, - function (next) { - if (!plugins.hasListeners('filter:uploadImage')) { - data.thumb = '/assets/uploads/files/' + filename; - return callback(); - } - - plugins.fireHook('filter:uploadImage', { image: { path: pathToUpload, name: '' }, uid: data.uid }, next); - }, - function (uploadedFile, next) { - file.delete(pathToUpload); - data.thumb = uploadedFile.url; - next(); - }, - ], function (err) { - if (err) { - file.delete(pathToUpload); + try { + const type = res.headers['content-type']; + if (!type.match(/image./)) { + throw new Error('[[error:invalid-file]]'); } - callback(err); - }); + + var extension = path.extname(data.thumb); + if (!extension) { + extension = '.' + mime.getExtension(type); + } + const filename = Date.now() + '-topic-thumb' + extension; + pathToUpload = path.join(nconf.get('upload_path'), 'files', filename); + + await pipeToFileAsync(data.thumb, pathToUpload); + + await file.isFileTypeAllowed(pathToUpload); + + await image.resizeImage({ + path: pathToUpload, + width: meta.config.topicThumbSize, + height: meta.config.topicThumbSize, + }); + + if (!plugins.hasListeners('filter:uploadImage')) { + data.thumb = '/assets/uploads/files/' + filename; + return; + } + + const uploadedFile = await plugins.fireHook('filter:uploadImage', { image: { path: pathToUpload, name: '' }, uid: data.uid }); + file.delete(pathToUpload); + data.thumb = uploadedFile.url; + } catch (err) { + file.delete(pathToUpload); + throw err; + } }; }; diff --git a/src/topics/tools.js b/src/topics/tools.js index 2435c97fdd..8e47e65391 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -14,315 +14,215 @@ module.exports = function (Topics) { var topicTools = {}; Topics.tools = topicTools; - topicTools.delete = function (tid, uid, callback) { - toggleDelete(tid, uid, true, callback); + topicTools.delete = async function (tid, uid) { + return await toggleDelete(tid, uid, true); }; - topicTools.restore = function (tid, uid, callback) { - toggleDelete(tid, uid, false, callback); + topicTools.restore = async function (tid, uid) { + return await toggleDelete(tid, uid, false); }; - function toggleDelete(tid, uid, isDelete, callback) { - var topicData; - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - privileges.topics.canDelete(tid, uid, next); - }, - function (canDelete, next) { - if (!canDelete) { - return next(new Error('[[error:no-privileges]]')); - } - Topics.getTopicFields(tid, ['tid', 'cid', 'uid', 'deleted', 'title', 'mainPid'], next); - }, - function (_topicData, next) { - topicData = _topicData; + async function toggleDelete(tid, uid, isDelete) { + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + const canDelete = await privileges.topics.canDelete(tid, uid); + if (!canDelete) { + throw new Error('[[error:no-privileges]]'); + } + const topicData = await Topics.getTopicFields(tid, ['tid', 'cid', 'uid', 'deleted', 'title', 'mainPid']); + if (topicData.deleted && isDelete) { + throw new Error('[[error:topic-already-deleted]]'); + } else if (!topicData.deleted && !isDelete) { + throw new Error('[[error:topic-already-restored]]'); + } - if (topicData.deleted && isDelete) { - return callback(new Error('[[error:topic-already-deleted]]')); - } else if (!topicData.deleted && !isDelete) { - return callback(new Error('[[error:topic-already-restored]]')); - } + if (isDelete) { + await Topics.delete(tid, uid); + } else { + await Topics.restore(tid); + } + await categories.updateRecentTidForCid(topicData.cid); - Topics[isDelete ? 'delete' : 'restore'](tid, uid, next); - }, - function (next) { - categories.updateRecentTidForCid(topicData.cid, next); - }, - function (next) { - topicData.deleted = isDelete ? 1 : 0; + topicData.deleted = isDelete ? 1 : 0; - if (isDelete) { - plugins.fireHook('action:topic.delete', { topic: topicData, uid: uid }); - } else { - plugins.fireHook('action:topic.restore', { topic: topicData, uid: uid }); - } - user.getUserFields(uid, ['username', 'userslug'], next); - }, - function (userData, next) { - next(null, { - tid: tid, - cid: topicData.cid, - isDelete: isDelete, - uid: uid, - user: userData, - }); - }, - ], callback); + if (isDelete) { + plugins.fireHook('action:topic.delete', { topic: topicData, uid: uid }); + } else { + plugins.fireHook('action:topic.restore', { topic: topicData, uid: uid }); + } + const userData = await user.getUserFields(uid, ['username', 'userslug']); + return { + tid: tid, + cid: topicData.cid, + isDelete: isDelete, + uid: uid, + user: userData, + }; } - topicTools.purge = function (tid, uid, callback) { - var cid; - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return callback(); - } - privileges.topics.canPurge(tid, uid, next); - }, - function (canPurge, next) { - if (!canPurge) { - return next(new Error('[[error:no-privileges]]')); - } - - Topics.getTopicField(tid, 'cid', next); - }, - function (_cid, next) { - cid = _cid; - - Topics.purgePostsAndTopic(tid, uid, next); - }, - function (next) { - next(null, { tid: tid, cid: cid, uid: uid }); - }, - ], callback); + topicTools.purge = async function (tid, uid) { + const exists = await Topics.exists(tid); + if (!exists) { + return; + } + const canPurge = await privileges.topics.canPurge(tid, uid); + if (!canPurge) { + throw new Error('[[error:no-privileges]]'); + } + const cid = await Topics.getTopicField(tid, 'cid'); + await Topics.purgePostsAndTopic(tid, uid); + return { tid: tid, cid: cid, uid: uid }; }; - topicTools.lock = function (tid, uid, callback) { - toggleLock(tid, uid, true, callback); + topicTools.lock = async function (tid, uid) { + return await toggleLock(tid, uid, true); }; - topicTools.unlock = function (tid, uid, callback) { - toggleLock(tid, uid, false, callback); + topicTools.unlock = async function (tid, uid) { + return await toggleLock(tid, uid, false); }; - function toggleLock(tid, uid, lock, callback) { - callback = callback || function () {}; + async function toggleLock(tid, uid, lock) { + const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid']); + if (!topicData || !topicData.cid) { + throw new Error('[[error:no-topic]]'); + } + const isAdminOrMod = await privileges.categories.isAdminOrMod(topicData.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + await Topics.setTopicField(tid, 'locked', lock ? 1 : 0); + topicData.isLocked = lock; - var topicData; - - async.waterfall([ - function (next) { - Topics.getTopicFields(tid, ['tid', 'uid', 'cid'], next); - }, - function (_topicData, next) { - topicData = _topicData; - if (!topicData || !topicData.cid) { - return next(new Error('[[error:no-topic]]')); - } - privileges.categories.isAdminOrMod(topicData.cid, uid, next); - }, - function (isAdminOrMod, next) { - if (!isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } - - Topics.setTopicField(tid, 'locked', lock ? 1 : 0, next); - }, - function (next) { - topicData.isLocked = lock; - - plugins.fireHook('action:topic.lock', { topic: _.clone(topicData), uid: uid }); - - next(null, topicData); - }, - ], callback); + plugins.fireHook('action:topic.lock', { topic: _.clone(topicData), uid: uid }); + return topicData; } - topicTools.pin = function (tid, uid, callback) { - togglePin(tid, uid, true, callback); + topicTools.pin = async function (tid, uid) { + return await togglePin(tid, uid, true); }; - topicTools.unpin = function (tid, uid, callback) { - togglePin(tid, uid, false, callback); + topicTools.unpin = async function (tid, uid) { + return await togglePin(tid, uid, false); }; - function togglePin(tid, uid, pin, callback) { - var topicData; - async.waterfall([ - function (next) { - Topics.getTopicData(tid, next); - }, - function (_topicData, next) { - topicData = _topicData; - if (!topicData) { - return callback(new Error('[[error:no-topic]]')); - } - privileges.categories.isAdminOrMod(_topicData.cid, uid, next); - }, - function (isAdminOrMod, next) { - if (!isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } + async function togglePin(tid, uid, pin) { + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + const isAdminOrMod = await privileges.categories.isAdminOrMod(topicData.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } - async.parallel([ - async.apply(Topics.setTopicField, tid, 'pinned', pin ? 1 : 0), - function (next) { - if (pin) { - async.parallel([ - async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), tid), - async.apply(db.sortedSetsRemove, [ - 'cid:' + topicData.cid + ':tids', - 'cid:' + topicData.cid + ':tids:posts', - 'cid:' + topicData.cid + ':tids:votes', - ], tid), - ], next); - } else { - async.parallel([ - async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid), - async.apply(db.sortedSetAddBulk, [ - ['cid:' + topicData.cid + ':tids', topicData.lastposttime, tid], - ['cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid], - ['cid:' + topicData.cid + ':tids:votes', parseInt(topicData.votes, 10) || 0, tid], - ]), - ], next); - } - }, - ], next); - }, - function (results, next) { - topicData.isPinned = pin; + const promises = [ + Topics.setTopicField(tid, 'pinned', pin ? 1 : 0), + ]; + if (pin) { + promises.push(db.sortedSetAdd('cid:' + topicData.cid + ':tids:pinned', Date.now(), tid)); + promises.push(db.sortedSetsRemove([ + 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':tids:posts', + 'cid:' + topicData.cid + ':tids:votes', + ], tid)); + } else { + promises.push(db.sortedSetRemove('cid:' + topicData.cid + ':tids:pinned', tid)); + promises.push(db.sortedSetAddBulk([ + ['cid:' + topicData.cid + ':tids', topicData.lastposttime, tid], + ['cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid], + ['cid:' + topicData.cid + ':tids:votes', parseInt(topicData.votes, 10) || 0, tid], + ])); + } - plugins.fireHook('action:topic.pin', { topic: _.clone(topicData), uid: uid }); + await Promise.all(promises); - next(null, topicData); - }, - ], callback); + topicData.isPinned = pin; + + plugins.fireHook('action:topic.pin', { topic: _.clone(topicData), uid: uid }); + + return topicData; } - topicTools.orderPinnedTopics = function (uid, data, callback) { - var cid; - async.waterfall([ - function (next) { - const tids = data.map(topic => topic && topic.tid); - Topics.getTopicsFields(tids, ['cid'], next); - }, - function (topicData, next) { - var uniqueCids = _.uniq(topicData.map(topicData => topicData && topicData.cid)); + topicTools.orderPinnedTopics = async function (uid, data) { + const tids = data.map(topic => topic && topic.tid); + const topicData = await Topics.getTopicsFields(tids, ['cid']); - if (uniqueCids.length > 1 || !uniqueCids.length || !uniqueCids[0]) { - return next(new Error('[[error:invalid-data]]')); - } - cid = uniqueCids[0]; + var uniqueCids = _.uniq(topicData.map(topicData => topicData && topicData.cid)); + if (uniqueCids.length > 1 || !uniqueCids.length || !uniqueCids[0]) { + throw new Error('[[error:invalid-data]]'); + } - privileges.categories.isAdminOrMod(cid, uid, next); - }, - function (isAdminOrMod, next) { - if (!isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } - async.eachSeries(data, function (topicData, next) { - async.waterfall([ - function (next) { - db.isSortedSetMember('cid:' + cid + ':tids:pinned', topicData.tid, next); - }, - function (isPinned, next) { - if (isPinned) { - db.sortedSetAdd('cid:' + cid + ':tids:pinned', topicData.order, topicData.tid, next); - } else { - setImmediate(next); - } - }, - ], next); - }, next); - }, - ], callback); + const cid = uniqueCids[0]; + + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + await async.eachSeries(data, async function (topicData) { + const isPinned = await db.isSortedSetMember('cid:' + cid + ':tids:pinned', topicData.tid); + if (isPinned) { + await db.sortedSetAdd('cid:' + cid + ':tids:pinned', topicData.order, topicData.tid); + } + }); }; - topicTools.move = function (tid, data, callback) { - var topic; - var oldCid; - var cid = parseInt(data.cid, 10); + topicTools.move = async function (tid, data) { + const cid = parseInt(data.cid, 10); + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + if (cid === topicData.cid) { + throw new Error('[[error:cant-move-topic-to-same-category]]'); + } + await db.sortedSetsRemove([ + 'cid:' + topicData.cid + ':tids', + 'cid:' + topicData.cid + ':tids:pinned', + 'cid:' + topicData.cid + ':tids:posts', + 'cid:' + topicData.cid + ':tids:votes', + 'cid:' + topicData.cid + ':tids:lastposttime', + 'cid:' + topicData.cid + ':recent_tids', + 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', + ], tid); - async.waterfall([ - function (next) { - Topics.getTopicData(tid, next); - }, - function (topicData, next) { - topic = topicData; - if (!topic) { - return next(new Error('[[error:no-topic]]')); - } - if (cid === topic.cid) { - return next(new Error('[[error:cant-move-topic-to-same-category]]')); - } - db.sortedSetsRemove([ - 'cid:' + topicData.cid + ':tids', - 'cid:' + topicData.cid + ':tids:pinned', - 'cid:' + topicData.cid + ':tids:posts', - 'cid:' + topicData.cid + ':tids:votes', - 'cid:' + topicData.cid + ':tids:lastposttime', - 'cid:' + topicData.cid + ':recent_tids', - 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', - ], tid, next); - }, - function (next) { - topic.postcount = topic.postcount || 0; - const votes = topic.upvotes - topic.downvotes; - db.sortedSetsAdd([ - 'cid:' + cid + ':tids:lastposttime', - 'cid:' + cid + ':uid:' + topic.uid + ':tids', - ...(topic.pinned ? ['cid:' + cid + ':tids:pinned'] : ['cid:' + cid + ':tids', 'cid:' + cid + ':tids:posts', 'cid:' + cid + ':tids:votes']), - ], [ - topic.lastposttime, - topic.timestamp, - ...(topic.pinned ? [Date.now()] : [topic.lastposttime, topic.postcount, votes]), - ], tid, next); - }, - function (next) { - oldCid = topic.cid; - categories.moveRecentReplies(tid, oldCid, cid, next); - }, - function (next) { - async.parallel([ - function (next) { - categories.incrementCategoryFieldBy(oldCid, 'topic_count', -1, next); - }, - function (next) { - categories.incrementCategoryFieldBy(cid, 'topic_count', 1, next); - }, - function (next) { - categories.updateRecentTid(cid, tid, next); - }, - function (next) { - categories.updateRecentTidForCid(oldCid, next); - }, - function (next) { - Topics.setTopicFields(tid, { - cid: cid, - oldCid: oldCid, - }, next); - }, - ], function (err) { - next(err); - }); - }, - function (next) { - var hookData = _.clone(data); - hookData.fromCid = oldCid; - hookData.toCid = cid; - hookData.tid = tid; - plugins.fireHook('action:topic.move', hookData); - next(); - }, - ], callback); + topicData.postcount = topicData.postcount || 0; + const votes = topicData.upvotes - topicData.downvotes; + + const bulk = [ + ['cid:' + cid + ':tids:lastposttime', topicData.lastposttime, tid], + ['cid:' + cid + ':uid:' + topicData.uid + ':tids', topicData.timestamp, tid], + ]; + if (topicData.pinned) { + bulk.push(['cid:' + cid + ':tids:pinned', Date.now(), tid]); + } else { + bulk.push(['cid:' + cid + ':tids', topicData.lastposttime, tid]); + bulk.push(['cid:' + cid + ':tids:posts', topicData.postcount, tid]); + bulk.push(['cid:' + cid + ':tids:votes', votes, tid]); + } + await db.sortedSetAddBulk(bulk); + + const oldCid = topicData.cid; + await categories.moveRecentReplies(tid, oldCid, cid); + + await Promise.all([ + categories.incrementCategoryFieldBy(oldCid, 'topic_count', -1), + categories.incrementCategoryFieldBy(cid, 'topic_count', 1), + categories.updateRecentTid(cid, tid), + categories.updateRecentTidForCid(oldCid), + Topics.setTopicFields(tid, { + cid: cid, + oldCid: oldCid, + }), + ]); + var hookData = _.clone(data); + hookData.fromCid = oldCid; + hookData.toCid = cid; + hookData.tid = tid; + + plugins.fireHook('action:topic.move', hookData); }; }; diff --git a/src/topics/unread.js b/src/topics/unread.js index 8259b7b24f..9ce3c67737 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -15,66 +15,47 @@ var utils = require('../utils'); var plugins = require('../plugins'); module.exports = function (Topics) { - Topics.getTotalUnread = function (uid, filter, callback) { - if (!callback) { - callback = filter; - filter = ''; - } - Topics.getUnreadTids({ cid: 0, uid: uid, count: true }, function (err, counts) { - callback(err, counts && counts[filter]); - }); + Topics.getTotalUnread = async function (uid, filter) { + filter = filter || ''; + const counts = await Topics.getUnreadTids({ cid: 0, uid: uid, count: true }); + return counts && counts[filter]; }; - Topics.getUnreadTopics = function (params, callback) { + Topics.getUnreadTopics = async function (params) { var unreadTopics = { showSelect: true, nextStart: 0, topics: [], }; + let tids = await Topics.getUnreadTids(params); + unreadTopics.topicCount = tids.length; - async.waterfall([ - function (next) { - Topics.getUnreadTids(params, next); - }, - function (tids, next) { - unreadTopics.topicCount = tids.length; + if (!tids.length) { + return unreadTopics; + } - if (!tids.length) { - return next(null, []); - } + tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); - tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); - - Topics.getTopicsByTids(tids, params.uid, next); - }, - function (topicData, next) { - if (!topicData.length) { - return next(null, unreadTopics); - } - Topics.calculateTopicIndices(topicData, params.start); - unreadTopics.topics = topicData; - unreadTopics.nextStart = params.stop + 1; - next(null, unreadTopics); - }, - ], callback); + const topicData = await Topics.getTopicsByTids(tids, params.uid); + if (!topicData.length) { + return unreadTopics; + } + Topics.calculateTopicIndices(topicData, params.start); + unreadTopics.topics = topicData; + unreadTopics.nextStart = params.stop + 1; + return unreadTopics; }; Topics.unreadCutoff = function () { return Date.now() - (meta.config.unreadCutoff * 86400000); }; - Topics.getUnreadTids = function (params, callback) { - async.waterfall([ - function (next) { - Topics.getUnreadData(params, next); - }, - function (results, next) { - next(null, params.count ? results.counts : results.tids); - }, - ], callback); + Topics.getUnreadTids = async function (params) { + const results = await Topics.getUnreadData(params); + return params.count ? results.counts : results.tids; }; - Topics.getUnreadData = function (params, callback) { + Topics.getUnreadData = async function (params) { const uid = parseInt(params.uid, 10); const counts = { '': 0, @@ -94,7 +75,7 @@ module.exports = function (Topics) { }; if (uid <= 0) { - return setImmediate(callback, null, noUnreadData); + return noUnreadData; } params.filter = params.filter || ''; @@ -104,45 +85,35 @@ module.exports = function (Topics) { if (params.cid && !Array.isArray(params.cid)) { params.cid = [params.cid]; } + const [ignoredTids, recentTids, userScores, tids_unread] = await Promise.all([ + user.getIgnoredTids(uid, 0, -1), + db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff), + db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff), + db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1), + ]); - async.waterfall([ - function (next) { - async.parallel({ - ignoredTids: function (next) { - user.getIgnoredTids(uid, 0, -1, next); - }, - recentTids: function (next) { - db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next); - }, - userScores: function (next) { - db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff, next); - }, - tids_unread: function (next) { - db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1, next); - }, - }, next); - }, - function (results, next) { - if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) { - return callback(null, noUnreadData); - } + if (recentTids && !recentTids.length && !tids_unread.length) { + return noUnreadData; + } - filterTopics(params, results, next); - }, - function (data, next) { - plugins.fireHook('filter:topics.getUnreadTids', { - uid: uid, - tids: data.tids, - counts: data.counts, - tidsByFilter: data.tidsByFilter, - cid: params.cid, - filter: params.filter, - }, next); - }, - ], callback); + const data = await filterTopics(params, { + ignoredTids: ignoredTids, + recentTids: recentTids, + userScores: userScores, + tids_unread: tids_unread, + }); + const result = await plugins.fireHook('filter:topics.getUnreadTids', { + uid: uid, + tids: data.tids, + counts: data.counts, + tidsByFilter: data.tidsByFilter, + cid: params.cid, + filter: params.filter, + }); + return result; }; - function filterTopics(params, results, callback) { + async function filterTopics(params, results) { const counts = { '': 0, new: 0, @@ -180,385 +151,253 @@ module.exports = function (Topics) { var uid = params.uid; var cids; var topicData; - var blockedUids; tids = tids.slice(0, 200); if (!tids.length) { - return callback(null, { counts: counts, tids: tids, tidsByFilter: tidsByFilter }); + return { counts: counts, tids: tids, tidsByFilter: tidsByFilter }; } + const blockedUids = await user.blocks.list(uid); - async.waterfall([ - function (next) { - user.blocks.list(uid, next); - }, - function (_blockedUids, next) { - blockedUids = _blockedUids; - filterTidsThatHaveBlockedPosts({ - uid: uid, - tids: tids, - blockedUids: blockedUids, - recentTids: results.recentTids, - }, next); - }, - function (_tids, next) { - tids = _tids; - Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount'], next); - }, - function (_topicData, next) { - topicData = _topicData; - cids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); + tids = await filterTidsThatHaveBlockedPosts({ + uid: uid, + tids: tids, + blockedUids: blockedUids, + recentTids: results.recentTids, + }); - async.parallel({ - isTopicsFollowed: function (next) { - db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next); - }, - categoryWatchState: function (next) { - categories.getWatchState(cids, uid, next); - }, - readableCids: function (next) { - privileges.categories.filterCids('read', cids, uid, next); - }, - }, next); - }, - function (results, next) { - cid = cid && cid.map(String); - results.readableCids = results.readableCids.map(String); - const userCidState = _.zipObject(cids, results.categoryWatchState); + topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount']); + cids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); - topicData.forEach(function (topic, index) { - function cidMatch(topicCid) { - return (!cid || (cid.length && cid.includes(String(topicCid)))) && results.readableCids.includes(String(topicCid)); - } + const [isTopicsFollowed, categoryWatchState, readCids] = await Promise.all([ + db.sortedSetScores('uid:' + uid + ':followed_tids', tids), + categories.getWatchState(cids, uid), + privileges.categories.filterCids('read', cids, uid), + ]); + cid = cid && cid.map(String); + const readableCids = readCids.map(String); + const userCidState = _.zipObject(cids, categoryWatchState); - if (topic && topic.cid && cidMatch(topic.cid) && !blockedUids.includes(parseInt(topic.uid, 10))) { - topic.tid = parseInt(topic.tid, 10); - if ((results.isTopicsFollowed[index] || userCidState[topic.cid] === categories.watchStates.watching)) { - tidsByFilter[''].push(topic.tid); - } + topicData.forEach(function (topic, index) { + function cidMatch(topicCid) { + return (!cid || (cid.length && cid.includes(String(topicCid)))) && readableCids.includes(String(topicCid)); + } - if (results.isTopicsFollowed[index]) { - tidsByFilter.watched.push(topic.tid); - } + if (topic && topic.cid && cidMatch(topic.cid) && !blockedUids.includes(parseInt(topic.uid, 10))) { + topic.tid = parseInt(topic.tid, 10); + if ((isTopicsFollowed[index] || userCidState[topic.cid] === categories.watchStates.watching)) { + tidsByFilter[''].push(topic.tid); + } - if (topic.postcount <= 1) { - tidsByFilter.unreplied.push(topic.tid); - } + if (isTopicsFollowed[index]) { + tidsByFilter.watched.push(topic.tid); + } - if (!userRead[topic.tid]) { - tidsByFilter.new.push(topic.tid); - } - } - }); - counts[''] = tidsByFilter[''].length; - counts.watched = tidsByFilter.watched.length; - counts.unreplied = tidsByFilter.unreplied.length; - counts.new = tidsByFilter.new.length; + if (topic.postcount <= 1) { + tidsByFilter.unreplied.push(topic.tid); + } - next(null, { - counts: counts, - tids: tidsByFilter[params.filter], - tidsByFilter: tidsByFilter, - }); - }, - ], callback); + if (!userRead[topic.tid]) { + tidsByFilter.new.push(topic.tid); + } + } + }); + counts[''] = tidsByFilter[''].length; + counts.watched = tidsByFilter.watched.length; + counts.unreplied = tidsByFilter.unreplied.length; + counts.new = tidsByFilter.new.length; + + return { + counts: counts, + tids: tidsByFilter[params.filter], + tidsByFilter: tidsByFilter, + }; } - function filterTidsThatHaveBlockedPosts(params, callback) { + async function filterTidsThatHaveBlockedPosts(params) { if (!params.blockedUids.length) { - return setImmediate(callback, null, params.tids); + return params.tids; } const topicScores = _.mapValues(_.keyBy(params.recentTids, 'value'), 'score'); - db.sortedSetScores('uid:' + params.uid + ':tids_read', params.tids, function (err, results) { - if (err) { - return callback(err); - } - const userScores = _.zipObject(params.tids, results); + const results = await db.sortedSetScores('uid:' + params.uid + ':tids_read', params.tids); - async.filter(params.tids, function (tid, next) { - doesTidHaveUnblockedUnreadPosts(tid, { - blockedUids: params.blockedUids, - topicTimestamp: topicScores[tid], - userLastReadTimestamp: userScores[tid], - }, next); - }, callback); + const userScores = _.zipObject(params.tids, results); + + return await async.filter(params.tids, async function (tid) { + return await doesTidHaveUnblockedUnreadPosts(tid, { + blockedUids: params.blockedUids, + topicTimestamp: topicScores[tid], + userLastReadTimestamp: userScores[tid], + }); }); } - function doesTidHaveUnblockedUnreadPosts(tid, params, callback) { + async function doesTidHaveUnblockedUnreadPosts(tid, params) { var userLastReadTimestamp = params.userLastReadTimestamp; if (!userLastReadTimestamp) { - return setImmediate(callback, null, true); + return true; } var start = 0; var count = 3; var done = false; var hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp; if (!params.blockedUids.length) { - return setImmediate(callback, null, hasUnblockedUnread); + return hasUnblockedUnread; } - async.whilst(function (next) { - next(null, !done); - }, function (_next) { - async.waterfall([ - function (next) { - db.getSortedSetRangeByScore('tid:' + tid + ':posts', start, count, userLastReadTimestamp, '+inf', next); - }, - function (pidsSinceLastVisit, next) { - if (!pidsSinceLastVisit.length) { - done = true; - return _next(); - } + while (!done) { + /* eslint-disable no-await-in-loop */ + const pidsSinceLastVisit = await db.getSortedSetRangeByScore('tid:' + tid + ':posts', start, count, userLastReadTimestamp, '+inf'); + if (!pidsSinceLastVisit.length) { + return hasUnblockedUnread; + } + let postData = await posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid']); + postData = postData.filter(function (post) { + return !params.blockedUids.includes(parseInt(post.uid, 10)); + }); - posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid'], next); - }, - function (postData, next) { - postData = postData.filter(function (post) { - return !params.blockedUids.includes(parseInt(post.uid, 10)); - }); - - done = postData.length > 0; - hasUnblockedUnread = postData.length > 0; - start += count; - next(); - }, - ], _next); - }, function (err) { - callback(err, hasUnblockedUnread); - }); + done = postData.length > 0; + hasUnblockedUnread = postData.length > 0; + start += count; + } + return hasUnblockedUnread; } - Topics.pushUnreadCount = function (uid, callback) { - callback = callback || function () {}; - + Topics.pushUnreadCount = async function (uid) { if (!uid || parseInt(uid, 10) <= 0) { - return setImmediate(callback); + return; } - - async.waterfall([ - function (next) { - Topics.getUnreadTids({ uid: uid, count: true }, next); - }, - function (results, next) { - require('../socket.io').in('uid_' + uid).emit('event:unread.updateCount', { - unreadTopicCount: results[''], - unreadNewTopicCount: results.new, - unreadWatchedTopicCount: results.watched, - unreadUnrepliedTopicCount: results.unreplied, - }); - setImmediate(next); - }, - ], callback); + const results = await Topics.getUnreadTids({ uid: uid, count: true }); + require('../socket.io').in('uid_' + uid).emit('event:unread.updateCount', { + unreadTopicCount: results[''], + unreadNewTopicCount: results.new, + unreadWatchedTopicCount: results.watched, + unreadUnrepliedTopicCount: results.unreplied, + }); }; - Topics.markAsUnreadForAll = function (tid, callback) { - Topics.markCategoryUnreadForAll(tid, callback); + Topics.markAsUnreadForAll = async function (tid) { + await Topics.markCategoryUnreadForAll(tid); }; - Topics.markAsRead = function (tids, uid, callback) { - callback = callback || function () {}; + Topics.markAsRead = async function (tids, uid) { if (!Array.isArray(tids) || !tids.length) { - return setImmediate(callback, null, false); + return false; } - tids = _.uniq(tids).filter(function (tid) { - return tid && utils.isNumber(tid); + tids = _.uniq(tids).filter(tid => tid && utils.isNumber(tid)); + + if (!tids.length) { + return false; + } + const [topicScores, userScores] = await Promise.all([ + db.sortedSetScores('topics:recent', tids), + db.sortedSetScores('uid:' + uid + ':tids_read', tids), + ]); + + tids = tids.filter(function (tid, index) { + return topicScores[index] && (!userScores[index] || userScores[index] < topicScores[index]); }); if (!tids.length) { - return setImmediate(callback, null, false); + return false; } - async.waterfall([ - function (next) { - async.parallel({ - topicScores: async.apply(db.sortedSetScores, 'topics:recent', tids), - userScores: async.apply(db.sortedSetScores, 'uid:' + uid + ':tids_read', tids), - }, next); - }, - function (results, next) { - tids = tids.filter(function (tid, index) { - return results.topicScores[index] && (!results.userScores[index] || results.userScores[index] < results.topicScores[index]); - }); + var now = Date.now(); + var scores = tids.map(() => now); + const [topicData] = await Promise.all([ + Topics.getTopicsFields(tids, ['cid']), + db.sortedSetAdd('uid:' + uid + ':tids_read', scores, tids), + db.sortedSetRemove('uid:' + uid + ':tids_unread', tids), + ]); - if (!tids.length) { - return callback(null, false); - } + var cids = _.uniq(topicData.map(t => t && t.cid).filter(Boolean)); + await categories.markAsRead(cids, uid); - var now = Date.now(); - var scores = tids.map(function () { - return now; - }); - - async.parallel({ - markRead: async.apply(db.sortedSetAdd, 'uid:' + uid + ':tids_read', scores, tids), - markUnread: async.apply(db.sortedSetRemove, 'uid:' + uid + ':tids_unread', tids), - topicData: async.apply(Topics.getTopicsFields, tids, ['cid']), - }, next); - }, - function (results, next) { - var cids = results.topicData.map(function (topic) { - return topic && topic.cid; - }).filter(Boolean); - - cids = _.uniq(cids); - - categories.markAsRead(cids, uid, next); - }, - function (next) { - plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids }); - next(null, true); - }, - ], callback); + plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids }); + return true; }; - Topics.markAllRead = function (uid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', Topics.unreadCutoff(), next); - }, - function (tids, next) { - Topics.markTopicNotificationsRead(tids, uid); - Topics.markAsRead(tids, uid, next); - }, - function (markedRead, next) { - db.delete('uid:' + uid + ':tids_unread', next); - }, - ], callback); + Topics.markAllRead = async function (uid) { + const tids = await db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', Topics.unreadCutoff()); + Topics.markTopicNotificationsRead(tids, uid); + await Topics.markAsRead(tids, uid); + await db.delete('uid:' + uid + ':tids_unread'); }; - Topics.markTopicNotificationsRead = function (tids, uid, callback) { - callback = callback || function () {}; + Topics.markTopicNotificationsRead = async function (tids, uid) { if (!Array.isArray(tids) || !tids.length) { - return callback(); + return; } - - async.waterfall([ - function (next) { - user.notifications.getUnreadByField(uid, 'tid', tids, next); - }, - function (nids, next) { - notifications.markReadMultiple(nids, uid, next); - }, - function (next) { - user.notifications.pushCount(uid); - next(); - }, - ], callback); + const nids = await user.notifications.getUnreadByField(uid, 'tid', tids); + await notifications.markReadMultiple(nids, uid); + user.notifications.pushCount(uid); }; - Topics.markCategoryUnreadForAll = function (tid, callback) { - async.waterfall([ - function (next) { - Topics.getTopicField(tid, 'cid', next); - }, - function (cid, next) { - categories.markAsUnreadForAll(cid, next); - }, - ], callback); + Topics.markCategoryUnreadForAll = async function (tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + await categories.markAsUnreadForAll(cid); }; - Topics.hasReadTopics = function (tids, uid, callback) { + Topics.hasReadTopics = async function (tids, uid) { if (!(parseInt(uid, 10) > 0)) { - return setImmediate(callback, null, tids.map(() => false)); + return tids.map(() => false); } + const [topicScores, userScores, tids_unread, blockedUids] = await Promise.all([ + db.sortedSetScores('topics:recent', tids), + db.sortedSetScores('uid:' + uid + ':tids_read', tids), + db.sortedSetScores('uid:' + uid + ':tids_unread', tids), + user.blocks.list(uid), + ]); - async.waterfall([ - function (next) { - async.parallel({ - topicScores: function (next) { - db.sortedSetScores('topics:recent', tids, next); - }, - userScores: function (next) { - db.sortedSetScores('uid:' + uid + ':tids_read', tids, next); - }, - tids_unread: function (next) { - db.sortedSetScores('uid:' + uid + ':tids_unread', tids, next); - }, - blockedUids: function (next) { - user.blocks.list(uid, next); - }, - }, next); - }, - function (results, next) { - var cutoff = Topics.unreadCutoff(); - var result = tids.map(function (tid, index) { - var read = !results.tids_unread[index] && - (results.topicScores[index] < cutoff || - !!(results.userScores[index] && results.userScores[index] >= results.topicScores[index])); - return { tid: tid, read: read, index: index }; - }); + var cutoff = Topics.unreadCutoff(); + var result = tids.map(function (tid, index) { + var read = !tids_unread[index] && + (topicScores[index] < cutoff || + !!(userScores[index] && userScores[index] >= topicScores[index])); + return { tid: tid, read: read, index: index }; + }); - async.map(result, function (data, next) { - if (data.read) { - return next(null, true); - } - doesTidHaveUnblockedUnreadPosts(data.tid, { - topicTimestamp: results.topicScores[data.index], - userLastReadTimestamp: results.userScores[data.index], - blockedUids: results.blockedUids, - }, function (err, hasUnblockedUnread) { - if (err) { - return next(err); - } - if (!hasUnblockedUnread) { - data.read = true; - } - next(null, data.read); - }); - }, next); - }, - ], callback); - }; - - Topics.hasReadTopic = function (tid, uid, callback) { - Topics.hasReadTopics([tid], uid, function (err, hasRead) { - callback(err, Array.isArray(hasRead) && hasRead.length ? hasRead[0] : false); + return await async.map(result, async function (data) { + if (data.read) { + return true; + } + const hasUnblockedUnread = await doesTidHaveUnblockedUnreadPosts(data.tid, { + topicTimestamp: topicScores[data.index], + userLastReadTimestamp: userScores[data.index], + blockedUids: blockedUids, + }); + if (!hasUnblockedUnread) { + data.read = true; + } + return data.read; }); }; - Topics.markUnread = function (tid, uid, callback) { - async.waterfall([ - function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - db.sortedSetRemove('uid:' + uid + ':tids_read', tid, next); - }, - function (next) { - db.sortedSetAdd('uid:' + uid + ':tids_unread', Date.now(), tid, next); - }, - ], callback); + Topics.hasReadTopic = async function (tid, uid) { + const hasRead = await Topics.hasReadTopics([tid], uid); + return Array.isArray(hasRead) && hasRead.length ? hasRead[0] : false; }; - Topics.filterNewTids = function (tids, uid, callback) { - if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, []); + Topics.markUnread = async function (tid, uid) { + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); } - async.waterfall([ - function (next) { - db.sortedSetScores('uid:' + uid + ':tids_read', tids, next); - }, - function (scores, next) { - tids = tids.filter((tid, index) => tid && !scores[index]); - next(null, tids); - }, - ], callback); + await db.sortedSetRemove('uid:' + uid + ':tids_read', tid); + await db.sortedSetAdd('uid:' + uid + ':tids_unread', Date.now(), tid); }; - Topics.filterUnrepliedTids = function (tids, callback) { - async.waterfall([ - function (next) { - db.sortedSetScores('topics:posts', tids, next); - }, - function (scores, next) { - tids = tids.filter((tid, index) => tid && scores[index] <= 1); - next(null, tids); - }, - ], callback); + Topics.filterNewTids = async function (tids, uid) { + if (parseInt(uid, 10) <= 0) { + return []; + } + const scores = await db.sortedSetScores('uid:' + uid + ':tids_read', tids); + return tids.filter((tid, index) => tid && !scores[index]); + }; + + Topics.filterUnrepliedTids = async function (tids) { + const scores = await db.sortedSetScores('topics:posts', tids); + return tids.filter((tid, index) => tid && scores[index] <= 1); }; }; diff --git a/src/topics/user.js b/src/topics/user.js index 6471dabb39..1043e0e6de 100644 --- a/src/topics/user.js +++ b/src/topics/user.js @@ -3,17 +3,16 @@ var db = require('../database'); module.exports = function (Topics) { - Topics.isOwner = function (tid, uid, callback) { + Topics.isOwner = async function (tid, uid) { uid = parseInt(uid, 10); if (uid <= 0) { - return setImmediate(callback, null, false); + return false; } - Topics.getTopicField(tid, 'uid', function (err, author) { - callback(err, author === uid); - }); + const author = await Topics.getTopicField(tid, 'uid'); + return author === uid; }; - Topics.getUids = function (tid, callback) { - db.getSortedSetRevRangeByScore('tid:' + tid + ':posters', 0, -1, '+inf', 1, callback); + Topics.getUids = async function (tid) { + return await db.getSortedSetRevRangeByScore('tid:' + tid + ':posters', 0, -1, '+inf', 1); }; }; diff --git a/src/user/blocks.js b/src/user/blocks.js index 934a3cba87..a0a1b8ea44 100644 --- a/src/user/blocks.js +++ b/src/user/blocks.js @@ -78,7 +78,7 @@ module.exports = function (User) { User.blocks.add = function (targetUid, uid, callback) { async.waterfall([ - async.apply(this.applyChecks, true, targetUid, uid), + async.apply(User.blocks.applyChecks, true, targetUid, uid), async.apply(db.sortedSetAdd.bind(db), 'uid:' + uid + ':blocked_uids', Date.now(), targetUid), async.apply(User.incrementUserFieldBy, uid, 'blocksCount', 1), function (_blank, next) { @@ -91,7 +91,7 @@ module.exports = function (User) { User.blocks.remove = function (targetUid, uid, callback) { async.waterfall([ - async.apply(this.applyChecks, false, targetUid, uid), + async.apply(User.blocks.applyChecks, false, targetUid, uid), async.apply(db.sortedSetRemove.bind(db), 'uid:' + uid + ':blocked_uids', targetUid), async.apply(User.decrementUserFieldBy, uid, 'blocksCount', 1), function (_blank, next) { diff --git a/test/authentication.js b/test/authentication.js index 604608b242..2920a44b20 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -203,7 +203,6 @@ describe('authentication', function () { loginUser('regular', 'regularpwd', function (err, response, body, jar) { assert.ifError(err); assert(body); - request({ url: nconf.get('url') + '/api/me', json: true, diff --git a/test/batch.js b/test/batch.js new file mode 100644 index 0000000000..4c98302439 --- /dev/null +++ b/test/batch.js @@ -0,0 +1,115 @@ +'use strict'; + +var async = require('async'); +var assert = require('assert'); + +var db = require('./mocks/databasemock'); + +var batch = require('../src/batch'); + +describe('batch', function () { + const scores = []; + const values = []; + before(function (done) { + for (let i = 0; i < 100; i++) { + scores.push(i); + values.push('val' + i); + } + db.sortedSetAdd('processMe', scores, values, done); + }); + + it('should process sorted set with callbacks', function (done) { + let total = 0; + batch.processSortedSet('processMe', function (items, next) { + items.forEach(function (item) { + total += item.score; + }); + + setImmediate(next); + }, { + withScores: true, + interval: 50, + batch: 10, + }, function (err) { + assert.ifError(err); + assert.strictEqual(total, 4950); + done(); + }); + }); + + it('should process sorted set with callbacks', function (done) { + let total = 0; + batch.processSortedSet('processMe', function (values, next) { + values.forEach(function (val) { + total += val.length; + }); + + setImmediate(next); + }, function (err) { + assert.ifError(err); + assert.strictEqual(total, 490); + done(); + }); + }); + + it('should process sorted set with async/await', async function () { + let total = 0; + await batch.processSortedSet('processMe', function (values, next) { + values.forEach(function (val) { + total += val.length; + }); + + setImmediate(next); + }, {}); + + assert.strictEqual(total, 490); + }); + + it('should process sorted set with async/await', async function () { + let total = 0; + await batch.processSortedSet('processMe', async function (values) { + values.forEach(function (val) { + total += val.length; + }); + await db.getObject('doesnotexist'); + }, {}); + + assert.strictEqual(total, 490); + }); + + it('should process array with callbacks', function (done) { + let total = 0; + batch.processArray(scores, function (nums, next) { + nums.forEach(function (n) { + total += n; + }); + + setImmediate(next); + }, { + withScores: true, + interval: 50, + batch: 10, + }, function (err) { + assert.ifError(err); + assert.strictEqual(total, 4950); + done(); + }); + }); + + it('should process array with async/await', async function () { + let total = 0; + await batch.processArray(scores, function (nums, next) { + nums.forEach(function (n) { + total += n; + }); + + setImmediate(next); + }, { + withScores: true, + interval: 50, + batch: 10, + }); + + assert.strictEqual(total, 4950); + }); +}); diff --git a/test/controllers.js b/test/controllers.js index 73ae2d31b5..7133f3ee2a 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -1395,11 +1395,13 @@ describe('Controllers', function () { request(nconf.get('url') + '/api/user/foo', { }, function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 200); - user.getUserField(fooUid, 'profileviews', function (err, viewcount) { - assert.ifError(err); - assert(viewcount > 0); - done(); - }); + setTimeout(function () { + user.getUserField(fooUid, 'profileviews', function (err, viewcount) { + assert.ifError(err); + assert(viewcount > 0); + done(); + }); + }, 500); }); }); diff --git a/test/database/keys.js b/test/database/keys.js index cbd763465d..d603d6a4e6 100644 --- a/test/database/keys.js +++ b/test/database/keys.js @@ -236,6 +236,17 @@ describe('Key methods', function () { }); }); }); + + it('should not error if old key does not exist', function (done) { + db.rename('doesnotexist', 'anotherdoesnotexist', function (err) { + assert.ifError(err); + db.exists('anotherdoesnotexist', function (err, exists) { + assert.ifError(err); + assert(!exists); + done(); + }); + }); + }); }); describe('type', function () { diff --git a/test/database/list.js b/test/database/list.js index 76e7029f5c..b432099709 100644 --- a/test/database/list.js +++ b/test/database/list.js @@ -201,18 +201,27 @@ describe('List methods', function () { }); }); - - it('should get the length of a list', function (done) { - db.listAppend('getLengthList', 1, function (err) { - assert.ifError(err); - db.listAppend('getLengthList', 2, function (err) { + describe('listLength', function () { + it('should get the length of a list', function (done) { + db.listAppend('getLengthList', 1, function (err) { assert.ifError(err); - db.listLength('getLengthList', function (err, length) { + db.listAppend('getLengthList', 2, function (err) { assert.ifError(err); - assert.equal(length, 2); - done(); + db.listLength('getLengthList', function (err, length) { + assert.ifError(err); + assert.equal(length, 2); + done(); + }); }); }); }); + + it('should return 0 if list does not have any elements', function (done) { + db.listLength('doesnotexist', function (err, length) { + assert.ifError(err); + assert.strictEqual(length, 0); + done(); + }); + }); }); }); diff --git a/test/database/sorted.js b/test/database/sorted.js index 4d5acfef52..6da38340ae 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -684,7 +684,7 @@ describe('Sorted Set methods', function () { it('should return 0 if score is 0', function (done) { db.sortedSetScores('zeroScore', ['value1'], function (err, scores) { assert.ifError(err); - assert.strictEqual(0, scores[0]); + assert.strictEqual(scores[0], 0); done(); }); }); @@ -758,7 +758,7 @@ describe('Sorted Set methods', function () { }); }); - it('should return true if element is in sorted set with sre 0', function (done) { + it('should return true if element is in sorted set with score 0', function (done) { db.isSortedSetMember('zeroscore', 'itemwithzeroscore', function (err, isMember) { assert.ifError(err); assert.strictEqual(isMember, true); @@ -776,6 +776,15 @@ describe('Sorted Set methods', function () { done(); }); }); + + it('should return true if element is in sorted set with score 0', function (done) { + db.isSortedSetMembers('zeroscore', ['itemwithzeroscore'], function (err, isMembers) { + assert.ifError(err); + assert.equal(arguments.length, 2); + assert.deepEqual(isMembers, [true]); + done(); + }); + }); }); describe('isMemberOfSortedSets', function () { @@ -955,6 +964,20 @@ describe('Sorted Set methods', function () { }); }); }); + + it('should not remove anything if values is empty array', function (done) { + db.sortedSetAdd('removeNothing', [1, 2, 3], ['val1', 'val2', 'val3'], function (err) { + assert.ifError(err); + db.sortedSetRemove('removeNothing', [], function (err) { + assert.ifError(err); + db.getSortedSetRange('removeNothing', 0, -1, function (err, data) { + assert.ifError(err); + assert.deepStrictEqual(data, ['val1', 'val2', 'val3']); + done(); + }); + }); + }); + }); }); describe('sortedSetsRemove()', function () { diff --git a/test/topics.js b/test/topics.js index a8c26cb038..09524b9bc4 100644 --- a/test/topics.js +++ b/test/topics.js @@ -152,7 +152,7 @@ describe('Topic\'s', function () { assert.ok(result); socketPosts.getReplies({ uid: 0 }, newPost.pid, function (err, postData) { - assert.equal(err, null, 'posts.getReplies returned error'); + assert.ifError(err); assert.ok(postData); @@ -914,7 +914,7 @@ describe('Topic\'s', function () { title: 'topic for controller test', content: 'topic content', cid: topic.categoryId, - // thumb: 'http://i.imgur.com/64iBdBD.jpg', + thumb: 'http://i.imgur.com/64iBdBD.jpg', }, function (err, result) { assert.ifError(err); assert.ok(result);