From 3112d225fe1533dd77cfad7fff085d53d78c19f2 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Fri, 6 Jan 2017 18:30:07 -0200 Subject: [PATCH] Allow query, sort, and fields on the queryParams of the rest api --- packages/rocketchat-api/package.js | 5 ++ packages/rocketchat-api/server/api.js | 58 ++++++++++------- packages/rocketchat-api/server/v1/channels.js | 65 +++++++++++-------- packages/rocketchat-api/server/v1/groups.js | 21 +++--- .../server/v1/helpers/getPaginationItems.js | 30 +++++++++ .../server/v1/helpers/parseJsonQuery.js | 37 +++++++++++ packages/rocketchat-api/server/v1/im.js | 21 +++--- .../rocketchat-api/server/v1/integrations.js | 14 ++-- packages/rocketchat-api/server/v1/settings.js | 20 +++--- packages/rocketchat-api/server/v1/users.js | 48 +++++++++----- .../server/functions/getFullUserData.js | 2 +- .../server/models/_BaseCache.js | 2 +- 12 files changed, 222 insertions(+), 101 deletions(-) create mode 100644 packages/rocketchat-api/server/v1/helpers/getPaginationItems.js create mode 100644 packages/rocketchat-api/server/v1/helpers/parseJsonQuery.js diff --git a/packages/rocketchat-api/package.js b/packages/rocketchat-api/package.js index 30cb29b7077..6ea7c019ac7 100644 --- a/packages/rocketchat-api/package.js +++ b/packages/rocketchat-api/package.js @@ -16,8 +16,13 @@ Package.onUse(function(api) { api.addFiles('server/api.js', 'server'); api.addFiles('server/settings.js', 'server'); + //Register v1 helpers + api.addFiles('server/v1/helpers/getPaginationItems.js', 'server'); + api.addFiles('server/v1/helpers/parseJsonQuery.js', 'server'); + api.addFiles('server/default/info.js', 'server'); + //Add v1 routes api.addFiles('server/v1/channels.js', 'server'); api.addFiles('server/v1/chat.js', 'server'); api.addFiles('server/v1/groups.js', 'server'); diff --git a/packages/rocketchat-api/server/api.js b/packages/rocketchat-api/server/api.js index 8d69f20f90e..68d19158507 100644 --- a/packages/rocketchat-api/server/api.js +++ b/packages/rocketchat-api/server/api.js @@ -2,7 +2,9 @@ class API extends Restivus { constructor(properties) { super(properties); + this.logger = new Logger(`API ${properties.version ? properties.version : 'default'} Logger`, {}); this.authMethods = []; + this.helperMethods = new Map(); this.defaultFieldsToExclude = { joinCode: 0, $loki: 0, @@ -56,34 +58,43 @@ class API extends Restivus { }; } - // If the count query param is higher than the "API_Upper_Count_Limit" setting, then we limit that - // If the count query param isn't defined, then we set it to the "API_Default_Count" setting - // If the count is zero, then that means unlimited and is only allowed if the setting "API_Allow_Infinite_Count" is true - getPaginationItems(req) { - const hardUpperLimit = RocketChat.settings.get('API_Upper_Count_Limit') <= 0 ? 100 : RocketChat.settings.get('API_Upper_Count_Limit'); - const defaultCount = RocketChat.settings.get('API_Default_Count') <= 0 ? 50 : RocketChat.settings.get('API_Default_Count'); - const offset = req.queryParams.offset ? parseInt(req.queryParams.offset) : 0; - let count = defaultCount; - - // Ensure count is an appropiate amount - if (typeof req.queryParams.count !== 'undefined') { - count = parseInt(req.queryParams.count); - } else { - count = defaultCount; + addRoute(route, options, endpoints) { + //Note: required if the developer didn't provide options + if (typeof endpoints === 'undefined') { + endpoints = options; + options = {}; } - if (count > hardUpperLimit) { - count = hardUpperLimit; - } + //Note: This is required due to Restivus calling `addRoute` in the constructor of itself + if (this.helperMethods) { + Object.keys(endpoints).forEach((method) => { + if (typeof endpoints[method] === 'function') { + endpoints[method] = { action: endpoints[method] }; + } + + //Add a try/catch for each much + const originalAction = endpoints[method].action; + endpoints[method].action = function() { + let result; + try { + result = originalAction.apply(this); + } catch (e) { + return RocketChat.API.v1.failure(e.message, e.error); + } + + return result ? result : RocketChat.API.v1.success(); + }; - if (count === 0 && !RocketChat.settings.get('API_Allow_Infinite_Count')) { - count = defaultCount; + for (const [name, helperMethod] of this.helperMethods) { + endpoints[method][name] = helperMethod; + } + + //Allow the endpoints to make usage of the logger which respects the user's settings + endpoints[method].logger = this.logger; + }); } - return { - offset, - count - }; + super.addRoute(route, options, endpoints); } } @@ -122,7 +133,6 @@ const getUserAuth = function _getUserAuth() { }; }; - RocketChat.API.v1 = new API({ version: 'v1', useDefaultAuth: true, diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index c660d5800b8..1419017447e 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -199,7 +199,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { includeAllPublicChannels = this.queryParams.includeAllPublicChannels === 'true'; } - const query = { + let ourQuery = { $or: [{ channel: { $in: [`#${findResult.name}`] @@ -208,32 +208,36 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { }; if (includeAllPublicChannels) { - query.$or.push({ + ourQuery.$or.push({ channel: { $eq: [] } }); - query.$or.push({ + ourQuery.$or.push({ channel: { $in: ['all_public_channels'] } }); } - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); - const integrations = RocketChat.models.Integrations.find(query, { - sort: { _createdAt: 1 }, + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + ourQuery = Object.assign({}, query, ourQuery); + + const integrations = RocketChat.models.Integrations.find(ourQuery, { + sort: sort ? sort : { _createdAt: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }).fetch(); return RocketChat.API.v1.success({ integrations, count: integrations.length, offset, - total: RocketChat.models.Integrations.find(query).count() + total: RocketChat.models.Integrations.find(ourQuery).count() }); } }); @@ -410,36 +414,43 @@ RocketChat.API.v1.addRoute('channels.leave', { authRequired: true }, { }); RocketChat.API.v1.addRoute('channels.list', { authRequired: true }, { - get: function() { - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); - - const rooms = RocketChat.models.Rooms.findByType('c', { - sort: { name: 1 }, - skip: offset, - limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude - }).fetch(); - - return RocketChat.API.v1.success({ - channels: rooms, - count: rooms.length, - offset, - total: RocketChat.models.Rooms.findByType('c').count() - }); + get: { + //This is like this only to provide an example of how we routes can be defined :X + action: function() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { t: 'c' }); + + const rooms = RocketChat.models.Rooms.find(ourQuery, { + sort: sort ? sort : { name: 1 }, + skip: offset, + limit: count, + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) + }).fetch(); + + return RocketChat.API.v1.success({ + channels: rooms, + count: rooms.length, + offset, + total: RocketChat.models.Rooms.find(ourQuery).count() + }); + } } }); RocketChat.API.v1.addRoute('channels.list.joined', { authRequired: true }, { get: function() { - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); let rooms = _.pluck(RocketChat.models.Subscriptions.findByTypeAndUserId('c', this.userId).fetch(), '_room'); const totalCount = rooms.length; rooms = RocketChat.models.Rooms.processQueryOptionsOnResult(rooms, { - sort: { name: 1 }, + sort: sort ? sort : { name: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }); return RocketChat.API.v1.success({ diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index 68b106d2187..638f7ae2155 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -140,20 +140,22 @@ RocketChat.API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { channelsToSearch.push('all_private_groups'); } - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); - const query = { channel: { $in: channelsToSearch } }; - const integrations = RocketChat.models.Integrations.find(query, { - sort: { _createdAt: 1 }, + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { channel: { $in: channelsToSearch } }); + const integrations = RocketChat.models.Integrations.find(ourQuery, { + sort: sort ? sort : { _createdAt: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }).fetch(); return RocketChat.API.v1.success({ integrations, count: integrations.length, offset, - total: RocketChat.models.Integrations.find(query).count() + total: RocketChat.models.Integrations.find(ourQuery).count() }); } }); @@ -310,15 +312,16 @@ RocketChat.API.v1.addRoute('groups.leave', { authRequired: true }, { //List Private Groups a user has access to RocketChat.API.v1.addRoute('groups.list', { authRequired: true }, { get: function() { - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); let rooms = _.pluck(RocketChat.models.Subscriptions.findByTypeAndUserId('p', this.userId).fetch(), '_room'); const totalCount = rooms.length; rooms = RocketChat.models.Rooms.processQueryOptionsOnResult(rooms, { - sort: { name: 1 }, + sort: sort ? sort : { name: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }); return RocketChat.API.v1.success({ diff --git a/packages/rocketchat-api/server/v1/helpers/getPaginationItems.js b/packages/rocketchat-api/server/v1/helpers/getPaginationItems.js new file mode 100644 index 00000000000..dd1732df6c7 --- /dev/null +++ b/packages/rocketchat-api/server/v1/helpers/getPaginationItems.js @@ -0,0 +1,30 @@ +// If the count query param is higher than the "API_Upper_Count_Limit" setting, then we limit that +// If the count query param isn't defined, then we set it to the "API_Default_Count" setting +// If the count is zero, then that means unlimited and is only allowed if the setting "API_Allow_Infinite_Count" is true + +RocketChat.API.v1.helperMethods.set('getPaginationItems', function _getPaginationItems() { + const hardUpperLimit = RocketChat.settings.get('API_Upper_Count_Limit') <= 0 ? 100 : RocketChat.settings.get('API_Upper_Count_Limit'); + const defaultCount = RocketChat.settings.get('API_Default_Count') <= 0 ? 50 : RocketChat.settings.get('API_Default_Count'); + const offset = this.queryParams.offset ? parseInt(this.queryParams.offset) : 0; + let count = defaultCount; + + // Ensure count is an appropiate amount + if (typeof this.queryParams.count !== 'undefined') { + count = parseInt(this.queryParams.count); + } else { + count = defaultCount; + } + + if (count > hardUpperLimit) { + count = hardUpperLimit; + } + + if (count === 0 && !RocketChat.settings.get('API_Allow_Infinite_Count')) { + count = defaultCount; + } + + return { + offset, + count + }; +}); diff --git a/packages/rocketchat-api/server/v1/helpers/parseJsonQuery.js b/packages/rocketchat-api/server/v1/helpers/parseJsonQuery.js new file mode 100644 index 00000000000..073ebf4e83f --- /dev/null +++ b/packages/rocketchat-api/server/v1/helpers/parseJsonQuery.js @@ -0,0 +1,37 @@ +RocketChat.API.v1.helperMethods.set('parseJsonQuery', function _parseJsonQuery() { + let sort; + if (this.queryParams.sort) { + try { + sort = JSON.parse(this.queryParams.sort); + } catch (e) { + this.logger.warn(`Invalid sort parameter provided "${this.queryParams.sort}":`, e); + throw new Meteor.Error('error-invalid-sort', `Invalid sort parameter provided: "${this.queryParams.sort}"`, { helperMethod: 'parseJsonQuery' }); + } + } + + let fields; + if (this.queryParams.fields) { + try { + fields = JSON.parse(this.queryParams.fields); + } catch (e) { + this.logger.warn(`Invalid fields parameter provided "${this.queryParams.fields}":`, e); + throw new Meteor.Error('error-invalid-fields', `Invalid fields parameter provided: "${this.queryParams.fields}"`, { helperMethod: 'parseJsonQuery' }); + } + } + + let query; + if (this.queryParams.query) { + try { + query = JSON.parse(this.queryParams.query); + } catch (e) { + this.logger.warn(`Invalid query parameter provided "${this.queryParams.query}":`, e); + throw new Meteor.Error('error-invalid-query', `Invalid query parameter provided: "${this.queryParams.query}"`, { helperMethod: 'parseJsonQuery' }); + } + } + + return { + sort, + fields, + query + }; +}); diff --git a/packages/rocketchat-api/server/v1/im.js b/packages/rocketchat-api/server/v1/im.js index c39cad0c489..8dc83bdb2af 100644 --- a/packages/rocketchat-api/server/v1/im.js +++ b/packages/rocketchat-api/server/v1/im.js @@ -89,15 +89,16 @@ RocketChat.API.v1.addRoute('im.history', { authRequired: true }, { RocketChat.API.v1.addRoute('im.list', { authRequired: true }, { get: function() { - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); let rooms = _.pluck(RocketChat.models.Subscriptions.findByTypeAndUserId('d', this.userId).fetch(), '_room'); const totalCount = rooms.length; rooms = RocketChat.models.Rooms.processQueryOptionsOnResult(rooms, { - sort: { name: 1 }, + sort: sort ? sort : { name: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }); return RocketChat.API.v1.success({ @@ -115,19 +116,23 @@ RocketChat.API.v1.addRoute('im.list.everyone', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); - const rooms = RocketChat.models.Rooms.findByType('d', { - sort: { name: 1 }, + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { t: 'd' }); + + const rooms = RocketChat.models.Rooms.find(ourQuery, { + sort: sort ? sort : { name: 1 }, skip: offset, limit: count, - fields: RocketChat.API.v1.defaultFieldsToExclude + fields: Object.assign({}, fields, RocketChat.API.v1.defaultFieldsToExclude) }).fetch(); return RocketChat.API.v1.success({ ims: rooms, offset, count: rooms.length, - total: RocketChat.models.Rooms.findByType('d').count() + total: RocketChat.models.Rooms.find(ourQuery).count() }); } }); diff --git a/packages/rocketchat-api/server/v1/integrations.js b/packages/rocketchat-api/server/v1/integrations.js index fc58c2c41b8..770b75672eb 100644 --- a/packages/rocketchat-api/server/v1/integrations.js +++ b/packages/rocketchat-api/server/v1/integrations.js @@ -42,18 +42,22 @@ RocketChat.API.v1.addRoute('integrations.list', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); - const integrations = RocketChat.models.Integrations.find({}, { - sort: { ts: -1 }, + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query); + const integrations = RocketChat.models.Integrations.find(ourQuery, { + sort: sort ? sort : { ts: -1 }, skip: offset, - limit: count + limit: count, + fields }).fetch(); return RocketChat.API.v1.success({ integrations: integrations, offset, items: integrations.length, - total: RocketChat.models.Integrations.find().count() + total: RocketChat.models.Integrations.find(ourQuery).count() }); } }); diff --git a/packages/rocketchat-api/server/v1/settings.js b/packages/rocketchat-api/server/v1/settings.js index 686cc361751..6226e351307 100644 --- a/packages/rocketchat-api/server/v1/settings.js +++ b/packages/rocketchat-api/server/v1/settings.js @@ -1,31 +1,31 @@ // settings endpoints RocketChat.API.v1.addRoute('settings', { authRequired: true }, { get() { - const { offset, count } = RocketChat.API.v1.getPaginationItems(this); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); - const query = { + let ourQuery = { hidden: { $ne: true } }; if (!RocketChat.authz.hasPermission(this.userId, 'view-privileged-setting')) { - query.public = { $ne: false }; + ourQuery.public = { $ne: false }; } - const settings = RocketChat.models.Settings.find(query, { - sort: { _id: 1 }, + ourQuery = Object.assign({}, query, ourQuery); + + const settings = RocketChat.models.Settings.find(ourQuery, { + sort: sort ? sort : { _id: 1 }, skip: offset, limit: count, - fields: { - _id: 1, - value: 1 - } + fields: Object.assign({ _id: 1, value: 1 }, fields) }).fetch(); return RocketChat.API.v1.success({ settings, count: settings.length, offset, - total: RocketChat.models.Settings.find(query).count() + total: RocketChat.models.Settings.find(ourQuery).count() }); } }); diff --git a/packages/rocketchat-api/server/v1/users.js b/packages/rocketchat-api/server/v1/users.js index b1dbdfbb9be..1b6cc51613c 100644 --- a/packages/rocketchat-api/server/v1/users.js +++ b/packages/rocketchat-api/server/v1/users.js @@ -116,27 +116,43 @@ RocketChat.API.v1.addRoute('users.info', { authRequired: true }, { RocketChat.API.v1.addRoute('users.list', { authRequired: true }, { get: function() { - let limit = -1; - - if (typeof this.queryParams.limit !== 'undefined') { - limit = parseInt(this.queryParams.limit); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let fieldsToKeepFromRegularUsers; + if (!RocketChat.authz.hasPermission(this.userId, 'view-full-other-user-info')) { + fieldsToKeepFromRegularUsers = { + avatarOrigin: 0, + emails: 0, + phone: 0, + statusConnection: 0, + createdAt: 0, + lastLogin: 0, + services: 0, + requirePasswordChange: 0, + requirePasswordChangeReason: 0, + roles: 0, + statusDefault: 0, + _updatedAt: 0, + customFields: 0 + }; } - let result = undefined; - try { - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('getFullUserData', { filter: '', limit }); - }); - } catch (e) { - return RocketChat.API.v1.failure(e.name + ': ' + e.message); - } + const ourQuery = Object.assign({}, query); + const ourFields = Object.assign({}, fields, fieldsToKeepFromRegularUsers, RocketChat.API.v1.defaultFieldsToExclude); - if (!result) { - return RocketChat.API.v1.failure('Failed to get the users data.'); - } + const users = RocketChat.models.Users.find(ourQuery, { + sort: sort ? sort : { username: 1 }, + skip: offset, + limit: count, + fields: ourFields + }).fetch(); return RocketChat.API.v1.success({ - users: result + users, + count: users.length, + offset, + total: RocketChat.models.Users.find(ourQuery).count() }); } }); diff --git a/packages/rocketchat-lib/server/functions/getFullUserData.js b/packages/rocketchat-lib/server/functions/getFullUserData.js index 241620e06d9..7e77af13e36 100644 --- a/packages/rocketchat-lib/server/functions/getFullUserData.js +++ b/packages/rocketchat-lib/server/functions/getFullUserData.js @@ -22,7 +22,7 @@ RocketChat.getFullUserData = function({userId, filter, limit}) { roles: 1, customFields: 1 }); - } else if (limit !== -1) { + } else if (limit !== 0) { limit = 1; } diff --git a/packages/rocketchat-lib/server/models/_BaseCache.js b/packages/rocketchat-lib/server/models/_BaseCache.js index 3cd05e76f10..0e2aaab6299 100644 --- a/packages/rocketchat-lib/server/models/_BaseCache.js +++ b/packages/rocketchat-lib/server/models/_BaseCache.js @@ -524,7 +524,7 @@ class ModelsBaseCache extends EventEmitter { } if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { - console.error('Can\'t mix remove and get fields'); + console.warn('Can\'t mix remove and get fields'); fieldsToRemove.splice(0, fieldsToRemove.length); }