From f08a6a442ef217edf9f0de67ec4d6336f1f854ec Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Wed, 8 Apr 2026 00:31:29 +0530 Subject: [PATCH 1/3] Enhance tenant replication process by adding default configuration fetching and improving backfill validation. Updated tenant service to include a method for fetching default tenant configuration and modified the tenant consumer to utilize this configuration during replication. Improved error handling in backfill script for missing tenant fields. --- src/generics/kafka/consumers/tenant.js | 31 ++++--- src/scripts/backfillTenantData.js | 12 ++- src/services/tenant.js | 121 ++++++++++++++++++------- 3 files changed, 117 insertions(+), 47 deletions(-) diff --git a/src/generics/kafka/consumers/tenant.js b/src/generics/kafka/consumers/tenant.js index ef79271af..c6b7c55a5 100644 --- a/src/generics/kafka/consumers/tenant.js +++ b/src/generics/kafka/consumers/tenant.js @@ -42,22 +42,29 @@ var messageReceived = function (message) { updated_by: created_by ? created_by.toString() : null, } const { created } = await tenantQueries.upsert(tenantData) - if (created) { - await tenantService.replicateConfigFromDefaultTenant(code, org_id, org_code) - - // Build materialized views for the new tenant + if (created || message.backfill) { + await tenantService.replicateConfigFromDefaultTenant( + code, + org_id, + org_code, + message.defaultConfig || null + ) + // Rebuild materialized views — entity_types may have changed during replication const entityTypesGroupedByModel = await materializedViewsService.triggerViewBuild(code) - // Register periodic refresh jobs in scheduler service for the new tenant - const baseInterval = process.env.REFRESH_VIEW_INTERVAL + if (created) { + // Register periodic refresh jobs only for new tenants (existing ones already have jobs) + const baseInterval = process.env.REFRESH_VIEW_INTERVAL - for (const { modelName } of entityTypesGroupedByModel) { - const interval = - (modelName === 'UserExtension' && process.env.USER_EXTENSION_REFRESH_VIEW_INTERVAL) || - (modelName === 'Session' && process.env.SESSION_REFRESH_VIEW_INTERVAL) || - baseInterval + for (const { modelName } of entityTypesGroupedByModel) { + const interval = + (modelName === 'UserExtension' && + process.env.USER_EXTENSION_REFRESH_VIEW_INTERVAL) || + (modelName === 'Session' && process.env.SESSION_REFRESH_VIEW_INTERVAL) || + baseInterval - materializedViewsService.scheduleViewRefreshJob(code, modelName, interval) + materializedViewsService.scheduleViewRefreshJob(code, modelName, interval) + } } } break diff --git a/src/scripts/backfillTenantData.js b/src/scripts/backfillTenantData.js index e4f5f4581..f2a8490ed 100644 --- a/src/scripts/backfillTenantData.js +++ b/src/scripts/backfillTenantData.js @@ -32,6 +32,7 @@ const fs = require('fs') const path = require('path') const csv = require('csv-parser') const tenantConsumer = require('@generics/kafka/consumers/tenant') +const TenantService = require('@services/tenant') /** * Parses a CSV file and returns an array of row objects. @@ -63,9 +64,11 @@ async function backfillTenants(tenants, options = {}) { let success = 0 let failed = 0 + const defaultConfig = await TenantService.fetchDefaultTenantConfig() + for (const tenant of tenants) { - if (!tenant.code || !tenant.name) { - console.error(`[SKIP] Missing required field (code or name):`, tenant) + if (!tenant.code || !tenant.name || !tenant.org_id || !tenant.org_code) { + console.error(`[SKIP] Missing required field (code, name, org_id, or org_code):`, tenant) failed++ continue } @@ -84,9 +87,10 @@ async function backfillTenants(tenants, options = {}) { status: tenant.status || 'ACTIVE', description: tenant.description || null, logo: tenant.logo || null, - org_id: tenant.org_id || process.env.DEFAULT_ORG_ID, - org_code: tenant.org_code || process.env.DEFAULT_ORGANISATION_CODE, + org_id: tenant.org_id, + org_code: tenant.org_code, backfill: true, + defaultConfig, } try { diff --git a/src/services/tenant.js b/src/services/tenant.js index 741ff07f2..f7f009cee 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -67,7 +67,62 @@ async function replicateWithIdMap({ label, fetchSource, transform, bulkCreate, f } module.exports = class TenantService { - static async replicateConfigFromDefaultTenant(newTenantCode, newOrgId, newOrgCode) { + static async fetchDefaultTenantConfig() { + const defaultTenantCode = process.env.DEFAULT_TENANT_CODE + const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE + + if (!defaultTenantCode || !defaultOrgCode) { + throw new Error('DEFAULT_TENANT_CODE and DEFAULT_ORGANISATION_CODE env vars are required for replication') + } + + const [ + notificationTemplates, + forms, + entityTypes, + questions, + questionSets, + reportTypes, + reports, + reportQueries, + reportRoleMappings, + roleExtensions, + ] = await Promise.all([ + NotificationTemplateQueries.findTemplatesByFilter({ + organization_code: defaultOrgCode, + tenant_code: defaultTenantCode, + }), + FormQueries.findFormsByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + EntityTypeQueries.findAllEntityTypes([defaultOrgCode], defaultTenantCode, null), + QuestionQueries.find({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), + QuestionSetQueries.findQuestionSets({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), + ReportTypeQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + ReportQueries.findAllReports({ organization_code: defaultOrgCode }, defaultTenantCode), + ReportQueryQueries.findReportQueries({ organization_code: defaultOrgCode }, defaultTenantCode), + ReportRoleMappingQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + RoleExtensionQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + ]) + + const entityTypeIds = entityTypes.map((et) => et.id) + const entities = entityTypeIds.length + ? await EntityQueries.findAllEntities({ entity_type_id: { [Op.in]: entityTypeIds } }, defaultTenantCode) + : [] + + return { + notificationTemplates, + forms, + entityTypes, + entities, + questions, + questionSets, + reportTypes, + reports, + reportQueries, + reportRoleMappings, + roleExtensions, + } + } + + static async replicateConfigFromDefaultTenant(newTenantCode, newOrgId, newOrgCode, defaultConfig = null) { const defaultTenantCode = process.env.DEFAULT_TENANT_CODE const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE @@ -85,6 +140,7 @@ module.exports = class TenantService { } const newOrgIdStr = newOrgId.toString() const resolvedOrgCode = newOrgCode || defaultOrgCode + const config = defaultConfig || (await TenantService.fetchDefaultTenantConfig()) const baseTransform = (extra = {}) => @@ -97,14 +153,31 @@ module.exports = class TenantService { const transaction = await db.sequelize.transaction() try { + // ── 0. Clear existing config for this tenant (ensures fresh data on re-run) ── + const configTables = [ + 'entities', + 'entity_types', // entities first (references entity_types) + 'question_sets', + 'questions', + 'notification_templates', + 'forms', + 'report_role_mapping', + 'report_queries', + 'reports', + 'report_types', + 'role_extensions', + ] + for (const table of configTables) { + await db.sequelize.query(`DELETE FROM "${table}" WHERE tenant_code = :tenantCode`, { + replacements: { tenantCode: newTenantCode }, + transaction, + }) + } + // ── 1. Notification Templates ──────────────────────────────────────── await replicateResource({ label: 'Notification templates', - fetchSource: () => - NotificationTemplateQueries.findTemplatesByFilter({ - organization_code: defaultOrgCode, - tenant_code: defaultTenantCode, - }), + fetchSource: () => Promise.resolve(config.notificationTemplates), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => NotificationTemplateQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -113,8 +186,7 @@ module.exports = class TenantService { // ── 2. Forms ───────────────────────────────────────────────────────── await replicateResource({ label: 'Forms', - fetchSource: () => - FormQueries.findFormsByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.forms), transform: baseTransform({ organization_id: newOrgIdStr, version: 0 }), bulkCreate: (items, opts) => FormQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -123,7 +195,7 @@ module.exports = class TenantService { // ── 3. Entity Types ─────────────────────────────────────────────────── const entityTypeIdMap = await replicateWithIdMap({ label: 'Entity types', - fetchSource: () => EntityTypeQueries.findAllEntityTypes([defaultOrgCode], defaultTenantCode, null), + fetchSource: () => Promise.resolve(config.entityTypes), transform: baseTransform({ organization_id: newOrgIdStr, parent_id: null }), bulkCreate: (items, opts) => EntityTypeQueries.bulkCreate(items, newTenantCode, opts), fetchExisting: () => EntityTypeQueries.findAllEntityTypes([defaultOrgCode], newTenantCode, null), @@ -138,11 +210,8 @@ module.exports = class TenantService { await replicateResource({ label: 'Entities', fetchSource: () => - EntityQueries.findAllEntities( - { entity_type_id: { [Op.in]: oldEntityTypeIds } }, - defaultTenantCode - ), - filter: (e) => entityTypeIdMap[e.entity_type_id] !== undefined, + Promise.resolve(config.entities.filter((e) => oldEntityTypeIds.includes(e.entity_type_id))), + filter: (e) => entityTypeIdMap[e.entity_type_id] != null, transform: (e) => ({ ...baseTransform()(e), entity_type_id: entityTypeIdMap[e.entity_type_id], @@ -155,8 +224,7 @@ module.exports = class TenantService { // ── 5. Questions ────────────────────────────────────────────────────── const questionIdMap = await replicateWithIdMap({ label: 'Questions', - fetchSource: () => - QuestionQueries.find({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), + fetchSource: () => Promise.resolve(config.questions), transform: baseTransform(), bulkCreate: (items, opts) => QuestionQueries.bulkCreate(items, newTenantCode, { ...opts, returning: true }), @@ -168,11 +236,7 @@ module.exports = class TenantService { // ── 6. Question Sets ────────────────────────────────────────────────── await replicateResource({ label: 'Question sets', - fetchSource: () => - QuestionSetQueries.findQuestionSets({ - organization_code: defaultOrgCode, - tenant_code: defaultTenantCode, - }), + fetchSource: () => Promise.resolve(config.questionSets), transform: (qs) => ({ ...baseTransform()(qs), questions: (qs.questions || []).map((qId) => { @@ -187,8 +251,7 @@ module.exports = class TenantService { // ── 7. Report Types ─────────────────────────────────────────────────── await replicateResource({ label: 'Report types', - fetchSource: () => - ReportTypeQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.reportTypes), transform: baseTransform(), bulkCreate: (items, opts) => ReportTypeQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -197,8 +260,7 @@ module.exports = class TenantService { // ── 8. Reports ──────────────────────────────────────────────────────── await replicateResource({ label: 'Reports', - fetchSource: () => - ReportQueries.findAllReports({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.reports), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => ReportQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -207,8 +269,7 @@ module.exports = class TenantService { // ── 9. Report Queries ───────────────────────────────────────────────── await replicateResource({ label: 'Report queries', - fetchSource: () => - ReportQueryQueries.findReportQueries({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.reportQueries), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => ReportQueryQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -217,8 +278,7 @@ module.exports = class TenantService { // ── 10. Report Role Mappings ────────────────────────────────────────── await replicateResource({ label: 'Report role mappings', - fetchSource: () => - ReportRoleMappingQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.reportRoleMappings), transform: baseTransform(), bulkCreate: (items, opts) => ReportRoleMappingQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -227,8 +287,7 @@ module.exports = class TenantService { // ── 11. Role Extensions ─────────────────────────────────────────────── await replicateResource({ label: 'Role extensions', - fetchSource: () => - RoleExtensionQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), + fetchSource: () => Promise.resolve(config.roleExtensions), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => RoleExtensionQueries.bulkCreate(items, newTenantCode, opts), transaction, From 5785c271c38139e31a0ea0493a4f59022fa54e45 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 14 Apr 2026 13:31:47 +0530 Subject: [PATCH 2/3] REVERTING THE CODE BACK TO ORIGINAL --- src/generics/kafka/consumers/tenant.js | 31 +++---- src/scripts/backfillTenantData.js | 30 ++++-- src/services/tenant.js | 121 +++++++------------------ 3 files changed, 63 insertions(+), 119 deletions(-) diff --git a/src/generics/kafka/consumers/tenant.js b/src/generics/kafka/consumers/tenant.js index c6b7c55a5..ef79271af 100644 --- a/src/generics/kafka/consumers/tenant.js +++ b/src/generics/kafka/consumers/tenant.js @@ -42,29 +42,22 @@ var messageReceived = function (message) { updated_by: created_by ? created_by.toString() : null, } const { created } = await tenantQueries.upsert(tenantData) - if (created || message.backfill) { - await tenantService.replicateConfigFromDefaultTenant( - code, - org_id, - org_code, - message.defaultConfig || null - ) - // Rebuild materialized views — entity_types may have changed during replication + if (created) { + await tenantService.replicateConfigFromDefaultTenant(code, org_id, org_code) + + // Build materialized views for the new tenant const entityTypesGroupedByModel = await materializedViewsService.triggerViewBuild(code) - if (created) { - // Register periodic refresh jobs only for new tenants (existing ones already have jobs) - const baseInterval = process.env.REFRESH_VIEW_INTERVAL + // Register periodic refresh jobs in scheduler service for the new tenant + const baseInterval = process.env.REFRESH_VIEW_INTERVAL - for (const { modelName } of entityTypesGroupedByModel) { - const interval = - (modelName === 'UserExtension' && - process.env.USER_EXTENSION_REFRESH_VIEW_INTERVAL) || - (modelName === 'Session' && process.env.SESSION_REFRESH_VIEW_INTERVAL) || - baseInterval + for (const { modelName } of entityTypesGroupedByModel) { + const interval = + (modelName === 'UserExtension' && process.env.USER_EXTENSION_REFRESH_VIEW_INTERVAL) || + (modelName === 'Session' && process.env.SESSION_REFRESH_VIEW_INTERVAL) || + baseInterval - materializedViewsService.scheduleViewRefreshJob(code, modelName, interval) - } + materializedViewsService.scheduleViewRefreshJob(code, modelName, interval) } } break diff --git a/src/scripts/backfillTenantData.js b/src/scripts/backfillTenantData.js index f2a8490ed..805ee9b49 100644 --- a/src/scripts/backfillTenantData.js +++ b/src/scripts/backfillTenantData.js @@ -15,6 +15,10 @@ * Safe to re-run: the consumer uses findOrCreate (idempotent) and * replication only runs for genuinely new tenants. * + * After the backfill, correctTenantData runs automatically to fix/refresh + * config for all tenants in the CSV (handles already-existing tenants whose + * config may be missing or stale). + * * Usage: * node src/scripts/backfillTenantData.js * node src/scripts/backfillTenantData.js --dry-run @@ -32,7 +36,7 @@ const fs = require('fs') const path = require('path') const csv = require('csv-parser') const tenantConsumer = require('@generics/kafka/consumers/tenant') -const TenantService = require('@services/tenant') +const { correctTenants } = require('./correctTenantData') /** * Parses a CSV file and returns an array of row objects. @@ -53,22 +57,21 @@ function parseCsv(filePath) { /** * Backfills tenant data from an array of tenant records. * Each record is passed to the tenant Kafka consumer as a create event. + * After all tenants are processed, correctTenants runs to fix/refresh config. * * @param {Array} tenants - Array of { code, name, org_id, org_code, status?, description?, logo? } * @param {object} options * @param {boolean} options.dryRun - If true, only logs what would happen - * @returns` {Promise<{ success: number, failed: number, total: number }>} + * @returns {Promise<{ success: number, failed: number, total: number }>} */ async function backfillTenants(tenants, options = {}) { const { dryRun = false } = options let success = 0 let failed = 0 - const defaultConfig = await TenantService.fetchDefaultTenantConfig() - for (const tenant of tenants) { - if (!tenant.code || !tenant.name || !tenant.org_id || !tenant.org_code) { - console.error(`[SKIP] Missing required field (code, name, org_id, or org_code):`, tenant) + if (!tenant.code || !tenant.name) { + console.error(`[SKIP] Missing required field (code or name):`, tenant) failed++ continue } @@ -87,10 +90,8 @@ async function backfillTenants(tenants, options = {}) { status: tenant.status || 'ACTIVE', description: tenant.description || null, logo: tenant.logo || null, - org_id: tenant.org_id, - org_code: tenant.org_code, - backfill: true, - defaultConfig, + org_id: tenant.org_id || process.env.DEFAULT_ORG_ID, + org_code: tenant.org_code || process.env.DEFAULT_ORGANISATION_CODE, } try { @@ -151,6 +152,15 @@ if (require.main === module) { console.log(`Success: ${result.success}`) console.log(`Failed: ${result.failed}`) + if (!isDryRun) { + console.log('\n=== Running Tenant Data Correction ===') + const correction = await correctTenants(tenants) + console.log('\n=== Correction Summary ===') + console.log(`Total: ${correction.total}`) + console.log(`Success: ${correction.success}`) + console.log(`Failed: ${correction.failed}`) + } + process.exit(result.failed > 0 ? 1 : 0) } catch (err) { console.error('Fatal error:', err.message) diff --git a/src/services/tenant.js b/src/services/tenant.js index f7f009cee..741ff07f2 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -67,62 +67,7 @@ async function replicateWithIdMap({ label, fetchSource, transform, bulkCreate, f } module.exports = class TenantService { - static async fetchDefaultTenantConfig() { - const defaultTenantCode = process.env.DEFAULT_TENANT_CODE - const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE - - if (!defaultTenantCode || !defaultOrgCode) { - throw new Error('DEFAULT_TENANT_CODE and DEFAULT_ORGANISATION_CODE env vars are required for replication') - } - - const [ - notificationTemplates, - forms, - entityTypes, - questions, - questionSets, - reportTypes, - reports, - reportQueries, - reportRoleMappings, - roleExtensions, - ] = await Promise.all([ - NotificationTemplateQueries.findTemplatesByFilter({ - organization_code: defaultOrgCode, - tenant_code: defaultTenantCode, - }), - FormQueries.findFormsByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), - EntityTypeQueries.findAllEntityTypes([defaultOrgCode], defaultTenantCode, null), - QuestionQueries.find({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), - QuestionSetQueries.findQuestionSets({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), - ReportTypeQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), - ReportQueries.findAllReports({ organization_code: defaultOrgCode }, defaultTenantCode), - ReportQueryQueries.findReportQueries({ organization_code: defaultOrgCode }, defaultTenantCode), - ReportRoleMappingQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), - RoleExtensionQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), - ]) - - const entityTypeIds = entityTypes.map((et) => et.id) - const entities = entityTypeIds.length - ? await EntityQueries.findAllEntities({ entity_type_id: { [Op.in]: entityTypeIds } }, defaultTenantCode) - : [] - - return { - notificationTemplates, - forms, - entityTypes, - entities, - questions, - questionSets, - reportTypes, - reports, - reportQueries, - reportRoleMappings, - roleExtensions, - } - } - - static async replicateConfigFromDefaultTenant(newTenantCode, newOrgId, newOrgCode, defaultConfig = null) { + static async replicateConfigFromDefaultTenant(newTenantCode, newOrgId, newOrgCode) { const defaultTenantCode = process.env.DEFAULT_TENANT_CODE const defaultOrgCode = process.env.DEFAULT_ORGANISATION_CODE @@ -140,7 +85,6 @@ module.exports = class TenantService { } const newOrgIdStr = newOrgId.toString() const resolvedOrgCode = newOrgCode || defaultOrgCode - const config = defaultConfig || (await TenantService.fetchDefaultTenantConfig()) const baseTransform = (extra = {}) => @@ -153,31 +97,14 @@ module.exports = class TenantService { const transaction = await db.sequelize.transaction() try { - // ── 0. Clear existing config for this tenant (ensures fresh data on re-run) ── - const configTables = [ - 'entities', - 'entity_types', // entities first (references entity_types) - 'question_sets', - 'questions', - 'notification_templates', - 'forms', - 'report_role_mapping', - 'report_queries', - 'reports', - 'report_types', - 'role_extensions', - ] - for (const table of configTables) { - await db.sequelize.query(`DELETE FROM "${table}" WHERE tenant_code = :tenantCode`, { - replacements: { tenantCode: newTenantCode }, - transaction, - }) - } - // ── 1. Notification Templates ──────────────────────────────────────── await replicateResource({ label: 'Notification templates', - fetchSource: () => Promise.resolve(config.notificationTemplates), + fetchSource: () => + NotificationTemplateQueries.findTemplatesByFilter({ + organization_code: defaultOrgCode, + tenant_code: defaultTenantCode, + }), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => NotificationTemplateQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -186,7 +113,8 @@ module.exports = class TenantService { // ── 2. Forms ───────────────────────────────────────────────────────── await replicateResource({ label: 'Forms', - fetchSource: () => Promise.resolve(config.forms), + fetchSource: () => + FormQueries.findFormsByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform({ organization_id: newOrgIdStr, version: 0 }), bulkCreate: (items, opts) => FormQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -195,7 +123,7 @@ module.exports = class TenantService { // ── 3. Entity Types ─────────────────────────────────────────────────── const entityTypeIdMap = await replicateWithIdMap({ label: 'Entity types', - fetchSource: () => Promise.resolve(config.entityTypes), + fetchSource: () => EntityTypeQueries.findAllEntityTypes([defaultOrgCode], defaultTenantCode, null), transform: baseTransform({ organization_id: newOrgIdStr, parent_id: null }), bulkCreate: (items, opts) => EntityTypeQueries.bulkCreate(items, newTenantCode, opts), fetchExisting: () => EntityTypeQueries.findAllEntityTypes([defaultOrgCode], newTenantCode, null), @@ -210,8 +138,11 @@ module.exports = class TenantService { await replicateResource({ label: 'Entities', fetchSource: () => - Promise.resolve(config.entities.filter((e) => oldEntityTypeIds.includes(e.entity_type_id))), - filter: (e) => entityTypeIdMap[e.entity_type_id] != null, + EntityQueries.findAllEntities( + { entity_type_id: { [Op.in]: oldEntityTypeIds } }, + defaultTenantCode + ), + filter: (e) => entityTypeIdMap[e.entity_type_id] !== undefined, transform: (e) => ({ ...baseTransform()(e), entity_type_id: entityTypeIdMap[e.entity_type_id], @@ -224,7 +155,8 @@ module.exports = class TenantService { // ── 5. Questions ────────────────────────────────────────────────────── const questionIdMap = await replicateWithIdMap({ label: 'Questions', - fetchSource: () => Promise.resolve(config.questions), + fetchSource: () => + QuestionQueries.find({ organization_code: defaultOrgCode, tenant_code: defaultTenantCode }), transform: baseTransform(), bulkCreate: (items, opts) => QuestionQueries.bulkCreate(items, newTenantCode, { ...opts, returning: true }), @@ -236,7 +168,11 @@ module.exports = class TenantService { // ── 6. Question Sets ────────────────────────────────────────────────── await replicateResource({ label: 'Question sets', - fetchSource: () => Promise.resolve(config.questionSets), + fetchSource: () => + QuestionSetQueries.findQuestionSets({ + organization_code: defaultOrgCode, + tenant_code: defaultTenantCode, + }), transform: (qs) => ({ ...baseTransform()(qs), questions: (qs.questions || []).map((qId) => { @@ -251,7 +187,8 @@ module.exports = class TenantService { // ── 7. Report Types ─────────────────────────────────────────────────── await replicateResource({ label: 'Report types', - fetchSource: () => Promise.resolve(config.reportTypes), + fetchSource: () => + ReportTypeQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform(), bulkCreate: (items, opts) => ReportTypeQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -260,7 +197,8 @@ module.exports = class TenantService { // ── 8. Reports ──────────────────────────────────────────────────────── await replicateResource({ label: 'Reports', - fetchSource: () => Promise.resolve(config.reports), + fetchSource: () => + ReportQueries.findAllReports({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => ReportQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -269,7 +207,8 @@ module.exports = class TenantService { // ── 9. Report Queries ───────────────────────────────────────────────── await replicateResource({ label: 'Report queries', - fetchSource: () => Promise.resolve(config.reportQueries), + fetchSource: () => + ReportQueryQueries.findReportQueries({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => ReportQueryQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -278,7 +217,8 @@ module.exports = class TenantService { // ── 10. Report Role Mappings ────────────────────────────────────────── await replicateResource({ label: 'Report role mappings', - fetchSource: () => Promise.resolve(config.reportRoleMappings), + fetchSource: () => + ReportRoleMappingQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform(), bulkCreate: (items, opts) => ReportRoleMappingQueries.bulkCreate(items, newTenantCode, opts), transaction, @@ -287,7 +227,8 @@ module.exports = class TenantService { // ── 11. Role Extensions ─────────────────────────────────────────────── await replicateResource({ label: 'Role extensions', - fetchSource: () => Promise.resolve(config.roleExtensions), + fetchSource: () => + RoleExtensionQueries.findAllByFilter({ organization_code: defaultOrgCode }, defaultTenantCode), transform: baseTransform({ organization_id: newOrgIdStr }), bulkCreate: (items, opts) => RoleExtensionQueries.bulkCreate(items, newTenantCode, opts), transaction, From e88cbb678de8cc1c3f97ad2c430bcbd96fd7f7b7 Mon Sep 17 00:00:00 2001 From: sumanvpacewisdom Date: Tue, 14 Apr 2026 13:36:39 +0530 Subject: [PATCH 3/3] new script --- src/scripts/correctTenantData.js | 189 +++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/scripts/correctTenantData.js diff --git a/src/scripts/correctTenantData.js b/src/scripts/correctTenantData.js new file mode 100644 index 000000000..278570a39 --- /dev/null +++ b/src/scripts/correctTenantData.js @@ -0,0 +1,189 @@ +'use strict' + +/** + * Tenant Data Correction Script + * + * For each tenant in the CSV this script: + * 1. Deletes all existing config data for that tenant + * (notification_templates, forms, entity_types, entities, questions, + * question_sets, report_types, reports, report_queries, + * report_role_mapping, role_extensions) + * 2. Re-replicates fresh config from the default tenant + * + * Use this to fix tenants that already exist but have missing or stale config. + * Safe to re-run — the delete step ensures a clean slate each time. + * + * CSV format (header row required): + * code,org_id,org_code + * + * Usage: + * node src/scripts/correctTenantData.js + * node src/scripts/correctTenantData.js --dry-run + * + * Example CSV: + * code,org_id,org_code + * tenant_alpha,85,default_code + * tenant_beta,86,default_code + */ + +require('module-alias/register') +require('dotenv').config({ path: `${__dirname}/../.env` }) + +const fs = require('fs') +const path = require('path') +const csv = require('csv-parser') +const db = require('@database/models/index') +const TenantService = require('@services/tenant') + +// Tables to clear, in the order that respects foreign-key dependencies +// (entities references entity_types, so entities must be deleted first) +const CONFIG_TABLES = [ + 'entities', + 'entity_types', + 'question_sets', + 'questions', + 'notification_templates', + 'forms', + 'report_role_mapping', + 'report_queries', + 'reports', + 'report_types', + 'role_extensions', +] + +/** + * Parses a CSV file and returns an array of row objects. + * @param {string} filePath + * @returns {Promise>} + */ +function parseCsv(filePath) { + return new Promise((resolve, reject) => { + const rows = [] + fs.createReadStream(filePath) + .pipe(csv()) + .on('data', (row) => rows.push(row)) + .on('end', () => resolve(rows)) + .on('error', (err) => reject(err)) + }) +} + +/** + * Deletes all config rows for a given tenant_code inside a transaction. + * @param {string} tenantCode + * @param {object} transaction - Sequelize transaction + */ +async function deleteTenantConfig(tenantCode, transaction) { + for (const table of CONFIG_TABLES) { + await db.sequelize.query(`DELETE FROM "${table}" WHERE tenant_code = :tenantCode`, { + replacements: { tenantCode }, + transaction, + }) + } + console.log(`[DELETE] Config cleared for tenant: ${tenantCode}`) +} + +/** + * Corrects tenant config data for an array of tenants. + * For each tenant: deletes existing config then re-replicates from default tenant. + * + * @param {Array} tenants - Array of { code, org_id, org_code } + * @param {object} options + * @param {boolean} options.dryRun - If true, only logs what would happen + * @returns {Promise<{ success: number, failed: number, total: number }>} + */ +async function correctTenants(tenants, options = {}) { + const { dryRun = false } = options + let success = 0 + let failed = 0 + + for (const tenant of tenants) { + if (!tenant.code || !tenant.org_id || !tenant.org_code) { + console.error(`[SKIP] Missing required field (code, org_id, or org_code):`, tenant) + failed++ + continue + } + + if (dryRun) { + console.log(`[DRY RUN] Would correct: ${tenant.code}`) + continue + } + + try { + console.log(`\n[Correcting] ${tenant.code} ...`) + + // Step 1: delete existing config inside a transaction + const transaction = await db.sequelize.transaction() + try { + await deleteTenantConfig(tenant.code, transaction) + await transaction.commit() + } catch (err) { + await transaction.rollback() + throw err + } + + // Step 2: re-replicate fresh config from the default tenant + await TenantService.replicateConfigFromDefaultTenant(tenant.code, tenant.org_id, tenant.org_code) + + console.log(`[Done] ${tenant.code}`) + success++ + } catch (err) { + console.error(`[Failed] ${tenant.code} — ${err.message}`) + failed++ + } + } + + return { success, failed, total: tenants.length } +} + +// Export for programmatic use (e.g. called from backfillTenantData.js) +module.exports = { correctTenants, parseCsv } + +// ── CLI entry point ────────────────────────────────────────────────────────── +if (require.main === module) { + const args = process.argv.slice(2) + const isDryRun = args.includes('--dry-run') + const csvPath = args.find((a) => !a.startsWith('--')) + + if (!csvPath) { + console.error('Usage: node src/scripts/correctTenantData.js [--dry-run]') + console.error('\nCSV format (header row required):') + console.error(' code,org_id,org_code') + process.exit(1) + } + + const resolvedPath = path.resolve(csvPath) + if (!fs.existsSync(resolvedPath)) { + console.error(`File not found: ${resolvedPath}`) + process.exit(1) + } + + ;(async () => { + try { + console.log('=== Tenant Correction Script ===') + console.log(`CSV file: ${resolvedPath}`) + if (isDryRun) console.log('*** DRY RUN — no changes will be made ***') + console.log('') + + const tenants = await parseCsv(resolvedPath) + console.log(`Parsed ${tenants.length} row(s) from CSV.\n`) + + if (!tenants.length) { + console.log('CSV is empty. Nothing to do.') + process.exit(0) + } + + const result = await correctTenants(tenants, { dryRun: isDryRun }) + + console.log('\n=== Correction Summary ===') + console.log(`Total: ${result.total}`) + console.log(`Success: ${result.success}`) + console.log(`Failed: ${result.failed}`) + + process.exit(result.failed > 0 ? 1 : 0) + } catch (err) { + console.error('Fatal error:', err.message) + console.error(err.stack) + process.exit(1) + } + })() +}