diff --git a/migrations/elasticsearch_sync.js b/migrations/elasticsearch_sync.js index 349be0b6..b4734c3c 100644 --- a/migrations/elasticsearch_sync.js +++ b/migrations/elasticsearch_sync.js @@ -260,6 +260,39 @@ function getRequestBody(indexName) { }, }, }, + invites: { + type: 'nested', + properties: { + createdAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + createdBy: { + type: 'integer', + }, + email: { + type: 'string', + index: 'not_analyzed', + }, + id: { + type: 'long', + }, + role: { + type: 'string', + index: 'not_analyzed', + }, + updatedAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + updatedBy: { + type: 'integer', + }, + userId: { + type: 'long', + }, + }, + }, name: { type: 'string', }, diff --git a/src/models/project.js b/src/models/project.js index 946289f2..31a984f1 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -98,7 +98,7 @@ module.exports = function defineProject(sequelize, DataTypes) { if (parameters.filters.id.$in.length === 0) { parameters.filters.id.$in.push(-1); } - query += `AND id IN (${parameters.filters.id.$in}) `; + query += `AND projects.id IN (${parameters.filters.id.$in}) `; } else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) { query += `AND id = ${parameters.filters.id} `; } @@ -107,32 +107,51 @@ module.exports = function defineProject(sequelize, DataTypes) { const statusFilter = parameters.filters.status; if (_.isObject(statusFilter)) { const statuses = statusFilter.$in.join("','"); - query += `AND status IN ('${statuses}') `; + query += `AND projects.status IN ('${statuses}') `; } else if (_.isString(statusFilter)) { - query += `AND status ='${statusFilter}'`; + query += `AND projects.status ='${statusFilter}'`; } } if (_.has(parameters.filters, 'type')) { - query += `AND type = '${parameters.filters.type}' `; + query += `AND projects.type = '${parameters.filters.type}' `; } if (_.has(parameters.filters, 'keyword')) { - query += `AND "projectFullText" ~ lower('${parameters.filters.keyword}')`; + query += `AND projects."projectFullText" ~ lower('${parameters.filters.keyword}')`; } - const attributesStr = `"${parameters.attributes.join('","')}"`; + let joinQuery = ''; + if (_.has(parameters.filters, 'userId') || _.has(parameters.filters, 'email')) { + query += ` AND (members."userId" = ${parameters.filters.userId} + OR invites."userId" = ${parameters.filters.userId} + OR invites."email" = '${parameters.filters.email}') GROUP BY projects.id`; + + joinQuery = `LEFT OUTER JOIN project_members AS members ON projects.id = members."projectId" + LEFT OUTER JOIN project_member_invites AS invites ON projects.id = invites."projectId"`; + } + + let attributesStr = _.map(parameters.attributes, attr => `projects."${attr}"`); + attributesStr = `${attributesStr.join(',')}`; const orderStr = `"${parameters.order[0][0]}" ${parameters.order[0][1]}`; // select count of projects - return sequelize.query(`SELECT COUNT(1) FROM projects WHERE ${query}`, + return sequelize.query(`SELECT COUNT(1) FROM projects AS projects + ${joinQuery} + WHERE ${query}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, raw: true, }) .then((fcount) => { - const count = fcount[0].count; + let count = fcount.length; + if (fcount.length === 1) { + count = fcount[0].count; + } + // select project attributes - return sequelize.query(`SELECT ${attributesStr} FROM projects WHERE ${query} ORDER BY ` + - ` ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, + return sequelize.query(`SELECT ${attributesStr} FROM projects AS projects + ${joinQuery} + WHERE ${query} ORDER BY ` + + ` projects.${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, raw: true, diff --git a/src/routes/admin/project-create-index.js b/src/routes/admin/project-create-index.js index d23738ea..d4a00d4b 100644 --- a/src/routes/admin/project-create-index.js +++ b/src/routes/admin/project-create-index.js @@ -265,6 +265,39 @@ function getRequestBody(indexName, docType) { }, }, }, + invites: { + type: 'nested', + properties: { + createdAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + createdBy: { + type: 'integer', + }, + email: { + type: 'string', + index: 'not_analyzed', + }, + id: { + type: 'long', + }, + role: { + type: 'string', + index: 'not_analyzed', + }, + updatedAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + updatedBy: { + type: 'integer', + }, + userId: { + type: 'long', + }, + }, + }, name: { type: 'string', }, diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index c01b138f..187eb6b8 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -133,30 +133,10 @@ module.exports = [ } // regular users can only see projects they are members of (or invited, handled bellow) - const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); - - return getProjectIds - .then((accessibleProjectIds) => { - let allowedProjectIds = accessibleProjectIds; - // get projects with pending invite for current user - const invites = models.ProjectMemberInvite.getProjectInvitesForUser( - req.authUser.email, - req.authUser.userId); - if (invites) { - allowedProjectIds = _.union(allowedProjectIds, invites); - } - // filter based on accessible - if (_.get(criteria.filters, 'id', null)) { - criteria.filters.id.$in = _.intersection( - allowedProjectIds, - criteria.filters.id.$in, - ); - } else { - criteria.filters.id = { $in: allowedProjectIds }; - } - return retrieveProjects(req, criteria, sort, req.query.fields); - }) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); + criteria.filters.userId = req.authUser.userId; + criteria.filters.email = req.authUser.email; + return retrieveProjects(req, criteria, sort, req.query.fields) + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); }, ]; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 73cd3f5d..52bf3b25 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -102,6 +102,56 @@ const buildEsFullTextQuery = (keyword, matchType, singleFieldName) => { }; }; +/** + * Build ES query search request body based on userId and email + * + * @param {String} userId the user id + * @param {String} email the email + * @return {Array} query + */ +const buildEsShouldQuery = (userId, email) => { + const should = []; + if (userId) { + should.push({ + nested: { + path: 'members', + query: { + query_string: { + query: userId, + fields: ['members.userId'], + }, + }, + }, + }); + should.push({ + nested: { + path: 'invites', + query: { + query_string: { + query: userId, + fields: ['invites.userId'], + }, + }, + }, + }); + } + + if (email) { + should.push({ + nested: { + path: 'invites', + query: { + query_string: { + query: email, + fields: ['invites.email'], + }, + }, + }, + }); + } + return should; +}; + /** * Build ES query search request body based on value, keyword, matchType and fieldName * @@ -234,6 +284,7 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { // prepare the elasticsearch filter criteria const boolQuery = []; let mustQuery = []; + let shouldQuery = []; let fullTextQuery; if (_.has(criteria, 'filters.id.$in')) { boolQuery.push({ @@ -269,6 +320,10 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { ['members.firstName', 'members.lastName'])); } + if (_.has(criteria, 'filters.userId') || _.has(criteria, 'filters.email')) { + shouldQuery = buildEsShouldQuery(criteria.filters.userId, criteria.filters.email); + } + if (_.has(criteria, 'filters.status.$in')) { // status is an array boolQuery.push({ @@ -348,6 +403,21 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { must: mustQuery, }); } + + if (shouldQuery.length > 0) { + const newBody = { query: { bool: { must: [] } } }; + newBody.query.bool.must.push({ + bool: { + should: shouldQuery, + }, + }); + if (mustQuery.length > 0 || boolQuery.length > 0) { + newBody.query.bool.must.push(body.query); + } + + body.query = newBody.query; + } + if (fullTextQuery) { body.query = _.merge(body.query, fullTextQuery); if (body.query.bool) { @@ -355,7 +425,7 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { } } - if (fullTextQuery || boolQuery.length > 0 || mustQuery.length > 0) { + if (fullTextQuery || boolQuery.length > 0 || mustQuery.length > 0 || shouldQuery.length > 0) { searchCriteria.body = body; } return searchCriteria; @@ -427,7 +497,6 @@ module.exports = [ offset: req.query.offset || 0, }; req.log.info(criteria); - if (!memberOnly && (util.hasAdminRole(req) || util.hasRoles(req, MANAGER_ROLES))) { @@ -437,32 +506,11 @@ module.exports = [ .catch(err => next(err)); } - // regular users can only see projects they are members of (or invited, handled bellow) - const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); - - return getProjectIds - .then((accessibleProjectIds) => { - const allowedProjectIds = accessibleProjectIds; - // get projects with pending invite for current user - const invites = models.ProjectMemberInvite.getProjectInvitesForUser( - req.authUser.email, - req.authUser.userId); - - return invites.then((ids => _.union(allowedProjectIds, ids))); - }) - .then((allowedProjectIds) => { - // filter based on accessible - if (_.get(criteria.filters, 'id', null)) { - criteria.filters.id.$in = _.intersection( - allowedProjectIds, - criteria.filters.id.$in, - ); - } else { - criteria.filters.id = { $in: allowedProjectIds }; - } - return retrieveProjects(req, criteria, sort, req.query.fields); - }) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); + // regular users can only see projects they are members of (or invited, handled below) + criteria.filters.email = req.authUser.email; + criteria.filters.userId = req.authUser.userId; + return retrieveProjects(req, criteria, sort, req.query.fields) + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); }, ];