Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/constants/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = {
otpExpirationTime: process.env.OTP_EXP_TIME, // In Seconds,
ADMIN_ROLE: 'admin',
ORG_ADMIN_ROLE: 'org_admin',
TENANT_ADMIN_ROLE: 'tenant_admin',
USER_ROLE: 'user',
SESSION_MANAGER_ROLE: 'session_manager',
PUBLIC_ROLE: 'public',
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/v1/organization-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ module.exports = class OrganizationFeature {
)
: await organizationFeatureService.list(
req.decodedToken.tenant_code,
req.decodedToken.organization_code
req.decodedToken.organization_code,
req.decodedToken.roles
)
} catch (error) {
return error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('feature_role_mapping', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
},
feature_code: {
type: Sequelize.STRING,
allowNull: false,
},
role_title: {
type: Sequelize.STRING,
allowNull: false,
},
organization_code: {
type: Sequelize.STRING,
allowNull: false,
},
tenant_code: {
type: Sequelize.STRING,
allowNull: false,
},
created_by: {
type: Sequelize.INTEGER,
allowNull: false,
},
updated_by: {
type: Sequelize.INTEGER,
allowNull: true,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
deleted_at: {
allowNull: true,
type: Sequelize.DATE,
},
})

// Add composite primary key
await queryInterface.addConstraint('feature_role_mapping', {
fields: ['id', 'tenant_code'],
type: 'primary key',
name: 'pk_feature_role_mapping',
})
},

async down(queryInterface) {
await queryInterface.dropTable('feature_role_mapping')
},
}
87 changes: 87 additions & 0 deletions src/database/migrations/20251002164938-add-new-feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Check if SCP feature already exists
const scpFeature = await queryInterface.sequelize.query("SELECT code FROM features WHERE code = 'scp'", {
type: Sequelize.QueryTypes.SELECT,
})

// Insert SCP feature if it doesn't exist
if (scpFeature.length === 0) {
await queryInterface.bulkInsert('features', [
{
code: 'scp',
label: 'Self Creation Portal',
display_order: 9,
description: 'SCP capability',
created_at: new Date(),
updated_at: new Date(),
},
])
}

// Get all organizations
const tenants = await queryInterface.sequelize.query('SELECT code FROM tenants WHERE deleted_at IS NULL', {
type: Sequelize.QueryTypes.SELECT,
})

// Get default orgs for each organization
const organizationFeatureData = []
for (const tenant of tenants) {
const orgExist = await queryInterface.sequelize.query(
'SELECT code, tenant_code FROM organizations WHERE deleted_at IS NULL AND tenant_code = :tenantCode AND code = :orgCode',
{
replacements: {
tenantCode: tenant.code,
orgCode: process.env.DEFAULT_ORGANISATION_CODE || 'default_code',
},
type: Sequelize.QueryTypes.SELECT,
}
)

if (orgExist.length > 0) {
const orgFeatureExist = await queryInterface.sequelize.query(
'SELECT organization_code FROM organization_features WHERE tenant_code = :tenantCode AND organization_code = :orgCode AND feature_code = :featureCode',
{
replacements: {
tenantCode: tenant.code,
orgCode: process.env.DEFAULT_ORGANISATION_CODE || 'default_code',
featureCode: 'scp',
},
type: Sequelize.QueryTypes.SELECT,
}
)
if (orgFeatureExist.length === 0) {
organizationFeatureData.push({
organization_code: process.env.DEFAULT_ORGANISATION_CODE || 'default_code',
tenant_code: tenant.code,
feature_code: 'scp',
feature_name: 'SCP',
enabled: true,
display_order: 9,
created_at: new Date(),
updated_at: new Date(),
})
}
}
}

if (organizationFeatureData.length > 0) {
await queryInterface.bulkInsert('organization_features', organizationFeatureData)
}
},

