diff --git a/docker-compose-mentoring.yml b/docker-compose-mentoring.yml index 71da1f90a..2e6200ba2 100644 --- a/docker-compose-mentoring.yml +++ b/docker-compose-mentoring.yml @@ -137,13 +137,13 @@ services: networks: - elevate_net interface: - build: '../interface/' + build: '../interface-service/' image: elevate/interface:1.0 volumes: - - ../interface/src/:/var/src + - ../interface-service/src/:/var/src ports: - '3569:3569' - command: ['nodemon', 'app.js'] + command: ['node', 'app.js'] networks: - elevate_net # master: @@ -183,10 +183,10 @@ services: citus: image: citusdata/citus:11.2.0 container_name: 'citus_master' - #ports: - # - 5432:5432 - expose: - - 5432 + ports: + - 5432:5432 + # expose: + # - 5432 # command: > # bash -c "while ! pg_isready -h localhost -U postgres -q; do sleep 1; done && # psql -h localhost -U postgres -d -c 'CREATE EXTENSION citus; SELECT create_distributed_table(\"notification_templates\", \"id\");'" @@ -197,6 +197,8 @@ services: PGPASSWORD: '${POSTGRES_PASSWORD:-postgres}' POSTGRES_DB: 'user-local' POSTGRES_HOST_AUTH_METHOD: '${POSTGRES_HOST_AUTH_METHOD:-trust}' + POSTGRES_LOG_STATEMENT: 'all' # Enable query logging (set to 'all' for all queries) + networks: - elevate_net pgadmin: diff --git a/src/api-doc/MentorED-Mentoring.postman_collection.json b/src/api-doc/MentorED-Mentoring.postman_collection.json index 795e9c505..87f04d249 100644 --- a/src/api-doc/MentorED-Mentoring.postman_collection.json +++ b/src/api-doc/MentorED-Mentoring.postman_collection.json @@ -1,10 +1,9 @@ { "info": { - "_postman_id": "5d4d24cb-3c5c-4486-8d85-bd10eeaae9c3", + "_postman_id": "d8b7643b-0f23-4360-aa77-1608b9b940e5", "name": "MentorED-Mentoring", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "6350183", - "_collection_link": "https://cloudy-eclipse-8615.postman.co/workspace/MentorED~099846e5-b1d5-4043-84b9-1e9ac3fc1e4f/collection/6350183-5d4d24cb-3c5c-4486-8d85-bd10eeaae9c3?action=share&source=collection_link&creator=6350183" + "_exporter_id": "21498549" }, "item": [ { @@ -402,7 +401,7 @@ } }, "url": { - "raw": "{{MentoringBaseUrl}}mentoring/v1/sessions/list?page=1&limit=2&status=published,completed", + "raw": "{{MentoringBaseUrl}}mentoring/v1/sessions/list?page=1&limit=2&recommended_for=hm,deo", "host": ["{{MentoringBaseUrl}}mentoring"], "path": ["v1", "sessions", "list"], "query": [ @@ -414,14 +413,15 @@ "key": "limit", "value": "2" }, - { - "key": "status", - "value": "published,completed" - }, { "key": "search", "value": "ankit", "disabled": true + }, + { + "key": "recommended_for", + "value": "hm,deo", + "description": "Filters to be applied should be passed like this." } ] }, @@ -543,7 +543,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Leadership session by rajesh\",\n \"description\": \"description\",\n \"start_date\": 1693479180,\n \"end_date\": 1693486379,\n \"recommended_for\": [\n \"hm\"\n ],\n \"categories\": [\n \"Educational leadership\"\n ],\n \"medium\": [\n \"1\"\n ],\n \"time_zone\": \"Asia/Calcutta\",\n \"image\": [\n \"users/1232s2133sdd1-12e2dasd3123.png\"\n ]\n}", + "raw": "{\n \"title\": \"Leadership session \",\n \"description\": \"description\",\n \"start_date\": 1700647200,\n \"end_date\": 1700650800,\n \"recommended_for\": [\n \"hm\"\n ],\n \"categories\": [\n \"educational_leadership\"\n ],\n \"medium\": [\n \"en_in\"\n ],\n \"time_zone\": \"Asia/Calcutta\",\n \"image\": [\n \"users/1232s2133sdd1-12e2dasd3123.png\"\n ]\n}", "options": { "raw": { "language": "json" @@ -737,7 +737,7 @@ "name": "mentees", "item": [ { - "name": "Mentees Sessions", + "name": "Mentees Enrolled Sessions", "request": { "method": "GET", "header": [ @@ -748,14 +748,10 @@ } ], "url": { - "raw": "{{MentoringBaseUrl}}mentoring/v1/mentees/sessions?enrolled=true&page=1&limit=20", + "raw": "{{MentoringBaseUrl}}mentoring/v1/mentees/sessions?page=1&limit=20", "host": ["{{MentoringBaseUrl}}mentoring"], "path": ["v1", "mentees", "sessions"], "query": [ - { - "key": "enrolled", - "value": "true" - }, { "key": "search", "value": "my data", @@ -892,7 +888,7 @@ "response": [] }, { - "name": "Get Mentor Profile", + "name": "Mentor Details", "request": { "method": "GET", "header": [ @@ -903,15 +899,15 @@ } ], "url": { - "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/profile/1", + "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/details/1", "host": ["{{MentoringBaseUrl}}mentoring"], - "path": ["v1", "mentors", "profile", "1"] + "path": ["v1", "mentors", "details", "1"] } }, "response": [] }, { - "name": "Get Mentor upcommingSession", + "name": "Upcoming Sessions", "request": { "method": "GET", "header": [ @@ -922,7 +918,7 @@ } ], "url": { - "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/upcomingSessions/1?page=1&limit=5", + "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/upcomingSessions/1?page=1&limit=5&recommended_for=hm", "host": ["{{MentoringBaseUrl}}mentoring"], "path": ["v1", "mentors", "upcomingSessions", "1"], "query": [ @@ -938,6 +934,10 @@ "key": "search", "value": "testing5", "disabled": true + }, + { + "key": "recommended_for", + "value": "hm" } ] } @@ -969,6 +969,50 @@ } }, "response": [] + }, + { + "name": "Mentor List", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/list", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "mentors", "list"] + } + }, + "response": [] + }, + { + "name": "Crearted Sessions", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/mentors/createdSessions?status=PUBLISHED,COMPLETED", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "mentors", "createdSessions"], + "query": [ + { + "key": "status", + "value": "PUBLISHED,COMPLETED" + } + ] + } + }, + "response": [] } ] }, @@ -1334,6 +1378,94 @@ } }, "response": [] + }, + { + "name": "Build views", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/admin/triggerViewRebuild", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "admin", "triggerViewRebuild"] + } + }, + "response": [] + }, + { + "name": "Build views (Internal)", + "request": { + "method": "GET", + "header": [ + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/admin/triggerViewRebuildInternal", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "admin", "triggerViewRebuildInternal"] + } + }, + "response": [] + }, + { + "name": "Refresh Views", + "request": { + "method": "GET", + "header": [ + { + "key": "internal_access_token", + "value": "{{internal_access_token}}", + "type": "text", + "disabled": true + }, + { + "key": "X-auth-token", + "value": "bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/admin/triggerPeriodicViewRefresh", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "admin", "triggerPeriodicViewRefresh"] + } + }, + "response": [] + }, + { + "name": "Refresh Views (Internal)", + "request": { + "method": "GET", + "header": [ + { + "key": "internal_access_token", + "value": "{{internal_access_token}}", + "type": "text" + } + ], + "url": { + "raw": "{{MentoringBaseUrl}}mentoring/v1/admin/triggerPeriodicViewRefreshInternal?model_name=Session", + "host": ["{{MentoringBaseUrl}}mentoring"], + "path": ["v1", "admin", "triggerPeriodicViewRefreshInternal"], + "query": [ + { + "key": "model_name", + "value": "Session" + } + ] + } + }, + "response": [] } ] }, @@ -1467,7 +1599,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"user_id\":1,\n\t\"current_roles\":[\"mentor\"],\n\t\"new_roles\":[\"mentee\"]\n}" + "raw": "{\n \"user_id\": 2,\n \"current_roles\": [\n \"mentee\"\n ],\n \"new_roles\": [\n \"mentor\"\n ]\n}" }, "url": { "raw": "{{MentoringBaseUrl}}mentoring/v1/org-admin/roleChange", diff --git a/src/constants/common.js b/src/constants/common.js index 5e1de9d1e..5938a9c5f 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -66,6 +66,8 @@ module.exports = { '/org-admin/roleChange', '/org-admin/updateOrganization', '/org-admin/deactivateUpcomingSession', + '/admin/triggerPeriodicViewRefreshInternal', + '/admin/triggerViewRebuildInternal', ], COMPLETED_STATUS: 'COMPLETED', PUBLISHED_STATUS: 'PUBLISHED', @@ -128,6 +130,9 @@ module.exports = { }, CURRENT: 'CURRENT', ALL: 'ALL', + ASSOCIATED: 'ASSOCIATED', PATCH_METHOD: 'PATCH', GET_METHOD: 'GET', + excludedQueryParams: ['enrolled'], + materializedViewsPrefix: 'm_', } diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index c889ef101..a307eca84 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -6,7 +6,9 @@ */ // Dependencies -const userService = require('@services/admin') +const adminService = require('@services/admin') +const common = require('@constants/common') +const httpStatusCode = require('@generics/http-status') module.exports = class admin { /** @@ -20,10 +22,54 @@ module.exports = class admin { async userDelete(req) { try { - const userDelete = await userService.userDelete(req.decodedToken, req.query.userId) + const userDelete = await adminService.userDelete(req.decodedToken, req.query.userId) return userDelete } catch (error) { return error } } + + async triggerViewRebuild(req) { + try { + if (!req.decodedToken.roles.some((role) => role.title === common.ADMIN_ROLE)) { + return common.failureResponse({ + message: 'UNAUTHORIZED_REQUEST', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } + const userDelete = await adminService.triggerViewRebuild(req.decodedToken) + return userDelete + } catch (error) { + return error + } + } + async triggerPeriodicViewRefresh(req) { + try { + if (!req.decodedToken.roles.some((role) => role.title === common.ADMIN_ROLE)) { + return common.failureResponse({ + message: 'UNAUTHORIZED_REQUEST', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } + return await adminService.triggerPeriodicViewRefresh(req.decodedToken) + } catch (err) { + console.log(err) + } + } + async triggerViewRebuildInternal(req) { + try { + return await adminService.triggerViewRebuild() + } catch (error) { + return error + } + } + async triggerPeriodicViewRefreshInternal(req) { + try { + return await adminService.triggerPeriodicViewRefreshInternal(req.query.model_name) + } catch (err) { + console.log(err) + } + } } diff --git a/src/controllers/v1/mentees.js b/src/controllers/v1/mentees.js index 1e4ee9490..202998445 100644 --- a/src/controllers/v1/mentees.js +++ b/src/controllers/v1/mentees.js @@ -42,11 +42,9 @@ module.exports = class Mentees { try { const sessions = await menteesService.sessions( req.decodedToken.id, - req.query.enrolled, req.pageNo, req.pageSize, - req.searchText, - isAMentor(req.decodedToken.roles) + req.searchText ) return sessions } catch (error) { @@ -77,13 +75,13 @@ module.exports = class Mentees { } /** - * Mentees homefeed API. + * Mentees home feed API. * @method * @name homeFeed * @param {Object} req - request data. * @param {String} req.decodedToken.id - User Id. * @param {Boolean} req.decodedToken.isAMentor - true/false. - * @returns {JSON} - Mentees homefeed response. + * @returns {JSON} - Mentees home feed response. */ async homeFeed(req) { @@ -93,7 +91,8 @@ module.exports = class Mentees { isAMentor(req.decodedToken.roles), req.pageNo, req.pageSize, - req.searchText + req.searchText, + req.query ) return homeFeed } catch (error) { @@ -186,4 +185,4 @@ module.exports = class Mentees { // return error // } // } -} \ No newline at end of file +} diff --git a/src/controllers/v1/mentors.js b/src/controllers/v1/mentors.js index 8d449e49c..85e625869 100644 --- a/src/controllers/v1/mentors.js +++ b/src/controllers/v1/mentors.js @@ -28,7 +28,9 @@ module.exports = class Mentors { req.pageNo, req.pageSize, req.searchText, - req.params.menteeId ? req.params.menteeId : req?.decodedToken?.id + req.params.menteeId ? req.params.menteeId : req?.decodedToken?.id, + req.query, + isAMentor(req.decodedToken.roles) ) } catch (error) { return error @@ -45,7 +47,7 @@ module.exports = class Mentors { * @param {Boolean} isAMentor - user mentor or not. * @returns {JSON} - mentors profile details */ - async profile(req) { + async details(req) { try { return await mentorsService.read(req.params.id, '', req.decodedToken.id, isAMentor(req.decodedToken.roles)) } catch (error) { @@ -96,6 +98,58 @@ module.exports = class Mentors { } } + /** + * List of available mentors. + * @method + * @name list + * @param {Number} req.pageNo - page no. + * @param {Number} req.pageSize - page size limit. + * @param {String} req.searchText - search text. + * @param {Number} req.decodedToken.id - userId. + * @param {Boolean} isAMentor - user mentor or not. + * @returns {JSON} - List of mentors. + */ + + async list(req) { + try { + return await mentorsService.list( + req.pageNo, + req.pageSize, + req.searchText, + req.query, + req.decodedToken.id, + isAMentor(req.decodedToken.roles) + ) + } catch (error) { + return error + } + } + + /** + * List of sessions created by mentor. + * @method + * @name list + * @param {Object} req - Request data. + * @param {String} req.decodedToken.id - Mentors user id. + * @returns {JSON} - Returns sharable link of the mentor. + */ + + async createdSessions(req) { + try { + const sessionDetails = await mentorsService.createdSessions( + req.decodedToken.id, + req.pageNo, + req.pageSize, + req.searchText, + req.query.status, + req.decodedToken.roles + ) + return sessionDetails + } catch (error) { + return error + } + } + //To be removed later // /** // * Create a new mentor extension. diff --git a/src/controllers/v1/notifications.js b/src/controllers/v1/notifications.js index 3d2c0577c..ed5b01965 100644 --- a/src/controllers/v1/notifications.js +++ b/src/controllers/v1/notifications.js @@ -20,7 +20,11 @@ module.exports = class Notifications { async emailCronJob(req) { try { // Make a call to notification service - notificationsService.sendNotification(req.body.jobId, req.body.emailTemplateCode) + notificationsService.sendNotification( + req.body.job_id, + req.body.email_template_code, + req.body.job_creator_org_id ? parseInt(req.body.job_creator_org_id, 10) : '' + ) return { statusCode: httpStatusCode.ok, } diff --git a/src/controllers/v1/sessions.js b/src/controllers/v1/sessions.js index e1e02890a..9b4e967c5 100644 --- a/src/controllers/v1/sessions.js +++ b/src/controllers/v1/sessions.js @@ -79,7 +79,7 @@ module.exports = class Sessions { } /** - * Sessions list + * Get all upcoming sessions by available mentors * @method * @name list * @param {Object} req -request data. @@ -97,7 +97,8 @@ module.exports = class Sessions { req.pageNo, req.pageSize, req.searchText, - req.query.status + req.query, + isAMentor(req.decodedToken.roles) ) return sessionDetails } catch (error) { diff --git a/src/controllers/v1/users.js b/src/controllers/v1/users.js index ba201d225..dd83217ae 100644 --- a/src/controllers/v1/users.js +++ b/src/controllers/v1/users.js @@ -42,21 +42,12 @@ module.exports = class Users { * @param {Number} req.pageNo - page no. * @param {Number} req.pageSize - page size limit. * @param {String} req.searchText - search text. - * @param {Number} req.decodedToken.id - userId. - * @param {Boolean} isAMentor - user mentor or not. * @returns {JSON} - List of user. */ async list(req) { try { - const listUser = await userService.list( - req.query.type, - req.pageNo, - req.pageSize, - req.searchText, - req.decodedToken.id, - isAMentor(req.decodedToken.roles) - ) + const listUser = await userService.list(req.query.type, req.pageNo, req.pageSize, req.searchText) return listUser } catch (error) { return error diff --git a/src/database/migrations/20231115113837-update-column-visible-to-organization.js b/src/database/migrations/20231115113837-update-column-visible-to-organization.js new file mode 100644 index 000000000..30ff02e41 --- /dev/null +++ b/src/database/migrations/20231115113837-update-column-visible-to-organization.js @@ -0,0 +1,32 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + /* await queryInterface.changeColumn('user_extensions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.INTEGER), + }) + + await queryInterface.changeColumn('mentor_extensions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.INTEGER), + }) */ + return queryInterface.changeColumn('sessions', 'visible_to_organizations', { + type: + Sequelize.ARRAY(Sequelize.INTEGER) + + 'USING CAST("visible_to_organizations" as ' + + Sequelize.ARRAY(Sequelize.INTEGER) + + ')', + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('user_extensions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.STRING), + }) + await queryInterface.changeColumn('mentor_extensions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.STRING), + }) + await queryInterface.changeColumn('sessions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.STRING), + }) + }, +} diff --git a/src/database/models/sessions.js b/src/database/models/sessions.js index f1257532a..5bf5a6e57 100644 --- a/src/database/models/sessions.js +++ b/src/database/models/sessions.js @@ -107,7 +107,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: true, }, visible_to_organizations: { - type: DataTypes.ARRAY(DataTypes.STRING), + type: DataTypes.ARRAY(DataTypes.INTEGER), allowNull: true, }, mentor_org_id: { diff --git a/src/database/queries/entityType.js b/src/database/queries/entityType.js index 19f7836c8..7cd2dc37b 100644 --- a/src/database/queries/entityType.js +++ b/src/database/queries/entityType.js @@ -23,7 +23,7 @@ module.exports = class UserEntityData { } } - static async findAllEntityTypes(orgId, attributes) { + static async findAllEntityTypes(orgId, attributes, filter = {}) { try { const entityData = await EntityType.findAll({ where: { @@ -35,6 +35,7 @@ module.exports = class UserEntityData { org_id: orgId, }, ], + ...filter, }, attributes, raw: true, @@ -97,4 +98,34 @@ module.exports = class UserEntityData { return error } } + + static async findAllEntityTypesAndEntities(filter) { + try { + return await EntityType.findAll({ + where: { + ...filter, + }, + include: [ + { + model: Entity, + required: false, + where: { + /* [Op.or]: [ + { + created_by: 0, + }, + { + created_by: userId, + }, + ], */ + status: 'ACTIVE', + }, + as: 'entities', + }, + ], + }) + } catch (error) { + return error + } + } } diff --git a/src/database/queries/mentorExtension.js b/src/database/queries/mentorExtension.js index 17046009d..38ca28dd5 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -1,4 +1,8 @@ const MentorExtension = require('@database/models/index').MentorExtension // Adjust the path accordingly +const { QueryTypes } = require('sequelize') +const sequelize = require('sequelize') +const Sequelize = require('@database/models/index').sequelize +const common = require('@constants/common') module.exports = class MentorExtensionQueries { static async getColumns() { @@ -21,7 +25,7 @@ module.exports = class MentorExtensionQueries { if (data.user_id) { delete data['user_id'] } - const whereClause = customFilter ? customFilter : { user_id: userId }; + const whereClause = customFilter ? customFilter : { user_id: userId } return await MentorExtension.update(data, { where: whereClause, ...options, @@ -36,11 +40,11 @@ module.exports = class MentorExtensionQueries { const queryOptions = { where: { user_id: userId }, raw: true, - }; - + } + // If attributes are passed update query if (attributes.length > 0) { - queryOptions.attributes = attributes; + queryOptions.attributes = attributes } const mentor = await MentorExtension.findOne(queryOptions) return mentor @@ -68,7 +72,7 @@ module.exports = class MentorExtensionQueries { { designation: null, area_of_expertise: [], - education_qualification: [], + education_qualification: null, rating: null, meta: null, stats: null, @@ -79,7 +83,6 @@ module.exports = class MentorExtensionQueries { external_session_visibility: null, external_mentor_visibility: null, deleted_at: Date.now(), - org_id, }, { where: { @@ -96,7 +99,7 @@ module.exports = class MentorExtensionQueries { try { const result = await MentorExtension.findAll({ where: { - user_id: ids, // Assuming "user_id" is the field you want to match + user_id: ids, }, ...options, returning: true, @@ -108,4 +111,94 @@ module.exports = class MentorExtensionQueries { throw error } } + + static async getAllMentors(options = {}) { + try { + const result = await MentorExtension.findAll({ + ...options, + returning: true, + raw: true, + }) + + return result + } catch (error) { + throw error + } + } + + static async getMentorsByUserIdsFromView(ids, page, limit, filter, saasFilter) { + try { + const filterConditions = [] + let saasFilterCondition = [] + + if (filter && typeof filter === 'object') { + for (const key in filter) { + if (Array.isArray(filter[key])) { + filterConditions.push(`"${key}" @> ARRAY[:${key}]::character varying[]`) + } + } + } + const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' + + // SAAS related filtering + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key]) && saasFilter.visibility) { + saasFilterCondition.push( + `("${key}" @> ARRAY[:${key}]::integer[] OR "visibility" = '${saasFilter.visibility}')` + ) + } else if (Array.isArray(saasFilter[key])) { + saasFilterCondition.push(`"${key}" @> ARRAY[:${key}]::integer[]`) + } else { + saasFilterCondition.push(`${key} = ${saasFilter[key]}`) + } + } + } + const saasFilterClause = saasFilterCondition.length > 0 ? `AND ` + saasFilterCondition[0] : '' + + const query = ` + SELECT + user_id, + rating, + visibility, + org_id + FROM + ${common.materializedViewsPrefix + MentorExtension.tableName} + WHERE + user_id IN (${ids.join(',')}) + ${filterClause} + ${saasFilterClause} + OFFSET + :offset + LIMIT + :limit; + ` + const replacements = { + offset: limit * (page - 1), + limit: limit, + ...filter, // Add filter parameters to replacements + } + + // Replace saas related query replacements + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key])) { + replacements[key] = saasFilter[key] + } + } + } + + const sessionAttendeesData = await Sequelize.query(query, { + type: QueryTypes.SELECT, + replacements: replacements, + }) + + return { + data: sessionAttendeesData, + count: sessionAttendeesData.length, + } + } catch (error) { + return error + } + } } diff --git a/src/database/queries/notificationTemplate.js b/src/database/queries/notificationTemplate.js index 06b24a774..2f09fd436 100644 --- a/src/database/queries/notificationTemplate.js +++ b/src/database/queries/notificationTemplate.js @@ -1,16 +1,40 @@ const NotificationTemplate = require('@database/models/index').NotificationTemplate +const { getDefaultOrgId } = require('@helpers/getDefaultOrgId') +const { Op } = require('sequelize') module.exports = class NotificationTemplateData { - static async findOneEmailTemplate(code) { + static async findOneEmailTemplate(code, orgId) { try { - const templateData = await NotificationTemplate.findOne({ - where: { - code, - type: 'email', - status: 'active', - }, + const defaultOrgId = await getDefaultOrgId() + if (!defaultOrgId) { + return common.failureResponse({ + message: 'DEFAULT_ORG_ID_NOT_SET', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + /**If data exists for both `orgId` and `defaultOrgId`, the query will return the first matching records + * we will filter required data based on condition from it + * if orgId passed -> get template defined by particular org or get default org template + */ + const filter = { + code: code, + type: 'email', + status: 'active', + org_id: orgId ? { [Op.or]: [orgId, defaultOrgId] } : defaultOrgId, + } + + let templateData = await NotificationTemplate.findAll({ + where: filter, + raw: true, }) + // If there are multiple results, find the one matching orgId + templateData = templateData.find((template) => template.org_id === orgId) || templateData[0] + + // If no data is found, set an empty object + templateData = templateData || {} + if (templateData && templateData.email_header) { const header = await this.getEmailHeader(templateData.email_header) if (header && header.body) { @@ -24,7 +48,6 @@ module.exports = class NotificationTemplateData { templateData.body += footer.body } } - return templateData } catch (error) { return error @@ -39,6 +62,7 @@ module.exports = class NotificationTemplateData { type: 'emailHeader', status: 'active', }, + raw: true, }) return headerData @@ -55,6 +79,7 @@ module.exports = class NotificationTemplateData { type: 'emailFooter', status: 'active', }, + raw: true, }) return footerData diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index c5376eca2..d876556b1 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -1,10 +1,12 @@ const Session = require('@database/models/index').Session -const { Op, literal } = require('sequelize') +const { Op, literal, QueryTypes } = require('sequelize') const common = require('@constants/common') const sequelize = require('sequelize') const moment = require('moment') const SessionOwnership = require('../models/index').SessionOwnership +const Sequelize = require('@database/models/index').sequelize + exports.getColumns = async () => { try { return await Object.keys(Session.rawAttributes) @@ -12,6 +14,15 @@ exports.getColumns = async () => { return error } } + +exports.getModelName = async () => { + try { + return await Session.name + } catch (error) { + return error + } +} + exports.create = async (data) => { try { return await Session.create(data) @@ -504,7 +515,7 @@ exports.getUpcomingSessions = async (page, limit, search, userId) => { 'created_at', 'meeting_info', 'visibility', - 'mentor_org_id' + 'mentor_org_id', /* ['meetingInfo.platform', 'meetingInfo.platform'], ['meetingInfo.value', 'meetingInfo.value'], */ ], @@ -513,10 +524,6 @@ exports.getUpcomingSessions = async (page, limit, search, userId) => { raw: true, }) return sessionData - return { - data: sessionData.rows, - count: sessionData.count, - } } catch (error) { console.error(error) return error @@ -551,3 +558,208 @@ exports.mentorsSessionWithPendingFeedback = async (mentorId, options = {}, compl return error } } + +exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter, saasFilter) => { + try { + const currentEpochTime = Math.floor(Date.now() / 1000) + let filterConditions = [] + let saasFilterCondition = [] + + if (filter && typeof filter === 'object') { + for (const key in filter) { + if (Array.isArray(filter[key])) { + filterConditions.push(`"${key}" @> ARRAY[:${key}]::character varying[]`) + } + } + } + const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' + + // SAAS related filtering + let saasFilterOrgIdClause = '' + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key]) && saasFilter.visibility) { + saasFilterCondition.push( + `("${key}" @> ARRAY[:${key}]::integer[] OR "visibility" = '${saasFilter.visibility}')` + ) + } else if (Array.isArray(saasFilter[key])) { + saasFilterCondition.push(`"${key}" @> ARRAY[:${key}]::integer[]`) + } else { + saasFilterCondition.push(`${key} = ${saasFilter[key]}`) + } + } + } + const saasFilterClause = saasFilterCondition.length > 0 ? `AND ` + saasFilterCondition[0] : '' + + const query = ` + WITH filtered_sessions AS ( + SELECT id, title, description, start_date, end_date, status, image, mentor_id, visibility, mentor_org_id, created_at, + (meeting_info - 'link' ) AS meeting_info + FROM m_${Session.tableName} + WHERE + title ILIKE :search + AND mentor_id != :userId + AND end_date > :currentEpochTime + AND status IN ('PUBLISHED', 'LIVE') + ${filterClause} + ${saasFilterClause} + ) + SELECT id, title, description, start_date, end_date, status, image, mentor_id, created_at, visibility, mentor_org_id, meeting_info, + COUNT(*) OVER () as total_count + FROM filtered_sessions + ORDER BY created_at DESC + OFFSET :offset + LIMIT :limit; + ` + + const replacements = { + search: `%${search}%`, + userId: userId, + currentEpochTime: currentEpochTime, + offset: limit * (page - 1), + limit: limit, + } + + if (filter && typeof filter === 'object') { + for (const key in filter) { + if (Array.isArray(filter[key])) { + replacements[key] = filter[key] + } + } + } + + // Replace saas related query replacements + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key])) { + replacements[key] = saasFilter[key] + } + } + } + + const sessionIds = await Sequelize.query(query, { + type: QueryTypes.SELECT, + replacements: replacements, + }) + + return { + rows: sessionIds, + count: sessionIds.length > 0 ? sessionIds[0].total_count : 0, + } + } catch (error) { + console.error(error) + throw error + } +} + +exports.findAllByIds = async (ids) => { + try { + return await Session.findAll({ + where: { + id: ids, + }, + raw: true, + order: [['created_at', 'DESC']], + }) + } catch (error) { + return error + } +} + +exports.getMentorsUpcomingSessionsFromView = async (page, limit, search, mentorId, filter, saasFilter) => { + try { + const currentEpochTime = Math.floor(Date.now() / 1000) + + const filterConditions = [] + let saasFilterCondition = [] + + if (filter && typeof filter === 'object') { + for (const key in filter) { + if (Array.isArray(filter[key])) { + filterConditions.push(`"${key}" @> ARRAY[:${key}]::character varying[]`) + } + } + } + const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' + + // SAAS related filtering + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key]) && saasFilter.visibility) { + saasFilterCondition.push( + `("${key}" @> ARRAY[:${key}]::integer[] OR "visibility" = '${saasFilter.visibility}')` + ) + } else if (Array.isArray(saasFilter[key])) { + saasFilterCondition.push(`"${key}" @> ARRAY[:${key}]::integer[]`) + } else { + saasFilterCondition.push(`${key} = ${saasFilter[key]}`) + } + } + } + + const saasFilterClause = saasFilterCondition.length > 0 ? `AND ` + saasFilterCondition[0] : '' + + const query = ` + SELECT + id, + title, + description, + start_date, + end_date, + status, + image, + mentor_id, + meeting_info, + visibility, + mentor_org_id + FROM + ${common.materializedViewsPrefix + Session.tableName} + WHERE + mentor_id = :mentorId + AND status = 'PUBLISHED' + AND start_date > :currentEpochTime + AND started_at IS NULL + AND ( + LOWER(title) LIKE :search + ) + ${filterClause} + ${saasFilterClause} + ORDER BY + start_date ASC + OFFSET + :offset + LIMIT + :limit; + ` + + const replacements = { + mentorId: mentorId, + currentEpochTime: currentEpochTime, + search: `%${search.toLowerCase()}%`, + offset: limit * (page - 1), + limit: limit, + ...filter, // Add filter parameters to replacements + } + + // Replace saas related query replacements + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key])) { + replacements[key] = saasFilter[key] + } + } + } + + const sessionAttendeesData = await Sequelize.query(query, { + type: QueryTypes.SELECT, + replacements: replacements, + }) + + return { + data: sessionAttendeesData, + count: sessionAttendeesData.length, + } + } catch (error) { + return error + } +} diff --git a/src/database/queries/userExtension.js b/src/database/queries/userExtension.js index 34a8f20d5..9fd78faa6 100644 --- a/src/database/queries/userExtension.js +++ b/src/database/queries/userExtension.js @@ -68,7 +68,7 @@ module.exports = class MenteeExtensionQueries { { designation: null, area_of_expertise: [], - education_qualification: [], + education_qualification: null, rating: null, meta: null, stats: null, @@ -79,7 +79,6 @@ module.exports = class MenteeExtensionQueries { external_session_visibility: null, external_mentor_visibility: null, deleted_at: Date.now(), - org_id, }, { where: { diff --git a/src/database/seeders/20230822124704-add_entity_types_and_entities.js b/src/database/seeders/20230822124704-add_entity_types_and_entities.js index 408dc1b82..63542928d 100644 --- a/src/database/seeders/20230822124704-add_entity_types_and_entities.js +++ b/src/database/seeders/20230822124704-add_entity_types_and_entities.js @@ -222,7 +222,7 @@ module.exports = { const entityTypeRow = { value: key, label: convertToWords(key), - data_type: 'STRING', + data_type: 'character varying', status: 'ACTIVE', updated_at: new Date(), created_at: new Date(), @@ -235,9 +235,9 @@ module.exports = { // Check if the key is in sessionEntityTypes before adding model_names if (sessionEntityTypes.includes(key)) { - entityTypeRow.model_names = ['sessions'] + entityTypeRow.model_names = ['Session'] } else { - entityTypeRow.model_names = ['mentor_extensions', 'user_extensions'] + entityTypeRow.model_names = ['MentorExtension', 'UserExtension'] } if (key === 'location') { entityTypeRow.allow_custom_entities = false diff --git a/src/database/seeders/20231103090632-seed-forms.js b/src/database/seeders/20231103090632-seed-forms.js new file mode 100644 index 000000000..5d3e97b32 --- /dev/null +++ b/src/database/seeders/20231103090632-seed-forms.js @@ -0,0 +1,906 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + const formData = [ + { + type: 'editProfile', + sub_type: 'editProfileForm', + data: JSON.stringify({ + fields: { + controls: [ + { + name: 'name', + label: 'Your name', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + placeHolder: 'Please enter your full name', + errorMessage: { + required: 'Enter your name', + pattern: 'This field can only contain alphabets', + }, + validators: { + required: true, + pattern: '^[^0-9!@#%$&()\\-`.+,/"]*$', + }, + options: [], + meta: { + showValidationError: true, + }, + }, + { + name: 'location', + label: 'Select your location', + value: [], + class: 'ion-no-margin', + type: 'select', + position: 'floating', + errorMessage: { + required: 'Please select your location', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'location', + errorLabel: 'Location', + }, + }, + { + name: 'designation', + label: 'Your role', + class: 'ion-no-margin', + value: [{}], + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter your role', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'designation', + addNewPopupHeader: 'Add a new role', + showSelectAll: true, + showAddOption: true, + errorLabel: 'Designation', + }, + }, + { + name: 'experience', + label: 'Your experience in years', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + placeHolder: 'Ex. 5 years', + errorMessage: { + required: 'Enter your experience in years', + }, + isNumberOnly: true, + validators: { + required: true, + maxLength: 2, + }, + options: [], + }, + { + name: 'about', + label: 'Tell us about yourself', + value: '', + class: 'ion-no-margin', + type: 'textarea', + position: 'floating', + errorMessage: { + required: 'This field cannot be empty', + }, + placeHolder: 'Please use only 150 characters', + validators: { + required: true, + maxLength: 150, + }, + options: [], + }, + { + name: 'area_of_expertise', + label: 'Your expertise', + class: 'ion-no-margin', + value: [], + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter your expertise', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'area_of_expertise', + addNewPopupHeader: 'Add your expertise', + showSelectAll: true, + showAddOption: true, + errorLabel: 'Expertise', + }, + }, + { + name: 'education_qualification', + label: 'Education qualification', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + errorMessage: { + required: 'Enter education qualification', + }, + placeHolder: 'Ex. BA, B.ED', + validators: { + required: true, + }, + options: [], + meta: { + errorLabel: 'Education qualification', + }, + }, + { + name: 'languages', + label: 'Languages', + class: 'ion-no-margin', + value: [], + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter language', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'languages', + addNewPopupHeader: 'Add new language', + showSelectAll: true, + showAddOption: true, + errorLabel: 'Medium', + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'session', + sub_type: 'sessionForm', + data: JSON.stringify({ + fields: { + controls: [ + { + name: 'title', + label: 'Session title', + value: '', + class: 'ion-no-margin', + type: 'text', + placeHolder: 'Ex. Name of your session', + position: 'floating', + errorMessage: { + required: 'Enter session title', + }, + validators: { + required: true, + }, + }, + { + name: 'description', + label: 'Description', + value: '', + class: 'ion-no-margin', + type: 'textarea', + placeHolder: 'Tell the community something about your session', + position: 'floating', + errorMessage: { + required: 'Enter description', + }, + validators: { + required: true, + }, + }, + { + name: 'start_date', + label: 'Start date', + class: 'ion-no-margin', + value: '', + displayFormat: 'DD/MMM/YYYY HH:mm', + dependedChild: 'end_date', + type: 'date', + placeHolder: 'YYYY-MM-DD hh:mm', + errorMessage: { + required: 'Enter start date', + }, + position: 'floating', + validators: { + required: true, + }, + }, + { + name: 'end_date', + label: 'End date', + class: 'ion-no-margin', + value: '', + displayFormat: 'DD/MMM/YYYY HH:mm', + dependedParent: 'start_date', + type: 'date', + placeHolder: 'YYYY-MM-DD hh:mm', + errorMessage: { + required: 'Enter end date', + }, + validators: { + required: true, + }, + }, + { + name: 'recommended_for', + label: 'Recommended for', + class: 'ion-no-margin', + value: '', + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter recommended for', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'recommended_for', + addNewPopupHeader: 'Recommended for', + addNewPopupSubHeader: 'Who is this session for?', + showSelectAll: true, + showAddOption: true, + }, + }, + { + name: 'categories', + label: 'Categories', + class: 'ion-no-margin', + value: '', + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter categories', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'categories', + addNewPopupHeader: 'Add a new category', + showSelectAll: true, + showAddOption: true, + }, + }, + { + name: 'medium', + label: 'Select medium', + alertLabel: 'medium', + class: 'ion-no-margin', + value: '', + type: 'chip', + position: '', + disabled: false, + errorMessage: { + required: 'Enter select medium', + }, + validators: { + required: true, + }, + options: [], + meta: { + entityType: 'medium', + addNewPopupHeader: 'Add new language', + showSelectAll: true, + showAddOption: true, + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'termsAndConditions', + sub_type: 'termsAndConditionsForm', + data: JSON.stringify({ + fields: { + controls: [ + { + name: 'termsAndConditions', + label: "

