Skip to content
11 changes: 11 additions & 0 deletions src/database/queries/mentorExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ module.exports = class MentorExtensionQueries {
try {
data = { ...data, is_mentor: true }

// Remove partition key columns - Citus doesn't allow updating these
if (data.user_id) {
delete data['user_id']
}
if (data.tenant_code) {
delete data['tenant_code']
}

let whereClause
if (_.isEmpty(customFilter)) {
Expand Down Expand Up @@ -159,10 +163,17 @@ module.exports = class MentorExtensionQueries {

for (const [key, attribute] of Object.entries(modelAttributes)) {
// Skip primary key or explicitly excluded fields
// FIX: Added organization_code and tenant_code to exclusion list
// ROOT CAUSE: These fields have NOT NULL constraints in the database.
// When removeMentorDetails() tried to set them to null, Sequelize threw:
// "notNull Violation: UserExtension.organization_code cannot be null"
// SOLUTION: Exclude these fields from nullification to maintain data integrity
if (
attribute.primaryKey ||
key === 'user_id' ||
key === 'organization_id' || // required field
key === 'organization_code' || // FIX: required NOT NULL field - cannot be nullified
key === 'tenant_code' || // FIX: required NOT NULL field - cannot be nullified
key === 'created_at' ||
key === 'updated_at' ||
key === 'is_mentor' // has default value
Expand Down
4 changes: 4 additions & 0 deletions src/database/queries/roleExtentions.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ module.exports = class RoleExtensionService {

static async updateRoleExtension(title, updateData, tenantCode) {
try {
// Remove partition key columns - Citus doesn't allow updating these
if (updateData.tenant_code) {
delete updateData['tenant_code']
}
Comment on lines +80 to +82
Copy link
Copy Markdown
Collaborator

@nevil-mathew nevil-mathew Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be removed in the validator right if added in the req body?

const filter = { title: title, tenant_code: tenantCode }

const [rowsUpdated, updatedExtension] = await RoleExtension.update(updateData, {
Expand Down
13 changes: 11 additions & 2 deletions src/database/queries/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1099,22 +1099,31 @@ exports.getSessionsAssignedToMentor = async (mentorUserId, tenantCode) => {
}
}

exports.getSessionsAssignedToMentor = async (mentorUserId) => {
// FIX: Added tenantCode parameter and included it in JOIN and WHERE clauses
// ROOT CAUSE: Citus distributed database requires JOINs to include the distribution column (tenant_code).
// Original query failed with: "complex joins are only supported when all distributed tables are
// co-located and joined on their distribution columns"
// SOLUTION: Added tenant_code to both the JOIN condition (line 1108) and WHERE clause (line 1112)
// to ensure Citus can properly route the query across distributed shards.
exports.getSessionsAssignedToMentor = async (mentorUserId, tenantCode) => {
try {
const query = `
SELECT s.*, sa.mentee_id
FROM ${Session.tableName} s
LEFT JOIN session_attendees sa ON s.id = sa.session_id
WHERE s.mentor_id = :mentorUserId
AND s.tenant_code = sa.tenant_code
WHERE s.mentor_id = :mentorUserId
AND s.start_date > :currentTime
AND s.deleted_at IS NULL
AND s.tenant_code = :tenantCode
`

const sessionsToDelete = await Sequelize.query(query, {
type: QueryTypes.SELECT,
replacements: {
mentorUserId,
currentTime: Math.floor(Date.now() / 1000),
tenantCode,
},
})

Expand Down
4 changes: 4 additions & 0 deletions src/database/queries/userExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ module.exports = class MenteeExtensionQueries {

static async updateMenteeExtension(userId, data, options = {}, customFilter = {}, tenantCode) {
try {
// Remove partition key columns - Citus doesn't allow updating these
if (data.user_id) {
delete data['user_id']
}
if (data.tenant_code) {
delete data['tenant_code']
}
let whereClause
if (_.isEmpty(customFilter)) {
whereClause = { user_id: userId, tenant_code: tenantCode }
Expand Down
41 changes: 41 additions & 0 deletions src/helpers/organizationCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

/**
* Organization Cache Helper
*
* FIX: Created this wrapper to break circular dependency
*
* ROOT CAUSE: Circular dependency chain caused "getDefaults is not a function" error:
* cacheHelper.js → getDefaultOrgId.js → user.js → cacheHelper.js
*
* When Node.js loads these modules:
* 1. cacheHelper.js starts loading, imports getDefaultOrgId.js
* 2. getDefaultOrgId.js imports user.js (for userRequests.fetchOrgDetails)
* 3. user.js imports cacheHelper.js
* 4. cacheHelper.js is not fully loaded yet, returns partial/empty exports
* 5. getDefaults function is undefined at this point
*
* SOLUTION: user.js now imports this wrapper instead of cacheHelper directly.
* This breaks the cycle because:
* cacheHelper.js → getDefaultOrgId.js → user.js → organizationCache.js → cacheHelper.js
*
* Even though there's still a path to cacheHelper, the actual cacheHelper functions
* are only called at RUNTIME (inside set/get functions), not at MODULE LOAD TIME.
* By the time these functions are called, all modules are fully loaded.
*/

const cacheHelper = require('@generics/cacheHelper')

/**
* Set organization details in cache
*/
exports.set = async (tenantCode, orgCode, orgId, data) => {
return await cacheHelper.organizations.set(tenantCode, orgCode, orgId, data)
}

/**
* Get organization details from cache
*/
exports.get = async (tenantCode, orgCode, orgId) => {
return await cacheHelper.organizations.get(tenantCode, orgCode, orgId)
}
14 changes: 6 additions & 8 deletions src/requests/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ const httpStatusCode = require('@generics/http-status')
const responses = require('@helpers/responses')
const common = require('@constants/common')
const { Op } = require('sequelize')
const cacheHelper = require('@generics/cacheHelper')
const usersHelper = require('@helpers/users')
// FIX: Use organizationCache wrapper instead of cacheHelper directly
// This breaks the circular dependency: cacheHelper → getDefaultOrgId → user.js → cacheHelper
// See helpers/organizationCache.js for detailed explanation
const organizationCache = require('@helpers/organizationCache')

const menteeQueries = require('@database/queries/userExtension')
const organisationExtensionQueries = require('@database/queries/organisationExtension')
// Removed cacheHelper to break circular dependency with getDefaultOrgId

const emailEncryption = require('@utils/emailEncryption')
const _ = require('lodash')
Expand Down Expand Up @@ -99,7 +101,7 @@ const getOrgDetails = async function ({ organizationId, tenantCode }) {
// If we got the data, populate the cache for future use
if (organizationDetails?.organization_code) {
try {
await cacheHelper.organizations.set(
await organizationCache.set(
tenantCode,
organizationDetails.organization_code,
organizationId,
Expand Down Expand Up @@ -885,11 +887,7 @@ const getUserDetailedListUsingCache = async function (userIds, tenantCode, delet
const cacheResults = await Promise.all(
organizations.map(async (org) => ({
org,
orgCachedData: await cacheHelper.organizations.get(
tenantCode,
org.organization_code,
org.organization_id
),
orgCachedData: await organizationCache.get(tenantCode, org.organization_code, org.organization_id),
}))
)

Expand Down
60 changes: 39 additions & 21 deletions src/services/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ module.exports = class AdminService {
let result = {}

// Step 1: Fetch user details
let getUserDetails = []
let userInfo = null
let userTenantCode = tenantCode

// Optimization: If admin, query directly without tenant restriction (1 query)
Expand All @@ -182,18 +182,17 @@ module.exports = class AdminService {
getUserDetails = userDetail ? [userDetail] : []
} else {
// Regular user deleting themselves - use tenant code from token (optimized path)
getUserDetails = await menteeQueries.getUsersByUserIds([userId], {}, tenantCode)
const getUserDetails = await menteeQueries.getUsersByUserIds([userId], {}, tenantCode)
userInfo = getUserDetails?.[0]
}

if (!getUserDetails || getUserDetails.length === 0) {
if (!userInfo) {
return responses.failureResponse({
statusCode: httpStatusCode.bad_request,
message: 'USER_NOT_FOUND',
result,
})
}

const userInfo = getUserDetails[0]
const isMentor = userInfo.is_mentor === true

// Step 2: Check if user is a session manager
Expand Down Expand Up @@ -763,7 +762,17 @@ module.exports = class AdminService {
const sentRequestsData = sentRequests.rows || []

// Get requests where user is requestee (received requests)
const sessionRequestMapping = await sessionRequestMappingQueries.getSessionsMapping(userId, tenantCode)
// FIX: Added missing 'status' parameter to getSessionsMapping call
// ROOT CAUSE: Function signature is getSessionsMapping(userId, status, tenantCode) but was called
// with only (userId, tenantCode). This caused tenantCode to be passed as 'status' parameter,
// and the actual tenantCode parameter received 'undefined'.
// Error: "WHERE parameter 'tenant_code' has invalid 'undefined' value"
// SOLUTION: Pass all three parameters in correct order: userId, status, tenantCode
const sessionRequestMapping = await sessionRequestMappingQueries.getSessionsMapping(
userId,
common.CONNECTIONS_STATUS.REQUESTED,
tenantCode
)
const sessionRequestIds = Array.isArray(sessionRequestMapping)
? sessionRequestMapping.map((s) => s.request_session_id)
: []
Expand Down Expand Up @@ -1133,7 +1142,7 @@ module.exports = class AdminService {
orgCode: orgCodes,
templateData: { menteeName },
subjectData: { menteeName },
tenantCodes,
tenantCode: tenantCodes,
})
}

Expand Down Expand Up @@ -1190,7 +1199,7 @@ module.exports = class AdminService {
sessionTime: sessionDateTime.format('hh:mm A'),
},
subjectData: { sessionName: sessionDetails.title },
tenantCodes,
tenantCode: tenantCodes,
})
} catch (error) {
console.error('Error notifying mentor about private session cancellation:', error)
Expand Down Expand Up @@ -1408,28 +1417,37 @@ module.exports = class AdminService {
}
}

// FIX: Changed 'tenantCodes,' to 'tenantCode: tenantCodes,'
// ROOT CAUSE: Using shorthand 'tenantCodes,' creates property {tenantCodes: tenantCodes}
// but sendGenericNotification() expects {tenantCode: ...} (singular, not plural).
// This caused template lookup to fail with undefined tenant, leading to:
// "Cannot read properties of undefined (reading 'replace')" when composing email body.
// SOLUTION: Explicitly map the parameter name: tenantCode: tenantCodes
static async notifyMenteesAboutMentorDeletion(mentees, mentorName, orgCodes, tenantCodes) {
return await NotificationHelper.sendGenericNotification({
recipients: mentees,
templateCode: process.env.MENTOR_DELETION_NOTIFICATION_EMAIL_TEMPLATE,
orgCode: orgCodes,
templateData: { mentorName },
subjectData: { mentorName },
tenantCodes,
tenantCode: tenantCodes,
})
}

// FIX: Removed JOIN with non-existent request_session_mapping table
// ROOT CAUSE: Original query tried to JOIN with 'request_session_mapping' table that doesn't exist:
// "relation 'request_session_mapping' does not exist"
// SOLUTION: Query session_request table directly since requestee_id is already a column in it.
// The RequestSession model (line 15-18) already has requestee_id field, no mapping table needed.
static async getPendingSessionRequestsForMentor(mentorUserId, tenantCode) {
try {
const query = `
SELECT rs.*, rm.requestee_id
FROM ${RequestSession.tableName} rs
INNER JOIN request_session_mapping rm ON rs.id = rm.request_session_id
WHERE rm.requestee_id = :mentorUserId
AND rs.status = :requestedStatus
AND rs.deleted_at IS NULL
AND rs.tenant_code = :tenantCode
AND rm.tenant_code = :tenantCode
SELECT *
FROM ${RequestSession.tableName}
WHERE requestee_id = :mentorUserId
AND status = :requestedStatus
AND deleted_at IS NULL
AND tenant_code = :tenantCode
`

const pendingRequests = await sequelize.query(query, {
Expand Down Expand Up @@ -1476,7 +1494,7 @@ module.exports = class AdminService {
orgCode: orgCodes,
templateData: { sessionName: request.title },
subjectData: { sessionName: request.title },
tenantCodes,
tenantCode: tenantCodes,
})
}
}
Expand Down Expand Up @@ -1532,7 +1550,7 @@ module.exports = class AdminService {
orgCode: orgCodes,
templateData: { mentorName, sessionList },
subjectData: { mentorName },
tenantCodes,
tenantCode: tenantCodes,
})
}
})
Expand Down Expand Up @@ -1589,7 +1607,7 @@ module.exports = class AdminService {
orgCode: orgCodes,
templateData: { menteeName: menteeName, sessionList: sessionList },
subjectData: { menteeName: menteeName },
tenantCodes,
tenantCode: tenantCodes,
})
}
})
Expand Down Expand Up @@ -1643,7 +1661,7 @@ module.exports = class AdminService {
orgCode: orgCodes,
templateData: { sessionName: session.title },
subjectData: { sessionName: session.title },
tenantCodes,
tenantCode: tenantCodes,
})
}
}
Expand Down