async down(queryInterface) {
// Remove organization_feature records only for the SCP feature
await queryInterface.bulkDelete('organization_features', {
feature_code: 'scp',
})

// Remove SCP feature
await queryInterface.bulkDelete('features', {
code: 'scp',
})
},
}
139 changes: 139 additions & 0 deletions src/database/migrations/20251002165109-map-roles-to-features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Fetch all enabled features for all organizations from the `organization_features` table.
const organizationsFeatures = await queryInterface.sequelize.query(
'SELECT feature_code, tenant_code, organization_code FROM organization_features WHERE deleted_at IS NULL',
{ type: Sequelize.QueryTypes.SELECT }
)

// Fetch all possible features available in the system.
const allFeatures = await queryInterface.sequelize.query('SELECT code FROM features', {
type: Sequelize.QueryTypes.SELECT,
})
// Define a baseline set of default features.
let defaultFeatures = ['project', 'mentoring', 'survey', 'observation', 'reports', 'mitra', 'programs']
// Define the feature set for each user role. This is the core mapping logic.
const roleMappings = {
content_creator: ['scp', ...defaultFeatures],
reviewer: ['scp', 'learn', ...defaultFeatures],
creator: ['scp', 'learn', ...defaultFeatures],
rollout_manager: ['scp', ...defaultFeatures],
program_manager: ['scp', ...defaultFeatures],
program_designer: ['scp', ...defaultFeatures],
learner: ['learn', ...defaultFeatures],
mentee: defaultFeatures,
mentor: defaultFeatures,
session_manager: defaultFeatures,
report_admin: ['reports', 'learn', ...defaultFeatures],
state_manager: ['reports', 'learn', ...defaultFeatures],
district_manager: ['reports', 'learn', ...defaultFeatures],
admin: allFeatures.map((f) => f.code), // Admins get all features.
org_admin: allFeatures.map((f) => f.code), // Org admins also get all features.
tenant_admin: allFeatures.map((f) => f.code), // Tenant admins also get all features.
}

// Create a lookup map (`orgFeaturesMap`) for efficient access to enabled features per organization.
const orgFeaturesMap = new Map()
organizationsFeatures.forEach((item) => {
const orgKey = `${item.organization_code}|${item.tenant_code}`
if (!orgFeaturesMap.has(orgKey)) {
orgFeaturesMap.set(orgKey, new Set())
}
orgFeaturesMap.get(orgKey).add(item.feature_code)
})

// Extract unique organizations
const uniqueOrgsMap = new Map()
organizationsFeatures.forEach((item) => {
const key = `${item.organization_code}|${item.tenant_code}`
if (!uniqueOrgsMap.has(key)) {
uniqueOrgsMap.set(key, {
organization_code: item.organization_code,
tenant_code: item.tenant_code,
})
}
})
const uniqueOrgs = Array.from(uniqueOrgsMap.values())

console.log(`Total organization_features rows: ${organizationsFeatures.length}`)
console.log(`Unique organizations: ${uniqueOrgs.length}`)

// Prepare arrays to collect mappings and track duplicates/skipped
const featureRoleMappingData = []
const seenMappings = new Set()
const skippedMappings = []

// Main loop: Process each organization to create feature-role mappings
for (const org of uniqueOrgs) {
const orgKey = `${org.organization_code}|${org.tenant_code}`
const enabledFeatures = orgFeaturesMap.get(orgKey) || new Set()

// Fetch roles available for this tenant
const tenantRoles = await queryInterface.sequelize.query(
'SELECT title FROM user_roles WHERE tenant_code = ? AND deleted_at IS NULL',
{
replacements: [org.tenant_code],
type: Sequelize.QueryTypes.SELECT,
}
)
const tenantRoleTitles = tenantRoles.map((role) => role.title)

// Loop through each role and its features
for (const [role, features] of Object.entries(roleMappings)) {
if (tenantRoleTitles.includes(role)) {
for (const featureCode of features) {
const key = `${featureCode}|${role}|${org.organization_code}|${org.tenant_code}`

// ✅ Check if feature exists in organization_features
if (!enabledFeatures.has(featureCode)) {
skippedMappings.push({
reason: 'Feature not enabled for org',
feature: featureCode,
role: role,
org: org.organization_code,
tenant: org.tenant_code,
})
continue // Skip this mapping
}

if (!seenMappings.has(key)) {
seenMappings.add(key)
featureRoleMappingData.push({
role_title: role,
feature_code: featureCode,
organization_code: org.organization_code,
tenant_code: org.tenant_code,
created_by: 0,
updated_by: 0,
created_at: new Date(),
updated_at: new Date(),
})
}
}
}
}
}

