From 4016fe9e55db184077c6b4a580ddf30d4a6a35b0 Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Fri, 10 Nov 2023 16:29:58 +0530 Subject: [PATCH 01/30] notification template selection related changes --- src/controllers/v1/notifications.js | 6 ++- src/database/queries/notificationTemplate.js | 41 ++++++++++++++++---- src/requests/scheduler.js | 15 ++++--- src/services/admin.js | 10 +++-- src/services/issues.js | 8 ++-- src/services/notifications.js | 4 +- src/services/org-admin.js | 5 ++- src/services/sessions.js | 25 +++++++----- 8 files changed, 79 insertions(+), 35 deletions(-) diff --git a/src/controllers/v1/notifications.js b/src/controllers/v1/notifications.js index 3d2c0577c..c03fd668e 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.jobId, + req.body.emailTemplateCode, + req.body.jobCreatorOrgId ? parseInt(req.body.jobCreatorOrgId, 10) : '' + ) return { statusCode: httpStatusCode.ok, } 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/requests/scheduler.js b/src/requests/scheduler.js index bf4bf4454..fddd30105 100644 --- a/src/requests/scheduler.js +++ b/src/requests/scheduler.js @@ -15,13 +15,14 @@ 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 {string} notificationTemplate - The template for the notification. + * @param {number} orgId - OrganisationId of job creator. + * @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, notificationTemplate, orgId) { const bodyData = { jobName: jobName, email: email, @@ -30,6 +31,7 @@ const createSchedulerJob = function (jobId, delay, jobName, notificationTemplate method: 'post', header: { internal_access_token: process.env.INTERNAL_ACCESS_TOKEN }, }, + jobOptions: { jobId: jobId, delay: delay, @@ -37,6 +39,7 @@ const createSchedulerJob = function (jobId, delay, jobName, notificationTemplate removeOnComplete: true, removeOnFail: false, attempts: 1, + creatorOrgId: orgId ? orgId : '', }, } diff --git a/src/services/admin.js b/src/services/admin.js index 589b95312..96ac5c47b 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -40,7 +40,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 +69,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) { 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/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 3d68006aa..ceb9f4aff 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -76,7 +76,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..b38c1f986 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -5,7 +5,6 @@ const httpStatusCode = require('@generics/http-status') const apiEndpoints = require('@constants/endpoints') const common = require('@constants/common') 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 const request = require('request') @@ -18,6 +17,7 @@ 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') @@ -168,7 +168,8 @@ module.exports = class SessionsHelper { jobsToCreate[jobIndex].jobId, jobsToCreate[jobIndex].delay, jobsToCreate[jobIndex].jobName, - jobsToCreate[jobIndex].emailTemplate + jobsToCreate[jobIndex].emailTemplate, + orgId ) } @@ -381,12 +382,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) @@ -750,8 +753,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 +829,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) { From 2a49d4f8d9d82389102b4cf9cf45214f810c03be Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Fri, 10 Nov 2023 17:55:12 +0530 Subject: [PATCH 02/30] init checkpoint --- docker-compose-mentoring.yml | 8 +- src/constants/common.js | 2 + src/controllers/v1/admin.js | 19 + src/controllers/v1/mentees.js | 1 - src/controllers/v1/mentors.js | 47 +- src/controllers/v1/sessions.js | 4 +- src/database/models/m_mentor_extensions.js | 74 ++ src/database/models/m_sessions.js | 113 +++ src/database/models/m_user_extensions.js | 74 ++ src/database/queries/entityType.js | 35 +- src/database/queries/mentorExtension.js | 67 +- src/database/queries/sessions.js | 158 ++- ...822124704-add_entity_types_and_entities.js | 6 +- .../seeders/20231103090632-seed-forms.js | 906 ++++++++++++++++++ src/generics/materializedViews.js | 450 +++++++++ src/generics/utils.js | 59 +- src/nodemon.json | 3 + src/package.json | 1 + src/requests/user.js | 25 + src/services/admin.js | 28 + src/services/mentees.js | 29 +- src/services/mentors.js | 167 +++- src/services/sessions.js | 106 +- src/services/users.js | 4 +- 24 files changed, 2265 insertions(+), 121 deletions(-) create mode 100644 src/database/models/m_mentor_extensions.js create mode 100644 src/database/models/m_sessions.js create mode 100644 src/database/models/m_user_extensions.js create mode 100644 src/database/seeders/20231103090632-seed-forms.js create mode 100644 src/generics/materializedViews.js create mode 100644 src/nodemon.json diff --git a/docker-compose-mentoring.yml b/docker-compose-mentoring.yml index 59dff5719..7a133ecb0 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: @@ -195,6 +195,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/constants/common.js b/src/constants/common.js index 94a4a400c..e5bb24c53 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -111,4 +111,6 @@ module.exports = { 'mentoring_session_fifteen_min_', ], ORG_ADMIN_ROLE: 'org_admin', + excludedQueryParams: ['enrolled'], + materializedViewsPrefix: 'm_', } diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index c889ef101..5607acd55 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -26,4 +26,23 @@ module.exports = class admin { return error } } + + async triggerViewRebuild(req) { + try { + const userDelete = await userService.triggerViewRebuild(req.decodedToken, req.query.userId) + return userDelete + } catch (error) { + return error + } + } + async triggerPeriodicViewRefresh(req, res) { + try { + const userDelete = await userService.triggerPeriodicViewRefresh(req.decodedToken, req.query.userId) + return userDelete + await adminService.triggerPeriodicViewRefresh() + res.send({ message: 'Triggered' }) + } catch (err) { + console.log(err) + } + } } diff --git a/src/controllers/v1/mentees.js b/src/controllers/v1/mentees.js index d077ee900..496264c13 100644 --- a/src/controllers/v1/mentees.js +++ b/src/controllers/v1/mentees.js @@ -42,7 +42,6 @@ module.exports = class Mentees { try { const sessions = await menteesService.sessions( req.decodedToken.id, - req.query.enrolled, req.pageNo, req.pageSize, req.searchText diff --git a/src/controllers/v1/mentors.js b/src/controllers/v1/mentors.js index 4ebadc221..cc6ed773a 100644 --- a/src/controllers/v1/mentors.js +++ b/src/controllers/v1/mentors.js @@ -27,7 +27,8 @@ 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 ) } catch (error) { return error @@ -42,7 +43,7 @@ module.exports = class Mentors { * @param {String} req.params.id - mentor Id. * @returns {JSON} - mentors profile details */ - async profile(req) { + async details(req) { try { return await mentorsService.read(req.params.id) } catch (error) { @@ -93,6 +94,48 @@ module.exports = class Mentors { } } + /** + * List of available mentors. + * @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 list(req) { + try { + return await mentorsService.list(req.pageNo, req.pageSize, req.searchText, req.query) + } 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/sessions.js b/src/controllers/v1/sessions.js index eb5f4ccf2..0eec64a83 100644 --- a/src/controllers/v1/sessions.js +++ b/src/controllers/v1/sessions.js @@ -77,7 +77,7 @@ module.exports = class Sessions { } /** - * Sessions list + * Get all upcoming sessions by available mentors * @method * @name list * @param {Object} req -request data. @@ -95,7 +95,7 @@ module.exports = class Sessions { req.pageNo, req.pageSize, req.searchText, - req.query.status + req.query ) return sessionDetails } catch (error) { diff --git a/src/database/models/m_mentor_extensions.js b/src/database/models/m_mentor_extensions.js new file mode 100644 index 000000000..b8cd10234 --- /dev/null +++ b/src/database/models/m_mentor_extensions.js @@ -0,0 +1,74 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const m_mentor_extensions = sequelize.define( + 'm_mentor_extensions', + { + user_id: { + type: DataTypes.INTEGER, + primaryKey: true, + }, + designation: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + area_of_expertise: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + education_qualification: { + type: DataTypes.STRING, + }, + rating: { + type: DataTypes.JSON, + }, + meta: { + type: DataTypes.JSONB, + }, + stats: { + type: DataTypes.JSONB, + }, + tags: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + configs: { + type: DataTypes.JSON, + }, + visibility: { + type: DataTypes.STRING, + }, + organisation_ids: { + type: DataTypes.STRING, + }, + external_session_visibility: { + type: DataTypes.STRING, + }, + external_mentor_visibility: { + type: DataTypes.STRING, + }, + custom_entity_text: { + type: DataTypes.JSON, + }, + experience: { + type: DataTypes.STRING, + }, + created_at: { + type: DataTypes.DATE, + }, + updated_at: { + type: DataTypes.DATE, + }, + deleted_at: { + type: DataTypes.DATE, + }, + location: { + type: DataTypes.STRING, + }, + }, + { + sequelize, + modelName: 'm_mentor_extensions', + tableName: 'm_mentor_extensions', + freezeTableName: true, + paranoid: true, + } + ) + return m_mentor_extensions +} diff --git a/src/database/models/m_sessions.js b/src/database/models/m_sessions.js new file mode 100644 index 000000000..020354d93 --- /dev/null +++ b/src/database/models/m_sessions.js @@ -0,0 +1,113 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const m_sessions = sequelize.define( + 'm_sessions', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + }, + description: { + type: DataTypes.STRING, + }, + recommended_for: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + categories: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + medium: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + image: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + mentor_id: { + type: DataTypes.INTEGER, + }, + session_reschedule: { + type: DataTypes.INTEGER, + }, + status: { + type: DataTypes.STRING, + }, + time_zone: { + type: DataTypes.STRING, + }, + start_date: { + type: DataTypes.STRING, + }, + end_date: { + type: DataTypes.STRING, + }, + mentee_password: { + type: DataTypes.STRING, + }, + mentor_password: { + type: DataTypes.STRING, + }, + started_at: { + type: DataTypes.DATE, + }, + share_link: { + type: DataTypes.STRING, + }, + completed_at: { + type: DataTypes.DATE, + }, + is_feedback_skipped: { + type: DataTypes.BOOLEAN, + }, + mentee_feedback_question_set: { + type: DataTypes.STRING, + }, + mentor_feedback_question_set: { + type: DataTypes.STRING, + }, + meeting_info: { + type: DataTypes.JSONB, + }, + meta: { + type: DataTypes.JSONB, + }, + visibility: { + type: DataTypes.STRING, + }, + organization_ids: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + mentor_org_id: { + type: DataTypes.INTEGER, + }, + seats_remaining: { + type: DataTypes.INTEGER, + }, + seats_limit: { + type: DataTypes.INTEGER, + }, + custom_entity_text: { + type: DataTypes.JSON, + }, + created_at: { + type: DataTypes.DATE, + }, + updated_at: { + type: DataTypes.DATE, + }, + deleted_at: { + type: DataTypes.DATE, + }, + }, + { + sequelize, + modelName: 'm_sessions', + tableName: 'm_sessions', + freezeTableName: true, + paranoid: true, + } + ) + return m_sessions +} diff --git a/src/database/models/m_user_extensions.js b/src/database/models/m_user_extensions.js new file mode 100644 index 000000000..1286f0768 --- /dev/null +++ b/src/database/models/m_user_extensions.js @@ -0,0 +1,74 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + const m_user_extensions = sequelize.define( + 'm_user_extensions', + { + user_id: { + type: DataTypes.INTEGER, + primaryKey: true, + }, + designation: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + area_of_expertise: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + education_qualification: { + type: DataTypes.STRING, + }, + rating: { + type: DataTypes.JSON, + }, + meta: { + type: DataTypes.JSONB, + }, + stats: { + type: DataTypes.JSONB, + }, + tags: { + type: DataTypes.ARRAY(DataTypes.STRING), + }, + configs: { + type: DataTypes.JSON, + }, + visibility: { + type: DataTypes.STRING, + }, + organisation_ids: { + type: DataTypes.STRING, + }, + external_session_visibility: { + type: DataTypes.STRING, + }, + external_mentor_visibility: { + type: DataTypes.STRING, + }, + custom_entity_text: { + type: DataTypes.JSON, + }, + experience: { + type: DataTypes.STRING, + }, + created_at: { + type: DataTypes.DATE, + }, + updated_at: { + type: DataTypes.DATE, + }, + deleted_at: { + type: DataTypes.DATE, + }, + location: { + type: DataTypes.STRING, + }, + }, + { + sequelize, + modelName: 'm_user_extensions', + tableName: 'm_user_extensions', + freezeTableName: true, + paranoid: true, + } + ) + return m_user_extensions +} diff --git a/src/database/queries/entityType.js b/src/database/queries/entityType.js index 2add675ad..4ef15c396 100644 --- a/src/database/queries/entityType.js +++ b/src/database/queries/entityType.js @@ -13,7 +13,7 @@ module.exports = class UserEntityData { static async findOneEntityType(filter, options = {}) { try { - return await EntityType.findOne({ + return await EntityType.findOne({ where: filter, ...options, raw: true, @@ -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, @@ -115,4 +116,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 e1ece788c..a007bf119 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -1,5 +1,9 @@ const MentorExtension = require('@database/models/index').MentorExtension // Adjust the path accordingly +const sequelize = require('sequelize') +const Sequelize = require('@database/models/index').sequelize +const common = require('@constants/common') + module.exports = class MentorExtensionQueries { static async getColumns() { try { @@ -90,7 +94,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, @@ -102,4 +106,65 @@ 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) { + try { + const filterConditions = [] + + 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 ')}` : '' + + const sessionAttendeesData = await Sequelize.query( + ` + SELECT + user_id, + rating + FROM + ${common.materializedViewsPrefix + MentorExtension.tableName} + WHERE + user_id IN (${ids.join(',')}) + ${filterClause} + OFFSET + :offset + LIMIT + :limit; + `, + { + replacements: { + offset: limit * (page - 1), + limit: limit, + ...filter, // Add filter parameters to replacements + }, + type: sequelize.QueryTypes.SELECT, + } + ) + + return { + data: sessionAttendeesData, + count: sessionAttendeesData.length, + } + } catch (error) { + return error + } + } } diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index a72fec31c..18021143d 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -1,10 +1,17 @@ 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 +const knex = require('knex')({ + client: 'pg', + connection: process.env.DEV_DATABASE_URL, + debug: true, +}) + exports.getColumns = async () => { try { return await Object.keys(Session.rawAttributes) @@ -511,10 +518,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 @@ -549,3 +552,148 @@ exports.mentorsSessionWithPendingFeedback = async (mentorId, options = {}, compl return error } } + +exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter) => { + try { + const currentEpochTime = Math.floor(Date.now() / 1000) + let filterConditions = [] + + 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 ')}` : '' + const query = ` + WITH filtered_sessions AS ( + SELECT id, title, description, start_date, end_date, status, image, mentor_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} + ) + SELECT id, title, description, start_date, end_date, status, image, mentor_id, created_at, 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] + } + } + } + + 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) => { + try { + const currentEpochTime = Math.floor(Date.now() / 1000) + + const filterConditions = [] + + 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 ')}` : '' + + const sessionAttendeesData = await Sequelize.query( + ` + SELECT + id, + title, + description, + start_date, + end_date, + status, + image, + mentor_id, + meeting_info + 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} + ORDER BY + start_date ASC + OFFSET + :offset + LIMIT + :limit; + `, + { + replacements: { + mentorId: mentorId, + currentEpochTime: currentEpochTime, + search: `%${search.toLowerCase()}%`, + offset: limit * (page - 1), + limit: limit, + ...filter, // Add filter parameters to replacements + }, + type: sequelize.QueryTypes.SELECT, + } + ) + + return { + data: sessionAttendeesData, + count: sessionAttendeesData.length, + } + } catch (error) { + return error + } +} 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: "<div class='wrapper'><p>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.</p><p>By creating an account, you have read, understood, and agree to the <br /> <a class='links' href='https://shikshalokam.org/mentoring/term-of-use'>Terms of Use</a> and <a class='links' href='https://shikshalokam.org/mentoring/privacy-policy'>Privacy Policy.</p></div>", + value: "I've read and agree to the User Agreement <br /> 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<ul><li>Account deletion takes 2 days to process. You will receive an email notification when complete.</li><li>Your previous session data will be retained.</li><li>You will be un-enrolled from enrolled sessions.</li></ul>', + menterMessage: + 'Please note the following points<ul><li>Account deletion takes 2 days to process. You will receive an email notification when complete.</li><li>Your previous session data will be retained.</li><li>Sessions created by you will be deleted.</li><li>You will be un-enrolled from enrolled sessions.</li></ul>', + 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/generics/materializedViews.js b/src/generics/materializedViews.js new file mode 100644 index 000000000..4487e1749 --- /dev/null +++ b/src/generics/materializedViews.js @@ -0,0 +1,450 @@ +'use strict' +const entityTypeQueries = require('@database/queries/entityType') +const { sequelize } = require('@database/models/index') +const fs = require('fs') +const path = require('path') +const Sequelize = require('sequelize') + +const modelPath = require('../.sequelizerc') +const utils = require('@generics/utils') +/* async function createSequelizeModelFromMaterializedView(materializedViewName, modelName) { + try { + const model = require('@database/models/index')[modelName] + const primaryKeys = model.primaryKeyAttributes + + const [results, metadata] = await sequelize.query( + `SELECT attname AS column_name, atttypid::regtype AS data_type FROM pg_attribute WHERE attrelid = 'public.${materializedViewName}'::regclass AND attnum > 0;` + ) + + if (results.length === 0) { + throw new Error(`Materialized view '${materializedViewName}' not found.`) + } + + const mapDataTypes = (data_type) => { + const dataTypeMappings = { + integer: 'DataTypes.INTEGER', + 'character varying': 'DataTypes.STRING', + 'character varying[]': 'DataTypes.ARRAY(DataTypes.STRING)', + boolean: 'DataTypes.BOOLEAN', + 'timestamp with time zone': 'DataTypes.DATE', + jsonb: 'DataTypes.JSONB', + json: 'DataTypes.JSON', + } + + return dataTypeMappings[data_type] || 'DataTypes.STRING' + } + + const attributes = results.map((row) => { + if (primaryKeys.includes(row.column_name)) { + return `${row.column_name}: { + type: ${mapDataTypes(row.data_type)}, + primaryKey: true, + },` + } + return `${row.column_name}: { + type: ${mapDataTypes(row.data_type)}, + },` + }) + + const modelFileContent = `'use strict'; + module.exports = (sequelize, DataTypes) => { + const ${materializedViewName} = sequelize.define( + '${materializedViewName}', + { + ${attributes.join('\n ')} + }, + { + sequelize, + modelName: '${materializedViewName}', + tableName: '${materializedViewName}', + freezeTableName: true, + paranoid: true, + } + ); + return ${materializedViewName}; + };` + + const outputFileName = path.join(modelPath['models-path'], `${materializedViewName}.js`) + fs.writeFileSync(outputFileName, modelFileContent, 'utf-8') + + return modelFileContent + } catch (error) { + console.error('Error:', error) + throw error + } +} */ +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 { + console.log(rawAttributes) + 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 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 = `m_${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 { + await Promise.all( + modelEntityTypes.entityTypeValueList.map(async (attribute) => { + return await sequelize.query( + `CREATE INDEX m_idx_${model.tableName}_${attribute} ON m_${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 = `m_${tableName}_${generateRandomCode(8)}` + //const checkOriginalViewQuery = `SELECT EXISTS (SELECT 1 FROM pg_materialized_views WHERE viewname = 'm_${tableName}');`; + const checkOriginalViewQuery = `SELECT COUNT(*) from pg_matviews where matviewname = 'm_${tableName}';` + const renameOriginalViewQuery = `ALTER MATERIALIZED VIEW m_${tableName} RENAME TO ${randomViewName};` + const renameNewViewQuery = `ALTER MATERIALIZED VIEW ${temporaryMaterializedViewName} RENAME TO m_${tableName};` + + const temp = await sequelize.query(checkOriginalViewQuery) + console.log('VIEW EXISTS: ', temp[0][0].count) + if (temp[0][0].count > 0) await sequelize.query(renameOriginalViewQuery, { transaction: t }) + else randomViewName = null + await sequelize.query(renameNewViewQuery, { transaction: t }) + await t.commit() + console.log('Transaction committed successfully') + return randomViewName + } catch (error) { + await t.rollback() + console.error('Error executing transaction:', error) + } +} + +const createViewUniqueIndexOnPK = async (model) => { + try { + const primaryKeys = model.primaryKeyAttributes + /* CREATE UNIQUE INDEX unique_index_name +ON my_materialized_view (column1, column2); */ + const result = await sequelize.query(` + CREATE UNIQUE INDEX m_unique_index_${model.tableName}_${primaryKeys.map((key) => `_${key}`)} + ON m_${model.tableName} (${primaryKeys.map((key) => `${key}`).join(', ')});`) + console.log('UNIQUE RESULT: ', result) + } catch (err) { + console.log(err) + } +} + +const generateMaterializedView = async (modelEntityTypes) => { + try { + //console.log('MODEL ENTITY TYPES:', modelEntityTypes); + const model = require('@database/models/index')[modelEntityTypes.modelName] + console.log('MODEL: ', modelEntityTypes.modelName) + const { concreteAttributes, metaAttributes } = await filterConcreteAndMetaAttributes( + Object.keys(model.rawAttributes), + modelEntityTypes.entityTypeValueList + ) + //console.log('GENERATE MATERIALIZED VIEW: ', concreteAttributes, metaAttributes); + + const concreteFields = await rawAttributesTypeModifier(model.rawAttributes) + console.log(concreteFields, '-=-=-=---------') + const metaFields = await modelEntityTypes.entityTypes + .map((entity) => { + if (metaAttributes.includes(entity.value)) return entity + else null + }) + .filter(Boolean) + console.log('MODIFIED TYPES: ', concreteFields) + console.log('META FIELDS: ', metaFields) + //if (metaFields.length == 0) return + + const { materializedViewGenerationQuery, temporaryMaterializedViewName } = await materializedViewQueryBuilder( + model, + concreteFields, + metaFields + ) + console.log('QUERY:', materializedViewGenerationQuery) + + await sequelize.query(materializedViewGenerationQuery) + console.log('GENERATED:') + const randomViewName = await renameMaterializedView(temporaryMaterializedViewName, model.tableName) + if (randomViewName) await deleteMaterializedView(randomViewName) + await createIndexesOnAllowFilteringFields(model, modelEntityTypes) + await createViewUniqueIndexOnPK(model) + /* createSequelizeModelFromMaterializedView(`m_${model.tableName}`, modelEntityTypes.modelName) + .then((modelRes) => { + console.log(`Sequelize model created for '${model.tableName}' and written to '${model.tableName}.js'.`) + }) + .catch((error) => { + console.error('Error:', error) + }) */ + } 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) + + 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 + -- Input is an object, initialize the result array + result := ARRAY[]::text[]; + -- Loop through the object and add keys to the result array + FOR element IN SELECT jsonb_object_keys(input_jsonb) + LOOP + result := array_append(result, element); + END LOOP; + ELSIF jsonb_typeof(input_jsonb) = 'array' THEN + -- Input is an array, initialize the result array + result := ARRAY[]::text[]; + -- Loop through the array and add elements to the result array + FOR element IN SELECT jsonb_array_elements_text(input_jsonb) + LOOP + result := array_append(result, element); + END LOOP; + ELSE + -- If input is neither an object nor an array, return an empty array + result := ARRAY[]::text[]; + END IF; + RETURN result; + END; + $$ LANGUAGE plpgsql; + ` + + // Execute the SQL statement to create the function + sequelize + .query(createFunctionSQL) + .then(() => { + console.log('Function created successfully') + }) + .catch((error) => { + console.error('Error creating function:', error) + }) + + await Promise.all( + entityTypesGroupedByModel.map(async (modelEntityTypes) => { + return generateMaterializedView(modelEntityTypes) + }) + ) + /* const materializedViewName = 'm_sessions' // Replace with the actual materialized view name + + createSequelizeModelFromMaterializedView(materializedViewName, 'Session') + .then((model) => { + console.log( + `Sequelize model created for '${materializedViewName}' and written to '${materializedViewName}.js'.` + ) + }) + .catch((error) => { + console.error('Error:', error) + }) */ + + 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 }) => { + 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) + } +} + +const refreshMaterializedView = async (modelName) => { + try { + const model = require('@database/models/index')[modelName] + const [result, metadata] = await sequelize.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY m_${model.tableName}`) + console.log(result, metadata) + } catch (err) { + console.log(err) + } +} + +const refreshNextView = (currentIndex, modelNames) => { + try { + if (currentIndex < modelNames.length) { + refreshMaterializedView(modelNames[currentIndex]) + currentIndex++ + } else currentIndex = 0 + return currentIndex + } catch (err) { + console.log(err) + } +} + +const triggerPeriodicViewRefresh = async () => { + try { + const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() + const modelNames = await modelNameCollector(allowFilteringEntityTypes) + console.log('MODEL NAME: ', modelNames) + const interval = 1 * 60 * 1000 // 1 minute * 60 seconds per minute * 1000 milliseconds per second + let currentIndex = 0 + setInterval(() => { + currentIndex = refreshNextView(currentIndex, modelNames) + }, interval / modelNames.length) + currentIndex = refreshNextView(currentIndex, modelNames) + } catch (err) { + console.log(err) + } +} + +const adminService = { + triggerViewBuild, + triggerPeriodicViewRefresh, +} + +module.exports = adminService diff --git a/src/generics/utils.js b/src/generics/utils.js index 37bfe2264..42895f3ad 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -259,7 +259,7 @@ function restructureBody(requestBody, entityData, allowedKeys) { } } } - + console.log('-=-==-=', requestBodyValue) if (Array.isArray(requestBodyValue)) { const entityTypeExists = entityData.find((entity) => entity.value === requestBodyKey) @@ -350,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 @@ -399,6 +410,49 @@ function generateCheckSum(queryHash) { const checksum = shasum.digest('hex') return checksum } + +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, getCurrentMonthRange, @@ -429,4 +483,7 @@ module.exports = { getTimeDifferenceInMilliseconds, deleteProperties, generateCheckSum, + generateWhereClause, + validateFilters, + processQueryParametersWithExclusions, } diff --git a/src/nodemon.json b/src/nodemon.json new file mode 100644 index 000000000..e9aa8979c --- /dev/null +++ b/src/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["database/models/*.js", "README"] +} diff --git a/src/package.json b/src/package.json index 0db20fb2c..db63f10be 100644 --- a/src/package.json +++ b/src/package.json @@ -42,6 +42,7 @@ "i18next-http-middleware": "^3.2.1", "jsonwebtoken": "^8.5.1", "kafkajs": "^2.2.3", + "knex": "^3.0.1", "lodash": "^4.17.21", "md5": "^2.3.0", "module-alias": "^2.2.3", diff --git a/src/requests/user.js b/src/requests/user.js index b9fe20e67..df25d7844 100644 --- a/src/requests/user.js +++ b/src/requests/user.js @@ -173,10 +173,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/services/admin.js b/src/services/admin.js index 589b95312..91d7f7350 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 { /** @@ -143,4 +144,31 @@ module.exports = class AdminHelper { return error } } + + static async triggerViewRebuild(decodedToken, userId) { + try { + const result = await adminService.triggerViewBuild() + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_REMOVED_SUCCESSFULLY', + result, + }) + } catch (error) { + console.error('An error occurred in userDelete:', error) + return error + } + } + static async triggerPeriodicViewRefresh(decodedToken, userId) { + try { + const result = await adminService.triggerPeriodicViewRefresh() + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: 'USER_REMOVED_SUCCESSFULLY', + result, + }) + } catch (error) { + console.error('An error occurred in userDelete:', error) + return error + } + } } diff --git a/src/services/mentees.js b/src/services/mentees.js index ec9918955..d446324d9 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -61,21 +61,15 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of sessions */ - static async sessions(userId, enrolledSessions, page, limit, search = '') { + 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) - } 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) - } + /* 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) + return common.successResponse({ statusCode: httpStatusCode.ok, message: 'SESSION_FETCHED_SUCCESSFULLY', @@ -301,9 +295,16 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of all sessions */ - static async getAllSessions(page, limit, search, userId) { - const sessions = await sessionQueries.getUpcomingSessions(page, limit, search, userId) + static async getAllSessions(page, limit, search, userId, queryParams) { + let query = utils.processQueryParametersWithExclusions(queryParams) + + let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ + status: 'ACTIVE', + }) + + let filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'MentorExtension') + const sessions = await sessionQueries.getUpcomingSessionsFromView(page, limit, search, userId, filteredQuery) sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) sessions.rows = await this.sessionMentorDetails(sessions.rows) return sessions diff --git a/src/services/mentors.js b/src/services/mentors.js index 1388d647f..817353d5e 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -9,6 +9,8 @@ const _ = require('lodash') const sessionAttendeesQueries = require('@database/queries/sessionAttendees') const sessionQueries = require('@database/queries/sessions') const entityTypeQueries = require('@database/queries/entityType') +const moment = require('moment') +const { Op } = require('sequelize') module.exports = class MentorsHelper { /** @@ -21,8 +23,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) { 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({ @@ -32,7 +41,13 @@ module.exports = class MentorsHelper { }) } - let upcomingSessions = await sessionQueries.getMentorsUpcomingSessions(page, limit, search, id) + let upcomingSessions = await sessionQueries.getMentorsUpcomingSessionsFromView( + page, + limit, + search, + id, + filteredQuery + ) if (!upcomingSessions.data.length) { return common.successResponse({ @@ -447,4 +462,152 @@ module.exports = class MentorsHelper { return error } } + /** + * Get user list. + * @method + * @name create + * @param {Number} pageSize - Page size. + * @param {Number} pageNo - Page number. + * @param {String} searchText - Search text. + * @returns {JSON} - User list. + */ + + static async list(pageNo, pageSize, searchText, queryParams) { + 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) + + const ids = userDetails.data.result.data.map((item) => item.values[0].id) + + let extensionDetails = await mentorQueries.getMentorsByUserIdsFromView(ids, pageNo, pageSize, filteredQuery) + console.log(extensionDetails) + const extensionDataMap = new Map(extensionDetails.data.map((newItem) => [newItem.user_id, newItem])) + + userDetails.data.result.data.forEach((existingItem, index) => { + 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 } + } else { + // Remove item if user id is not found in extensionDataMap + userDetails.data.result.data.splice(index, 1) + } + delete existingItem.values[0].user_id + }) + userDetails.data.result.count = extensionDetails.count + return common.successResponse({ + statusCode: httpStatusCode.ok, + message: userDetails.data.message, + result: userDetails.data.result, + }) + } catch (error) { + throw error + } + } + /** + * 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/sessions.js b/src/services/sessions.js index 50ecef05b..221f97803 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -4,8 +4,8 @@ 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 notificationTemplateData = require('@db/notification-template/query') +//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 const request = require('request') @@ -27,6 +27,7 @@ const utils = require('@generics/utils') const sessionMentor = require('./mentors') const bigBlueButtonService = require('./bigBlueButton') +const menteeService = require('@services/mentees') module.exports = class SessionsHelper { /** * Create session. @@ -50,13 +51,13 @@ module.exports = class SessionsHelper { } const timeSlot = await this.isTimeSlotAvailable(loggedInUserId, bodyData.start_date, bodyData.end_date) - if (timeSlot.isTimeSlotAvailable === false) { + /* if (timeSlot.isTimeSlotAvailable === false) { return common.failureResponse({ message: { key: 'INVALID_TIME_SELECTION', interpolation: { sessionName: timeSlot.sessionName } }, statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) - } + } */ let duration = moment.duration(moment.unix(bodyData.end_date).diff(moment.unix(bodyData.start_date))) let elapsedMinutes = duration.asMinutes() @@ -86,7 +87,7 @@ module.exports = class SessionsHelper { validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) - let res = utils.validateInput(bodyData, validationData, 'sessions') + let res = utils.validateInput(bodyData, validationData, 'Session') if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -97,7 +98,7 @@ module.exports = class SessionsHelper { } let sessionModel = await sessionQueries.getColumns() bodyData = utils.restructureBody(bodyData, validationData, sessionModel) - + console.log(bodyData) bodyData.meeting_info = { platform: process.env.DEFAULT_MEETING_SERVICE, value: process.env.DEFAULT_MEETING_SERVICE, @@ -220,7 +221,7 @@ module.exports = class SessionsHelper { validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) - let res = utils.validateInput(bodyData, validationData, 'sessions') + let res = utils.validateInput(bodyData, validationData, 'Session') if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -475,7 +476,7 @@ module.exports = class SessionsHelper { responseCode: 'CLIENT_ERROR', }) } - + console.log(sessionDetails) if (userId) { let sessionAttendee = await sessionAttendeesQueries.findOne({ session_id: sessionDetails.id, @@ -526,93 +527,30 @@ module.exports = class SessionsHelper { } /** - * Session list. + * Sessions list * @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 + * @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 list(loggedInUserId, page, limit, search, status) { + static async list(loggedInUserId, page, limit, search, queryParams) { 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 allSessions = await menteeService.getAllSessions(page, limit, search, loggedInUserId, queryParams) - let arrayOfStatus = [] - if (status && status != '') { - arrayOfStatus = status.split(',') + const result = { + data: allSessions.rows, + count: allSessions.count, } - 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.failureResponse({ - message: 'SESSION_NOT_FOUND', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - result: [], - }) - } - - sessionDetails.rows = await sessionMentor.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 }, + result, }) } catch (error) { throw error diff --git a/src/services/users.js b/src/services/users.js index 53760dfe1..23562053b 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -33,6 +33,8 @@ module.exports = class UserHelper { } const extensionDataMap = new Map(extensionDetails.map((newItem) => [newItem.user_id, newItem])) + console.log(extensionDataMap) + console.log(userDetails.data.result) userDetails.data.result.data.forEach((existingItem) => { const user_id = existingItem.values[0].id @@ -42,7 +44,7 @@ module.exports = class UserHelper { } delete existingItem.values[0].user_id }) - + console.log(userDetails.data.result) return common.successResponse({ statusCode: httpStatusCode.ok, message: userDetails.data.message, From 127fc88407929a9ede460bd6fe8bfbb9cdc08aee Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Fri, 10 Nov 2023 19:18:54 +0530 Subject: [PATCH 03/30] updated multiple services with saas policy --- src/controllers/v1/mentees.js | 10 ++++----- src/controllers/v1/mentors.js | 8 ++++++- src/controllers/v1/sessions.js | 3 ++- src/database/queries/mentorExtension.js | 4 +++- src/database/queries/sessions.js | 4 ++-- src/services/mentees.js | 10 ++++----- src/services/mentors.js | 29 +++++++++++++++++++------ src/services/sessions.js | 12 ++++++++-- 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/controllers/v1/mentees.js b/src/controllers/v1/mentees.js index db1ea1520..202998445 100644 --- a/src/controllers/v1/mentees.js +++ b/src/controllers/v1/mentees.js @@ -44,8 +44,7 @@ module.exports = class Mentees { req.decodedToken.id, req.pageNo, req.pageSize, - req.searchText, - isAMentor(req.decodedToken.roles) + req.searchText ) return sessions } catch (error) { @@ -76,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) { @@ -92,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) { diff --git a/src/controllers/v1/mentors.js b/src/controllers/v1/mentors.js index a028ae6a6..7d20148ba 100644 --- a/src/controllers/v1/mentors.js +++ b/src/controllers/v1/mentors.js @@ -108,7 +108,13 @@ module.exports = class Mentors { async list(req) { try { - return await mentorsService.list(req.pageNo, req.pageSize, req.searchText, req.query) + return await mentorsService.list( + req.pageNo, + req.pageSize, + req.searchText, + req.query, + isAMentor(req.decodedToken.roles) + ) } catch (error) { return error } diff --git a/src/controllers/v1/sessions.js b/src/controllers/v1/sessions.js index 6197ac553..9b4e967c5 100644 --- a/src/controllers/v1/sessions.js +++ b/src/controllers/v1/sessions.js @@ -97,7 +97,8 @@ module.exports = class Sessions { req.pageNo, req.pageSize, req.searchText, - req.query + req.query, + isAMentor(req.decodedToken.roles) ) return sessionDetails } catch (error) { diff --git a/src/database/queries/mentorExtension.js b/src/database/queries/mentorExtension.js index 92403d7d5..8dda81a4d 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -144,7 +144,9 @@ module.exports = class MentorExtensionQueries { ` SELECT user_id, - rating + rating, + visibility, + org_id FROM ${common.materializedViewsPrefix + MentorExtension.tableName} WHERE diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index b530e1e36..515a492d9 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -565,7 +565,7 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' const query = ` WITH filtered_sessions AS ( - SELECT id, title, description, start_date, end_date, status, image, mentor_id, created_at, + 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 @@ -575,7 +575,7 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter AND status IN ('PUBLISHED', 'LIVE') ${filterClause} ) - SELECT id, title, description, start_date, end_date, status, image, mentor_id, created_at, meeting_info, + 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 diff --git a/src/services/mentees.js b/src/services/mentees.js index bc1d37f49..d9c530a48 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -157,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, @@ -309,7 +309,7 @@ module.exports = class MenteesHelper { * @returns {JSON} - List of all sessions */ - static async getAllSessions(page, limit, search, userId, queryParams) { + static async getAllSessions(page, limit, search, userId, queryParams, isAMentor) { let query = utils.processQueryParametersWithExclusions(queryParams) let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ @@ -414,7 +414,7 @@ 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) diff --git a/src/services/mentors.js b/src/services/mentors.js index 7230138aa..719ce5803 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -576,10 +576,12 @@ module.exports = class MentorsHelper { * @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) { + static async list(pageNo, pageSize, searchText, queryParams, isAMentor) { try { const query = utils.processQueryParametersWithExclusions(queryParams) let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ @@ -593,21 +595,34 @@ module.exports = class MentorsHelper { const ids = userDetails.data.result.data.map((item) => item.values[0].id) let extensionDetails = await mentorQueries.getMentorsByUserIdsFromView(ids, pageNo, pageSize, filteredQuery) - console.log(extensionDetails) + // Inside your function + extensionDetails.data = extensionDetails.data.filter((item) => item.visibility && item.org_id) + + // Filter user data based on SAAS policy + extensionDetails.data = await this.filterMentorListBasedOnSaasPolicy( + extensionDetails.data, + userId, + isAMentor + ) + const extensionDataMap = new Map(extensionDetails.data.map((newItem) => [newItem.user_id, newItem])) - userDetails.data.result.data.forEach((existingItem, index) => { + 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 } - } else { - // Remove item if user id is not found in extensionDataMap - userDetails.data.result.data.splice(index, 1) + delete existingItem.values[0].user_id + delete existingItem.values[0].visibility + delete existingItem.values[0].org_id + return true // Keep this item } - delete existingItem.values[0].user_id + + return false // Remove this item }) + userDetails.data.result.count = extensionDetails.count + return common.successResponse({ statusCode: httpStatusCode.ok, message: userDetails.data.message, diff --git a/src/services/sessions.js b/src/services/sessions.js index c7dd34825..2632dfce7 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -604,12 +604,20 @@ module.exports = class SessionsHelper { * @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. */ - static async list(loggedInUserId, page, limit, search, queryParams) { + static async list(loggedInUserId, page, limit, search, queryParams, isAMentor) { try { - let allSessions = await menteeService.getAllSessions(page, limit, search, loggedInUserId, queryParams) + let allSessions = await menteeService.getAllSessions( + page, + limit, + search, + loggedInUserId, + queryParams, + isAMentor + ) const result = { data: allSessions.rows, From 45cb10f22448273f30e3821ed53a7ec9ab20fef5 Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Fri, 10 Nov 2023 21:30:17 +0530 Subject: [PATCH 04/30] Modified RestructureBody Function --- src/generics/utils.js | 113 +++++++++++++------------------ src/validators/v1/entity-type.js | 2 +- 2 files changed, 49 insertions(+), 66 deletions(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index ef701a7f4..f0c64ce97 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -215,77 +215,61 @@ 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 entities = entityType.entities.map((entity) => 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)) + 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') - } - } - } + for (const currentFieldName in requestBody) { + const currentFieldValue = requestBody[currentFieldName] + if (!requestBody.meta) requestBody.meta = {} + 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], + if (recognizedEntities.length > 0) + if (allowedKeys.includes(currentFieldName)) requestBody[currentFieldName] = recognizedEntities + else requestBody.meta[currentFieldName] = recognizedEntities + if (customEntities.length > 0) requestBody.custom_entity_text[currentFieldName] = customEntities + } else { + if (!entityType.get('entities').has(currentFieldValue)) { + requestBody.custom_entity_text[currentFieldName] = { + value: 'other', + label: currentFieldValue, } - delete requestBody[requestBodyKey] - } + if (allowedKeys.includes(currentFieldName)) requestBody[currentFieldName] = 'other' + 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 return requestBody } catch (error) { console.error(error) @@ -426,7 +410,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 +418,6 @@ const validateRoleAccess = (roles, requiredRoles) => { } else { return roles.some((role) => requiredRoles.includes(role)) } - } const removeDefaultOrgEntityTypes = (entityTypes, orgId) => { 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') From b3698030240c3d1d77b3ca4ffd83268356c9e686 Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Fri, 10 Nov 2023 21:37:39 +0530 Subject: [PATCH 05/30] Added Comment For DB Write Error --- src/generics/utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index f0c64ce97..20c35e44e 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -262,7 +262,8 @@ function restructureBody(requestBody, entityData, allowedKeys) { value: 'other', label: currentFieldValue, } - if (allowedKeys.includes(currentFieldName)) requestBody[currentFieldName] = 'other' + 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 From 27265d7670ee3a444c0fd7c959c5561340ae22fa Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Fri, 10 Nov 2023 21:41:54 +0530 Subject: [PATCH 06/30] Custom_entity_text Field Clean Up Optimisation --- src/generics/utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index 20c35e44e..f1bf55c27 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -240,9 +240,9 @@ function restructureBody(requestBody, entityData, allowedKeys) { const doesAffectedFieldsExist = Object.keys(requestBody).some((element) => entityTypeMap.has(element)) if (!doesAffectedFieldsExist) return requestBody requestBody.custom_entity_text = {} + if (!requestBody.meta) requestBody.meta = {} for (const currentFieldName in requestBody) { const currentFieldValue = requestBody[currentFieldName] - if (!requestBody.meta) requestBody.meta = {} const entityType = entityTypeMap.get(currentFieldName) if (entityType && entityType.get('allow_custom_entities')) { if (Array.isArray(currentFieldValue)) { @@ -271,6 +271,7 @@ function restructureBody(requestBody, entityData, allowedKeys) { } } 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) From 2ab4e00a28a9bb639d3df86be1083a8ba9cc9c81 Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Sun, 12 Nov 2023 14:09:09 +0530 Subject: [PATCH 07/30] saas changes after view merge test --- src/controllers/v1/mentors.js | 13 +++++++++---- src/database/queries/sessions.js | 4 +++- src/services/mentees.js | 3 ++- src/services/mentors.js | 14 +++++++++++--- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/controllers/v1/mentors.js b/src/controllers/v1/mentors.js index 7d20148ba..85e625869 100644 --- a/src/controllers/v1/mentors.js +++ b/src/controllers/v1/mentors.js @@ -29,7 +29,8 @@ module.exports = class Mentors { req.pageSize, req.searchText, req.params.menteeId ? req.params.menteeId : req?.decodedToken?.id, - req.query + req.query, + isAMentor(req.decodedToken.roles) ) } catch (error) { return error @@ -101,9 +102,12 @@ module.exports = class Mentors { * List of available mentors. * @method * @name list - * @param {Object} req - Request data. - * @param {String} req.decodedToken.id - Mentors user id. - * @returns {JSON} - Returns sharable link of the mentor. + * @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) { @@ -113,6 +117,7 @@ module.exports = class Mentors { req.pageSize, req.searchText, req.query, + req.decodedToken.id, isAMentor(req.decodedToken.roles) ) } catch (error) { diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index 515a492d9..d6ed55883 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -654,7 +654,9 @@ exports.getMentorsUpcomingSessionsFromView = async (page, limit, search, mentorI status, image, mentor_id, - meeting_info + meeting_info, + visibility, + mentor_org_id FROM ${common.materializedViewsPrefix + Session.tableName} WHERE diff --git a/src/services/mentees.js b/src/services/mentees.js index d9c530a48..bd841483f 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -82,7 +82,7 @@ module.exports = class MenteesHelper { 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) + const sessions = await this.getMySessions(page, limit, search, userId) return common.successResponse({ statusCode: httpStatusCode.ok, @@ -319,6 +319,7 @@ module.exports = class MenteesHelper { let filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'MentorExtension') const sessions = await sessionQueries.getUpcomingSessionsFromView(page, limit, search, userId, filteredQuery) + sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) // Filter sessions based on saas policy {session contain enrolled + upcoming session} diff --git a/src/services/mentors.js b/src/services/mentors.js index 719ce5803..1eaf965a9 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -16,6 +16,7 @@ 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 { /** @@ -28,7 +29,7 @@ module.exports = class MentorsHelper { * @param {String} search - Search text. * @returns {JSON} - mentors upcoming session details */ - static async upcomingSessions(id, page, limit, search = '', menteeUserId, queryParams) { + static async upcomingSessions(id, page, limit, search = '', menteeUserId, queryParams, isAMentor) { try { const query = utils.processQueryParametersWithExclusions(queryParams) console.log(query) @@ -70,6 +71,13 @@ module.exports = class MentorsHelper { if (menteeUserId && id != menteeUserId) { upcomingSessions.data = await this.menteeSessionDetails(upcomingSessions.data, menteeUserId) } + + // Filter upcoming sessions based on saas policy + upcomingSessions.data = await menteesService.filterSessionsBasedOnSaasPolicy( + upcomingSessions.data, + menteeUserId, + isAMentor + ) return common.successResponse({ statusCode: httpStatusCode.ok, message: 'UPCOMING_SESSION_FETCHED', @@ -581,7 +589,7 @@ module.exports = class MentorsHelper { * @returns {JSON} - User list. */ - static async list(pageNo, pageSize, searchText, queryParams, isAMentor) { + static async list(pageNo, pageSize, searchText, queryParams, userId, isAMentor) { try { const query = utils.processQueryParametersWithExclusions(queryParams) let validationData = await entityTypeQueries.findAllEntityTypesAndEntities({ @@ -599,7 +607,7 @@ module.exports = class MentorsHelper { extensionDetails.data = extensionDetails.data.filter((item) => item.visibility && item.org_id) // Filter user data based on SAAS policy - extensionDetails.data = await this.filterMentorListBasedOnSaasPolicy( + extensionDetails.data = await usersService.filterMentorListBasedOnSaasPolicy( extensionDetails.data, userId, isAMentor From 75978056b648a746942bb11297360ed2b96efa40 Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Tue, 14 Nov 2023 13:36:08 +0530 Subject: [PATCH 08/30] scheduler related change added --- src/controllers/v1/notifications.js | 6 +++--- src/requests/scheduler.js | 8 +++----- src/services/sessions.js | 9 +++++++-- src/validators/v1/notifications.js | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/controllers/v1/notifications.js b/src/controllers/v1/notifications.js index c03fd668e..ed5b01965 100644 --- a/src/controllers/v1/notifications.js +++ b/src/controllers/v1/notifications.js @@ -21,9 +21,9 @@ module.exports = class Notifications { try { // Make a call to notification service notificationsService.sendNotification( - req.body.jobId, - req.body.emailTemplateCode, - req.body.jobCreatorOrgId ? parseInt(req.body.jobCreatorOrgId, 10) : '' + 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/requests/scheduler.js b/src/requests/scheduler.js index fddd30105..2dcc172db 100644 --- a/src/requests/scheduler.js +++ b/src/requests/scheduler.js @@ -18,11 +18,10 @@ const mentoringBaseurl = `http://localhost:${process.env.APPLICATION_PORT}` * @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 {number} orgId - OrganisationId of job creator. + * @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, orgId) { +const createSchedulerJob = function (jobId, delay, jobName, requestBody = {}) { const bodyData = { jobName: jobName, email: email, @@ -30,16 +29,15 @@ 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, - creatorOrgId: orgId ? orgId : '', }, } diff --git a/src/services/sessions.js b/src/services/sessions.js index b38c1f986..e0bb96711 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -163,13 +163,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, - orgId + reqBody ) } 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') }, } From adad75a5527698ed46ab39edc3b41d13b45131c5 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 02:26:27 +0530 Subject: [PATCH 09/30] added scripts --- docker-compose-mentoring.yml | 8 +- src/constants/common.js | 2 + src/controllers/v1/admin.js | 43 +++++- src/generics/materializedViews.js | 234 +++++++++--------------------- src/locales/en.json | 4 +- src/scripts/psqlFunction.js | 63 ++++++++ src/scripts/viewsScript.js | 138 ++++++++++++++++++ src/services/admin.js | 23 ++- src/services/mentees.js | 4 +- src/services/mentors.js | 2 +- src/validators/v1/mentees.js | 9 +- 11 files changed, 338 insertions(+), 192 deletions(-) create mode 100644 src/scripts/psqlFunction.js create mode 100644 src/scripts/viewsScript.js diff --git a/docker-compose-mentoring.yml b/docker-compose-mentoring.yml index 86f866e3e..2e6200ba2 100644 --- a/docker-compose-mentoring.yml +++ b/docker-compose-mentoring.yml @@ -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 <database_name> -c 'CREATE EXTENSION citus; SELECT create_distributed_table(\"notification_templates\", \"id\");'" diff --git a/src/constants/common.js b/src/constants/common.js index f6844a4cf..c56c528ea 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', diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 5607acd55..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,7 +22,7 @@ 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 @@ -29,18 +31,43 @@ module.exports = class admin { async triggerViewRebuild(req) { try { - const userDelete = await userService.triggerViewRebuild(req.decodedToken, req.query.userId) + 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, res) { + async triggerPeriodicViewRefresh(req) { try { - const userDelete = await userService.triggerPeriodicViewRefresh(req.decodedToken, req.query.userId) - return userDelete - await adminService.triggerPeriodicViewRefresh() - res.send({ message: 'Triggered' }) + 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/generics/materializedViews.js b/src/generics/materializedViews.js index 4487e1749..ce7328b13 100644 --- a/src/generics/materializedViews.js +++ b/src/generics/materializedViews.js @@ -1,78 +1,10 @@ 'use strict' const entityTypeQueries = require('@database/queries/entityType') const { sequelize } = require('@database/models/index') -const fs = require('fs') -const path = require('path') -const Sequelize = require('sequelize') - -const modelPath = require('../.sequelizerc') const utils = require('@generics/utils') -/* async function createSequelizeModelFromMaterializedView(materializedViewName, modelName) { - try { - const model = require('@database/models/index')[modelName] - const primaryKeys = model.primaryKeyAttributes - - const [results, metadata] = await sequelize.query( - `SELECT attname AS column_name, atttypid::regtype AS data_type FROM pg_attribute WHERE attrelid = 'public.${materializedViewName}'::regclass AND attnum > 0;` - ) - - if (results.length === 0) { - throw new Error(`Materialized view '${materializedViewName}' not found.`) - } - - const mapDataTypes = (data_type) => { - const dataTypeMappings = { - integer: 'DataTypes.INTEGER', - 'character varying': 'DataTypes.STRING', - 'character varying[]': 'DataTypes.ARRAY(DataTypes.STRING)', - boolean: 'DataTypes.BOOLEAN', - 'timestamp with time zone': 'DataTypes.DATE', - jsonb: 'DataTypes.JSONB', - json: 'DataTypes.JSON', - } - - return dataTypeMappings[data_type] || 'DataTypes.STRING' - } +const common = require('@constants/common') - const attributes = results.map((row) => { - if (primaryKeys.includes(row.column_name)) { - return `${row.column_name}: { - type: ${mapDataTypes(row.data_type)}, - primaryKey: true, - },` - } - return `${row.column_name}: { - type: ${mapDataTypes(row.data_type)}, - },` - }) - - const modelFileContent = `'use strict'; - module.exports = (sequelize, DataTypes) => { - const ${materializedViewName} = sequelize.define( - '${materializedViewName}', - { - ${attributes.join('\n ')} - }, - { - sequelize, - modelName: '${materializedViewName}', - tableName: '${materializedViewName}', - freezeTableName: true, - paranoid: true, - } - ); - return ${materializedViewName}; - };` - - const outputFileName = path.join(modelPath['models-path'], `${materializedViewName}.js`) - fs.writeFileSync(outputFileName, modelFileContent, 'utf-8') - - return modelFileContent - } catch (error) { - console.error('Error:', error) - throw error - } -} */ +let refreshInterval const groupByModelNames = async (entityTypes) => { const groupedData = new Map() entityTypes.forEach((item) => { @@ -108,7 +40,6 @@ const filterConcreteAndMetaAttributes = async (modelAttributes, attributesList) const rawAttributesTypeModifier = async (rawAttributes) => { try { - console.log(rawAttributes) const outputArray = [] for (const key in rawAttributes) { const columnInfo = rawAttributes[key] @@ -144,6 +75,41 @@ const rawAttributesTypeModifier = async (rawAttributes) => { 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' @@ -158,7 +124,7 @@ const generateRandomCode = (length) => { const materializedViewQueryBuilder = async (model, concreteFields, metaFields) => { try { const tableName = model.tableName - const temporaryMaterializedViewName = `m_${tableName}_${generateRandomCode(8)}` + const temporaryMaterializedViewName = `${common.materializedViewsPrefix}${tableName}_${generateRandomCode(8)}` const concreteFieldsQuery = await concreteFields .map((data) => { return `${data.key}::${data.type} as ${data.key}` @@ -196,7 +162,7 @@ const createIndexesOnAllowFilteringFields = async (model, modelEntityTypes) => { await Promise.all( modelEntityTypes.entityTypeValueList.map(async (attribute) => { return await sequelize.query( - `CREATE INDEX m_idx_${model.tableName}_${attribute} ON m_${model.tableName} (${attribute});` + `CREATE INDEX ${common.materializedViewsPrefix}idx_${model.tableName}_${attribute} ON ${common.materializedViewsPrefix}${model.tableName} (${attribute});` ) }) ) @@ -216,19 +182,19 @@ const deleteMaterializedView = async (viewName) => { const renameMaterializedView = async (temporaryMaterializedViewName, tableName) => { const t = await sequelize.transaction() try { - let randomViewName = `m_${tableName}_${generateRandomCode(8)}` - //const checkOriginalViewQuery = `SELECT EXISTS (SELECT 1 FROM pg_materialized_views WHERE viewname = 'm_${tableName}');`; - const checkOriginalViewQuery = `SELECT COUNT(*) from pg_matviews where matviewname = 'm_${tableName}';` - const renameOriginalViewQuery = `ALTER MATERIALIZED VIEW m_${tableName} RENAME TO ${randomViewName};` - const renameNewViewQuery = `ALTER MATERIALIZED VIEW ${temporaryMaterializedViewName} RENAME TO m_${tableName};` + 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) - console.log('VIEW EXISTS: ', temp[0][0].count) + if (temp[0][0].count > 0) await sequelize.query(renameOriginalViewQuery, { transaction: t }) else randomViewName = null await sequelize.query(renameNewViewQuery, { transaction: t }) await t.commit() - console.log('Transaction committed successfully') + return randomViewName } catch (error) { await t.rollback() @@ -239,12 +205,12 @@ const renameMaterializedView = async (temporaryMaterializedViewName, tableName) const createViewUniqueIndexOnPK = async (model) => { try { const primaryKeys = model.primaryKeyAttributes - /* CREATE UNIQUE INDEX unique_index_name -ON my_materialized_view (column1, column2); */ + const result = await sequelize.query(` - CREATE UNIQUE INDEX m_unique_index_${model.tableName}_${primaryKeys.map((key) => `_${key}`)} - ON m_${model.tableName} (${primaryKeys.map((key) => `${key}`).join(', ')});`) - console.log('UNIQUE RESULT: ', result) + 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) } @@ -252,47 +218,36 @@ ON my_materialized_view (column1, column2); */ const generateMaterializedView = async (modelEntityTypes) => { try { - //console.log('MODEL ENTITY TYPES:', modelEntityTypes); const model = require('@database/models/index')[modelEntityTypes.modelName] - console.log('MODEL: ', modelEntityTypes.modelName) + const { concreteAttributes, metaAttributes } = await filterConcreteAndMetaAttributes( Object.keys(model.rawAttributes), modelEntityTypes.entityTypeValueList ) - //console.log('GENERATE MATERIALIZED VIEW: ', concreteAttributes, metaAttributes); const concreteFields = await rawAttributesTypeModifier(model.rawAttributes) - console.log(concreteFields, '-=-=-=---------') + const metaFields = await modelEntityTypes.entityTypes .map((entity) => { if (metaAttributes.includes(entity.value)) return entity else null }) .filter(Boolean) - console.log('MODIFIED TYPES: ', concreteFields) - console.log('META FIELDS: ', metaFields) - //if (metaFields.length == 0) return + + const modifiedMetaFields = await metaAttributesTypeModifier(metaFields) const { materializedViewGenerationQuery, temporaryMaterializedViewName } = await materializedViewQueryBuilder( model, concreteFields, - metaFields + modifiedMetaFields ) - console.log('QUERY:', materializedViewGenerationQuery) await sequelize.query(materializedViewGenerationQuery) - console.log('GENERATED:') + const randomViewName = await renameMaterializedView(temporaryMaterializedViewName, model.tableName) if (randomViewName) await deleteMaterializedView(randomViewName) await createIndexesOnAllowFilteringFields(model, modelEntityTypes) await createViewUniqueIndexOnPK(model) - /* createSequelizeModelFromMaterializedView(`m_${model.tableName}`, modelEntityTypes.modelName) - .then((modelRes) => { - console.log(`Sequelize model created for '${model.tableName}' and written to '${model.tableName}.js'.`) - }) - .catch((error) => { - console.error('Error:', error) - }) */ } catch (err) { console.log(err) } @@ -317,63 +272,11 @@ const triggerViewBuild = async () => { const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() const entityTypesGroupedByModel = await groupByModelNames(allowFilteringEntityTypes) - 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 - -- Input is an object, initialize the result array - result := ARRAY[]::text[]; - -- Loop through the object and add keys to the result array - FOR element IN SELECT jsonb_object_keys(input_jsonb) - LOOP - result := array_append(result, element); - END LOOP; - ELSIF jsonb_typeof(input_jsonb) = 'array' THEN - -- Input is an array, initialize the result array - result := ARRAY[]::text[]; - -- Loop through the array and add elements to the result array - FOR element IN SELECT jsonb_array_elements_text(input_jsonb) - LOOP - result := array_append(result, element); - END LOOP; - ELSE - -- If input is neither an object nor an array, return an empty array - result := ARRAY[]::text[]; - END IF; - RETURN result; - END; - $$ LANGUAGE plpgsql; - ` - - // Execute the SQL statement to create the function - sequelize - .query(createFunctionSQL) - .then(() => { - console.log('Function created successfully') - }) - .catch((error) => { - console.error('Error creating function:', error) - }) - await Promise.all( entityTypesGroupedByModel.map(async (modelEntityTypes) => { return generateMaterializedView(modelEntityTypes) }) ) - /* const materializedViewName = 'm_sessions' // Replace with the actual materialized view name - - createSequelizeModelFromMaterializedView(materializedViewName, 'Session') - .then((model) => { - console.log( - `Sequelize model created for '${materializedViewName}' and written to '${materializedViewName}.js'.` - ) - }) - .catch((error) => { - console.error('Error:', error) - }) */ return entityTypesGroupedByModel } catch (err) { @@ -388,7 +291,6 @@ const modelNameCollector = async (entityTypes) => { 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) => { @@ -397,7 +299,6 @@ const modelNameCollector = async (entityTypes) => { ) }) ) - console.log(modelSet) return [...modelSet.values()] } catch (err) { console.log(err) @@ -407,8 +308,10 @@ const modelNameCollector = async (entityTypes) => { const refreshMaterializedView = async (modelName) => { try { const model = require('@database/models/index')[modelName] - const [result, metadata] = await sequelize.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY m_${model.tableName}`) - console.log(result, metadata) + const [result, metadata] = await sequelize.query( + `REFRESH MATERIALIZED VIEW CONCURRENTLY ${common.materializedViewsPrefix}${model.tableName}` + ) + return metadata } catch (err) { console.log(err) } @@ -419,7 +322,10 @@ const refreshNextView = (currentIndex, modelNames) => { if (currentIndex < modelNames.length) { refreshMaterializedView(modelNames[currentIndex]) currentIndex++ - } else currentIndex = 0 + } else { + console.info('All views refreshed. Stopping further refreshes.') + clearInterval(refreshInterval) // Stop the setInterval loop + } return currentIndex } catch (err) { console.log(err) @@ -430,12 +336,15 @@ const triggerPeriodicViewRefresh = async () => { try { const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() const modelNames = await modelNameCollector(allowFilteringEntityTypes) - console.log('MODEL NAME: ', modelNames) - const interval = 1 * 60 * 1000 // 1 minute * 60 seconds per minute * 1000 milliseconds per second + const interval = process.env.REFRESH_VIEW_INTERNAL let currentIndex = 0 - setInterval(() => { + + // 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) @@ -445,6 +354,7 @@ const triggerPeriodicViewRefresh = async () => { const adminService = { triggerViewBuild, triggerPeriodicViewRefresh, + refreshMaterializedView, } module.exports = adminService 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/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..8ee9c33ff --- /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://mentoring:${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_INTERNAL / modelNames.length + modelNames.map((model, index) => { + createSchedulerJob( + 'repeatable_view_job' + model, + process.env.REFRESH_VIEW_INTERNAL, + '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 91d7f7350..dd00df237 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -145,26 +145,37 @@ module.exports = class AdminHelper { } } - static async triggerViewRebuild(decodedToken, userId) { + static async triggerViewRebuild(decodedToken) { try { const result = await adminService.triggerViewBuild() return common.successResponse({ statusCode: httpStatusCode.ok, - message: 'USER_REMOVED_SUCCESSFULLY', - result, + message: 'MATERIALIZED_VIEW_GENERATED_SUCCESSFULLY', }) } catch (error) { console.error('An error occurred in userDelete:', error) return error } } - static async triggerPeriodicViewRefresh(decodedToken, userId) { + static async triggerPeriodicViewRefresh(decodedToken) { try { const result = await adminService.triggerPeriodicViewRefresh() return common.successResponse({ statusCode: httpStatusCode.ok, - message: 'USER_REMOVED_SUCCESSFULLY', - result, + 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) diff --git a/src/services/mentees.js b/src/services/mentees.js index bd841483f..f5b76e73d 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -551,7 +551,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', @@ -636,7 +636,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 1eaf965a9..d58881a36 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -314,7 +314,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', 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) => { From 4eaf37dc3b8a73a73a6b4bde260e54536058f5e0 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 12:13:41 +0530 Subject: [PATCH 10/30] updated postman collection --- ...MentorED-Mentoring.postman_collection.json | 174 +++++++++++++++--- 1 file changed, 153 insertions(+), 21 deletions(-) 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", From 7ab95a74e7229cdfd938b939f521b96cde2b931c Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Wed, 15 Nov 2023 12:44:29 +0530 Subject: [PATCH 11/30] initial changes for associated saas --- src/constants/common.js | 1 + src/database/models/sessions.js | 2 +- src/database/queries/sessions.js | 36 ++++++++++- src/services/mentees.js | 105 ++++++++++++++++++++++--------- 4 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index f6844a4cf..c8654a800 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -128,6 +128,7 @@ module.exports = { }, CURRENT: 'CURRENT', ALL: 'ALL', + ASSOCIATED: 'ASSOCIATED', PATCH_METHOD: 'PATCH', GET_METHOD: 'GET', excludedQueryParams: ['enrolled'], 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/sessions.js b/src/database/queries/sessions.js index d6ed55883..7cf136783 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -550,10 +550,11 @@ exports.mentorsSessionWithPendingFeedback = async (mentorId, options = {}, compl } } -exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter) => { +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) { @@ -563,6 +564,26 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter } } const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' + console.log('line 567 saasFilter : ', saasFilter) + // 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]}`) + } + } + } + console.log('line 576 saasFilter : ', saasFilter) + const saasFilterClause = saasFilterCondition.length > 0 ? `AND ` + saasFilterCondition[0] : '' + + console.log('line 567 saasFilterClause : ', saasFilterClause) const query = ` WITH filtered_sessions AS ( SELECT id, title, description, start_date, end_date, status, image, mentor_id, visibility, mentor_org_id, created_at, @@ -574,6 +595,7 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter 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 @@ -589,6 +611,8 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter currentEpochTime: currentEpochTime, offset: limit * (page - 1), limit: limit, + // mentor_org_id: null + // mentor_org_id: saasFilter.mentor_org_id ? saasFilter.mentor_org_id : '' } if (filter && typeof filter === 'object') { @@ -599,6 +623,16 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter } } + // Replace saas related query replacements + if (saasFilter && typeof saasFilter === 'object') { + for (const key in saasFilter) { + if (Array.isArray(saasFilter[key])) { + replacements[key] = saasFilter[key] + } + } + } + + console.log('Yeahhh+++++++++:', query, replacements) const sessionIds = await Sequelize.query(query, { type: QueryTypes.SELECT, replacements: replacements, diff --git a/src/services/mentees.js b/src/services/mentees.js index bd841483f..a4d666ebf 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -318,12 +318,21 @@ module.exports = class MenteesHelper { let filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'MentorExtension') - const sessions = await sessionQueries.getUpcomingSessionsFromView(page, limit, search, userId, filteredQuery) - + const saasFilter = await this.filterSessionsBasedOnSaasPolicy(userId, isAMentor) + console.log('saasFilter :', saasFilter) + const sessions = await sessionQueries.getUpcomingSessionsFromView( + page, + limit, + search, + userId, + filteredQuery, + saasFilter + ) + console.log('sessions :', sessions) sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) - // Filter sessions based on saas policy {session contain enrolled + upcoming session} - sessions.rows = await this.filterSessionsBasedOnSaasPolicy(sessions.rows, userId, isAMentor) + // // Filter sessions based on saas policy {session contain enrolled + upcoming session} + // sessions.rows = await this.filterSessionsBasedOnSaasPolicy(sessions.rows, userId, isAMentor) sessions.rows = await this.sessionMentorDetails(sessions.rows) @@ -339,11 +348,11 @@ module.exports = class MenteesHelper { * @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 - } + // if (sessions.length === 0) { + // return sessions + // } let userPolicyDetails // If user is mentor - fetch policy details from mentor extensions else fetch from userExtension @@ -351,6 +360,7 @@ module.exports = class MenteesHelper { userPolicyDetails = await mentorQueries.getMentorExtension(userId, [ 'external_session_visibility', 'org_id', + 'visible_to_organizations', ]) // Throw error if mentor extension not found @@ -365,6 +375,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) { @@ -375,30 +386,66 @@ 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 - } - }) - ) + // 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.concat([userPolicyDetails.org_id]); + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + ? userPolicyDetails.visible_to_organizations.concat([userPolicyDetails.org_id]) + : [userPolicyDetails.org_id] + } else if (userPolicyDetails.external_session_visibility === common.ALL) { + filter.visible_to_organizations = userPolicyDetails.visible_to_organizations + ? userPolicyDetails.visible_to_organizations.concat([userPolicyDetails.org_id]) + : [userPolicyDetails.org_id] + filter.visibility = common.ALL + } + // 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) + // sessions = filteredSessions.filter((session) => session !== undefined) } - return sessions + + // 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) + // } + return filter } catch (err) { return err } From 74627d59b9fa8f2f7f5a3673f437f700a839252c Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 13:05:07 +0530 Subject: [PATCH 12/30] fixed validateInput utils function --- src/database/queries/sessions.js | 9 +++++++++ src/generics/utils.js | 16 ++++++++-------- src/services/sessions.js | 7 ++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index d6ed55883..01dfdd262 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -14,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) diff --git a/src/generics/utils.js b/src/generics/utils.js index 0578295e7..eeebecaff 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) { diff --git a/src/services/sessions.js b/src/services/sessions.js index 2632dfce7..83c0b04b4 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -100,7 +100,7 @@ module.exports = class SessionsHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(bodyData, validationData, 'Session') + let res = utils.validateInput(bodyData, validationData, await sessionQueries.getModelName()) if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', @@ -111,7 +111,7 @@ module.exports = class SessionsHelper { } let sessionModel = await sessionQueries.getColumns() bodyData = utils.restructureBody(bodyData, validationData, sessionModel) - console.log(bodyData) + bodyData.meeting_info = { platform: process.env.DEFAULT_MEETING_SERVICE, value: process.env.DEFAULT_MEETING_SERVICE, @@ -261,7 +261,8 @@ module.exports = class SessionsHelper { //validationData = utils.removeParentEntityTypes(JSON.parse(JSON.stringify(validationData))) const validationData = removeDefaultOrgEntityTypes(entityTypes, orgId) - let res = utils.validateInput(bodyData, validationData, 'Session') + let res = utils.validateInput(bodyData, validationData, await sessionQueries.getModelName()) + if (!res.success) { return common.failureResponse({ message: 'SESSION_CREATION_FAILED', From 24c1a46b6eb12d29f079f2e92ec7917b68471116 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 15:25:03 +0530 Subject: [PATCH 13/30] updated sessions.js --- src/services/sessions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/sessions.js b/src/services/sessions.js index 83c0b04b4..81ba31697 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -5,7 +5,7 @@ const httpStatusCode = require('@generics/http-status') const apiEndpoints = require('@constants/endpoints') const common = require('@constants/common') //const sessionData = require('@db/sessions/queries') -//const notificationTemplateData = require('@db/notification-template/query') +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 const request = require('request') From d382defa88e9cff4fb080e1d7eab29641109915e Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Wed, 15 Nov 2023 16:21:37 +0530 Subject: [PATCH 14/30] Partially Modified Response Processor --- src/generics/utils.js | 60 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index f1bf55c27..46026f22f 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -220,11 +220,16 @@ const entityTypeMapGenerator = (entityTypeData) => { try { const entityTypeMap = new Map() entityTypeData.forEach((entityType) => { - const entities = entityType.entities.map((entity) => entity.value) + 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) } }) @@ -278,25 +283,56 @@ function restructureBody(requestBody, entityData, allowedKeys) { } } -function processDbResponse(session, entityType) { - if (session.meta) { - entityType.forEach((entity) => { +function processDbResponse(responseBody, entityTypes) { + console.log('RESPONSE BODY: ', responseBody) + console.log('ENTITY TYPE: ', entityTypes) + const entityTypeMap = entityTypeMapGenerator(entityTypes) + console.log('ENTITY MAP: ', entityTypeMap) + Object.keys(responseBody).forEach((field) => { + const value = responseBody[field] + if (value !== null && entityTypeMap.has(field)) { + const entityType = entityTypeMap.get(field) + if (Array.isArray(value)) { + } else { + if (entityType.get('entities').has(value)) { + responseBody[field] = { + value, + label: entityType.get('labels').get(value), + } + } else { + responseBody[field] = { + value: 'other', + label: entityTypeMap.get('labels'), + } + } + } + } + }) + + if (responseBody.meta) { + Object.keys(responseBody.meta).forEach((field) => { + const value = responseBody.meta[field] + }) + + entityTypes.forEach((entity) => { const entityTypeValue = entity.value - if (session?.meta?.hasOwnProperty(entityTypeValue)) { + if (responseBody?.meta?.hasOwnProperty(entityTypeValue)) { // Move the key from session.meta to session root level - session[entityTypeValue] = session.meta[entityTypeValue] + responseBody[entityTypeValue] = responseBody.meta[entityTypeValue] // Delete the key from session.meta - delete session.meta[entityTypeValue] + + delete responseBody.meta[entityTypeValue] } }) } - const output = { ...session } // Create a copy of the session object + const output = { ...responseBody } // Create a copy of the session object for (const key in output) { - if (entityType.some((entity) => entity.value === key) && output[key] !== null) { - const matchingEntity = entityType.find((entity) => entity.value === key) + if (entityTypes.some((entity) => entity.value === key) && output[key] !== null) { + const matchingEntity = entityTypes.find((entity) => entity.value === key) const matchingValues = matchingEntity.entities + .filter((entity) => (Array.isArray(output[key]) ? output[key].includes(entity.value) : false)) .map((entity) => ({ value: entity.value, @@ -317,8 +353,8 @@ function processDbResponse(session, entityType) { } } - if (output.meta && output.meta[key] && entityType.some((entity) => entity.value === output.meta[key].value)) { - const matchingEntity = entityType.find((entity) => entity.value === output.meta[key].value) + if (output.meta && output.meta[key] && entityTypes.some((entity) => entity.value === output.meta[key].value)) { + const matchingEntity = entityTypes.find((entity) => entity.value === output.meta[key].value) output.meta[key] = { value: matchingEntity.value, label: matchingEntity.label, From 6037944735c232417dcac0f07c197483ee96b829 Mon Sep 17 00:00:00 2001 From: Priyanka Pradeep <priyanka@tunerlabs.com> Date: Wed, 15 Nov 2023 17:21:42 +0530 Subject: [PATCH 15/30] migration for alter extension table --- ...7-update-column-visible-to-organization.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/database/migrations/20231115113837-update-column-visible-to-organization.js 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..d181ebafd --- /dev/null +++ b/src/database/migrations/20231115113837-update-column-visible-to-organization.js @@ -0,0 +1,22 @@ +'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), + }) + }, + + 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), + }) + }, +} From dcc988cae9ebf386ac3342e72f5a541da0d331dd Mon Sep 17 00:00:00 2001 From: Priyanka Pradeep <priyanka@tunerlabs.com> Date: Wed, 15 Nov 2023 17:30:17 +0530 Subject: [PATCH 16/30] seesion table alter --- .../20231115113837-update-column-visible-to-organization.js | 6 ++++++ src/database/models/sessions.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/database/migrations/20231115113837-update-column-visible-to-organization.js b/src/database/migrations/20231115113837-update-column-visible-to-organization.js index d181ebafd..4da855fd7 100644 --- a/src/database/migrations/20231115113837-update-column-visible-to-organization.js +++ b/src/database/migrations/20231115113837-update-column-visible-to-organization.js @@ -9,6 +9,9 @@ module.exports = { await queryInterface.changeColumn('mentor_extensions', 'visible_to_organizations', { type: Sequelize.ARRAY(Sequelize.INTEGER), }) + await queryInterface.changeColumn('sessions', 'visible_to_organizations', { + type: Sequelize.ARRAY(Sequelize.INTEGER), + }) }, down: async (queryInterface, Sequelize) => { @@ -18,5 +21,8 @@ module.exports = { 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: { From a860b4b2aa234372677a83d5eed74c9a33cc05cf Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Wed, 15 Nov 2023 17:59:31 +0530 Subject: [PATCH 17/30] Modified Migrations --- .../20231115113837-update-column-visible-to-organization.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/migrations/20231115113837-update-column-visible-to-organization.js b/src/database/migrations/20231115113837-update-column-visible-to-organization.js index 4da855fd7..024b9d5df 100644 --- a/src/database/migrations/20231115113837-update-column-visible-to-organization.js +++ b/src/database/migrations/20231115113837-update-column-visible-to-organization.js @@ -2,13 +2,13 @@ module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.changeColumn('user_extensions', 'visible_to_organizations', { + /* 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), - }) + }) */ await queryInterface.changeColumn('sessions', 'visible_to_organizations', { type: Sequelize.ARRAY(Sequelize.INTEGER), }) From fbc4e30c69f0b1cda7ba457b07c4c20ea77a1a8a Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Wed, 15 Nov 2023 18:07:20 +0530 Subject: [PATCH 18/30] Modified Migration' --- ...0231115113837-update-column-visible-to-organization.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/database/migrations/20231115113837-update-column-visible-to-organization.js b/src/database/migrations/20231115113837-update-column-visible-to-organization.js index 024b9d5df..30ff02e41 100644 --- a/src/database/migrations/20231115113837-update-column-visible-to-organization.js +++ b/src/database/migrations/20231115113837-update-column-visible-to-organization.js @@ -9,8 +9,12 @@ module.exports = { await queryInterface.changeColumn('mentor_extensions', 'visible_to_organizations', { type: Sequelize.ARRAY(Sequelize.INTEGER), }) */ - await queryInterface.changeColumn('sessions', '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) + + ')', }) }, From c299f769d5ef404f338674406e47708f67eac034 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 18:24:39 +0530 Subject: [PATCH 19/30] update url --- src/scripts/viewsScript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/viewsScript.js b/src/scripts/viewsScript.js index 8ee9c33ff..f869dff34 100644 --- a/src/scripts/viewsScript.js +++ b/src/scripts/viewsScript.js @@ -5,7 +5,7 @@ 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://mentoring:${process.env.APPLICATION_PORT}` +const mentoringBaseurl = `http://localhost:${process.env.APPLICATION_PORT}` const apiEndpoints = require('../constants/endpoints') /** From 2ce6ab940ec95c92ba686f3b416eead8159b9749 Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Wed, 15 Nov 2023 18:45:52 +0530 Subject: [PATCH 20/30] saas changes with associated option --- src/controllers/v1/users.js | 11 +- src/database/queries/mentorExtension.js | 53 ++++++-- src/database/queries/sessions.js | 117 ++++++++++------- src/services/mentees.js | 67 +--------- src/services/mentors.js | 162 ++++++++++++++++++++---- src/services/sessions.js | 80 +++++++++++- src/services/users.js | 79 +----------- 7 files changed, 335 insertions(+), 234 deletions(-) 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/queries/mentorExtension.js b/src/database/queries/mentorExtension.js index 8dda81a4d..75415c558 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -1,5 +1,5 @@ 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') @@ -127,9 +127,10 @@ module.exports = class MentorExtensionQueries { } } - static async getMentorsByUserIdsFromView(ids, page, limit, filter) { + static async getMentorsByUserIdsFromView(ids, page, limit, filter, saasFilter) { try { const filterConditions = [] + let saasFilterCondition = [] if (filter && typeof filter === 'object') { for (const key in filter) { @@ -140,8 +141,23 @@ module.exports = class MentorExtensionQueries { } const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' - const sessionAttendeesData = await Sequelize.query( - ` + // 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, @@ -152,20 +168,31 @@ module.exports = class MentorExtensionQueries { WHERE user_id IN (${ids.join(',')}) ${filterClause} + ${saasFilterClause} OFFSET :offset LIMIT :limit; - `, - { - replacements: { - offset: limit * (page - 1), - limit: limit, - ...filter, // Add filter parameters to replacements - }, - type: sequelize.QueryTypes.SELECT, + ` + 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, diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index 7cf136783..fa8b8838d 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -662,11 +662,12 @@ exports.findAllByIds = async (ids) => { } } -exports.getMentorsUpcomingSessionsFromView = async (page, limit, search, mentorId, filter) => { +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) { @@ -677,50 +678,78 @@ exports.getMentorsUpcomingSessionsFromView = async (page, limit, search, mentorI } const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' - const sessionAttendeesData = await Sequelize.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} - ORDER BY - start_date ASC - OFFSET - :offset - LIMIT - :limit; - `, - { - replacements: { - mentorId: mentorId, - currentEpochTime: currentEpochTime, - search: `%${search.toLowerCase()}%`, - offset: limit * (page - 1), - limit: limit, - ...filter, // Add filter parameters to replacements - }, - type: sequelize.QueryTypes.SELECT, + // 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, diff --git a/src/services/mentees.js b/src/services/mentees.js index 9985c6853..8f580ca84 100644 --- a/src/services/mentees.js +++ b/src/services/mentees.js @@ -318,8 +318,9 @@ module.exports = class MenteesHelper { let filteredQuery = utils.validateFilters(query, JSON.parse(JSON.stringify(validationData)), 'MentorExtension') + // Create saas fiter for view query const saasFilter = await this.filterSessionsBasedOnSaasPolicy(userId, isAMentor) - console.log('saasFilter :', saasFilter) + const sessions = await sessionQueries.getUpcomingSessionsFromView( page, limit, @@ -328,11 +329,8 @@ module.exports = class MenteesHelper { filteredQuery, saasFilter ) - console.log('sessions :', sessions) - sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) - // // Filter sessions based on saas policy {session contain enrolled + upcoming session} - // sessions.rows = await this.filterSessionsBasedOnSaasPolicy(sessions.rows, userId, isAMentor) + sessions.rows = await this.menteeSessionDetails(sessions.rows, userId) sessions.rows = await this.sessionMentorDetails(sessions.rows) @@ -343,17 +341,12 @@ 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(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) { @@ -392,59 +385,12 @@ module.exports = class MenteesHelper { 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.concat([userPolicyDetails.org_id]); filter.visible_to_organizations = userPolicyDetails.visible_to_organizations - ? userPolicyDetails.visible_to_organizations.concat([userPolicyDetails.org_id]) - : [userPolicyDetails.org_id] } else if (userPolicyDetails.external_session_visibility === common.ALL) { filter.visible_to_organizations = userPolicyDetails.visible_to_organizations - ? userPolicyDetails.visible_to_organizations.concat([userPolicyDetails.org_id]) - : [userPolicyDetails.org_id] filter.visibility = common.ALL } - // 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) } - - // 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) - // } return filter } catch (err) { return err @@ -466,9 +412,6 @@ module.exports = class MenteesHelper { 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( @@ -613,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, diff --git a/src/services/mentors.js b/src/services/mentors.js index d58881a36..4eb941b0b 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,6 @@ 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') @@ -47,12 +47,16 @@ module.exports = class MentorsHelper { }) } + // Filter upcoming sessions based on saas policy + const saasFilter = await menteesService.filterSessionsBasedOnSaasPolicy(menteeUserId, isAMentor) + let upcomingSessions = await sessionQueries.getMentorsUpcomingSessionsFromView( page, limit, search, id, - filteredQuery + filteredQuery, + saasFilter ) if (!upcomingSessions.data.length) { @@ -72,12 +76,6 @@ module.exports = class MentorsHelper { upcomingSessions.data = await this.menteeSessionDetails(upcomingSessions.data, menteeUserId) } - // Filter upcoming sessions based on saas policy - upcomingSessions.data = await menteesService.filterSessionsBasedOnSaasPolicy( - upcomingSessions.data, - menteeUserId, - isAMentor - ) return common.successResponse({ statusCode: httpStatusCode.ok, message: 'UPCOMING_SESSION_FETCHED', @@ -329,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, @@ -491,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) { @@ -501,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', @@ -577,6 +580,69 @@ 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 @@ -602,15 +668,15 @@ module.exports = class MentorsHelper { const ids = userDetails.data.result.data.map((item) => item.values[0].id) - let extensionDetails = await mentorQueries.getMentorsByUserIdsFromView(ids, pageNo, pageSize, filteredQuery) - // Inside your function - extensionDetails.data = extensionDetails.data.filter((item) => item.visibility && item.org_id) - // Filter user data based on SAAS policy - extensionDetails.data = await usersService.filterMentorListBasedOnSaasPolicy( - extensionDetails.data, - userId, - isAMentor + 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])) @@ -640,6 +706,56 @@ module.exports = class MentorsHelper { 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 diff --git a/src/services/sessions.js b/src/services/sessions.js index 2632dfce7..c7be51c8a 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -12,6 +12,7 @@ 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') @@ -140,6 +141,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({ @@ -531,14 +535,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,6 +595,74 @@ module.exports = class SessionsHelper { } } + /** + * @description - check if session is accessible based on user's saas policy. + * @method + * @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 checkIfSessionIsAccessible(sessions, userId, isAMentor) { + try { + 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', + }) + } + + // 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 + } + } + return isAccessible + } catch (err) { + return err + } + } + /** * Sessions list * @method 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 - } - } } From cf014137dd913f5224518d44495c1ffc72d9cd68 Mon Sep 17 00:00:00 2001 From: VISHNU <apple@Apples-MacBook-Pro.local> Date: Wed, 15 Nov 2023 19:07:14 +0530 Subject: [PATCH 21/30] consoles removed --- src/database/queries/sessions.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/database/queries/sessions.js b/src/database/queries/sessions.js index fa8b8838d..c3d2118e5 100644 --- a/src/database/queries/sessions.js +++ b/src/database/queries/sessions.js @@ -564,7 +564,7 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter } } const filterClause = filterConditions.length > 0 ? `AND ${filterConditions.join(' AND ')}` : '' - console.log('line 567 saasFilter : ', saasFilter) + // SAAS related filtering let saasFilterOrgIdClause = '' if (saasFilter && typeof saasFilter === 'object') { @@ -580,10 +580,8 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter } } } - console.log('line 576 saasFilter : ', saasFilter) const saasFilterClause = saasFilterCondition.length > 0 ? `AND ` + saasFilterCondition[0] : '' - console.log('line 567 saasFilterClause : ', saasFilterClause) const query = ` WITH filtered_sessions AS ( SELECT id, title, description, start_date, end_date, status, image, mentor_id, visibility, mentor_org_id, created_at, @@ -611,8 +609,6 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter currentEpochTime: currentEpochTime, offset: limit * (page - 1), limit: limit, - // mentor_org_id: null - // mentor_org_id: saasFilter.mentor_org_id ? saasFilter.mentor_org_id : '' } if (filter && typeof filter === 'object') { @@ -632,7 +628,6 @@ exports.getUpcomingSessionsFromView = async (page, limit, search, userId, filter } } - console.log('Yeahhh+++++++++:', query, replacements) const sessionIds = await Sequelize.query(query, { type: QueryTypes.SELECT, replacements: replacements, From 4bffe26df4547a5b17bc7772b4bf1812f4c5ed74 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Wed, 15 Nov 2023 19:36:41 +0530 Subject: [PATCH 22/30] made index creation unique --- src/envVariables.js | 5 +++++ src/generics/materializedViews.js | 6 ++++-- src/scripts/viewsScript.js | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) 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 index ce7328b13..7d26a50f4 100644 --- a/src/generics/materializedViews.js +++ b/src/generics/materializedViews.js @@ -159,8 +159,10 @@ const materializedViewQueryBuilder = async (model, concreteFields, metaFields) = const createIndexesOnAllowFilteringFields = async (model, modelEntityTypes) => { try { + const uniqueEntityTypeValueList = [...new Set(modelEntityTypes.entityTypeValueList)] + await Promise.all( - modelEntityTypes.entityTypeValueList.map(async (attribute) => { + uniqueEntityTypeValueList.map(async (attribute) => { return await sequelize.query( `CREATE INDEX ${common.materializedViewsPrefix}idx_${model.tableName}_${attribute} ON ${common.materializedViewsPrefix}${model.tableName} (${attribute});` ) @@ -336,7 +338,7 @@ const triggerPeriodicViewRefresh = async () => { try { const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() const modelNames = await modelNameCollector(allowFilteringEntityTypes) - const interval = process.env.REFRESH_VIEW_INTERNAL + const interval = process.env.REFRESH_VIEW_INTERVAL let currentIndex = 0 // Using the mockSetInterval function to simulate setInterval diff --git a/src/scripts/viewsScript.js b/src/scripts/viewsScript.js index f869dff34..56d859e68 100644 --- a/src/scripts/viewsScript.js +++ b/src/scripts/viewsScript.js @@ -105,11 +105,11 @@ const triggerPeriodicViewRefresh = async () => { const allowFilteringEntityTypes = await getAllowFilteringEntityTypes() const modelNames = await modelNameCollector(allowFilteringEntityTypes) - let offset = process.env.REFRESH_VIEW_INTERNAL / modelNames.length + let offset = process.env.REFRESH_VIEW_INTERVAL / modelNames.length modelNames.map((model, index) => { createSchedulerJob( 'repeatable_view_job' + model, - process.env.REFRESH_VIEW_INTERNAL, + process.env.REFRESH_VIEW_INTERVAL, 'repeatable_view_job' + model, true, mentoringBaseurl + '/mentoring/v1/admin/triggerPeriodicViewRefreshInternal?model_name=' + model, From 3aa2de06dc36f1684bb3fb713ed2c3ad69cd3028 Mon Sep 17 00:00:00 2001 From: Priyanka Pradeep <priyanka@tunerlabs.com> Date: Thu, 16 Nov 2023 00:00:03 +0530 Subject: [PATCH 23/30] seeder for mentee enhancement questions --- ...20231115170949-add-new-mentee-questions.js | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/database/seeders/20231115170949-add-new-mentee-questions.js 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) => {}, +} From 2f501d23f2fc1e866cc31b0a028d10c09415b53f Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Thu, 16 Nov 2023 09:39:14 +0530 Subject: [PATCH 24/30] Removing Modification --- src/generics/utils.js | 53 +++++++++---------------------------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index 46026f22f..b80e75dae 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -283,56 +283,25 @@ function restructureBody(requestBody, entityData, allowedKeys) { } } -function processDbResponse(responseBody, entityTypes) { - console.log('RESPONSE BODY: ', responseBody) - console.log('ENTITY TYPE: ', entityTypes) - const entityTypeMap = entityTypeMapGenerator(entityTypes) - console.log('ENTITY MAP: ', entityTypeMap) - Object.keys(responseBody).forEach((field) => { - const value = responseBody[field] - if (value !== null && entityTypeMap.has(field)) { - const entityType = entityTypeMap.get(field) - if (Array.isArray(value)) { - } else { - if (entityType.get('entities').has(value)) { - responseBody[field] = { - value, - label: entityType.get('labels').get(value), - } - } else { - responseBody[field] = { - value: 'other', - label: entityTypeMap.get('labels'), - } - } - } - } - }) - - if (responseBody.meta) { - Object.keys(responseBody.meta).forEach((field) => { - const value = responseBody.meta[field] - }) - - entityTypes.forEach((entity) => { +function processDbResponse(session, entityType) { + if (session.meta) { + entityType.forEach((entity) => { const entityTypeValue = entity.value - if (responseBody?.meta?.hasOwnProperty(entityTypeValue)) { + if (session?.meta?.hasOwnProperty(entityTypeValue)) { // Move the key from session.meta to session root level - responseBody[entityTypeValue] = responseBody.meta[entityTypeValue] + session[entityTypeValue] = session.meta[entityTypeValue] // Delete the key from session.meta - - delete responseBody.meta[entityTypeValue] + delete session.meta[entityTypeValue] } }) } - const output = { ...responseBody } // Create a copy of the session object + const output = { ...session } // Create a copy of the session object for (const key in output) { - if (entityTypes.some((entity) => entity.value === key) && output[key] !== null) { - const matchingEntity = entityTypes.find((entity) => entity.value === key) + 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)) .map((entity) => ({ value: entity.value, @@ -353,8 +322,8 @@ function processDbResponse(responseBody, entityTypes) { } } - if (output.meta && output.meta[key] && entityTypes.some((entity) => entity.value === output.meta[key].value)) { - const matchingEntity = entityTypes.find((entity) => entity.value === output.meta[key].value) + if (output.meta && output.meta[key] && entityType.some((entity) => entity.value === output.meta[key].value)) { + const matchingEntity = entityType.find((entity) => entity.value === output.meta[key].value) output.meta[key] = { value: matchingEntity.value, label: matchingEntity.label, From 2910eddb0eeb87838dd9aaa5a9c8a206414da5f0 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Thu, 16 Nov 2023 12:11:23 +0530 Subject: [PATCH 25/30] added empty check to user response --- src/services/mentors.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/services/mentors.js b/src/services/mentors.js index d58881a36..006989a1d 100644 --- a/src/services/mentors.js +++ b/src/services/mentors.js @@ -600,6 +600,16 @@ module.exports = class MentorsHelper { 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) let extensionDetails = await mentorQueries.getMentorsByUserIdsFromView(ids, pageNo, pageSize, filteredQuery) From 5c25116a82c64384f814bdbc7331c8345337d327 Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Thu, 16 Nov 2023 14:49:45 +0530 Subject: [PATCH 26/30] ProcessDbResponse Fix --- src/generics/utils.js | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index b80e75dae..2a4f4ef34 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -283,42 +283,37 @@ function restructureBody(requestBody, entityData, allowedKeys) { } } -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 - } - return { - value: item, - label: item, - } - }) + output[key] = output[key].map((item) => ({ + value: item.value || item, + label: item.label || item, + })) } } @@ -335,13 +330,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 } From e3d4058cd1b596f9cc77ad16b3687b3eb6ffcfea Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Thu, 16 Nov 2023 15:27:02 +0530 Subject: [PATCH 27/30] Modified Array Entity Check --- src/generics/utils.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index 2a4f4ef34..e742c961d 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -310,10 +310,13 @@ function processDbResponse(responseBody, entityType) { if (matchingValues.length > 0) { output[key] = Array.isArray(output[key]) ? matchingValues : matchingValues[0] } else if (Array.isArray(output[key])) { - output[key] = output[key].map((item) => ({ - value: item.value || item, - label: item.label || item, - })) + output[key] = output[key].map((item) => { + if (item.value && item.label) return item + return { + value: item, + label: item, + } + }) } } From 3e2314b226afd3dc26fa91239b364088968f47a4 Mon Sep 17 00:00:00 2001 From: Nevil <nevil@tunerlabs.com> Date: Thu, 16 Nov 2023 15:32:23 +0530 Subject: [PATCH 28/30] update mentor and mentee delete query --- src/database/queries/mentorExtension.js | 3 +-- src/database/queries/userExtension.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/database/queries/mentorExtension.js b/src/database/queries/mentorExtension.js index 8dda81a4d..2ca4bb55b 100644 --- a/src/database/queries/mentorExtension.js +++ b/src/database/queries/mentorExtension.js @@ -72,7 +72,7 @@ module.exports = class MentorExtensionQueries { { designation: null, area_of_expertise: [], - education_qualification: [], + education_qualification: null, rating: null, meta: null, stats: null, @@ -83,7 +83,6 @@ module.exports = class MentorExtensionQueries { external_session_visibility: null, external_mentor_visibility: null, deleted_at: Date.now(), - org_id, }, { where: { 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: { From f0ffc5b001a64cb8f411cc42b31147b9da72bc6e Mon Sep 17 00:00:00 2001 From: Priyanka Pradeep <priyanka@tunerlabs.com> Date: Thu, 16 Nov 2023 17:00:58 +0530 Subject: [PATCH 29/30] expiry check for token --- src/middlewares/authenticator.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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) { From 13daf21d568811cd7630fe9f18cd7e49984d9910 Mon Sep 17 00:00:00 2001 From: joffinjoy <joffinjoy@gmail.com> Date: Thu, 16 Nov 2023 18:26:49 +0530 Subject: [PATCH 30/30] Added Missing Other Case --- src/generics/utils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/generics/utils.js b/src/generics/utils.js index e742c961d..b9589ef75 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -260,7 +260,10 @@ function restructureBody(requestBody, entityData, allowedKeys) { if (recognizedEntities.length > 0) if (allowedKeys.includes(currentFieldName)) requestBody[currentFieldName] = recognizedEntities else requestBody.meta[currentFieldName] = recognizedEntities - if (customEntities.length > 0) requestBody.custom_entity_text[currentFieldName] = customEntities + 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] = {