From 4be693f2e78774f58433619c037a5ef0f405c38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Sep 2020 23:20:49 -0400 Subject: [PATCH] feat: fullname search (#8641) * feat: fullname search * fix: take last element * fix: attempt to fix psql like query * feat: upgrade sript, another fix attempt * fix: psql test * fix: psql scan * feat: add debug for test * feat: test collate * feat: cleanup * fix: upgrade script --- src/controllers/admin/users.js | 2 +- src/database/postgres/sorted.js | 12 +++++----- src/upgrades/1.15.0/fullname_search_set.js | 26 ++++++++++++++++++++++ src/user/create.js | 4 ++++ src/user/delete.js | 4 ++++ src/user/profile.js | 8 +++++++ src/user/search.js | 2 +- test/user.js | 16 +++++++++++++ 8 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 src/upgrades/1.15.0/fullname_search_set.js diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 106775453f01..ef8a9fdb649b 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -42,7 +42,7 @@ usersController.search = async function (req, res) { match: query, limit: hardCap, }); - return data.map(data => data.split(':')[1]); + return data.map(data => data.split(':').pop()); }, }); diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index bb1af3d291d3..5733a2bb2fd0 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -582,15 +582,15 @@ DELETE FROM "legacy_zset" z if (min.match(/^\(/)) { q.values.push(min.substr(1)); q.suffix += 'GT'; - q.where += ` AND z."value" > $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" > $` + q.values.length + `::TEXT COLLATE "C"`; } else if (min.match(/^\[/)) { q.values.push(min.substr(1)); q.suffix += 'GE'; - q.where += ` AND z."value" >= $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" >= $` + q.values.length + `::TEXT COLLATE "C"`; } else { q.values.push(min); q.suffix += 'GE'; - q.where += ` AND z."value" >= $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" >= $` + q.values.length + `::TEXT COLLATE "C"`; } } @@ -598,15 +598,15 @@ DELETE FROM "legacy_zset" z if (max.match(/^\(/)) { q.values.push(max.substr(1)); q.suffix += 'LT'; - q.where += ` AND z."value" < $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" < $` + q.values.length + `::TEXT COLLATE "C"`; } else if (max.match(/^\[/)) { q.values.push(max.substr(1)); q.suffix += 'LE'; - q.where += ` AND z."value" <= $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" <= $` + q.values.length + `::TEXT COLLATE "C"`; } else { q.values.push(max); q.suffix += 'LE'; - q.where += ` AND z."value" <= $` + q.values.length + `::TEXT`; + q.where += ` AND z."value" <= $` + q.values.length + `::TEXT COLLATE "C"`; } } diff --git a/src/upgrades/1.15.0/fullname_search_set.js b/src/upgrades/1.15.0/fullname_search_set.js new file mode 100644 index 000000000000..0e28dc5ca74f --- /dev/null +++ b/src/upgrades/1.15.0/fullname_search_set.js @@ -0,0 +1,26 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); +const user = require('../../user'); + +module.exports = { + name: 'Create fullname search set', + timestamp: Date.UTC(2020, 8, 11), + method: async function () { + const progress = this.progress; + + await batch.processSortedSet('users:joindate', async function (uids) { + progress.incr(uids.length); + const userData = await user.getUsersFields(uids, ['uid', 'fullname']); + const bulkAdd = userData + .filter(u => u.uid && u.fullname) + .map(u => ['fullname:sorted', 0, u.fullname.toLowerCase() + ':' + u.uid]); + await db.sortedSetAddBulk(bulkAdd); + }, { + batch: 500, + progress: this.progress, + }); + }, +}; diff --git a/src/user/create.js b/src/user/create.js index 352ac000d9ac..45d93b8e2913 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -94,6 +94,10 @@ module.exports = function (User) { bulkAdd.push(['user:' + userData.uid + ':emails', timestamp, userData.email + ':' + timestamp]); } + if (userData.fullname) { + bulkAdd.push(['fullname:sorted', 0, userData.fullname.toLowerCase() + ':' + userData.uid]); + } + await Promise.all([ db.incrObjectField('global', 'userCount'), db.sortedSetAddBulk(bulkAdd), diff --git a/src/user/delete.js b/src/user/delete.js index ca8915774968..e5d2a6850ea3 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -136,6 +136,10 @@ module.exports = function (User) { bulkRemove.push(['email:sorted', userData.email.toLowerCase() + ':' + uid]); } + if (userData.fullname) { + bulkRemove.push(['fullname:sorted', userData.fullname.toLowerCase() + ':' + uid]); + } + await Promise.all([ db.sortedSetRemoveBulk(bulkRemove), db.decrObjectField('global', 'userCount'), diff --git a/src/user/profile.js b/src/user/profile.js index 8bddfc873fb4..05a6b8b3de6e 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -263,6 +263,14 @@ module.exports = function (User) { async function updateFullname(uid, newFullname) { const fullname = await User.getUserField(uid, 'fullname'); await updateUidMapping('fullname', uid, newFullname, fullname); + if (newFullname !== fullname) { + if (fullname) { + await db.sortedSetRemove('fullname:sorted', fullname.toLowerCase() + ':' + uid); + } + if (newFullname) { + await db.sortedSetAdd('fullname:sorted', 0, newFullname.toLowerCase() + ':' + uid); + } + } } User.changePassword = async function (uid, data) { diff --git a/src/user/search.js b/src/user/search.js index d1e2712209ce..b7bc57e83a4d 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -63,7 +63,7 @@ module.exports = function (User) { hardCap = hardCap || resultsPerPage * 10; const data = await db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap); - const uids = data.map(data => data.split(':')[1]); + const uids = data.map(data => data.split(':').pop()); return uids; } diff --git a/test/user.js b/test/user.js index e123ecbfa211..70bc1977fc32 100644 --- a/test/user.js +++ b/test/user.js @@ -399,6 +399,22 @@ describe('User', function () { }); }); + it('should search users by fullname', async function () { + const uid = await User.create({ username: 'fullnamesearch1', fullname: 'Mr. Fullname' }); + const data = await socketUser.search({ uid: adminUid }, { query: 'mr', searchBy: 'fullname' }); + assert(Array.isArray(data.users)); + assert.equal(data.users.length, 1); + assert.equal(uid, data.users[0].uid); + }); + + it('should search users by fullname', async function () { + const uid = await User.create({ username: 'fullnamesearch2', fullname: 'Baris:Usakli' }); + const data = await socketUser.search({ uid: adminUid }, { query: 'baris:', searchBy: 'fullname' }); + assert(Array.isArray(data.users)); + assert.equal(data.users.length, 1); + assert.equal(uid, data.users[0].uid); + }); + it('should return empty array if query is empty', function (done) { socketUser.search({ uid: testUid }, { query: '' }, function (err, data) { assert.ifError(err);