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
173 changes: 173 additions & 0 deletions src/database/migrations/20250729064710-org-code-fix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const { Sequelize } = require('sequelize')

module.exports = {
async up(queryInterface, Sequelize) {
let transaction
let fk_retainer = []
let table, fk_name, fkey, refTable, refKey

try {
// Start a transaction
transaction = await queryInterface.sequelize.transaction()

const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\\s+' OR code ~ '[A-Z]';`
const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};`
const enableFK = (table, fk_name, fkey, refTable, refKey) =>
`ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;`
const updateQuery = (table, key) =>
`UPDATE ${table} SET ${key} = LOWER(REGEXP_REPLACE(${key}, '\\s+', '_', 'g')) WHERE ${key} ~ '[A-Z|\\s+]';`

// Execute the query to fetch organizations with whitespace
const fetchOrg = await queryInterface.sequelize.query(ORG_FETCH_QUERY, {
type: Sequelize.QueryTypes.SELECT,
raw: true,
transaction,
})

if (fetchOrg.length > 0) {
// Disable foreign key constraints and store enable queries
table = 'organization_registration_codes'
fk_name = 'fk_organization_code_tenant_code_in_org_reg_code'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})

table = 'user_organizations'
fk_name = 'fk_user_organizations_organizations'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))

table = 'organization_user_invites'
fk_name = 'fk_org_user_invites_organization_id'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})

Comment on lines +53 to +63
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Duplicate foreign key constraint handling

The migration attempts to drop the same constraint twice for the organization_user_invites table but with different constraint names. This appears to be handling different naming conventions, but only one enableFK is added to fk_retainer.

Verify if both constraints exist and ensure both are properly restored:

 				table = 'organization_user_invites'
 				fk_name = 'fk_org_user_invites_org_code'
 				fkey = '(organization_code, tenant_code)'
 				refTable = 'organizations'
 				refKey = '(code, tenant_code)'
 				await queryInterface.sequelize.query(disableFK(table, fk_name), {
 					type: Sequelize.QueryTypes.RAW,
 					raw: true,
 					transaction,
 				})
 				fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))
 
 				table = 'organization_user_invites'
 				fk_name = 'fk_org_user_invites_organization_id'
 				fkey = '(organization_code, tenant_code)'
 				refTable = 'organizations'
 				refKey = '(code, tenant_code)'
 				await queryInterface.sequelize.query(disableFK(table, fk_name), {
 					type: Sequelize.QueryTypes.RAW,
 					raw: true,
 					transaction,
 				})
+				fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
table = 'organization_user_invites'
fk_name = 'fk_org_user_invites_organization_id'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
table = 'organization_user_invites'
fk_name = 'fk_org_user_invites_organization_id'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))
🤖 Prompt for AI Agents
In src/database/migrations/20250729064710-org-code-fix.js around lines 65 to 75,
the migration drops two foreign key constraints on the organization_user_invites
table but only adds one enableFK call to fk_retainer. Verify if both constraints
exist in the database schema and if so, add corresponding enableFK calls for
both constraints to fk_retainer to ensure both are properly restored during
rollback.

table = 'user_organization_roles'
fk_name = 'fk_user_org_roles_user_organizations'
fkey = '(user_id, organization_code, tenant_code)'
refTable = 'user_organizations'
refKey = '(user_id, organization_code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))

table = 'organization_features'
fk_name = 'fk_org_features_organization'
fkey = '(organization_code, tenant_code)'
refTable = 'organizations'
refKey = '(code, tenant_code)'
await queryInterface.sequelize.query(disableFK(table, fk_name), {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey))

// Update tables to remove whitespace
let updateTable = 'organizations'
let key = 'code'
const updateOrgs = await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})

updateTable = 'organization_registration_codes'
key = 'organization_code'
await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})

updateTable = 'organization_user_invites'
key = 'organization_code'
await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})

updateTable = 'user_organizations'
key = 'organization_code'
await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})

updateTable = 'user_organization_roles'
key = 'organization_code'
await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})
updateTable = 'organization_features'
key = 'organization_code'
await queryInterface.sequelize.query(updateQuery(updateTable, key), {
type: Sequelize.QueryTypes.UPDATE,
raw: true,
transaction,
})

// Verify the update
const fetchOrgs = await queryInterface.sequelize.query(ORG_FETCH_QUERY, {
type: Sequelize.QueryTypes.SELECT,
raw: true,
transaction,
})

// Re-enable foreign key constraints
let fk_retainerPromise = []
for (let i = 0; i < fk_retainer.length; i++) {
fk_retainerPromise.push(
queryInterface.sequelize.query(fk_retainer[i], {
type: Sequelize.QueryTypes.RAW,
raw: true,
transaction,
})
)
}

await Promise.all(fk_retainerPromise)

// Commit the transaction
await transaction.commit()
}
} catch (error) {
// Rollback transaction on error
if (transaction) await transaction.rollback()
console.error(`Error during transaction: ${error}`)
throw error
}
},

async down(queryInterface, Sequelize) {
console.warn(
'Down migration not implemented: Cannot reliably restore original whitespace in organization codes.'
)
},
Comment on lines +168 to +172
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement down migration for rollback capability

The down migration is not implemented, which means this migration cannot be rolled back. While the transformation is indeed irreversible (we can't know which underscores were originally spaces), you should at least document what manual steps would be needed for a rollback.

Consider implementing a partial down migration or better documentation:

async down(queryInterface, Sequelize) {
  throw new Error(
    'Down migration not supported. Manual intervention required:\n' +
    '1. Restore organization codes from backup\n' +
    '2. Update all related tables (organization_registration_codes, user_organizations, etc.)\n' +
    '3. Re-establish foreign key constraints\n' +
    'Original format cannot be automatically restored as spaces were converted to underscores.'
  );
}
🤖 Prompt for AI Agents
In src/database/migrations/20250729064710-org-code-fix.js around lines 182 to
186, the down migration is currently not implemented and only logs a warning,
which prevents rollback. Replace the console.warn with a down method that throws
an error explaining that down migration is not supported and provide detailed
instructions for manual rollback steps, including restoring from backup,
updating related tables, and re-establishing foreign key constraints.

}
2 changes: 1 addition & 1 deletion src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ module.exports = (app) => {
const version = (req.params.version.match(/^v\d+$/) || [])[0] // Match version like v1, v2, etc.
const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters, underscore, and hyphen
const file = req.params.file ? (req.params.file.match(/^[a-zA-Z0-9_-]+$/) || [])[0] : null // Same validation as controller, or null if file is not provided
const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters
const method = (req.params.method.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters
try {
if (!version || !controllerName || !method || (req.params.file && !file)) {
// Invalid input, return an error response
Expand Down
7 changes: 6 additions & 1 deletion src/validators/v1/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ const common = require('@constants/common')
module.exports = {
create: (req) => {
req.body = filterRequestBody(req.body, organization.create)
req.checkBody('code').trim().notEmpty().withMessage('code field is empty')
req.checkBody('code')
.trim()
.notEmpty()
.withMessage('code field is empty')
.matches(/^[a-z0-9_]+$/)
.withMessage('code is invalid. Only lowercase alphanumeric characters allowed')
req.checkBody('tenant_code').trim().notEmpty().withMessage('tenant_code field is empty')
req.checkBody('registration_codes')
.optional({ checkFalsy: true })
Expand Down