The Terms and Conditions constitute a legally binding agreement made between you and ShikshaLokam, concerning your access to and use of our mobile application MentorED.

By creating an account, you have read, understood, and agree to the
Terms of Use and Privacy Policy.

", + value: "I've read and agree to the User Agreement
and Privacy Policy", + class: 'ion-margin', + type: 'html', + position: 'floating', + validators: { + required: true, + minLength: 10, + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'faq', + sub_type: 'faqPage', + data: JSON.stringify({ + fields: { + controls: [ + { + name: 'faq1', + label: 'How do I sign-up on MentorED?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'Once you install the application, open the MentorED app.', + 'Click on the ‘Sign-up’ button.', + 'Select the role you want to sign up for and enter the basic information. You will receive an OTP on the registered email ID.', + 'Enter the OTP & click on verify.', + ], + }, + { + name: 'faq2', + label: 'What to do if I forget my password?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'On the login page, click on the ‘Forgot Password’ button.', + 'Enter your email ID and the new password.', + 'Click on the Reset password button.', + 'You will receive an OTP on the registered email ID.', + 'Once you enter the correct OTP, you will be able to login with the new password.', + ], + }, + { + name: 'faq3', + label: 'How do I complete my profile?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'On the homepage, in the bottom navigation bar click on the Profile icon to reach the Profile page.', + 'Click on the ‘Edit’ button to fill in or update your details.', + 'Click on the Submit button to save all your changes.', + ], + }, + { + name: 'faq4', + label: 'How do I create a session?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'On the home page select the ‘Created by me’ section.', + 'Click on the ‘Create New session’ or + icon on the top to create a new session.', + 'Enter all the profile details.', + 'Click on publish to make your session active for Mentees to enroll.', + ], + }, + { + name: 'faq5', + label: 'How do I enroll for a session?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'On home page, you will see the upcoming sessions.', + 'Click on View More to view all the sessions available.', + 'Click on the enroll button to get details about the session.', + 'Click on the Enroll button on the bottom bar to register for the session.', + ], + }, + { + name: 'faq6', + label: 'How do I find Mentors on MentorED?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'On the homepage click on the Mentor icon on the bottom navigation bar to see the list of Mentors available on the platform. From the list, you can click on the Mentor tab to learn more about them.', + ], + }, + { + name: 'faq7', + label: 'Cancel / Enroll for a session', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'From the My sessions tab on the homepage and click on the session you want to unregister from. Click on the cancel button at the bottom from the session page to cancel your enrollment.', + ], + }, + { + name: 'faq8', + label: 'How do I attend a session?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'Click on the session you want to attend from the My sessions tab.', + 'Click on the Join button to attend the session.', + ], + }, + { + name: 'faq9', + label: 'What will I be able to see on the Dashboard tab?', + value: '', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'As a Mentor, you will be able to see the number of sessions that you have created on MentorED and the number of sessions you have actually hosted on the platform.', + 'As a Mentee, you will be able to see the total number of sessions you have registered for and the total number of sessions you have attended among them.', + ], + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'helpVideos', + sub_type: 'videos', + data: JSON.stringify({ + template_name: 'defaultTemplate', + fields: { + controls: [ + { + name: 'helpVideo1', + label: 'How to sign up?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + { + name: 'helpVideo2', + label: 'How to set up a profile?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + { + name: 'helpVideo3', + label: 'How to enroll a session?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + { + name: 'helpVideo4', + label: 'How to join a session?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + { + name: 'helpVideo5', + label: 'How to start creating sessions?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + { + name: 'helpVideo6', + label: 'How to search for mentors?', + value: 'https://youtu.be/_QOu33z4LII', + class: 'ion-no-margin', + type: 'text', + position: 'floating', + validators: { + required: true, + }, + options: [ + 'https://fastly.picsum.photos/id/0/1920/1280.jpg?hmac=X3VbWxuAM2c1e21LhbXLKKyb-YGilwmraxFBBAjPrrY', + ], + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'platformApp', + sub_type: 'platformAppForm', + data: JSON.stringify({ + template_name: 'defaultTemplate', + fields: { + forms: [ + { + name: 'Google meet', + hint: 'To use google meet for your meeting, schedule a meeting on google meet and add meeting link below.', + value: 'Gmeet', + form: { + controls: [ + { + name: 'link', + label: 'Meet link', + value: '', + type: 'text', + platformPlaceHolder: 'Eg: https://meet.google.com/xxx-xxxx-xxx', + errorMessage: { + required: 'Please provide a meet link', + pattern: 'Please provide a valid meet link', + }, + validators: { + required: true, + pattern: '^https://meet.google.com/[a-z0-9-]+$', + }, + }, + ], + }, + }, + { + name: 'Zoom', + hint: 'To use zoom for your meeting, schedule a meeting on zoom and add meeting details below.', + value: 'Zoom', + form: { + controls: [ + { + name: 'link', + label: 'Zoom link', + value: '', + class: 'ion-no-margin', + type: 'text', + platformPlaceHolder: 'Eg: https://us05web.zoom.us/j/xxxxxxxxxx', + position: 'floating', + errorMessage: { + required: 'Please provide a meeting link', + pattern: 'Please provide a valid meeting link', + }, + validators: { + required: true, + pattern: + '^https?://(?:[a-z0-9-.]+)?zoom.(?:us|com.cn)/(?:j|my)/[0-9a-zA-Z?=.]+$', + }, + }, + { + name: 'meetingId', + label: 'Meeting ID', + value: '', + class: 'ion-no-margin', + type: 'number', + platformPlaceHolder: 'Eg: 123 456 7890', + position: 'floating', + errorMessage: { + required: 'Please provide a meeting ID', + }, + validators: { + required: true, + maxLength: 11, + }, + }, + { + name: 'password', + label: 'Passcode', + value: '', + type: 'text', + platformPlaceHolder: 'Eg: aBc1de', + errorMessage: { + required: 'Please provide a valid passcode', + }, + validators: { + required: true, + }, + }, + ], + }, + }, + { + name: 'WhatsApp', + hint: 'To use whatsapp for your meeting(32 people or less, create a call link on WhatsApp and add a link below.)', + value: 'Whatsapp', + form: { + controls: [ + { + name: 'link', + label: 'WhatsApp', + value: '', + type: 'text', + platformPlaceHolder: 'Eg: https://call.whatsapp.com/voice/xxxxxxxxxxxx', + errorMessage: { + required: 'Please provide a WhatsApp link.', + pattern: 'Please provide a valid WhatsApp link.', + }, + validators: { + required: true, + pattern: + '^https?://(?:[a-z0-9-.]+)?whatsapp.com/[voicedeo]+/[0-9a-zA-Z?=./]+$', + }, + }, + ], + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'helpApp', + sub_type: 'helpAppForm', + data: JSON.stringify({ + template_name: 'defaultTemplate', + fields: { + forms: [ + { + name: 'Report an issue', + value: 'Report an issue', + buttonText: 'SUBMIT', + form: { + controls: [ + { + name: 'description', + label: 'Report an issue', + value: '', + class: 'ion-margin', + position: 'floating', + platformPlaceHolder: 'Tell us more about the problem you faced', + errorMessage: { + required: 'Enter the issue', + }, + type: 'textarea', + validators: { + required: true, + }, + }, + ], + }, + }, + { + name: 'Request to delete my account', + menteeMessage: + 'Please note the following points', + menterMessage: + 'Please note the following points', + value: 'Request to delete my account', + buttonText: 'DELETE_ACCOUNT', + form: { + controls: [ + { + name: 'description', + label: 'Reason for deleting account', + value: '', + class: 'ion-margin', + position: 'floating', + platformPlaceHolder: 'Reason for deleting account', + errorMessage: '', + type: 'textarea', + validators: { + required: false, + }, + }, + ], + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'mentorQuestionnaire', + sub_type: 'mentorQuestionnaireForm', + data: JSON.stringify({ + fields: { + controls: [ + { + name: 'role', + label: 'Role', + value: '', + class: 'ion-margin', + type: 'text', + position: 'floating', + errorMessage: { + required: 'Enter your role', + pattern: 'This field can only contain alphabets', + }, + validators: { + required: true, + pattern: '^[a-zA-Z ]*$', + }, + }, + { + name: 'experience', + label: 'Year of Experience', + value: '', + class: 'ion-margin', + type: 'number', + position: 'floating', + errorMessage: { + required: 'Enter your experience', + pattern: 'This field can only contain numbers', + }, + validators: { + required: true, + }, + }, + { + name: 'area_of_expertise', + label: 'Area of Expertise', + value: '', + class: 'ion-margin', + type: 'chip', + meta: { + showSelectAll: true, + }, + position: 'floating', + errorMessage: { + required: 'Add your Expertise', + }, + options: [ + { + label: 'Scool Management', + value: 'SM', + }, + { + label: 'Technology', + value: 'Tech', + }, + { + label: 'Subjec Teaching', + value: 'ST', + }, + ], + validators: { + required: true, + }, + }, + { + name: 'about', + label: 'About', + value: '', + class: 'ion-margin', + type: 'textarea', + position: 'floating', + errorMessage: { + required: 'Tell us a few lines about yourself', + }, + validators: { + required: true, + }, + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + { + type: 'sampleCsvDownload', + sub_type: 'sampleCsvDownload', + data: JSON.stringify({ + fields: { + controls: [ + { + csvDownloadUrl: + 'https://drive.google.com/file/d/1ZDjsc7YLZKIwxmao-8PdEvnHppkMkXIE/view?usp=sharing', + }, + ], + }, + }), + version: 0, + updated_at: new Date(), + created_at: new Date(), + }, + ] + await queryInterface.bulkInsert('forms', formData, {}) + } catch (error) { + console.error('Error seeding forms:', error) + } + }, + down: async (queryInterface, Sequelize) => { + try { + await queryInterface.bulkDelete('forms', null, {}) + } catch (error) { + console.error('Error reverting form seeding:', error) + } + }, +} diff --git a/src/database/seeders/20231115170949-add-new-mentee-questions.js b/src/database/seeders/20231115170949-add-new-mentee-questions.js new file mode 100644 index 000000000..062a270ff --- /dev/null +++ b/src/database/seeders/20231115170949-add-new-mentee-questions.js @@ -0,0 +1,96 @@ +const questionModel = require('../queries/questions') +const _ = require('lodash') +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + let questionsFinalArray = [] + const questionsNames = [ + 'session_relevents_to_role', + 'mentee_thought_sharing_comfort', + 'mentee_learning_extent', + ] + const questionsArray = [ + { + name: 'session_relevents_to_role', + question: 'How relevant was the session to your role?', + }, + { + name: 'mentee_thought_sharing_comfort', + question: 'To what extent did you feel comfortable sharing your thoughts in the session?', + }, + { + name: 'mentee_learning_extent', + question: 'To what extent were you able to learn new skill or concept in the session?', + }, + ] + + //get mentee question set + const getQuestionSet = await queryInterface.sequelize.query( + 'SELECT * FROM question_sets WHERE code = :questionSetCode LIMIT 1', + { + replacements: { questionSetCode: 'MENTEE_QS1' }, + type: queryInterface.sequelize.QueryTypes.SELECT, + raw: true, + } + ) + + if (getQuestionSet.length > 0) { + const questionSetId = getQuestionSet[0].id + + const additionalObject = { + question_set_id: questionSetId, + status: 'PUBLISHED', + type: 'rating', + no_of_stars: 5, + rendering_data: { + validators: { + required: false, + }, + disable: false, + visible: true, + class: 'ion-margin', + }, + updated_at: new Date(), + created_at: new Date(), + } + + questionsFinalArray = questionsArray.map((question) => ({ ...question, ...additionalObject })) + questionsFinalArray.forEach((question) => { + question.rendering_data = JSON.stringify(question.rendering_data) + }) + + //INSERT QUESTIONS + await queryInterface.bulkInsert('questions', questionsFinalArray, {}) + + //get questions + const getQuestions = await queryInterface.sequelize.query( + 'SELECT id FROM questions WHERE name IN (:questionNames)', + { + replacements: { questionNames: questionsNames }, + type: queryInterface.sequelize.QueryTypes.SELECT, + raw: true, + } + ) + + if (getQuestions.length > 0) { + let questionIds = getQuestions.map((obj) => obj.id.toString()) + + const updateQuestionsIds = _.union(getQuestionSet[0].questions || [], questionIds) + + const updatedValues = { + questions: updateQuestionsIds, + } + + const updateCondition = { code: 'MENTEE_QS1' } + + //update question set + await queryInterface.bulkUpdate('question_sets', updatedValues, updateCondition, {}) + } + } + } catch (error) { + console.log(error) + } + }, + + down: async (queryInterface, Sequelize) => {}, +} diff --git a/src/envVariables.js b/src/envVariables.js index 466613603..ea6468149 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -208,6 +208,11 @@ let enviromentVariables = { optional: false, default: 'sl', }, + REFRESH_VIEW_INTERVAL: { + message: 'Interval to refresh views in milliseconds', + optional: false, + default: 540000, + }, } let success = true diff --git a/src/generics/materializedViews.js b/src/generics/materializedViews.js new file mode 100644 index 000000000..7d26a50f4 --- /dev/null +++ b/src/generics/materializedViews.js @@ -0,0 +1,362 @@ +'use strict' +const entityTypeQueries = require('@database/queries/entityType') +const { sequelize } = require('@database/models/index') +const utils = require('@generics/utils') +const common = require('@constants/common') + +let refreshInterval +const groupByModelNames = async (entityTypes) => { + const groupedData = new Map() + entityTypes.forEach((item) => { + item.model_names.forEach((modelName) => { + if (groupedData.has(modelName)) { + groupedData.get(modelName).entityTypes.push(item) + groupedData.get(modelName).entityTypeValueList.push(item.value) + } else + groupedData.set(modelName, { + modelName: modelName, + entityTypes: [item], + entityTypeValueList: [item.value], + }) + }) + }) + + return [...groupedData.values()] +} + +const filterConcreteAndMetaAttributes = async (modelAttributes, attributesList) => { + try { + const concreteAttributes = [] + const metaAttributes = [] + attributesList.forEach((attribute) => { + if (modelAttributes.includes(attribute)) concreteAttributes.push(attribute) + else metaAttributes.push(attribute) + }) + return { concreteAttributes, metaAttributes } + } catch (err) { + console.log(err) + } +} + +const rawAttributesTypeModifier = async (rawAttributes) => { + try { + const outputArray = [] + for (const key in rawAttributes) { + const columnInfo = rawAttributes[key] + const type = columnInfo.type.key + const subField = columnInfo.type.options?.type?.key + const typeMap = { + ARRAY: { + JSON: 'json[]', + STRING: 'character varying[]', + INTEGER: 'integer[]', + }, + INTEGER: 'integer', + DATE: 'timestamp with time zone', + BOOLEAN: 'boolean', + JSONB: 'jsonb', + JSON: 'json', + STRING: 'character varying', + BIGINT: 'bigint', + } + const conversion = typeMap[type] + if (conversion) { + if (type === 'DATE' && (key === 'createdAt' || key === 'updatedAt')) { + continue + } + outputArray.push({ + key: key, + type: subField ? typeMap[type][subField] : conversion, + }) + } + } + return outputArray + } catch (err) { + console.log(err) + } +} +const metaAttributesTypeModifier = (data) => { + try { + const typeMap = { + 'ARRAY[STRING]': 'character varying[]', + 'ARRAY[INTEGER]': 'integer[]', + 'ARRAY[TEXT]': 'text[]', + INTEGER: 'integer', + DATE: 'timestamp with time zone', + BOOLEAN: 'boolean', + JSONB: 'jsonb', + JSON: 'json', + STRING: 'character varying', + BIGINT: 'bigint', + } + + const outputArray = data.map((field) => { + const { data_type, model_names, ...rest } = field + const convertedDataType = typeMap[data_type] + + return convertedDataType + ? { + ...rest, + data_type: convertedDataType, + model_names: Array.isArray(model_names) + ? model_names.map((modelName) => `'${modelName}'`).join(', ') + : model_names, + } + : field + }) + + return outputArray + } catch (err) { + console.error(err) + } +} + +const generateRandomCode = (length) => { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * charset.length) + result += charset[randomIndex] + } + return result +} + +const materializedViewQueryBuilder = async (model, concreteFields, metaFields) => { + try { + const tableName = model.tableName + const temporaryMaterializedViewName = `${common.materializedViewsPrefix}${tableName}_${generateRandomCode(8)}` + const concreteFieldsQuery = await concreteFields + .map((data) => { + return `${data.key}::${data.type} as ${data.key}` + }) + .join(',\n') + const metaFieldsQuery = + metaFields.length > 0 + ? await metaFields + .map((data) => { + if (data.data_type == 'character varying[]') { + return `transform_jsonb_to_text_array(meta->'${data.value}')::${data.data_type} as ${data.value}` + } else { + return `(meta->>'${data.value}') as ${data.value}` + } + }) + .join(',\n') + : '' // Empty string if there are no meta fields + + const whereClause = utils.generateWhereClause(tableName) + + const materializedViewGenerationQuery = `CREATE MATERIALIZED VIEW ${temporaryMaterializedViewName} AS + SELECT + ${concreteFieldsQuery}${metaFieldsQuery && `,`}${metaFieldsQuery} + FROM public."${tableName}" + WHERE ${whereClause};` + + return { materializedViewGenerationQuery, temporaryMaterializedViewName } + } catch (err) { + console.log(err) + } +} + +const createIndexesOnAllowFilteringFields = async (model, modelEntityTypes) => { + try { + const uniqueEntityTypeValueList = [...new Set(modelEntityTypes.entityTypeValueList)] + + await Promise.all( + uniqueEntityTypeValueList.map(async (attribute) => { + return await sequelize.query( + `CREATE INDEX ${common.materializedViewsPrefix}idx_${model.tableName}_${attribute} ON ${common.materializedViewsPrefix}${model.tableName} (${attribute});` + ) + }) + ) + } catch (err) { + console.log(err) + } +} + +const deleteMaterializedView = async (viewName) => { + try { + await sequelize.query(`DROP MATERIALIZED VIEW ${viewName};`) + } catch (err) { + console.log(err) + } +} + +const renameMaterializedView = async (temporaryMaterializedViewName, tableName) => { + const t = await sequelize.transaction() + try { + let randomViewName = `${common.materializedViewsPrefix}${tableName}_${generateRandomCode(8)}` + + const checkOriginalViewQuery = `SELECT COUNT(*) from pg_matviews where matviewname = '${common.materializedViewsPrefix}${tableName}';` + const renameOriginalViewQuery = `ALTER MATERIALIZED VIEW ${common.materializedViewsPrefix}${tableName} RENAME TO ${randomViewName};` + const renameNewViewQuery = `ALTER MATERIALIZED VIEW ${temporaryMaterializedViewName} RENAME TO ${common.materializedViewsPrefix}${tableName};` + + const temp = await sequelize.query(checkOriginalViewQuery) + + if (temp[0][0].count > 0) await sequelize.query(renameOriginalViewQuery, { transaction: t }) + else randomViewName = null + await sequelize.query(renameNewViewQuery, { transaction: t }) + await t.commit() + + return randomViewName + } catch (error) { + await t.rollback() + console.error('Error executing transaction:', error) + } +} + +const createViewUniqueIndexOnPK = async (model) => { + try { + const primaryKeys = model.primaryKeyAttributes + + const result = await sequelize.query(` + CREATE UNIQUE INDEX ${common.materializedViewsPrefix}unique_index_${model.tableName}_${primaryKeys.map( + (key) => `_${key}` + )} + ON ${common.materializedViewsPrefix}${model.tableName} (${primaryKeys.map((key) => `${key}`).join(', ')});`) + } catch (err) { + console.log(err) + } +} + +const generateMaterializedView = async (modelEntityTypes) => { + try { + const model = require('@database/models/index')[modelEntityTypes.modelName] + + const { concreteAttributes, metaAttributes } = await filterConcreteAndMetaAttributes( + Object.keys(model.rawAttributes), + modelEntityTypes.entityTypeValueList + ) + + const concreteFields = await rawAttributesTypeModifier(model.rawAttributes) + + const metaFields = await modelEntityTypes.entityTypes + .map((entity) => { + if (metaAttributes.includes(entity.value)) return entity + else null + }) + .filter(Boolean) + + const modifiedMetaFields = await metaAttributesTypeModifier(metaFields) + + const { materializedViewGenerationQuery, temporaryMaterializedViewName } = await materializedViewQueryBuilder( + model, + concreteFields, + modifiedMetaFields + ) + + await sequelize.query(materializedViewGenerationQuery) + + const randomViewName = await renameMaterializedView(temporaryMaterializedViewName, model.tableName) + if (randomViewName) await deleteMaterializedView(randomViewName) + await createIndexesOnAllowFilteringFields(model, modelEntityTypes) + await createViewUniqueIndexOnPK(model) + } catch (err) { + console.log(err) + } +} + +const getAllowFilteringEntityTypes = async () => { + try { + return await entityTypeQueries.findAllEntityTypes( + 1, + ['id', 'value', 'label', 'data_type', 'org_id', 'has_entities', 'model_names'], + { + allow_filtering: true, + } + ) + } catch (err) { + console.log(err) + } +} + +const triggerViewBuild = async () => { + try { + const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() + const entityTypesGroupedByModel = await groupByModelNames(allowFilteringEntityTypes) + + await Promise.all( + entityTypesGroupedByModel.map(async (modelEntityTypes) => { + return generateMaterializedView(modelEntityTypes) + }) + ) + + return entityTypesGroupedByModel + } catch (err) { + console.log(err) + } +} + +//Refresh Flow + +const modelNameCollector = async (entityTypes) => { + try { + const modelSet = new Set() + await Promise.all( + entityTypes.map(async ({ model_names }) => { + if (model_names && Array.isArray(model_names)) + await Promise.all( + model_names.map((model) => { + if (!modelSet.has(model)) modelSet.add(model) + }) + ) + }) + ) + return [...modelSet.values()] + } catch (err) { + console.log(err) + } +} + +const refreshMaterializedView = async (modelName) => { + try { + const model = require('@database/models/index')[modelName] + const [result, metadata] = await sequelize.query( + `REFRESH MATERIALIZED VIEW CONCURRENTLY ${common.materializedViewsPrefix}${model.tableName}` + ) + return metadata + } catch (err) { + console.log(err) + } +} + +const refreshNextView = (currentIndex, modelNames) => { + try { + if (currentIndex < modelNames.length) { + refreshMaterializedView(modelNames[currentIndex]) + currentIndex++ + } else { + console.info('All views refreshed. Stopping further refreshes.') + clearInterval(refreshInterval) // Stop the setInterval loop + } + return currentIndex + } catch (err) { + console.log(err) + } +} + +const triggerPeriodicViewRefresh = async () => { + try { + const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() + const modelNames = await modelNameCollector(allowFilteringEntityTypes) + const interval = process.env.REFRESH_VIEW_INTERVAL + let currentIndex = 0 + + // Using the mockSetInterval function to simulate setInterval + refreshInterval = setInterval(() => { + currentIndex = refreshNextView(currentIndex, modelNames) + }, interval / modelNames.length) + + // Immediately trigger the first refresh + currentIndex = refreshNextView(currentIndex, modelNames) + } catch (err) { + console.log(err) + } +} + +const adminService = { + triggerViewBuild, + triggerPeriodicViewRefresh, + refreshMaterializedView, +} + +module.exports = adminService diff --git a/src/generics/utils.js b/src/generics/utils.js index ef701a7f4..c6c823e88 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -174,7 +174,14 @@ function validateInput(input, validationData, modelName) { const errors = [] for (const field of validationData) { const fieldValue = input[field.value] - //console.log('fieldValue', field.allow_custom_entities) + + if (modelName && !field.model_names.includes(modelName) && input[field.value]) { + errors.push({ + param: field.value, + msg: `${field.value} is not allowed for the ${modelName} model.`, + }) + } + if (!fieldValue || field.allow_custom_entities === true) { continue // Skip validation if the field is not present in the input or allow_custom_entities is true } @@ -194,13 +201,6 @@ function validateInput(input, validationData, modelName) { msg: `${fieldValue} is not a valid entity.`, }) } - - if (modelName && !field.model_names.includes(modelName)) { - errors.push({ - param: field.value, - msg: `${field.value} is not allowed for the ${modelName} model.`, - }) - } } if (errors.length === 0) { @@ -215,114 +215,106 @@ function validateInput(input, validationData, modelName) { errors: errors, } } -function restructureBody(requestBody, entityData, allowedKeys) { - try { - const requestBodyKeys = Object.keys(requestBody) - - const entityValues = entityData.map((entity) => entity.value) - const requestBodyKeysExists = requestBodyKeys.some((element) => entityValues.includes(element)) +const entityTypeMapGenerator = (entityTypeData) => { + try { + const entityTypeMap = new Map() + entityTypeData.forEach((entityType) => { + const labelsMap = new Map() + const entities = entityType.entities.map((entity) => { + labelsMap.set(entity.value, entity.label) + return entity.value + }) + if (!entityTypeMap.has(entityType.value)) { + const entityMap = new Map() + entityMap.set('allow_custom_entities', entityType.allow_custom_entities) + entityMap.set('entities', new Set(entities)) + entityMap.set('labels', labelsMap) + entityTypeMap.set(entityType.value, entityMap) + } + }) + return entityTypeMap + } catch (err) { + console.log(err) + } +} - if (!requestBodyKeysExists) { - return requestBody - } - const customEntities = {} +function restructureBody(requestBody, entityData, allowedKeys) { + try { + const entityTypeMap = entityTypeMapGenerator(entityData) + const doesAffectedFieldsExist = Object.keys(requestBody).some((element) => entityTypeMap.has(element)) + if (!doesAffectedFieldsExist) return requestBody requestBody.custom_entity_text = {} - for (const requestBodyKey in requestBody) { - if (requestBody.hasOwnProperty(requestBodyKey)) { - const requestBodyValue = requestBody[requestBodyKey] - const entityType = entityData.find((entity) => entity.value === requestBodyKey) - - if (entityType && entityType.allow_custom_entities) { - if (Array.isArray(requestBodyValue)) { - const customValues = [] - - for (const value of requestBodyValue) { - const entityExists = entityType.entities.find((entity) => entity.value === value) - - if (!entityExists) { - customEntities.custom_entity_text = customEntities.custom_entity_text || {} - customEntities.custom_entity_text[requestBodyKey] = - customEntities.custom_entity_text[requestBodyKey] || [] - customEntities.custom_entity_text[requestBodyKey].push({ - value: 'other', - label: value, - }) - customValues.push(value) - } - } - - if (customValues.length > 0) { - // Remove customValues from the original array - requestBody[requestBodyKey] = requestBody[requestBodyKey].filter( - (value) => !customValues.includes(value) - ) - } - for (const value of requestBodyValue) { - const entityExists = entityType.entities.find((entity) => entity.value === value) - - if (!entityExists) { - if (!requestBody[requestBodyKey].includes('other')) { - requestBody[requestBodyKey].push('other') - } - } - } + if (!requestBody.meta) requestBody.meta = {} + for (const currentFieldName in requestBody) { + const currentFieldValue = requestBody[currentFieldName] + const entityType = entityTypeMap.get(currentFieldName) + if (entityType && entityType.get('allow_custom_entities')) { + if (Array.isArray(currentFieldValue)) { + const recognizedEntities = [] + const customEntities = [] + for (const value of currentFieldValue) { + if (entityType.get('entities').has(value)) recognizedEntities.push(value) + else customEntities.push({ value: 'other', label: value }) } - } - - if (Array.isArray(requestBodyValue)) { - const entityTypeExists = entityData.find((entity) => entity.value === requestBodyKey) - - // Always move the key to the meta field if it's not allowed and is not a custom entity - if (!allowedKeys.includes(requestBodyKey) && entityTypeExists) { - requestBody.meta = { - ...(requestBody.meta || {}), - [requestBodyKey]: requestBody[requestBodyKey], - } - delete requestBody[requestBodyKey] + if (recognizedEntities.length > 0) + if (allowedKeys.includes(currentFieldName)) requestBody[currentFieldName] = recognizedEntities + else requestBody.meta[currentFieldName] = recognizedEntities + if (customEntities.length > 0) { + requestBody[currentFieldName].push('other') //This should cause error at DB write + requestBody.custom_entity_text[currentFieldName] = customEntities } + } else { + if (!entityType.get('entities').has(currentFieldValue)) { + requestBody.custom_entity_text[currentFieldName] = { + value: 'other', + label: currentFieldValue, + } + if (allowedKeys.includes(currentFieldName)) + requestBody[currentFieldName] = 'other' //This should cause error at DB write + else requestBody.meta[currentFieldName] = 'other' + } else if (!allowedKeys.includes(currentFieldName)) + requestBody.meta[currentFieldName] = currentFieldValue } } } - // Merge customEntities into requestBody - Object.assign(requestBody, customEntities) + if (Object.keys(requestBody.meta).length === 0) requestBody.meta = null + if (Object.keys(requestBody.custom_entity_text).length === 0) requestBody.custom_entity_text = null return requestBody } catch (error) { console.error(error) } } -function processDbResponse(session, entityType) { - if (session.meta) { +function processDbResponse(responseBody, entityType) { + if (responseBody.meta) { entityType.forEach((entity) => { const entityTypeValue = entity.value - if (session?.meta?.hasOwnProperty(entityTypeValue)) { - // Move the key from session.meta to session root level - session[entityTypeValue] = session.meta[entityTypeValue] - // Delete the key from session.meta - delete session.meta[entityTypeValue] + if (responseBody?.meta?.hasOwnProperty(entityTypeValue)) { + // Move the key from responseBody.meta to responseBody root level + responseBody[entityTypeValue] = responseBody.meta[entityTypeValue] + // Delete the key from responseBody.meta + delete responseBody.meta[entityTypeValue] } }) } - const output = { ...session } // Create a copy of the session object + const output = { ...responseBody } // Create a copy of the responseBody object for (const key in output) { if (entityType.some((entity) => entity.value === key) && output[key] !== null) { const matchingEntity = entityType.find((entity) => entity.value === key) const matchingValues = matchingEntity.entities - .filter((entity) => (Array.isArray(output[key]) ? output[key].includes(entity.value) : false)) + .filter((entity) => (Array.isArray(output[key]) ? output[key].includes(entity.value) : true)) .map((entity) => ({ value: entity.value, label: entity.label, })) if (matchingValues.length > 0) { - output[key] = matchingValues + output[key] = Array.isArray(output[key]) ? matchingValues : matchingValues[0] } else if (Array.isArray(output[key])) { output[key] = output[key].map((item) => { - if (item.value && item.label) { - return item - } + if (item.value && item.label) return item return { value: item, label: item, @@ -344,13 +336,9 @@ function processDbResponse(session, entityType) { // Merge "custom_entity_text" into the respective arrays for (const key in data.custom_entity_text) { - if (Array.isArray(data[key])) { - data[key] = [...data[key], ...data.custom_entity_text[key]] - } else { - data[key] = data.custom_entity_text[key] - } + if (Array.isArray(data[key])) data[key] = [...data[key], ...data.custom_entity_text[key]] + else data[key] = data.custom_entity_text[key] } - delete data.custom_entity_text return data } @@ -362,6 +350,17 @@ function removeParentEntityTypes(data) { const epochFormat = (date, format) => { return moment.unix(date).utc().format(format) } +function processQueryParametersWithExclusions(query) { + const queryArrays = {} + const excludedKeys = common.excludedQueryParams + for (const queryParam in query) { + if (query.hasOwnProperty(queryParam) && !excludedKeys.includes(queryParam)) { + queryArrays[queryParam] = query[queryParam].split(',').map((item) => item.trim()) + } + } + + return queryArrays +} /** * Calculate the time difference in milliseconds between a current date @@ -426,7 +425,7 @@ const validateRoleAccess = (roles, requiredRoles) => { if (!Array.isArray(requiredRoles)) { requiredRoles = [requiredRoles] } - + // Check the type of the first element. const firstElementType = typeof roles[0] if (firstElementType === 'object') { @@ -434,7 +433,6 @@ const validateRoleAccess = (roles, requiredRoles) => { } else { return roles.some((role) => requiredRoles.includes(role)) } - } const removeDefaultOrgEntityTypes = (entityTypes, orgId) => { @@ -445,6 +443,47 @@ const removeDefaultOrgEntityTypes = (entityTypes, orgId) => { }) return Array.from(entityTypeMap.values()) } +const generateWhereClause = (tableName) => { + let whereClause = '' + + switch (tableName) { + case 'sessions': + const currentEpochDate = Math.floor(new Date().getTime() / 1000) // Get current date in epoch format + whereClause = `deleted_at IS NULL AND start_date >= ${currentEpochDate}` + break + case 'mentor_extensions': + whereClause = `deleted_at IS NULL` + break + case 'user_extensions': + whereClause = `deleted_at IS NULL` + break + default: + whereClause = 'deleted_at IS NULL' + } + + return whereClause +} + +function validateFilters(input, validationData, modelName) { + const allValues = [] + validationData.forEach((item) => { + // Extract the 'value' property from the main object + allValues.push(item.value) + + // Extract the 'value' property from the 'entities' array + }) + console.log(allValues) + for (const key in input) { + if (input.hasOwnProperty(key)) { + if (allValues.includes(key)) { + continue + } else { + delete input[key] + } + } + } + return input +} module.exports = { hash: hash, @@ -478,4 +517,7 @@ module.exports = { generateCheckSum, validateRoleAccess, removeDefaultOrgEntityTypes, + generateWhereClause, + validateFilters, + processQueryParametersWithExclusions, } diff --git a/src/locales/en.json b/src/locales/en.json index 3ce3d7e5e..86c70c37b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -110,5 +110,7 @@ "SESSION_DEACTIVATED_SUCCESSFULLY": "Session deactivated successfully.", "SUCCESSFULLY_UNENROLLED_FROM_UPCOMING_SESSION": "Unrolled from upcoming sessions successfully.", "PROFILE_RESTRICTED": "User can't access this profile", - "SESSION_RESTRICTED": "User can't access this session" + "SESSION_RESTRICTED": "User can't access this session", + "MATERIALIZED_VIEW_GENERATED_SUCCESSFULLY": "Materialized views generated successfully", + "MATERIALIZED_VIEW_REFRESH_INITIATED_SUCCESSFULLY": "Materialized views refresh initiated successfully" } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index fe391ca2b..1fc6462e7 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -66,10 +66,19 @@ module.exports = async function (req, res, next) { try { decodedToken = jwt.verify(authHeaderArray[1], process.env.ACCESS_TOKEN_SECRET) } catch (err) { - err.statusCode = httpStatusCode.unauthorized - err.responseCode = 'UNAUTHORIZED' - err.message = 'ACCESS_TOKEN_EXPIRED' - throw err + if (err.name === 'TokenExpiredError') { + throw common.failureResponse({ + message: 'ACCESS_TOKEN_EXPIRED', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } else { + throw common.failureResponse({ + message: 'UNAUTHORIZED_REQUEST', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } } if (!decodedToken) { diff --git a/src/requests/scheduler.js b/src/requests/scheduler.js index bf4bf4454..2dcc172db 100644 --- a/src/requests/scheduler.js +++ b/src/requests/scheduler.js @@ -15,13 +15,13 @@ const mentoringBaseurl = `http://localhost:${process.env.APPLICATION_PORT}` /** * Create a scheduler job. * - * @param {string} jobId - The unique identifier for the job. - * @param {number} delay - The delay in milliseconds before the job is executed. - * @param {string} jobName - The name of the job. - * @param {string} notificationTemplate - The template for the notification. - * @param {function} callback - The callback function to handle the result of the job creation. + * @param {string} jobId - The unique identifier for the job. + * @param {number} delay - The delay in milliseconds before the job is executed. + * @param {string} jobName - The name of the job. + * @param {Object} requestBody - Job api call request body. + * @param {function} callback - The callback function to handle the result of the job creation. */ -const createSchedulerJob = function (jobId, delay, jobName, notificationTemplate) { +const createSchedulerJob = function (jobId, delay, jobName, requestBody = {}) { const bodyData = { jobName: jobName, email: email, @@ -29,11 +29,12 @@ const createSchedulerJob = function (jobId, delay, jobName, notificationTemplate url: mentoringBaseurl + '/mentoring/v1/notifications/emailCronJob', method: 'post', header: { internal_access_token: process.env.INTERNAL_ACCESS_TOKEN }, + reqBody: requestBody, }, + jobOptions: { jobId: jobId, delay: delay, - emailTemplate: notificationTemplate, removeOnComplete: true, removeOnFail: false, attempts: 1, diff --git a/src/requests/user.js b/src/requests/user.js index 003e00ec5..a1d04a768 100644 --- a/src/requests/user.js +++ b/src/requests/user.js @@ -180,10 +180,35 @@ const list = function (userType, pageNo, pageSize, searchText) { }) } +/** + * User list. + * @method + * @name list + * @param {Boolean} userType - mentor/mentee. + * @param {Number} page - page No. + * @param {Number} limit - page limit. + * @param {String} search - search field. + * @returns {JSON} - List of users + */ + +const listWithoutLimit = function (userType, searchText) { + return new Promise(async (resolve, reject) => { + try { + const apiUrl = userBaseUrl + endpoints.USERS_LIST + '?type=' + userType + '&search=' + searchText + const userDetails = await requests.get(apiUrl, false, true) + + return resolve(userDetails) + } catch (error) { + return reject(error) + } + }) +} + module.exports = { fetchDefaultOrgDetails, details, getListOfUserDetails, list, share, + listWithoutLimit, } diff --git a/src/scripts/psqlFunction.js b/src/scripts/psqlFunction.js new file mode 100644 index 000000000..d7e62536b --- /dev/null +++ b/src/scripts/psqlFunction.js @@ -0,0 +1,63 @@ +const { Sequelize } = require('sequelize') +require('dotenv').config({ path: '../.env' }) + +const nodeEnv = process.env.NODE_ENV || 'development' + +let databaseUrl + +switch (nodeEnv) { + case 'production': + databaseUrl = process.env.PROD_DATABASE_URL + break + case 'test': + databaseUrl = process.env.TEST_DATABASE_URL + break + default: + databaseUrl = process.env.DEV_DATABASE_URL +} + +if (!databaseUrl) { + console.error(`${nodeEnv} DATABASE_URL not found in environment variables.`) + process.exit(1) +} + +const sequelize = new Sequelize(databaseUrl, { + dialect: 'postgres', +}) + +const createFunctionSQL = ` + CREATE OR REPLACE FUNCTION transform_jsonb_to_text_array(input_jsonb jsonb) RETURNS text[] AS $$ + DECLARE + result text[]; + element text; + BEGIN + IF jsonb_typeof(input_jsonb) = 'object' THEN + result := ARRAY[]::text[]; + FOR element IN SELECT jsonb_object_keys(input_jsonb) + LOOP + result := array_append(result, element); + END LOOP; + ELSIF jsonb_typeof(input_jsonb) = 'array' THEN + result := ARRAY[]::text[]; + FOR element IN SELECT jsonb_array_elements_text(input_jsonb) + LOOP + result := array_append(result, element); + END LOOP; + ELSE + result := ARRAY[]::text[]; + END IF; + RETURN result; + END; + $$ LANGUAGE plpgsql; +` + +sequelize + .query(createFunctionSQL) + .then((res) => { + console.log('Function created successfully', res) + sequelize.close() + }) + .catch((error) => { + console.error('Error creating function:', error) + sequelize.close() + }) diff --git a/src/scripts/viewsScript.js b/src/scripts/viewsScript.js new file mode 100644 index 000000000..56d859e68 --- /dev/null +++ b/src/scripts/viewsScript.js @@ -0,0 +1,138 @@ +// Dependencies +const request = require('request') +require('dotenv').config({ path: '../.env' }) +const entityTypeQueries = require('../database/queries/entityType') + +// Data +const schedulerServiceUrl = process.env.SCHEDULER_SERVICE_HOST // Port address on which the scheduler service is running +const mentoringBaseurl = `http://localhost:${process.env.APPLICATION_PORT}` +const apiEndpoints = require('../constants/endpoints') + +/** + * Create a scheduler job. + * + * @param {string} jobId - The unique identifier for the job. + * @param {number} interval - The delay in milliseconds before the job is executed. + * @param {string} jobName - The name of the job. + * @param {string} modelName - The template for the notification. + */ +const createSchedulerJob = function (jobId, interval, jobName, repeat, url, offset) { + const bodyData = { + jobName: jobName, + email: [process.env.SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID], + request: { + url, + method: 'post', + header: { internal_access_token: process.env.INTERNAL_ACCESS_TOKEN }, + }, + jobOptions: { + jobId: jobId, + repeat: repeat + ? { every: Number(interval), offset } + : { every: Number(interval), limit: 1, immediately: true }, // Add limit only if repeat is false + removeOnComplete: 50, + removeOnFail: 200, + }, + } + + const options = { + headers: { + 'Content-Type': 'application/json', + }, + json: bodyData, + } + + const apiUrl = schedulerServiceUrl + process.env.SCHEDULER_SERVICE_BASE_URL + apiEndpoints.CREATE_SCHEDULER_JOB + + try { + request.post(apiUrl, options, (err, data) => { + if (err) { + console.error('Error in createSchedulerJob POST request:', err) + } else { + if (data.body.success) { + //console.log('Scheduler', data.body) + //console.log('Request made to scheduler successfully (createSchedulerJob)') + } else { + console.error('Error in createSchedulerJob POST request response:', data.body) + } + } + }) + } catch (error) { + console.error('Error in createSchedulerJob ', error) + } +} + +const getAllowFilteringEntityTypes = async () => { + try { + return await entityTypeQueries.findAllEntityTypes( + 1, + ['id', 'value', 'label', 'data_type', 'org_id', 'has_entities', 'model_names'], + { + allow_filtering: true, + } + ) + } catch (err) { + console.log(err) + } +} + +const modelNameCollector = async (entityTypes) => { + try { + const modelSet = new Set() + await Promise.all( + entityTypes.map(async ({ model_names }) => { + console.log(model_names) + if (model_names && Array.isArray(model_names)) + await Promise.all( + model_names.map((model) => { + if (!modelSet.has(model)) modelSet.add(model) + }) + ) + }) + ) + console.log(modelSet) + return [...modelSet.values()] + } catch (err) { + console.log(err) + } +} + +/** + * Trigger periodic view refresh for allowed entity types. + */ +const triggerPeriodicViewRefresh = async () => { + try { + const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() + const modelNames = await modelNameCollector(allowFilteringEntityTypes) + + let offset = process.env.REFRESH_VIEW_INTERVAL / modelNames.length + modelNames.map((model, index) => { + createSchedulerJob( + 'repeatable_view_job' + model, + process.env.REFRESH_VIEW_INTERVAL, + 'repeatable_view_job' + model, + true, + mentoringBaseurl + '/mentoring/v1/admin/triggerPeriodicViewRefreshInternal?model_name=' + model, + offset * index + ) + }) + } catch (err) { + console.log(err) + } +} +const buildMaterializedViews = async () => { + try { + createSchedulerJob( + 'BuildMaterializedViews', + 10000, + 'BuildMaterializedViews', + false, + mentoringBaseurl + '/mentoring/v1/admin/triggerViewRebuildInternal' + ) + } catch (err) { + console.log(err) + } +} +// Triggering the starting function +buildMaterializedViews() +triggerPeriodicViewRefresh() diff --git a/src/services/admin.js b/src/services/admin.js index 589b95312..23a0e3221 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -10,6 +10,7 @@ const notificationTemplateQueries = require('@database/queries/notificationTempl const mentorQueries = require('@database/queries/mentorExtension') const menteeQueries = require('@database/queries/userExtension') const userRequests = require('@requests/user') +const adminService = require('../generics/materializedViews') module.exports = class AdminHelper { /** @@ -40,7 +41,10 @@ module.exports = class AdminHelper { if (isMentor) { removedUserDetails = await mentorQueries.removeMentorDetails(userId) const removedSessionsDetail = await sessionQueries.removeAndReturnMentorSessions(userId) - result.isAttendeesNotified = await this.unenrollAndNotifySessionAttendees(removedSessionsDetail) + result.isAttendeesNotified = await this.unenrollAndNotifySessionAttendees( + removedSessionsDetail, + mentor.org_id ? mentor.org_id : '' + ) } else { removedUserDetails = await menteeQueries.removeMenteeDetails(userId) } @@ -66,10 +70,11 @@ module.exports = class AdminHelper { } } - static async unenrollAndNotifySessionAttendees(removedSessionsDetail) { + static async unenrollAndNotifySessionAttendees(removedSessionsDetail, orgId = '') { try { const templateData = await notificationTemplateQueries.findOneEmailTemplate( - process.env.MENTOR_SESSION_DELETE_EMAIL_TEMPLATE + process.env.MENTOR_SESSION_DELETE_EMAIL_TEMPLATE, + orgId ) for (const session of removedSessionsDetail) { @@ -143,4 +148,42 @@ module.exports = class AdminHelper { return error } } + + static async triggerViewRebuild(decodedToken) { + try { + const result = await adminService.triggerViewBuild() + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'MATERIALIZED_VIEW_GENERATED_SUCCESSFULLY', + }) + } catch (error) { + console.error('An error occurred in userDelete:', error) + return error + } + } + static async triggerPeriodicViewRefresh(decodedToken) { + try { + const result = await adminService.triggerPeriodicViewRefresh() + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'MATERIALIZED_VIEW_REFRESH_INITIATED_SUCCESSFULLY', + }) + } catch (error) { + console.error('An error occurred in userDelete:', error) + return error + } + } + static async triggerPeriodicViewRefreshInternal(modelName) { + try { + const result = await adminService.refreshMaterializedView(modelName) + console.log(result) + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'MATERIALIZED_VIEW_REFRESH_INITIATED_SUCCESSFULLY', + }) + } catch (error) { + console.error('An error occurred in userDelete:', error) + return error + } + } } diff --git a/src/services/issues.js b/src/services/issues.js index 84de54164..092963cda 100644 --- a/src/services/issues.js +++ b/src/services/issues.js @@ -3,8 +3,7 @@ const httpStatusCode = require('@generics/http-status') const utils = require('@generics/utils') const kafkaCommunication = require('@generics/kafka-communication') -const notificationTemplateData = require('@db/notification-template/query') - +const notificationTemplateQueries = require('@database/queries/notificationTemplate') const issueQueries = require('../database/queries/issue') module.exports = class issuesHelper { /** @@ -25,8 +24,9 @@ module.exports = class issuesHelper { bodyData.user_id = decodedToken.id if (process.env.ENABLE_EMAIL_FOR_REPORT_ISSUE === 'true') { - const templateData = await notificationTemplateData.findOneEmailTemplate( - process.env.REPORT_ISSUE_EMAIL_TEMPLATE_CODE + const templateData = await notificationTemplateQueries.findOneEmailTemplate( + process.env.REPORT_ISSUE_EMAIL_TEMPLATE_CODE, + decodedToken.organization_id ) let metaItems = '' diff --git a/src/services/mentees.js b/src/services/mentees.js index 87e828f01..8f580ca84 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -75,21 +75,15 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of sessions */ - static async sessions(userId, enrolledSessions, page, limit, search = '', isAMentor) { + static async sessions(userId, page, limit, search = '') { try { - let sessions = [] - - if (!enrolledSessions) { - /** Upcoming unenrolled sessions {All sessions}*/ - sessions = await this.getAllSessions(page, limit, search, userId, isAMentor) - } else { - /** Upcoming user's enrolled sessions {My sessions}*/ - /* Fetch sessions if it is not expired or if expired then either status is live or if mentor + /** Upcoming user's enrolled sessions {My sessions}*/ + /* Fetch sessions if it is not expired or if expired then either status is live or if mentor delays in starting session then status will remain published for that particular interval so fetch that also */ - /* TODO: Need to write cron job that will change the status of expired sessions from published to cancelled if not hosted by mentor */ - sessions = await this.getMySessions(page, limit, search, userId, isAMentor) - } + /* TODO: Need to write cron job that will change the status of expired sessions from published to cancelled if not hosted by mentor */ + const sessions = await this.getMySessions(page, limit, search, userId) + return common.successResponse({ statusCode: httpStatusCode.ok, message: 'SESSION_FETCHED_SUCCESSFULLY', @@ -163,15 +157,15 @@ module.exports = class MenteesHelper { * @returns {JSON} - Mentees homeFeed. */ - static async homeFeed(userId, isAMentor, page, limit, search) { + static async homeFeed(userId, isAMentor, page, limit, search, queryParams) { try { /* All Sessions */ - let allSessions = await this.getAllSessions(page, limit, search, userId, isAMentor) + let allSessions = await this.getAllSessions(page, limit, search, userId, queryParams, isAMentor) /* My Sessions */ - let mySessions = await this.getMySessions(page, limit, search, userId, isAMentor) + let mySessions = await this.getMySessions(page, limit, search, userId) const result = { all_sessions: allSessions.rows, @@ -315,13 +309,28 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of all sessions */ - static async getAllSessions(page, limit, search, userId, isAMentor) { - const sessions = await sessionQueries.getUpcomingSessions(page, limit, search, userId) + static async getAllSessions(page, limit, search, userId, queryParams, isAMentor) { + let query = utils.processQueryParametersWithExclusions(queryParams) - sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) + let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ + status: 'ACTIVE', + }) + + let filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'MentorExtension') + + // Create saas fiter for view query + const saasFilter = await this.filterSessionsBasedOnSaasPolicy(userId, isAMentor) - // Filter sessions based on saas policy {session contain enrolled + upcoming session} - sessions.rows = await this.filterSessionsBasedOnSaasPolicy(sessions.rows, userId, isAMentor) + const sessions = await sessionQueries.getUpcomingSessionsFromView( + page, + limit, + search, + userId, + filteredQuery, + saasFilter + ) + + sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) sessions.rows = await this.sessionMentorDetails(sessions.rows) @@ -332,23 +341,19 @@ module.exports = class MenteesHelper { * @description - filter sessions based on user's saas policy. * @method * @name filterSessionsBasedOnSaasPolicy - * @param {Array} sessions - Session data. * @param {Number} userId - User id. * @param {Boolean} isAMentor - user mentor or not. * @returns {JSON} - List of filtered sessions */ - static async filterSessionsBasedOnSaasPolicy(sessions, userId, isAMentor) { + static async filterSessionsBasedOnSaasPolicy(userId, isAMentor) { try { - if (sessions.length === 0) { - return sessions - } - let userPolicyDetails // If user is mentor - fetch policy details from mentor extensions else fetch from userExtension if (isAMentor) { userPolicyDetails = await mentorQueries.getMentorExtension(userId, [ 'external_session_visibility', 'org_id', + 'visible_to_organizations', ]) // Throw error if mentor extension not found @@ -363,6 +368,7 @@ module.exports = class MenteesHelper { userPolicyDetails = await menteeQueries.getMenteeExtension(userId, [ 'external_session_visibility', 'org_id', + 'visible_to_organizations', ]) // If no mentee present return error if (Object.keys(userPolicyDetails).length === 0) { @@ -373,30 +379,19 @@ module.exports = class MenteesHelper { }) } } - + let filter = {} if (userPolicyDetails.external_session_visibility && userPolicyDetails.org_id) { - // Filter sessions based on policy - const filteredSessions = await Promise.all( - sessions.map(async (session) => { - let enrolled = session.is_enrolled ? session.is_enrolled : false - if ( - session.visibility === common.CURRENT || - (session.visibility === common.ALL && - userPolicyDetails.external_session_visibility === common.CURRENT) - ) { - // Check if the session's mentor organization matches the user's organization. - if (session.mentor_org_id === userPolicyDetails.org_id || enrolled == true) { - return session - } - } else { - return session - } - }) - ) - // Remove any undefined elements (sessions that didn't meet the conditions) - sessions = filteredSessions.filter((session) => session !== undefined) + // generate filter based on condition + if (userPolicyDetails.external_session_visibility === common.CURRENT) { + filter.mentor_org_id = userPolicyDetails.org_id + } else if (userPolicyDetails.external_session_visibility === common.ASSOCIATED) { + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + } else if (userPolicyDetails.external_session_visibility === common.ALL) { + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + filter.visibility = common.ALL + } } - return sessions + return filter } catch (err) { return err } @@ -413,13 +408,10 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of enrolled sessions */ - static async getMySessions(page, limit, search, userId, isAMentor) { + static async getMySessions(page, limit, search, userId) { try { const upcomingSessions = await sessionQueries.getUpcomingSessions(page, limit, search, userId) - // // filter upcoming session based on policy ---> commented at level 1 saas changes. will need at level 3 - // upcomingSessions.rows = await this.filterSessionsBasedOnSaasPolicy(upcomingSessions.rows, userId, isAMentor) - const upcomingSessionIds = upcomingSessions.rows.map((session) => session.id) const usersUpcomingSessions = await sessionAttendeesQueries.usersUpcomingSessions( @@ -549,7 +541,7 @@ module.exports = class MenteesHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(data, validationData, 'user_extensions') + let res = utils.validateInput(data, validationData, 'UserExtension') if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -564,6 +556,10 @@ module.exports = class MenteesHelper { // construct policy object let saasPolicyData = await orgAdminService.constructOrgPolicyObject(organisationPolicy, true) + userOrgDetails.data.result.related_orgs = userOrgDetails.data.result.related_orgs + ? userOrgDetails.data.result.related_orgs.concat([saasPolicyData.org_id]) + : [saasPolicyData.org_id] + // Update mentee extension creation data data = { ...data, @@ -634,7 +630,7 @@ module.exports = class MenteesHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(data, validationData, 'user_extensions') + let res = utils.validateInput(data, validationData, 'UserExtension') if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', diff --git a/src/services/mentors.js b/src/services/mentors.js index 79f2e3ea2..c87043a80 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -4,6 +4,7 @@ const userRequests = require('@requests/user') const common = require('@constants/common') const httpStatusCode = require('@generics/http-status') const mentorQueries = require('@database/queries/mentorExtension') +const menteeQueries = require('@database/queries/userExtension') const { UniqueConstraintError } = require('sequelize') const _ = require('lodash') const sessionAttendeesQueries = require('@database/queries/sessionAttendees') @@ -14,7 +15,8 @@ const orgAdminService = require('@services/org-admin') const { getDefaultOrgId } = require('@helpers/getDefaultOrgId') const { Op } = require('sequelize') const { removeDefaultOrgEntityTypes } = require('@generics/utils') -const usersService = require('@services/users') +const moment = require('moment') +const menteesService = require('@services/mentees') module.exports = class MentorsHelper { /** @@ -27,8 +29,15 @@ module.exports = class MentorsHelper { * @param {String} search - Search text. * @returns {JSON} - mentors upcoming session details */ - static async upcomingSessions(id, page, limit, search = '', menteeUserId) { + static async upcomingSessions(id, page, limit, search = '', menteeUserId, queryParams, isAMentor) { try { + const query = utils.processQueryParametersWithExclusions(queryParams) + console.log(query) + let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ + status: 'ACTIVE', + }) + const filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'sessions') + const mentorsDetails = await mentorQueries.getMentorExtension(id) if (!mentorsDetails) { return common.failureResponse({ @@ -38,7 +47,17 @@ module.exports = class MentorsHelper { }) } - let upcomingSessions = await sessionQueries.getMentorsUpcomingSessions(page, limit, search, id) + // Filter upcoming sessions based on saas policy + const saasFilter = await menteesService.filterSessionsBasedOnSaasPolicy(menteeUserId, isAMentor) + + let upcomingSessions = await sessionQueries.getMentorsUpcomingSessionsFromView( + page, + limit, + search, + id, + filteredQuery, + saasFilter + ) if (!upcomingSessions.data.length) { return common.successResponse({ @@ -56,6 +75,7 @@ module.exports = class MentorsHelper { if (menteeUserId && id != menteeUserId) { upcomingSessions.data = await this.menteeSessionDetails(upcomingSessions.data, menteeUserId) } + return common.successResponse({ statusCode: httpStatusCode.ok, message: 'UPCOMING_SESSION_FETCHED', @@ -292,7 +312,7 @@ module.exports = class MentorsHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(data, validationData, 'mentor_extensions') + let res = utils.validateInput(data, validationData, 'MentorExtension') if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -307,6 +327,10 @@ module.exports = class MentorsHelper { // construct saas policy data let saasPolicyData = await orgAdminService.constructOrgPolicyObject(organisationPolicy, true) + userOrgDetails.data.result.related_orgs = userOrgDetails.data.result.related_orgs + ? userOrgDetails.data.result.related_orgs.concat([saasPolicyData.org_id]) + : [saasPolicyData.org_id] + // update mentee extension data data = { ...data, @@ -469,7 +493,11 @@ module.exports = class MentorsHelper { try { if (userId !== '' && isAMentor !== '') { // Get mentor visibility and org_id - let requstedMentorExtension = await mentorQueries.getMentorExtension(id, ['visibility', 'org_id']) + let requstedMentorExtension = await mentorQueries.getMentorExtension(id, [ + 'visibility', + 'org_id', + 'visible_to_organizations', + ]) // Throw error if extension not found if (Object.keys(requstedMentorExtension).length === 0) { @@ -479,14 +507,11 @@ module.exports = class MentorsHelper { }) } - requstedMentorExtension = await usersService.filterMentorListBasedOnSaasPolicy( - [requstedMentorExtension], - userId, - isAMentor - ) + // Check for accessibility for reading shared mentor profile + const isAccessible = await this.checkIfMentorIsAccessible([requstedMentorExtension], userId, isAMentor) // Throw access error - if (requstedMentorExtension.length === 0) { + if (!isAccessible) { return common.failureResponse({ statusCode: httpStatusCode.not_found, message: 'PROFILE_RESTRICTED', @@ -555,4 +580,290 @@ module.exports = class MentorsHelper { return error } } + + /** + * @description - check if mentor is accessible based on user's saas policy. + * @method + * @name checkIfMentorIsAccessible + * @param {Number} userId - User id. + * @param {Array} - Session data + * @param {Boolean} isAMentor - user mentor or not. + * @returns {JSON} - List of filtered sessions + */ + static async checkIfMentorIsAccessible(userData, userId, isAMentor) { + try { + const userPolicyDetails = isAMentor + ? await mentorQueries.getMentorExtension(userId, [ + 'external_mentor_visibility', + 'org_id', + 'visible_to_organizations', + ]) + : await menteeQueries.getMenteeExtension(userId, [ + 'external_mentor_visibility', + 'org_id', + 'visible_to_organizations', + ]) + + // Throw error if mentor/mentee extension not found + if (Object.keys(userPolicyDetails).length === 0) { + return common.failureResponse({ + statusCode: httpStatusCode.not_found, + message: isAMentor ? 'MENTORS_NOT_FOUND' : 'MENTEE_EXTENSION_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + + // check the accessibility conditions + let isAccessible = false + if (userPolicyDetails.external_mentor_visibility && userPolicyDetails.org_id) { + const { external_mentor_visibility, org_id, visible_to_organizations } = userPolicyDetails + const mentor = userData[0] + + switch (external_mentor_visibility) { + case common.CURRENT: + isAccessible = mentor.org_id === org_id + break + case common.ASSOCIATED: + isAccessible = mentor.visible_to_organizations.some((element) => + visible_to_organizations.includes(element) + ) + break + case common.ALL: + isAccessible = + mentor.visible_to_organizations.some((element) => + visible_to_organizations.includes(element) + ) || mentor.visibility === common.ALL + break + default: + break + } + } + return isAccessible + } catch (err) { + return err + } + } + /** + * Get user list. + * @method + * @name create + * @param {Number} pageSize - Page size. + * @param {Number} pageNo - Page number. + * @param {String} searchText - Search text. + * @param {JSON} queryParams - Query params. + * @param {Boolean} isAMentor - Is a mentor. + * @returns {JSON} - User list. + */ + + static async list(pageNo, pageSize, searchText, queryParams, userId, isAMentor) { + try { + const query = utils.processQueryParametersWithExclusions(queryParams) + let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ + status: 'ACTIVE', + }) + const filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'sessions') + + const userType = common.MENTOR_ROLE + const userDetails = await userRequests.listWithoutLimit(userType, searchText) + + if (userDetails.data.result.data.length == 0) { + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: userDetails.data.message, + result: { + data: [], + count: 0, + }, + }) + } + const ids = userDetails.data.result.data.map((item) => item.values[0].id) + + // Filter user data based on SAAS policy + const saasFilter = await this.filterMentorListBasedOnSaasPolicy(userId, isAMentor) + + let extensionDetails = await mentorQueries.getMentorsByUserIdsFromView( + ids, + pageNo, + pageSize, + filteredQuery, + saasFilter + ) + + const extensionDataMap = new Map(extensionDetails.data.map((newItem) => [newItem.user_id, newItem])) + + userDetails.data.result.data = userDetails.data.result.data.filter((existingItem) => { + const user_id = existingItem.values[0].id + if (extensionDataMap.has(user_id)) { + const newItem = extensionDataMap.get(user_id) + existingItem.values[0] = { ...existingItem.values[0], ...newItem } + delete existingItem.values[0].user_id + delete existingItem.values[0].visibility + delete existingItem.values[0].org_id + return true // Keep this item + } + + return false // Remove this item + }) + + userDetails.data.result.count = extensionDetails.count + + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: userDetails.data.message, + result: userDetails.data.result, + }) + } catch (error) { + throw error + } + } + + /** + * @description - Filter mentor list based on user's saas policy. + * @method + * @name filterMentorListBasedOnSaasPolicy + * @param {Number} userId - User id. + * @param {Boolean} isAMentor - user mentor or not. + * @returns {JSON} - List of filtered sessions + */ + static async filterMentorListBasedOnSaasPolicy(userId, isAMentor) { + try { + const userPolicyDetails = isAMentor + ? await mentorQueries.getMentorExtension(userId, [ + 'external_mentor_visibility', + 'org_id', + 'visible_to_organizations', + ]) + : await menteeQueries.getMenteeExtension(userId, [ + 'external_mentor_visibility', + 'org_id', + 'visible_to_organizations', + ]) + + // Throw error if mentor/mentee extension not found + if (Object.keys(userPolicyDetails).length === 0) { + return common.failureResponse({ + statusCode: httpStatusCode.not_found, + message: isAMentor ? 'MENTORS_NOT_FOUND' : 'MENTEE_EXTENSION_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) + } + const filter = {} + if (userPolicyDetails.external_mentor_visibility && userPolicyDetails.org_id) { + // Filter user data based on policy + // generate filter based on condition + if (userPolicyDetails.external_mentor_visibility === common.CURRENT) { + filter.org_id = userPolicyDetails.org_id + } else if (userPolicyDetails.external_mentor_visibility === common.ASSOCIATED) { + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + } else if (userPolicyDetails.external_mentor_visibility === common.ALL) { + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + filter.visibility = common.ALL + } + } + return filter + } catch (err) { + return err + } + } + + /** + * Sessions list + * @method + * @name list + * @param {Object} req -request data. + * @param {String} req.decodedToken.id - User Id. + * @param {String} req.pageNo - Page No. + * @param {String} req.pageSize - Page size limit. + * @param {String} req.searchText - Search text. + * @returns {JSON} - Session List. + */ + + static async createdSessions(loggedInUserId, page, limit, search, status, roles) { + try { + if (!utils.isAMentor(roles)) { + return common.failureResponse({ + statusCode: httpStatusCode.bad_request, + message: 'NOT_A_MENTOR', + responseCode: 'CLIENT_ERROR', + }) + } + // update sessions which having status as published/live and exceeds the current date and time + const currentDate = Math.floor(moment.utc().valueOf() / 1000) + const filterQuery = { + [Op.or]: [ + { + status: common.PUBLISHED_STATUS, + end_date: { + [Op.lt]: currentDate, + }, + }, + { + status: common.LIVE_STATUS, + 'meeting_info.value': { + [Op.ne]: common.BBB_VALUE, + }, + end_date: { + [Op.lt]: currentDate, + }, + }, + ], + } + + await sessionQueries.updateSession(filterQuery, { + status: common.COMPLETED_STATUS, + }) + + let arrayOfStatus = [] + if (status && status != '') { + arrayOfStatus = status.split(',') + } + + let filters = { + mentor_id: loggedInUserId, + } + if (arrayOfStatus.length > 0) { + // if (arrayOfStatus.includes(common.COMPLETED_STATUS) && arrayOfStatus.length == 1) { + // filters['endDateUtc'] = { + // $lt: moment().utc().format(), + // } + // } else + if (arrayOfStatus.includes(common.PUBLISHED_STATUS) && arrayOfStatus.includes(common.LIVE_STATUS)) { + filters['end_date'] = { + [Op.gte]: currentDate, + } + } + + filters['status'] = arrayOfStatus + } + + const sessionDetails = await sessionQueries.findAllSessions(page, limit, search, filters) + + if (sessionDetails.count == 0 || sessionDetails.rows.length == 0) { + return common.successResponse({ + message: 'SESSION_FETCHED_SUCCESSFULLY', + statusCode: httpStatusCode.ok, + result: [], + }) + } + + sessionDetails.rows = await this.sessionMentorDetails(sessionDetails.rows) + + //remove meeting_info details except value and platform + sessionDetails.rows.forEach((item) => { + if (item.meeting_info) { + item.meeting_info = { + value: item.meeting_info.value, + platform: item.meeting_info.platform, + } + } + }) + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'SESSION_FETCHED_SUCCESSFULLY', + result: { count: sessionDetails.count, data: sessionDetails.rows }, + }) + } catch (error) { + throw error + } + } } diff --git a/src/services/notifications.js b/src/services/notifications.js index 8ad93c67f..344e93565 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -15,7 +15,7 @@ module.exports = class Notifications { * @returns */ - static async sendNotification(notificationJobId, notificataionTemplate) { + static async sendNotification(notificationJobId, notificataionTemplate, jobCreatorOrgId = '') { try { // Data contains notificationJobId and notificationTemplate. // Extract sessionId from incoming notificationJobId. @@ -33,7 +33,7 @@ module.exports = class Notifications { }) // Get email template based on incoming request. - let emailTemplate = await notificationQueries.findOneEmailTemplate(notificataionTemplate) + let emailTemplate = await notificationQueries.findOneEmailTemplate(notificataionTemplate, jobCreatorOrgId) if (emailTemplate && sessions) { // if notificataionTemplate is {MENTEE_SESSION_REMAINDER_EMAIL_CODE} then notification to all personal registered for the session has to be send. diff --git a/src/services/org-admin.js b/src/services/org-admin.js index ad33a77bc..e9805dbe3 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -98,7 +98,10 @@ module.exports = class OrgAdminService { // Delete upcoming sessions of user as mentor const removedSessionsDetail = await sessionQueries.removeAndReturnMentorSessions(bodyData.user_id) - const isAttendeesNotified = await adminService.unenrollAndNotifySessionAttendees(removedSessionsDetail) + const isAttendeesNotified = await adminService.unenrollAndNotifySessionAttendees( + removedSessionsDetail, + mentorDetails.org_id ? mentorDetails.org_id : '' + ) // Delete mentor Extension if (isAttendeesNotified) { diff --git a/src/services/sessions.js b/src/services/sessions.js index a52029c69..cd3fd2736 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -4,7 +4,7 @@ const moment = require('moment-timezone') const httpStatusCode = require('@generics/http-status') const apiEndpoints = require('@constants/endpoints') const common = require('@constants/common') -const sessionData = require('@db/sessions/queries') +//const sessionData = require('@db/sessions/queries') const notificationTemplateData = require('@db/notification-template/query') const kafkaCommunication = require('@generics/kafka-communication') const apiBaseUrl = process.env.USER_SERVICE_HOST + process.env.USER_SERVICE_BASE_URL @@ -12,12 +12,14 @@ const request = require('request') const sessionQueries = require('@database/queries/sessions') const sessionAttendeesQueries = require('@database/queries/sessionAttendees') const mentorExtensionQueries = require('@database/queries/mentorExtension') +const menteeExtensionQueries = require('@database/queries/userExtension') const sessionEnrollmentQueries = require('@database/queries/sessionEnrollments') const postSessionQueries = require('@database/queries/postSessionDetail') const sessionOwnershipQueries = require('@database/queries/sessionOwnership') const entityTypeQueries = require('@database/queries/entityType') const entitiesQueries = require('@database/queries/entity') const { Op } = require('sequelize') +const notificationQueries = require('@database/queries/notificationTemplate') const schedulerRequest = require('@requests/scheduler') @@ -31,6 +33,7 @@ const { getDefaultOrgId } = require('@helpers/getDefaultOrgId') const { removeDefaultOrgEntityTypes } = require('@generics/utils') const menteesService = require('@services/mentees') +const menteeService = require('@services/mentees') module.exports = class SessionsHelper { /** * Create session. @@ -99,7 +102,7 @@ module.exports = class SessionsHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(bodyData, validationData, 'sessions') + let res = utils.validateInput(bodyData, validationData, await sessionQueries.getModelName()) if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -139,6 +142,9 @@ module.exports = class SessionsHelper { let organisationPolicy = await organisationExtensionQueries.findOrInsertOrganizationExtension(orgId) bodyData.visibility = organisationPolicy.session_visibility_policy bodyData.visible_to_organizations = userOrgDetails.data.result.related_orgs + ? userOrgDetails.data.result.related_orgs.concat([orgId]) + : [orgId] + const data = await sessionQueries.create(bodyData) await sessionOwnershipQueries.create({ @@ -163,12 +169,18 @@ module.exports = class SessionsHelper { for (let jobIndex = 0; jobIndex < jobsToCreate.length; jobIndex++) { // Append the session ID to the job ID jobsToCreate[jobIndex].jobId = jobsToCreate[jobIndex].jobId + data.id + + const reqBody = { + job_id: jobsToCreate[jobIndex].jobId, + email_template_code: jobsToCreate[jobIndex].emailTemplate, + job_creator_org_id: orgId, + } // Create the scheduler job with the calculated delay and other parameters await schedulerRequest.createSchedulerJob( jobsToCreate[jobIndex].jobId, jobsToCreate[jobIndex].delay, jobsToCreate[jobIndex].jobName, - jobsToCreate[jobIndex].emailTemplate + reqBody ) } @@ -260,7 +272,8 @@ module.exports = class SessionsHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(bodyData, validationData, 'sessions') + let res = utils.validateInput(bodyData, validationData, await sessionQueries.getModelName()) + if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -381,12 +394,14 @@ module.exports = class SessionsHelper { /* Find email template according to request type */ let templateData if (method == common.DELETE_METHOD) { - templateData = await notificationTemplateData.findOneEmailTemplate( - process.env.MENTOR_SESSION_DELETE_EMAIL_TEMPLATE + templateData = await notificationQueries.findOneEmailTemplate( + process.env.MENTOR_SESSION_DELETE_EMAIL_TEMPLATE, + orgId ) } else if (isSessionReschedule) { - templateData = await notificationTemplateData.findOneEmailTemplate( - process.env.MENTOR_SESSION_RESCHEDULE_EMAIL_TEMPLATE + templateData = await notificationQueries.findOneEmailTemplate( + process.env.MENTOR_SESSION_RESCHEDULE_EMAIL_TEMPLATE, + orgId ) console.log('Session rescheduled email code:', process.env.MENTOR_SESSION_RESCHEDULE_EMAIL_TEMPLATE) @@ -530,14 +545,10 @@ module.exports = class SessionsHelper { // check for accessibility if (userId !== '' && isAMentor !== '') { - let sessionPolicyCheck = await menteesService.filterSessionsBasedOnSaasPolicy( - [sessionDetails], - userId, - isAMentor - ) + let isAccessible = await this.checkIfSessionIsAccessible([sessionDetails], userId, isAMentor) // Throw access error - if (sessionPolicyCheck.length === 0) { + if (!isAccessible) { return common.failureResponse({ statusCode: httpStatusCode.not_found, message: 'SESSION_RESTRICTED', @@ -595,93 +606,106 @@ module.exports = class SessionsHelper { } /** - * Session list. + * @description - check if session is accessible based on user's saas policy. * @method - * @name list - * @param {String} loggedInUserId - LoggedIn user id. - * @param {Number} page - page no. - * @param {Number} limit - page size. - * @param {String} search - search text. - * @returns {JSON} - List of sessions + * @name checkIfSessionIsAccessible + * @param {Number} userId - User id. + * @param {Array} - Session data + * @param {Boolean} isAMentor - user mentor or not. + * @returns {JSON} - List of filtered sessions */ - - static async list(loggedInUserId, page, limit, search, status) { + static async checkIfSessionIsAccessible(sessions, userId, isAMentor) { try { - // update sessions which having status as published/live and exceeds the current date and time - const currentDate = Math.floor(moment.utc().valueOf() / 1000) - const filterQuery = { - [Op.or]: [ - { - status: common.PUBLISHED_STATUS, - end_date: { - [Op.lt]: currentDate, - }, - }, - { - status: common.LIVE_STATUS, - 'meeting_info.value': { - [Op.ne]: common.BBB_VALUE, - }, - end_date: { - [Op.lt]: currentDate, - }, - }, - ], - } - - await sessionQueries.updateSession(filterQuery, { - status: common.COMPLETED_STATUS, - }) - - let arrayOfStatus = [] - if (status && status != '') { - arrayOfStatus = status.split(',') + const userPolicyDetails = isAMentor + ? await mentorExtensionQueries.getMentorExtension(userId, [ + 'external_session_visibility', + 'org_id', + 'visible_to_organizations', + ]) + : await menteeExtensionQueries.getMenteeExtension(userId, [ + 'external_session_visibility', + 'org_id', + 'visible_to_organizations', + ]) + + // Throw error if mentor/mentee extension not found + if (Object.keys(userPolicyDetails).length === 0) { + return common.failureResponse({ + statusCode: httpStatusCode.not_found, + message: isAMentor ? 'MENTORS_NOT_FOUND' : 'MENTEE_EXTENSION_NOT_FOUND', + responseCode: 'CLIENT_ERROR', + }) } - let filters = { - mentor_id: loggedInUserId, - } - if (arrayOfStatus.length > 0) { - // if (arrayOfStatus.includes(common.COMPLETED_STATUS) && arrayOfStatus.length == 1) { - // filters['endDateUtc'] = { - // $lt: moment().utc().format(), - // } - // } else - if (arrayOfStatus.includes(common.PUBLISHED_STATUS) && arrayOfStatus.includes(common.LIVE_STATUS)) { - filters['end_date'] = { - [Op.gte]: currentDate, - } + // check the accessibility conditions + let isAccessible = false + if (userPolicyDetails.external_session_visibility && userPolicyDetails.org_id) { + const { external_session_visibility, org_id, visible_to_organizations } = userPolicyDetails + const session = sessions[0] + const isEnrolled = session.is_enrolled || false + + switch (external_session_visibility) { + case common.CURRENT: + isAccessible = isEnrolled || session.mentor_org_id === org_id + break + case common.ASSOCIATED: + isAccessible = + isEnrolled || + session.visible_to_organizations.some((element) => + visible_to_organizations.includes(element) + ) + break + case common.ALL: + isAccessible = + isEnrolled || + session.visible_to_organizations.some((element) => + visible_to_organizations.includes(element) + ) || + session.visibility === common.ALL + break + default: + break } - - filters['status'] = arrayOfStatus } + return isAccessible + } catch (err) { + return err + } + } - const sessionDetails = await sessionQueries.findAllSessions(page, limit, search, filters) + /** + * Sessions list + * @method + * @name list + * @param {Object} req -request data. + * @param {String} req.decodedToken.id - User Id. + * @param {String} req.pageNo - Page No. + * @param {String} req.pageSize - Page size limit. + * @param {String} req.searchText - Search text. + * @param {Boolean} isAMentor - Is a mentor. + * @returns {JSON} - Session List. + */ - /* if (sessionDetails.count == 0 || sessionDetails.rows.length == 0) { - return common.failureResponse({ - message: 'SESSION_NOT_FOUND', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - result: [], - }) - } */ + static async list(loggedInUserId, page, limit, search, queryParams, isAMentor) { + try { + let allSessions = await menteeService.getAllSessions( + page, + limit, + search, + loggedInUserId, + queryParams, + isAMentor + ) - sessionDetails.rows = await sessionMentor.sessionMentorDetails(sessionDetails.rows) + const result = { + data: allSessions.rows, + count: allSessions.count, + } - //remove meeting_info details except value and platform - sessionDetails.rows.forEach((item) => { - if (item.meeting_info) { - item.meeting_info = { - value: item.meeting_info.value, - platform: item.meeting_info.platform, - } - } - }) return common.successResponse({ statusCode: httpStatusCode.ok, message: 'SESSION_FETCHED_SUCCESSFULLY', - result: { count: sessionDetails.count, data: sessionDetails.rows }, + result, }) } catch (error) { console.log @@ -750,8 +774,9 @@ module.exports = class SessionsHelper { await sessionAttendeesQueries.create(attendee) await sessionEnrollmentQueries.create(_.omit(attendee, 'time_zone')) - const templateData = await notificationTemplateData.findOneEmailTemplate( - process.env.MENTEE_SESSION_ENROLLMENT_EMAIL_TEMPLATE + const templateData = await notificationQueries.findOneEmailTemplate( + process.env.MENTEE_SESSION_ENROLLMENT_EMAIL_TEMPLATE, + session.mentor_org_id ) if (templateData) { @@ -825,8 +850,9 @@ module.exports = class SessionsHelper { await sessionEnrollmentQueries.unEnrollFromSession(sessionId, userId) - const templateData = await notificationTemplateData.findOneEmailTemplate( - process.env.MENTEE_SESSION_CANCELLATION_EMAIL_TEMPLATE + const templateData = await notificationQueries.findOneEmailTemplate( + process.env.MENTEE_SESSION_CANCELLATION_EMAIL_TEMPLATE, + session.mentor_org_id ) if (templateData) { diff --git a/src/services/users.js b/src/services/users.js index 754c51b18..f1f39f71b 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -14,11 +14,10 @@ module.exports = class UserHelper { * @param {Number} pageNo - Page number. * @param {String} searchText - Search text. * @param {Number} searchText - userId. - * @param {Boolean} isAMentor - user mentor or not. * @returns {JSON} - User list. */ - static async list(userType, pageNo, pageSize, searchText, userId, isAMentor) { + static async list(userType, pageNo, pageSize, searchText) { try { const userDetails = await userRequests.list(userType, pageNo, pageSize, searchText) const ids = userDetails.data.result.data.map((item) => item.values[0].id) @@ -34,9 +33,6 @@ module.exports = class UserHelper { }) // Inside your function extensionDetails = extensionDetails.filter((item) => item.visibility && item.org_id) - - // Filter user data based on SAAS policy - extensionDetails = await this.filterMentorListBasedOnSaasPolicy(extensionDetails, userId, isAMentor) } const extensionDataMap = new Map(extensionDetails.map((newItem) => [newItem.user_id, newItem])) @@ -63,77 +59,4 @@ module.exports = class UserHelper { throw error } } - - /** - * @description - Filter mentor list based on user's saas policy. - * @method - * @name filterMentorListBasedOnSaasPolicy - * @param {Array} userData - User data. - * @param {Number} userId - User id. - * @param {Boolean} isAMentor - user mentor or not. - * @returns {JSON} - List of filtered sessions - */ - static async filterMentorListBasedOnSaasPolicy(userData, userId, isAMentor) { - try { - if (userData.length === 0) { - return userData - } - - let userPolicyDetails - // If user is mentor - fetch policy details from mentor extensions else fetch from userExtension - if (isAMentor) { - userPolicyDetails = await mentorQueries.getMentorExtension(userId, [ - 'external_mentor_visibility', - 'org_id', - ]) - - // Throw error if mentor extension not found - if (Object.keys(userPolicyDetails).length === 0) { - return common.failureResponse({ - statusCode: httpStatusCode.bad_request, - message: 'MENTORS_NOT_FOUND', - responseCode: 'CLIENT_ERROR', - }) - } - } else { - userPolicyDetails = await menteeQueries.getMenteeExtension(userId, [ - 'external_mentor_visibility', - 'org_id', - ]) - // If no mentee present return error - if (Object.keys(userPolicyDetails).length === 0) { - return common.failureResponse({ - statusCode: httpStatusCode.not_found, - message: 'MENTEE_EXTENSION_NOT_FOUND', - responseCode: 'CLIENT_ERROR', - }) - } - } - - if (userPolicyDetails.external_mentor_visibility && userPolicyDetails.org_id) { - // Filter user data based on policy - const filteredUserData = await Promise.all( - userData.map(async (user) => { - if ( - user.visibility === common.CURRENT || - (user.visibility === common.ALL && - userPolicyDetails.external_mentor_visibility === common.CURRENT) - ) { - // Check if the mentor's organization matches the user's organization(who is calling the api). - if (user.org_id === userPolicyDetails.org_id) { - return user - } - } else { - return user - } - }) - ) - // Remove any undefined elements (user that didn't meet the conditions) - userData = filteredUserData.filter((user) => user !== undefined) - } - return userData - } catch (err) { - return err - } - } } diff --git a/src/validators/v1/entity-type.js b/src/validators/v1/entity-type.js index f5a22d22a..05dbdc99b 100644 --- a/src/validators/v1/entity-type.js +++ b/src/validators/v1/entity-type.js @@ -11,7 +11,7 @@ module.exports = { .trim() .notEmpty() .withMessage('value field is empty') - .matches(/^[A-Za-z]+$/) + .matches(/^[A-Za-z_]+$/) .withMessage('value is invalid, must not contain spaces') req.checkBody('label') diff --git a/src/validators/v1/mentees.js b/src/validators/v1/mentees.js index c1d1d4a0e..148184038 100644 --- a/src/validators/v1/mentees.js +++ b/src/validators/v1/mentees.js @@ -6,14 +6,7 @@ */ module.exports = { - sessions: (req) => { - req.checkQuery('enrolled') - .notEmpty() - .withMessage('enrolled query is empty') - .isBoolean() - .withMessage('enrolled is invalid') - .toBoolean() - }, + sessions: (req) => {}, homeFeed: (req) => {}, reports: (req) => { diff --git a/src/validators/v1/notifications.js b/src/validators/v1/notifications.js index 447e0cbc0..34b91655b 100644 --- a/src/validators/v1/notifications.js +++ b/src/validators/v1/notifications.js @@ -8,7 +8,7 @@ module.exports = { emailCronJob: (req) => { // Validate incoming request body - req.checkBody('jobId').notEmpty().withMessage('jobId field is empty') - req.checkBody('emailTemplateCode').notEmpty().withMessage('emailTemplateCode field is empty') + req.checkBody('job_id').notEmpty().withMessage('job_id field is empty') + req.checkBody('email_template_code').notEmpty().withMessage('email_template_code field is empty') }, }