From 2ea1707d08d1b3dcd1916600652337b2de155b5d Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Mon, 11 May 2026 14:50:30 +0530 Subject: [PATCH 1/8] ci: update docker-image workflow with branch input, version validation, and git tagging Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker-image.yml | 93 ++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 1f18816fa..d626ac75e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Image +name: Build, Tag, and Push Docker Image on: workflow_dispatch: @@ -6,20 +6,28 @@ on: tag_version: description: 'Docker image tag version (e.g., 3.3.11)' required: true + branch: + description: 'Branch to build from (default: staging)' + required: false + default: 'staging' env: - DOCKER_IMAGE_NAME: elevate-mentoring # Configure your image name here + DOCKER_IMAGE_NAME: elevate-mentoring DOCKER_REGISTRY: docker.io DOCKER_NAMESPACE: shikshalokamqa +permissions: + contents: write + jobs: docker-image-build-and-push: - if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout code from target branch uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || 'staging' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -30,31 +38,64 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Get Docker tag version (fail if not provided) + - name: Get Docker tag version id: get-version + shell: bash run: | - # Use workflow_dispatch input if provided - if [ ! -z "${{ github.event.inputs.tag_version }}" ]; then - VERSION="${{ github.event.inputs.tag_version }}" - # Or use TAG_VERSION env if set (for push/PR) - elif [ ! -z "${TAG_VERSION}" ]; then - VERSION="${TAG_VERSION}" - else - echo "Error: Docker image version must be provided as workflow_dispatch input or TAG_VERSION env." + VERSION="${{ github.event.inputs.tag_version }}" + # Strip leading "v" if present + VERSION="${VERSION#v}" + # Enforce x.y.z or x.y.z-rc.1 format + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "Error: tag_version must look like 3.4.0 or 3.4.0-rc.1 (no other suffixes allowed)" + exit 1 + fi + printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT" + + - name: Check if Git tag already exists + run: | + VERSION="${{ steps.get-version.outputs.version }}" + git fetch --tags + if git rev-parse "$VERSION" >/dev/null 2>&1; then + echo "Error: Git tag $VERSION already exists" exit 1 fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Check if version exists on Docker Hub - id: check-version run: | - VERSION=${{ steps.get-version.outputs.version }} - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://hub.docker.com/v2/namespaces/${{ env.DOCKER_NAMESPACE }}/repositories/${{ env.DOCKER_IMAGE_NAME }}/tags/$VERSION") + VERSION="${{ steps.get-version.outputs.version }}" + + LOGIN_RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"username": "${{ secrets.DOCKERHUB_USERNAME }}", "password": "${{ secrets.DOCKERHUB_TOKEN }}"}' \ + "https://hub.docker.com/v2/users/login") + LOGIN_HTTP=$(echo "$LOGIN_RESPONSE" | tail -n1) + LOGIN_BODY=$(echo "$LOGIN_RESPONSE" | head -n-1) + + if [ "$LOGIN_HTTP" -ne 200 ]; then + echo "Error: Docker Hub login failed with HTTP $LOGIN_HTTP" + exit 1 + fi + + TOKEN=$(echo "$LOGIN_BODY" | jq -r .token) + if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "Error: Docker Hub login succeeded but returned no token" + exit 1 + fi + + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "https://hub.docker.com/v2/namespaces/${{ env.DOCKER_NAMESPACE }}/repositories/${{ env.DOCKER_IMAGE_NAME }}/tags/$VERSION") + if [ "$RESPONSE" -eq 200 ]; then echo "Error: Tag $VERSION already exists on Docker Hub" exit 1 + elif [ "$RESPONSE" -eq 404 ]; then + echo "Tag $VERSION not found on Docker Hub — safe to push" else - echo "Tag $VERSION does not exist, proceeding with build" + echo "Error: Unexpected HTTP $RESPONSE from Docker Hub tag check; aborting to fail safe" + exit 1 fi - name: Extract metadata @@ -78,10 +119,16 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Image digest - run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}" + - name: Create and push Git tag + run: | + VERSION="${{ steps.get-version.outputs.version }}" + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git tag -a "$VERSION" -m "Release $VERSION" + git push origin "$VERSION" - - name: Print pushed tags + - name: Job summary run: | - echo "Pushed tags:" - echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' \ No newline at end of file + echo "### Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY + echo "**Tag:** ${{ steps.get-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Digest:** ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY From 504bbd88d3ae3f8906c4bbffb3397157cfbb2e53 Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Wed, 20 May 2026 14:44:22 +0530 Subject: [PATCH 2/8] savepoint-1 --- src/generics/cacheHelper.js | 11 +++ src/helpers/entityTypeCache.js | 155 ++++++++++++--------------------- 2 files changed, 66 insertions(+), 100 deletions(-) diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js index 1a7969621..867efba5a 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') diff --git a/src/helpers/entityTypeCache.js b/src/helpers/entityTypeCache.js index 968c6e067..4af8f39a4 100644 --- a/src/helpers/entityTypeCache.js +++ b/src/helpers/entityTypeCache.js @@ -171,126 +171,81 @@ 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 = [] + // 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) - 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 : [], - })) - - // 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 - }) + 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 [] + + const results = [] + const cacheMisses = [] + + for (const entityType of entityTypes) { + try { + const cached = await cacheHelper.entityTypes.getCacheOnly( + tenantCode, + entityType.organization_code, + modelName, + entityType.value + ) + + if (cached && !Array.isArray(cached)) { + results.push(cached) + } else { + cacheMisses.push(entityType) } - - return formattedCachedEntities + } catch (cacheError) { + cacheMisses.push(entityType) } - } catch (cacheError) { - console.error(`Entity type cache read failed (cache+DB): ${cacheError.message}`, cacheError) - throw cacheError } - // Cache miss - fetch from database with user-centric approach - - let allEntityTypes = [] - 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) - } - } catch (dbError) { - console.error(`Failed to fetch entity types for model ${modelName} from database:`, dbError.message) - return [] - } + if (cacheMisses.length > 0) { + const missedIds = cacheMisses.map((e) => e.id) + const missedWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities( + { id: { [Op.in]: missedIds } }, + tenantCode + ) - // Cache individual entities using user tenant/org context (regardless of where they were found) - if (allEntityTypes && allEntityTypes.length > 0) { - for (const entityType of allEntityTypes) { + for (const entityTypeWithEntities of missedWithEntities) { try { await cacheHelper.entityTypes.set( - tenantCode, // Always cache under user context - orgCode, // Always cache under user context + tenantCode, + entityTypeWithEntities.organization_code, modelName, - entityType.value, - entityType + entityTypeWithEntities.value, + entityTypeWithEntities ) - } catch (individualCacheError) {} - } - 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 - } + } catch (cacheSetError) { + // silent — cache write failure must not block the response } - return true - }) + results.push(entityTypeWithEntities) + } } - return filteredEntityTypes + return results } catch (error) { - console.error(`❌ Failed to get entity types for model ${modelName}:`, error) + console.error(`Failed to get entity types for model ${modelName}:`, error) return [] } } From 76db9a01eed1001b442ce742c18ea88cd80409aa Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Wed, 20 May 2026 14:49:09 +0530 Subject: [PATCH 3/8] Restore docker-image.yml to master version Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker-image.yml | 93 ++++++++---------------------- 1 file changed, 23 insertions(+), 70 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d626ac75e..1f18816fa 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,4 +1,4 @@ -name: Build, Tag, and Push Docker Image +name: Build and Push Docker Image on: workflow_dispatch: @@ -6,28 +6,20 @@ on: tag_version: description: 'Docker image tag version (e.g., 3.3.11)' required: true - branch: - description: 'Branch to build from (default: staging)' - required: false - default: 'staging' env: - DOCKER_IMAGE_NAME: elevate-mentoring + DOCKER_IMAGE_NAME: elevate-mentoring # Configure your image name here DOCKER_REGISTRY: docker.io DOCKER_NAMESPACE: shikshalokamqa -permissions: - contents: write - jobs: docker-image-build-and-push: + if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - - name: Checkout code from target branch + - name: Checkout code uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.branch || 'staging' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -38,64 +30,31 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Get Docker tag version + - name: Get Docker tag version (fail if not provided) id: get-version - shell: bash run: | - VERSION="${{ github.event.inputs.tag_version }}" - # Strip leading "v" if present - VERSION="${VERSION#v}" - # Enforce x.y.z or x.y.z-rc.1 format - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then - echo "Error: tag_version must look like 3.4.0 or 3.4.0-rc.1 (no other suffixes allowed)" - exit 1 - fi - printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT" - - - name: Check if Git tag already exists - run: | - VERSION="${{ steps.get-version.outputs.version }}" - git fetch --tags - if git rev-parse "$VERSION" >/dev/null 2>&1; then - echo "Error: Git tag $VERSION already exists" + # Use workflow_dispatch input if provided + if [ ! -z "${{ github.event.inputs.tag_version }}" ]; then + VERSION="${{ github.event.inputs.tag_version }}" + # Or use TAG_VERSION env if set (for push/PR) + elif [ ! -z "${TAG_VERSION}" ]; then + VERSION="${TAG_VERSION}" + else + echo "Error: Docker image version must be provided as workflow_dispatch input or TAG_VERSION env." exit 1 fi + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Check if version exists on Docker Hub + id: check-version run: | - VERSION="${{ steps.get-version.outputs.version }}" - - LOGIN_RESPONSE=$(curl -s -w "\n%{http_code}" \ - -X POST \ - -H "Content-Type: application/json" \ - -d '{"username": "${{ secrets.DOCKERHUB_USERNAME }}", "password": "${{ secrets.DOCKERHUB_TOKEN }}"}' \ - "https://hub.docker.com/v2/users/login") - LOGIN_HTTP=$(echo "$LOGIN_RESPONSE" | tail -n1) - LOGIN_BODY=$(echo "$LOGIN_RESPONSE" | head -n-1) - - if [ "$LOGIN_HTTP" -ne 200 ]; then - echo "Error: Docker Hub login failed with HTTP $LOGIN_HTTP" - exit 1 - fi - - TOKEN=$(echo "$LOGIN_BODY" | jq -r .token) - if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then - echo "Error: Docker Hub login succeeded but returned no token" - exit 1 - fi - - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer $TOKEN" \ - "https://hub.docker.com/v2/namespaces/${{ env.DOCKER_NAMESPACE }}/repositories/${{ env.DOCKER_IMAGE_NAME }}/tags/$VERSION") - + VERSION=${{ steps.get-version.outputs.version }} + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://hub.docker.com/v2/namespaces/${{ env.DOCKER_NAMESPACE }}/repositories/${{ env.DOCKER_IMAGE_NAME }}/tags/$VERSION") if [ "$RESPONSE" -eq 200 ]; then echo "Error: Tag $VERSION already exists on Docker Hub" exit 1 - elif [ "$RESPONSE" -eq 404 ]; then - echo "Tag $VERSION not found on Docker Hub — safe to push" else - echo "Error: Unexpected HTTP $RESPONSE from Docker Hub tag check; aborting to fail safe" - exit 1 + echo "Tag $VERSION does not exist, proceeding with build" fi - name: Extract metadata @@ -119,16 +78,10 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Create and push Git tag - run: | - VERSION="${{ steps.get-version.outputs.version }}" - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git tag -a "$VERSION" -m "Release $VERSION" - git push origin "$VERSION" + - name: Image digest + run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}" - - name: Job summary + - name: Print pushed tags run: | - echo "### Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY - echo "**Tag:** ${{ steps.get-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Digest:** ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY + echo "Pushed tags:" + echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' \ No newline at end of file From 2b207a28a53e27096d954a9deea59a22cdc5a0b6 Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Wed, 20 May 2026 15:36:44 +0530 Subject: [PATCH 4/8] fix: add getCacheOnly to entityTypes namespace and fix getAllEntityTypesForModel cache strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getCacheOnly() to entityTypes namespace in cacheHelper — pure cache read with no DB fallback, consistent with sessions/mentors/mentees namespaces - Fix getAllEntityTypesForModel: add cache-read-first step using getCacheOnly before writing; fix write format from [entityType] (array) to entityTypeWithEntities (single object) to align with getEntityTypesAndEntitiesForModel convention - Both functions now share the same key convention (entityType.organization_code) and value format — warm entries written by sessions/details are reused by filterlist and other callers without redundant DB queries Co-Authored-By: Claude Sonnet 4.6 --- src/generics/cacheHelper.js | 73 ++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js index 867efba5a..756081b3a 100644 --- a/src/generics/cacheHelper.js +++ b/src/generics/cacheHelper.js @@ -653,48 +653,55 @@ const entityTypes = { */ 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 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 + const userEntityTypes = await entityTypeQueries.findUserEntityTypesAndEntities( + { + status: 'ACTIVE', + organization_code: { [Op.in]: orgCandidates }, + model_names: { [Op.contains]: [modelName] }, + }, + tenantCode + ) + + if (!userEntityTypes || userEntityTypes.length === 0) return [] + + const results = [] + const cacheMisses = [] + + for (const entityTypeWithEntities of userEntityTypes) { + const cached = await this.getCacheOnly( + tenantCode, + entityTypeWithEntities.organization_code, + modelName, + entityTypeWithEntities.value ) - if (userEntityTypes && userEntityTypes.length > 0) { - entityTypes.push(...userEntityTypes) - console.log( - `💾 Entity types for model ${modelName} found in user tenant/org: ${userEntityTypes.length} results` - ) + if (cached && !Array.isArray(cached)) { + results.push(cached) + } else { + cacheMisses.push(entityTypeWithEntities) } - } 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 - } + for (const entityTypeWithEntities of cacheMisses) { + try { + await this.set( + tenantCode, + entityTypeWithEntities.organization_code, + modelName, + entityTypeWithEntities.value, + entityTypeWithEntities + ) + } catch (cacheError) { + // silent — cache write failure must not block the response } - console.log( - `💾 Cached ${entityTypes.length} entity types for model ${modelName} under user context: tenant:${tenantCode}:org:${orgCode}` - ) + results.push(entityTypeWithEntities) } - return entityTypes || [] + return results } catch (error) { - console.error(`❌ Failed to get all entity types for model ${modelName}:`, error) + console.error(`Failed to get all entity types for model ${modelName}:`, error) return [] } }, From 3d517c1da40c006878ca4a4a5e0c9ac188170090 Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Wed, 20 May 2026 16:06:14 +0530 Subject: [PATCH 5/8] fix: refactor getAllEntityTypesForModel to use cache-first 3-step pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace always-DB approach with the same pattern used in getEntityTypesAndEntitiesForModel: - Step 1: findAllEntityTypes (cheap, no entities join) - Step 2: getCacheOnly per entity type — genuine cache check, no DB fallback - Step 3: findUserEntityTypesAndEntities only for cache misses Warm entries written by either function are now shared — sessions/details and filterlist reuse each other's cached values without redundant DB queries. Co-Authored-By: Claude Sonnet 4.6 --- src/generics/cacheHelper.js | 57 +++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js index 756081b3a..c22ce7e03 100644 --- a/src/generics/cacheHelper.js +++ b/src/generics/cacheHelper.js @@ -656,47 +656,56 @@ const entityTypes = { 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 + // Step 1 — cheap: entity type definitions only, no entities join + const entityTypeDefs = await entityTypeQueries.findAllEntityTypes( + { [Op.in]: orgCandidates }, + tenantCode, + undefined, + { status: 'ACTIVE', model_names: { [Op.contains]: [modelName] } } ) - if (!userEntityTypes || userEntityTypes.length === 0) return [] + if (!entityTypeDefs || entityTypeDefs.length === 0) return [] + // Step 2 — cache check per entity type using the entity's own org code const results = [] const cacheMisses = [] - for (const entityTypeWithEntities of userEntityTypes) { + for (const entityTypeDef of entityTypeDefs) { const cached = await this.getCacheOnly( tenantCode, - entityTypeWithEntities.organization_code, + entityTypeDef.organization_code, modelName, - entityTypeWithEntities.value + entityTypeDef.value ) if (cached && !Array.isArray(cached)) { results.push(cached) } else { - cacheMisses.push(entityTypeWithEntities) + cacheMisses.push(entityTypeDef) } } - for (const entityTypeWithEntities of cacheMisses) { - try { - await this.set( - tenantCode, - entityTypeWithEntities.organization_code, - modelName, - entityTypeWithEntities.value, - entityTypeWithEntities - ) - } catch (cacheError) { - // silent — cache write failure must not block the response + // Step 3 — entities join only for cache misses + 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 this.set( + tenantCode, + entityTypeWithEntities.organization_code, + modelName, + entityTypeWithEntities.value, + entityTypeWithEntities + ) + } catch (cacheError) { + // silent — cache write failure must not block the response + } + results.push(entityTypeWithEntities) } - results.push(entityTypeWithEntities) } return results From 113e6070771c9e3b623338c88677a4dd5bf36e1c Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Wed, 20 May 2026 16:47:28 +0530 Subject: [PATCH 6/8] refactor: consolidate entity type cache logic into entityTypeCache.js (DRY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move getAllEntityTypesForModel and getEntityTypesWithMentorOrg out of cacheHelper.js into entityTypeCache.js where all entity type cache logic now lives: - getEntityTypesWithMentorOrg delegates to getEntityTypesAndEntitiesForModel directly — getAllEntityTypesForModel is no longer needed as a separate function - organisationExtensionQueries imported at module level (was lazy require) - sessions.js updated to call entityTypeCache.getEntityTypesWithMentorOrg instead of cacheHelper.entityTypes.getEntityTypesWithMentorOrg Dependency graph is now one-way: sessions.js → entityTypeCache.js → cacheHelper.js Co-Authored-By: Claude Sonnet 4.6 --- src/generics/cacheHelper.js | 111 --------------------------------- src/helpers/entityTypeCache.js | 36 +++++++++++ src/services/sessions.js | 4 +- 3 files changed, 38 insertions(+), 113 deletions(-) diff --git a/src/generics/cacheHelper.js b/src/generics/cacheHelper.js index c22ce7e03..a85f26258 100644 --- a/src/generics/cacheHelper.js +++ b/src/generics/cacheHelper.js @@ -643,117 +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 { - const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE - const orgCandidates = [...new Set([orgCode, defaultOrgCode].filter(Boolean))] - - // Step 1 — cheap: entity type definitions only, no entities join - const entityTypeDefs = await entityTypeQueries.findAllEntityTypes( - { [Op.in]: orgCandidates }, - tenantCode, - undefined, - { status: 'ACTIVE', model_names: { [Op.contains]: [modelName] } } - ) - - if (!entityTypeDefs || entityTypeDefs.length === 0) return [] - - // Step 2 — cache check per entity type using the entity's own org code - const results = [] - const cacheMisses = [] - - for (const entityTypeDef of entityTypeDefs) { - const cached = await this.getCacheOnly( - tenantCode, - entityTypeDef.organization_code, - modelName, - entityTypeDef.value - ) - if (cached && !Array.isArray(cached)) { - results.push(cached) - } else { - cacheMisses.push(entityTypeDef) - } - } - - // Step 3 — entities join only for cache misses - 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 this.set( - tenantCode, - entityTypeWithEntities.organization_code, - modelName, - entityTypeWithEntities.value, - entityTypeWithEntities - ) - } catch (cacheError) { - // silent — cache write failure must not block the response - } - results.push(entityTypeWithEntities) - } - } - - return results - } 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 4af8f39a4..26b2c6330 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') @@ -326,9 +327,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, From 02a4277cc37ac8d3965b4464b1bacaeb3ea81d56 Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Thu, 21 May 2026 13:11:09 +0530 Subject: [PATCH 7/8] savepoint-1 --- src/helpers/entityTypeCache.js | 223 +++++++++++---------------------- 1 file changed, 76 insertions(+), 147 deletions(-) diff --git a/src/helpers/entityTypeCache.js b/src/helpers/entityTypeCache.js index 26b2c6330..95720ba97 100644 --- a/src/helpers/entityTypeCache.js +++ b/src/helpers/entityTypeCache.js @@ -32,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 [] } } @@ -198,57 +112,72 @@ async function getEntityTypesAndEntitiesForModel(modelName, tenantCode, orgCode, ) 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 [] + } +} - const results = [] - const cacheMisses = [] - - for (const entityType of entityTypes) { - try { - const cached = await cacheHelper.entityTypes.getCacheOnly( - tenantCode, - entityType.organization_code, - modelName, - entityType.value - ) +/** + * 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 = [] - if (cached && !Array.isArray(cached)) { - results.push(cached) - } else { - cacheMisses.push(entityType) - } - } catch (cacheError) { - cacheMisses.push(entityType) + for (const entityTypeDef of entityTypeDefs) { + try { + 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 (cacheError) { + cacheMisses.push(entityTypeDef) } + } - if (cacheMisses.length > 0) { - const missedIds = cacheMisses.map((e) => e.id) - const missedWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities( + if (cacheMisses.length > 0) { + const missedIds = cacheMisses.map((e) => e.id) + let missedWithEntities = [] + try { + 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 - } - results.push(entityTypeWithEntities) + } catch (dbError) { + console.error('Failed to fetch entity types from database:', dbError.message) + return results + } + 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 } + results.push(entityTypeWithEntities) } - - return results - } 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 From b005ab2ce1854e4f9da9350d7d54ae5f1baefdc5 Mon Sep 17 00:00:00 2001 From: borkarsaish65 Date: Thu, 21 May 2026 14:18:52 +0530 Subject: [PATCH 8/8] cr-comment-1 --- src/helpers/entityTypeCache.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/helpers/entityTypeCache.js b/src/helpers/entityTypeCache.js index 95720ba97..ab2191d3f 100644 --- a/src/helpers/entityTypeCache.js +++ b/src/helpers/entityTypeCache.js @@ -151,16 +151,10 @@ async function resolveEntityTypesWithCache(entityTypeDefs, tenantCode, modelName if (cacheMisses.length > 0) { const missedIds = cacheMisses.map((e) => e.id) - let missedWithEntities = [] - try { - missedWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities( - { id: { [Op.in]: missedIds } }, - tenantCode - ) - } catch (dbError) { - console.error('Failed to fetch entity types from database:', dbError.message) - return results - } + const missedWithEntities = await entityTypeQueries.findUserEntityTypesAndEntities( + { id: { [Op.in]: missedIds } }, + tenantCode + ) for (const entityTypeWithEntities of missedWithEntities) { try { await cacheHelper.entityTypes.set(