console.log(`Total mappings to insert: ${featureRoleMappingData.length}`)
console.log(`Skipped mappings: ${skippedMappings.length}`)

// Log first few skipped for debugging
if (skippedMappings.length > 0) {
console.log('Sample skipped mappings:')
console.log(skippedMappings.slice(0, 10))
}

// Insert all valid mappings into the database
if (featureRoleMappingData.length > 0) {
await queryInterface.bulkInsert('feature_role_mapping', featureRoleMappingData)
}
},

// Revert the migration by deleting all feature-role mappings
async down(queryInterface) {
await queryInterface.bulkDelete('feature_role_mapping', null, {})
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict'

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Add foreign key constraint for feature_code
await queryInterface.addConstraint('feature_role_mapping', {
fields: ['feature_code'],
type: 'foreign key',
name: 'fk_feature_role_mapping_feature_code',
references: {
table: 'features',
field: 'code',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
})

// Add foreign key constraint for tenant_code
await queryInterface.addConstraint('feature_role_mapping', {
fields: ['tenant_code'],
type: 'foreign key',
name: 'fk_feature_role_mapping_tenant_code',
references: {
table: 'tenants',
field: 'code',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
})

// Add composite foreign key for organization_code (organization_code, tenant_code) -> organizations (code, tenant_code)
await queryInterface.sequelize.query(`
ALTER TABLE feature_role_mapping
ADD CONSTRAINT fk_feature_role_mapping_organization_code
FOREIGN KEY (organization_code, tenant_code)
REFERENCES organizations (code, tenant_code)
ON UPDATE CASCADE
ON DELETE CASCADE;
`)

// Add composite foreign key for role_title (tenant_code, role_title) -> user_roles (tenant_code, title)
//commenting this as of now because of issue in user_roles table which is using the organization_id as foreign key
// await queryInterface.sequelize.query(`
// ALTER TABLE feature_role_mapping
// ADD CONSTRAINT fk_feature_role_mapping_role_title
// FOREIGN KEY (tenant_code, role_title)
// REFERENCES user_roles (tenant_code, title)
// ON UPDATE CASCADE
// ON DELETE NO ACTION;
// `)

// Unique constraint for feature_code, role_title, organization_code, tenant_code
await queryInterface.sequelize.query(`
CREATE UNIQUE INDEX feature_role_org_tenant_unique
ON feature_role_mapping (feature_code, role_title, organization_code, tenant_code)
WHERE deleted_at IS NULL;
`)

await queryInterface.sequelize.query(`
ALTER TABLE feature_role_mapping
ADD CONSTRAINT fk_org_feature_role_mapping_organization_code
FOREIGN KEY (feature_code, tenant_code, organization_code)
REFERENCES organization_features (feature_code, tenant_code, organization_code)
ON UPDATE CASCADE
ON DELETE CASCADE;
`)
},

async down(queryInterface, Sequelize) {
// Drop foreign key constraints
await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_tenant_code')
await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_organization_code')
// await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_role_title')
await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_feature_code')
await queryInterface.removeConstraint('feature_role_mapping', 'fk_org_feature_role_mapping_organization_code')

await queryInterface.sequelize.query('DROP INDEX IF EXISTS feature_role_org_tenant_unique;')
},
}
Loading