diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js index 1a7969621..a85f26258 100644 --- a/src/generics/cacheHelper.js +++ b/src/generics/cacheHelper.js @@ -610,6 +610,17 @@ const entityTypes = { }) }, + async getCacheOnly(tenantCode, orgCode, modelName, entityValue) { + try { + const compositeId = `model:${modelName}:${entityValue}` + const useInternal = nsUseInternal('entityTypes') + const cacheKey = await buildKey({ tenantCode, orgCode, ns: 'entityTypes', id: compositeId }) + return await get(cacheKey, { useInternal }) + } catch (error) { + return null + } + }, + async delete(tenantCode, orgCode, modelName, entityValue) { const compositeId = `model:${modelName}:${entityValue}` const useInternal = nsUseInternal('entityTypes') @@ -632,101 +643,6 @@ const entityTypes = { async clearAll(tenantCode, orgCode) { return await evictNamespace({ tenantCode, orgCode: orgCode, ns: 'entityTypes' }) }, - - /** - * Get all entity types for a specific model using direct database query - * @param {string} tenantCode - Tenant code - * @param {string} orgCode - Organization code - * @param {string} modelName - Model name (e.g., 'Session', 'UserExtension') - * @returns {Promise} Array of all entity types for the model - */ - async getAllEntityTypesForModel(tenantCode, orgCode, modelName) { - try { - // Get defaults internally for database query - let entityTypes = [] - try { - const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE - const orgCandidates = [...new Set([orgCode, defaultOrgCode].filter(Boolean))] - - const userEntityTypes = await entityTypeQueries.findUserEntityTypesAndEntities( - { - status: 'ACTIVE', - organization_code: { [Op.in]: orgCandidates }, - model_names: { [Op.contains]: [modelName] }, - }, - tenantCode - ) - if (userEntityTypes && userEntityTypes.length > 0) { - entityTypes.push(...userEntityTypes) - console.log( - `💾 Entity types for model ${modelName} found in user tenant/org: ${userEntityTypes.length} results` - ) - } - } catch (dbError) { - console.error(`Failed to fetch entity types for model ${modelName} from database:`, dbError.message) - return [] - } - - // Cache each entity type individually using standard cache pattern - if (entityTypes && entityTypes.length > 0) { - for (const entityType of entityTypes) { - try { - await this.set(tenantCode, orgCode, modelName, entityType.value, [entityType]) - } catch (cacheError) { - // Continue if caching fails for individual entity type - } - } - console.log( - `💾 Cached ${entityTypes.length} entity types for model ${modelName} under user context: tenant:${tenantCode}:org:${orgCode}` - ) - } - - return entityTypes || [] - } catch (error) { - console.error(`❌ Failed to get all entity types for model ${modelName}:`, error) - return [] - } - }, - - /** - * Get entity types for specific model with mentor org code resolution using standard cache - * @param {string} tenantCode - Tenant code - * @param {string} currentOrgCode - Current organization code - * @param {string} mentorOrganizationId - Mentor's organization ID (numeric) - * @param {string} modelName - Model name ('Session' or 'UserExtension') - * @returns {Promise} Array of entity types - */ - async getEntityTypesWithMentorOrg(tenantCode, currentOrgCode, mentorOrganizationId, modelName) { - try { - // Step 1: Get mentor organization code using organization cache - let mentorOrgCode = null - if (mentorOrganizationId) { - try { - const mentorOrg = await organizations.get(tenantCode, currentOrgCode, mentorOrganizationId) - mentorOrgCode = mentorOrg?.organization_code - } catch (orgCacheError) { - console.warn('Organization cache lookup failed, falling back to database query') - // Fallback: Direct database query for organization code - const organisationExtensionQueries = require('@database/queries/organisationExtension') - const orgData = await organisationExtensionQueries.findOne( - { organization_id: mentorOrganizationId }, - tenantCode, - { attributes: ['organization_code'], raw: true } - ) - mentorOrgCode = orgData?.organization_code - } - } - - // Step 2: Use mentor org code if available, otherwise current org code - const effectiveOrgCode = mentorOrgCode || currentOrgCode - - // Step 3: Get entity types for the specific model and org - return await this.getAllEntityTypesForModel(tenantCode, effectiveOrgCode, modelName) - } catch (error) { - console.error('Failed to get entity types with mentor org resolution:', error) - return [] - } - }, } /** diff --git a/src/helpers/entityTypeCache.js b/src/helpers/entityTypeCache.js index 968c6e067..ab2191d3f 100644 --- a/src/helpers/entityTypeCache.js +++ b/src/helpers/entityTypeCache.js @@ -1,6 +1,7 @@ // Dependencies const httpStatusCode = require('@generics/http-status') const entityTypeQueries = require('../database/queries/entityType') +const organisationExtensionQueries = require('@database/queries/organisationExtension') const { Op } = require('sequelize') const { getDefaults } = require('@helpers/getDefaultOrgId') const responses = require('@helpers/responses') @@ -31,120 +32,34 @@ async function getEntityTypesAndEntitiesWithCache(originalFilter, tenantCode, or const orgFilter = { [Op.in]: orgCodeArray.filter(Boolean) } try { - // If no modelName provided, use direct database query with user-centric approach + // No modelName — cache key cannot be built, go straight to DB if (!modelName) { - // Step 1: ALWAYS fetch from user tenant and org codes - let userFilter = { - ...originalFilter, - organization_code: orgFilter, - } - const userResults = await entityTypeQueries.findUserEntityTypesAndEntities(userFilter, tenantCode) - let dbResult = userResults ? [...userResults] : [] - - return dbResult || [] - } - - // Get entity values from filter for cache checking - const entityValues = originalFilter.value && originalFilter.value[Op.in] ? originalFilter.value[Op.in] : [] - - // If we have specific entity values, try to get them from cache first - if (entityValues.length > 0) { - let cachedEntities = [] - let hasCachedData = false - - // Check cache for each entity value using user codes - for (const entityValue of entityValues) { - try { - const cachedEntity = await cacheHelper.entityTypes.get(tenantCode, orgCode, modelName, entityValue) - - if (cachedEntity) { - cachedEntities.push(cachedEntity) - hasCachedData = true - } - } catch (cacheError) {} - } - - // If we found cached data, apply original filter logic and return - if (hasCachedData) { - const filteredData = cachedEntities.filter((entityType) => { - // Apply all original filter conditions - for (const [key, value] of Object.entries(originalFilter)) { - if (key === 'organization_code' || key === 'tenant_code') { - // Skip tenant/org filtering as cache is already scoped - continue - } - if (key === 'model_names' && value[Op.contains]) { - const requiredModels = value[Op.contains] - const entityModels = entityType.model_names || [] - const hasRequiredModel = requiredModels.some((reqModel) => entityModels.includes(reqModel)) - if (!hasRequiredModel) { - return false - } - } else if (key === 'value' && value[Op.in]) { - if (!value[Op.in].includes(entityType.value)) { - return false - } - } else if (Array.isArray(value)) { - if (!value.includes(entityType[key])) { - return false - } - } else { - if (entityType[key] !== value) { - return false - } - } - } - return true - }) - - return filteredData - } - } - - // Cache miss - fetch from database with user-centric approach - - let dbResult = null - try { - let userFilter = { - ...originalFilter, - organization_code: orgFilter, - } - dbResult = await entityTypeQueries.findUserEntityTypesAndEntities(userFilter, tenantCode) - } catch (dbError) { - console.error(`Failed to fetch entity types from database:`, dbError.message) - return [] - } - - // Cache individual entities using user tenant/org context (regardless of where they were found) - if (dbResult && dbResult.length > 0) { - for (const entityType of dbResult) { - try { - await cacheHelper.entityTypes.set( - tenantCode, // Always cache under user context - orgCode, // Always cache under user context - modelName, - entityType.value, - entityType - ) - } catch (cacheSetError) {} - } - console.log( - `💾 Cached ${dbResult.length} entity types under user context: tenant:${tenantCode}:org:${orgCode}` + return ( + (await entityTypeQueries.findUserEntityTypesAndEntities( + { ...originalFilter, organization_code: orgFilter }, + tenantCode + )) || [] ) } - return dbResult || [] + const entityTypeDefs = await entityTypeQueries.findAllEntityTypes( + orgFilter, + tenantCode, + undefined, + originalFilter + ) + + if (!entityTypeDefs || entityTypeDefs.length === 0) return [] + return resolveEntityTypesWithCache(entityTypeDefs, tenantCode, modelName) } catch (error) { - console.error(`❌ Failed to get entity types with cache:`, error) - // Fallback to database query with user codes + console.error('Failed to get entity types with cache:', error) try { - let userFilter = { - ...originalFilter, - organization_code: orgFilter, - } - return await entityTypeQueries.findUserEntityTypesAndEntities(userFilter, tenantCode) + return await entityTypeQueries.findUserEntityTypesAndEntities( + { ...originalFilter, organization_code: orgFilter }, + tenantCode + ) } catch (fallbackError) { - console.error(`❌ Fallback database query also failed:`, fallbackError) + console.error('Fallback database query also failed:', fallbackError) return [] } } @@ -171,128 +86,92 @@ async function getEntityTypesAndEntitiesForModel(modelName, tenantCode, orgCode, console.error('Failed to get defaults for getEntityTypesAndEntitiesForModel:', error.message) } - if (!defaults || !defaults.orgCode || !defaults.tenantCode) { + if (!defaults || !defaults.orgCode) { return responses.failureResponse({ - message: 'DEFAULT_ORG_CODE_OR_TENANT_CODE_NOT_SET', + message: 'DEFAULT_ORG_CODE_NOT_SET', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) } - // Try to get known entity types from cache first using user codes - const entityValues = - additionalFilters.value && additionalFilters.value[Op.in] ? additionalFilters.value[Op.in] : [] - const cachedEntities = [] - - try { - // Check cache for each entity value using user codes only - for (const entityValue of entityValues) { - try { - const cachedEntity = await cacheHelper.entityTypes.get(tenantCode, orgCode, modelName, entityValue) - - if (cachedEntity && cachedEntity.entities) { - cachedEntities.push(cachedEntity) - } - } catch (entityFetchError) { - // Silent fail for cache errors - } - } - - // If we found cached entities, format and apply filters - if (cachedEntities.length > 0) { - let formattedCachedEntities = cachedEntities.map((cachedEntity) => ({ - ...cachedEntity, - entities: Array.isArray(cachedEntity.entities) ? cachedEntity.entities : [], - })) + // Normalize orgCode: accept array or single string, always include default org + const orgCodeArray = Array.isArray(orgCode) ? [...orgCode] : [orgCode] + if (!orgCodeArray.includes(defaults.orgCode)) orgCodeArray.push(defaults.orgCode) + const cleanOrgCodes = orgCodeArray.filter(Boolean) - // Apply additional filters to cached results - if (additionalFilters && Object.keys(additionalFilters).length > 0) { - formattedCachedEntities = formattedCachedEntities.filter((entityType) => { - for (const [key, value] of Object.entries(additionalFilters)) { - if (Array.isArray(value)) { - if (!value.includes(entityType[key])) { - return false - } - } else if (entityType[key] !== value) { - return false - } - } - return true - }) - } - - return formattedCachedEntities - } - } catch (cacheError) { - console.error(`Entity type cache read failed (cache+DB): ${cacheError.message}`, cacheError) - throw cacheError + const typeFilter = { + status: 'ACTIVE', + model_names: { [Op.contains]: [modelName] }, + ...additionalFilters, } + const entityTypes = await entityTypeQueries.findAllEntityTypes( + { [Op.in]: cleanOrgCodes }, + tenantCode, + undefined, + typeFilter + ) + + if (!entityTypes || entityTypes.length === 0) return [] + return resolveEntityTypesWithCache(entityTypes, tenantCode, modelName) + } catch (error) { + console.error(`Failed to get entity types for model ${modelName}:`, error) + return [] + } +} - // Cache miss - fetch from database with user-centric approach +/** + * Shared Steps 2+3: check cache per entity type, fetch entities only for misses. + * Used by getEntityTypesAndEntitiesForModel and getEntityTypesAndEntitiesWithCache. + * @param {Array} entityTypeDefs - entity type rows from Step 1 (no entities join) + * @param {string} tenantCode + * @param {string} modelName + * @returns {Promise} entity types with entities + */ +async function resolveEntityTypesWithCache(entityTypeDefs, tenantCode, modelName) { + const results = [] + const cacheMisses = [] - let allEntityTypes = [] + for (const entityTypeDef of entityTypeDefs) { try { - // Step 1: ALWAYS fetch from user tenant and org codes - // Normalize orgCode: accept array or single string, always include default org - const orgCodeArray = Array.isArray(orgCode) ? [...orgCode] : [orgCode] - if (defaults.orgCode && !orgCodeArray.includes(defaults.orgCode)) { - orgCodeArray.push(defaults.orgCode) - } - const userFilter = { - status: 'ACTIVE', - organization_code: { [Op.in]: orgCodeArray.filter(Boolean) }, - model_names: { [Op.contains]: [modelName] }, - } - // Handle both array and single value for tenantCode - const userEntityTypes = await entityTypeQueries.findUserEntityTypesAndEntities(userFilter, tenantCode) - if (userEntityTypes && userEntityTypes.length > 0) { - allEntityTypes.push(...userEntityTypes) + const cached = await cacheHelper.entityTypes.getCacheOnly( + tenantCode, + entityTypeDef.organization_code, + modelName, + entityTypeDef.value + ) + if (cached && !Array.isArray(cached)) { + results.push(cached) + } else { + cacheMisses.push(entityTypeDef) } - } catch (dbError) { - console.error(`Failed to fetch entity types for model ${modelName} from database:`, dbError.message) - return [] + } catch (cacheError) { + cacheMisses.push(entityTypeDef) } + } - // Cache individual entities using user tenant/org context (regardless of where they were found) - if (allEntityTypes && allEntityTypes.length > 0) { - for (const entityType of allEntityTypes) { - try { - await cacheHelper.entityTypes.set( - tenantCode, // Always cache under user context - orgCode, // Always cache under user context - modelName, - entityType.value, - entityType - ) - } catch (individualCacheError) {} + if (cacheMisses.length > 0) { + const missedIds = cacheMisses.map((e) => e.id) + const missedWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities( + { id: { [Op.in]: missedIds } }, + tenantCode + ) + for (const entityTypeWithEntities of missedWithEntities) { + try { + await cacheHelper.entityTypes.set( + tenantCode, + entityTypeWithEntities.organization_code, + modelName, + entityTypeWithEntities.value, + entityTypeWithEntities + ) + } catch (cacheSetError) { + // silent — cache write failure must not block the response } - console.log( - `💾 Cached ${allEntityTypes.length} entity types for model ${modelName} under user context: tenant:${tenantCode}:org:${orgCode}` - ) - } - - // Apply additional filters to the database results - let filteredEntityTypes = allEntityTypes || [] - if (additionalFilters && Object.keys(additionalFilters).length > 0) { - filteredEntityTypes = filteredEntityTypes.filter((entityType) => { - for (const [key, value] of Object.entries(additionalFilters)) { - if (Array.isArray(value)) { - if (!value.includes(entityType[key])) { - return false - } - } else if (entityType[key] !== value) { - return false - } - } - return true - }) + results.push(entityTypeWithEntities) } - - return filteredEntityTypes - } catch (error) { - console.error(`❌ Failed to get entity types for model ${modelName}:`, error) - return [] } + + return results } // Cache filter function removed - use direct database queries instead @@ -371,9 +250,44 @@ async function getEntityTypeByValue(modelName, entityValue, tenantCode, orgCode) return found } +/** + * Resolve entity types for a model, using the mentor's org code when available. + * Delegates to getEntityTypesAndEntitiesForModel after resolving the effective org. + * @param {string} tenantCode + * @param {string} currentOrgCode - caller's org code (fallback if mentor org not found) + * @param {string} mentorOrganizationId - numeric org ID of the mentor (may be null) + * @param {string} modelName + */ +async function getEntityTypesWithMentorOrg(tenantCode, currentOrgCode, mentorOrganizationId, modelName) { + try { + let mentorOrgCode = null + if (mentorOrganizationId) { + try { + const mentorOrg = await cacheHelper.organizations.get(tenantCode, currentOrgCode, mentorOrganizationId) + mentorOrgCode = mentorOrg?.organization_code + } catch (orgCacheError) { + console.warn('Organization cache lookup failed, falling back to database query') + const orgData = await organisationExtensionQueries.findOne( + { organization_id: mentorOrganizationId }, + tenantCode, + { attributes: ['organization_code'], raw: true } + ) + mentorOrgCode = orgData?.organization_code + } + } + + const effectiveOrgCode = mentorOrgCode || currentOrgCode + return getEntityTypesAndEntitiesForModel(modelName, tenantCode, effectiveOrgCode) + } catch (error) { + console.error('Failed to get entity types with mentor org resolution:', error) + return [] + } +} + module.exports = { getEntityTypesAndEntitiesWithCache, getEntityTypesAndEntitiesForModel, getEntityTypeByValue, + getEntityTypesWithMentorOrg, clearModelCache, } diff --git a/src/services/sessions.js b/src/services/sessions.js index ffc5294e5..540bbbd90 100644 --- a/src/services/sessions.js +++ b/src/services/sessions.js @@ -1729,14 +1729,14 @@ module.exports = class SessionsHelper { let accessorEntityTypes = [] try { // Get Session model entity types - sessionEntityTypes = await cacheHelper.entityTypes.getEntityTypesWithMentorOrg( + sessionEntityTypes = await entityTypeCache.getEntityTypesWithMentorOrg( tenantCode, orgCode, sessionDetails.mentor_organization_id, sessionModelName ) - accessorEntityTypes = await cacheHelper.entityTypes.getEntityTypesWithMentorOrg( + accessorEntityTypes = await entityTypeCache.getEntityTypesWithMentorOrg( tenantCode, sessionAccessorDetails.organization_code, sessionAccessorDetails.organization_id,