PSA/server/migrations/20250930000003_cleanup_old_channels_tables.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

637 lines
24 KiB
JavaScript

/**
* Cleanup migration to drop old channels tables and channel_id columns
* This should only run AFTER verifying the boards migration was successful
*
* IMPORTANT: Only run this after:
* 1. The base rename migration (20250930000001) has completed
* 2. The EE Citus distribution migration (20250930000002) has completed (if using EE)
* 3. Application code has been updated to use boards instead of channels
* 4. Application has been tested with the new boards tables
* 5. All data has been verified
*
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.config = { transaction: false };
exports.up = async function(knex) {
console.log('Starting cleanup of old channels tables and columns...');
// Step 0: Update views that use channel_id to use board_id instead
// TODO: Handle this manually - update v_ticket_details view to use boards.board_id instead of channels.channel_id
console.log('Skipping view updates (handle manually)...');
//console.log('Updating views to use board_id instead of channel_id...');
//const viewExists = await knex.raw(`
// SELECT EXISTS (
// SELECT FROM pg_views
// WHERE viewname = 'v_ticket_details'
// ) as exists
//`);
//if (viewExists.rows[0].exists) {
// try {
// // Drop and recreate the view with correct columns
// await knex.raw(`DROP VIEW IF EXISTS v_ticket_details CASCADE`);
// // Recreate view with board_id instead of channel_id
// await knex.raw(`
// CREATE VIEW v_ticket_details AS
// SELECT
// t.tenant,
// t.ticket_id,
// t.ticket_number,
// t.title,
// t.url,
// c.company_name AS company,
// cn.full_name AS contact_name,
// s.name AS status,
// b.board_name AS channel,
// cat.category_name AS category,
// subcat.category_name AS subcategory,
// p.priority_name AS priority,
// sev.severity_name AS severity,
// u.urgency_name AS urgency,
// i.impact_name AS impact,
// ue.username AS entered_by,
// uu.username AS updated_by,
// ua.username AS assigned_to,
// uc.username AS closed_by,
// t.entered_at,
// t.updated_at,
// t.closed_at,
// t.is_closed
// FROM tickets t
// LEFT JOIN companies c ON t.tenant = c.tenant AND t.company_id = c.company_id
// LEFT JOIN contacts cn ON t.tenant = cn.tenant AND t.contact_name_id = cn.contact_name_id
// LEFT JOIN statuses s ON t.tenant = s.tenant AND t.status_id = s.status_id
// LEFT JOIN boards b ON t.tenant = b.tenant AND t.board_id = b.board_id
// LEFT JOIN categories cat ON t.tenant = cat.tenant AND t.category_id = cat.category_id
// LEFT JOIN categories subcat ON t.tenant = subcat.tenant AND t.subcategory_id = subcat.category_id
// LEFT JOIN priorities p ON t.tenant = p.tenant AND t.priority_id = p.priority_id
// LEFT JOIN severities sev ON t.tenant = sev.tenant AND t.severity_id = sev.severity_id
// LEFT JOIN urgencies u ON t.tenant = u.tenant AND t.urgency_id = u.urgency_id
// LEFT JOIN impacts i ON t.tenant = i.tenant AND t.impact_id = i.impact_id
// LEFT JOIN users ue ON t.tenant = ue.tenant AND t.entered_by = ue.user_id
// LEFT JOIN users uu ON t.tenant = uu.tenant AND t.updated_by = uu.user_id
// LEFT JOIN users ua ON t.tenant = ua.tenant AND t.assigned_to = ua.user_id
// LEFT JOIN users uc ON t.tenant = uc.tenant AND t.closed_by = uc.user_id
// `);
// console.log(' ✓ Recreated v_ticket_details view with board_id');
// } catch (error) {
// console.log(` ⚠ Could not update v_ticket_details view: ${error.message}`);
// console.log(' You may need to manually update this view');
// }
//}
// Step 1: Make channel_id nullable in related tables (if not already)
// This allows the app to use board_id while channel_id still exists
console.log('Making channel_id nullable in related tables...');
const categoriesHasChannelId = await knex.schema.hasColumn('categories', 'channel_id');
if (categoriesHasChannelId) {
await knex.schema.alterTable('categories', (table) => {
table.uuid('channel_id').nullable().alter();
});
console.log(' ✓ categories.channel_id is now nullable');
}
const ticketsHasChannelId = await knex.schema.hasColumn('tickets', 'channel_id');
if (ticketsHasChannelId) {
await knex.schema.alterTable('tickets', (table) => {
table.uuid('channel_id').nullable().alter();
});
console.log(' ✓ tickets.channel_id is now nullable');
}
const tagDefsExists = await knex.schema.hasTable('tag_definitions');
if (tagDefsExists) {
const tagDefsHasChannelId = await knex.schema.hasColumn('tag_definitions', 'channel_id');
if (tagDefsHasChannelId) {
await knex.schema.alterTable('tag_definitions', (table) => {
table.uuid('channel_id').nullable().alter();
});
console.log(' ✓ tag_definitions.channel_id is now nullable');
}
}
// Verify boards table exists before proceeding
const boardsExists = await knex.schema.hasTable('boards');
if (!boardsExists) {
console.log('ERROR: boards table does not exist! Aborting cleanup.');
throw new Error('boards table must exist before running cleanup migration');
}
const standardBoardsExists = await knex.schema.hasTable('standard_boards');
if (!standardBoardsExists) {
console.log('ERROR: standard_boards table does not exist! Aborting cleanup.');
throw new Error('standard_boards table must exist before running cleanup migration');
}
// Sync any new data that was added between migrations
const channelsExists = await knex.schema.hasTable('channels');
if (channelsExists) {
console.log('Syncing any new data from channels to boards...');
// Find records in channels that don't exist in boards
const newRecords = await knex.raw(`
SELECT c.*
FROM channels c
LEFT JOIN boards b ON c.tenant = b.tenant AND c.channel_id = b.board_id
WHERE b.board_id IS NULL
`);
if (newRecords.rows.length > 0) {
console.log(` Found ${newRecords.rows.length} new records to sync...`);
// Insert new records into boards
await knex.raw(`
INSERT INTO boards (
tenant, board_id, board_name, display_contact_name_id, display_priority,
display_severity, display_urgency, display_impact, display_category,
display_subcategory, display_assigned_to, display_status, display_due_date,
is_inactive, is_default, display_itil_impact, display_itil_urgency, category_type,
priority_type, display_order, description
)
SELECT
c.tenant, c.channel_id, c.channel_name, c.display_contact_name_id, c.display_priority,
c.display_severity, c.display_urgency, c.display_impact, c.display_category,
c.display_subcategory, c.display_assigned_to, c.display_status, c.display_due_date,
c.is_inactive, c.is_default, c.display_itil_impact, c.display_itil_urgency, c.category_type,
c.priority_type, c.display_order, c.description
FROM channels c
LEFT JOIN boards b ON c.tenant = b.tenant AND c.channel_id = b.board_id
WHERE b.board_id IS NULL
`);
console.log(` ✓ Synced ${newRecords.rows.length} new records`);
} else {
console.log(' ✓ All channels data already exists in boards');
}
}
// Sync standard_channels to standard_boards
const standardChannelsExistsForSync = await knex.schema.hasTable('standard_channels');
if (standardChannelsExistsForSync) {
console.log('Syncing any new data from standard_channels to standard_boards...');
const newStandardRecords = await knex.raw(`
SELECT sc.*
FROM standard_channels sc
LEFT JOIN standard_boards sb ON sc.id = sb.id
WHERE sb.id IS NULL
`);
if (newStandardRecords.rows.length > 0) {
console.log(` Found ${newStandardRecords.rows.length} new standard records to sync...`);
await knex.raw(`
INSERT INTO standard_boards (
id, board_name, description, display_order, is_inactive, is_default,
category_type, priority_type, created_at, updated_at
)
SELECT
sc.id, sc.channel_name, sc.description, sc.display_order, sc.is_inactive, sc.is_default,
sc.category_type, sc.priority_type, sc.created_at, sc.updated_at
FROM standard_channels sc
LEFT JOIN standard_boards sb ON sc.id = sb.id
WHERE sb.id IS NULL
`);
console.log(` ✓ Synced ${newStandardRecords.rows.length} new standard records`);
} else {
console.log(' ✓ All standard_channels data already exists in standard_boards');
}
}
// Step 1: If Citus is enabled, check if we need to undistribute tables
const citusEnabled = await knex.raw(`
SELECT EXISTS (
SELECT 1 FROM pg_extension WHERE extname = 'citus'
) as enabled
`);
if (citusEnabled.rows[0].enabled) {
console.log('Citus detected - checking if channels table is distributed...');
const channelsDistributed = await knex.raw(`
SELECT EXISTS (
SELECT 1 FROM pg_dist_partition
WHERE logicalrelid = 'channels'::regclass
) as distributed
`);
if (channelsDistributed.rows[0].distributed) {
console.log('Need to undistribute channels table before dropping...');
// First, get all foreign key constraints that reference the channels table
const foreignKeys = await knex.raw(`
SELECT
tc.table_name,
tc.constraint_name
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
WHERE tc.constraint_type = 'FOREIGN KEY'
AND ccu.table_name = 'channels'
`);
// Drop all foreign keys that reference channels
console.log('Dropping foreign key constraints referencing channels...');
for (const fk of foreignKeys.rows) {
console.log(` Dropping constraint ${fk.constraint_name} from ${fk.table_name}...`);
await knex.raw(`ALTER TABLE ${fk.table_name} DROP CONSTRAINT IF EXISTS ${fk.constraint_name}`);
}
// Now we can undistribute the channels table
await knex.raw(`SELECT undistribute_table('channels')`);
console.log(' ✓ Channels table undistributed');
} else {
// If not distributed, still need to drop the foreign keys
console.log('Channels table not distributed, dropping foreign key constraints...');
const categoriesFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'categories'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of categoriesFK.rows) {
await knex.raw(`ALTER TABLE categories DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from categories`);
}
const ticketsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tickets'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of ticketsFK.rows) {
await knex.raw(`ALTER TABLE tickets DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tickets`);
}
const hasTagsTable = await knex.schema.hasTable('tags');
if (hasTagsTable) {
const hasChannelId = await knex.schema.hasColumn('tags', 'channel_id');
if (hasChannelId) {
const tagsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tags'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of tagsFK.rows) {
await knex.raw(`ALTER TABLE tags DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tags`);
}
}
}
const hasTagDefinitions = await knex.schema.hasTable('tag_definitions');
if (hasTagDefinitions) {
const hasChannelId = await knex.schema.hasColumn('tag_definitions', 'channel_id');
if (hasChannelId) {
const tagDefsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tag_definitions'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of tagDefsFK.rows) {
await knex.raw(`ALTER TABLE tag_definitions DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tag_definitions`);
}
}
}
}
// Also undistribute standard_channels if it's a reference table
const standardChannelsExists = await knex.schema.hasTable('standard_channels');
if (standardChannelsExists) {
const standardDistributed = await knex.raw(`
SELECT EXISTS (
SELECT 1 FROM pg_dist_partition
WHERE logicalrelid = 'standard_channels'::regclass
) as distributed
`);
if (standardDistributed.rows[0].distributed) {
console.log('Undistributing standard_channels table before dropping...');
await knex.raw(`SELECT undistribute_table('standard_channels')`);
console.log(' ✓ Standard_channels table undistributed');
}
}
} else {
// No Citus, just drop the foreign keys normally
console.log('Dropping foreign key constraints referencing channels...');
const categoriesFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'categories'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of categoriesFK.rows) {
await knex.raw(`ALTER TABLE categories DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from categories`);
}
const ticketsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tickets'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of ticketsFK.rows) {
await knex.raw(`ALTER TABLE tickets DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tickets`);
}
const hasTagsTable = await knex.schema.hasTable('tags');
if (hasTagsTable) {
const hasChannelId = await knex.schema.hasColumn('tags', 'channel_id');
if (hasChannelId) {
const tagsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tags'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of tagsFK.rows) {
await knex.raw(`ALTER TABLE tags DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tags`);
}
}
}
const hasTagDefinitions = await knex.schema.hasTable('tag_definitions');
if (hasTagDefinitions) {
const hasChannelId = await knex.schema.hasColumn('tag_definitions', 'channel_id');
if (hasChannelId) {
const tagDefsFK = await knex.raw(`
SELECT conname
FROM pg_constraint
WHERE conrelid = 'tag_definitions'::regclass
AND confrelid = 'channels'::regclass
AND contype = 'f'
`);
for (const constraint of tagDefsFK.rows) {
await knex.raw(`ALTER TABLE tag_definitions DROP CONSTRAINT IF EXISTS ${constraint.conname}`);
console.log(` ✓ Dropped constraint ${constraint.conname} from tag_definitions`);
}
}
}
}
// Step 2: Drop old channel_id columns
console.log('Dropping old channel_id columns...');
const hasCategoriesChannelId = await knex.schema.hasColumn('categories', 'channel_id');
if (hasCategoriesChannelId) {
await knex.schema.alterTable('categories', (table) => {
table.dropColumn('channel_id');
});
console.log(' ✓ Dropped channel_id from categories');
}
const hasTicketsChannelId = await knex.schema.hasColumn('tickets', 'channel_id');
if (hasTicketsChannelId) {
await knex.schema.alterTable('tickets', (table) => {
table.dropColumn('channel_id');
});
console.log(' ✓ Dropped channel_id from tickets');
}
const hasTagDefinitions = await knex.schema.hasTable('tag_definitions');
if (hasTagDefinitions) {
const hasChannelId = await knex.schema.hasColumn('tag_definitions', 'channel_id');
if (hasChannelId) {
await knex.schema.alterTable('tag_definitions', (table) => {
table.dropColumn('channel_id');
});
console.log(' ✓ Dropped channel_id from tag_definitions');
}
}
// Step 3: Drop old tables
console.log('Dropping old tables...');
if (channelsExists) {
await knex.schema.dropTable('channels');
console.log(' ✓ Dropped channels table');
}
const standardChannelsExists = await knex.schema.hasTable('standard_channels');
if (standardChannelsExists) {
await knex.schema.dropTable('standard_channels');
console.log(' ✓ Dropped standard_channels table');
}
console.log('✓ Cleanup migration completed successfully');
console.log('Old channels tables and channel_id columns have been removed.');
};
/**
* Rollback - recreate the old channels tables from boards
* This allows rolling back if issues are discovered
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function(knex) {
console.log('Rolling back cleanup migration - recreating channels tables...');
// Step 1: Recreate channels table
console.log('Recreating channels table...');
await knex.schema.createTable('channels', (table) => {
table.uuid('tenant').notNullable();
table.uuid('channel_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
table.text('channel_name').notNullable();
table.boolean('display_contact_name_id').defaultTo(true);
table.boolean('display_priority').defaultTo(true);
table.boolean('display_severity').defaultTo(true);
table.boolean('display_urgency').defaultTo(true);
table.boolean('display_impact').defaultTo(true);
table.boolean('display_category').defaultTo(true);
table.boolean('display_subcategory').defaultTo(true);
table.boolean('display_assigned_to').defaultTo(true);
table.boolean('display_status').defaultTo(true);
table.boolean('display_due_date').defaultTo(true);
table.boolean('is_default').defaultTo(false);
table.boolean('display_itil_impact').defaultTo(false);
table.boolean('display_itil_urgency').defaultTo(false);
table.text('category_type').defaultTo('custom');
table.text('priority_type').defaultTo('custom');
table.integer('display_order').notNullable().defaultTo(0);
table.text('description');
table.primary(['tenant', 'channel_id']);
table.foreign('tenant').references('tenants.tenant');
});
await knex.raw(`
ALTER TABLE channels
ADD CONSTRAINT channels_category_type_check
CHECK (category_type IN ('custom', 'itil'))
`);
await knex.raw(`
ALTER TABLE channels
ADD CONSTRAINT channels_priority_type_check
CHECK (priority_type IN ('custom', 'itil'))
`);
await knex.raw('CREATE INDEX idx_channels_tenant_category_type ON channels(tenant, category_type)');
await knex.raw('CREATE INDEX idx_channels_tenant_priority_type ON channels(tenant, priority_type)');
// Step 2: Copy data back from boards to channels
console.log('Copying data from boards back to channels...');
await knex.raw(`
INSERT INTO channels (
tenant, channel_id, channel_name, display_contact_name_id, display_priority,
display_severity, display_urgency, display_impact, display_category,
display_subcategory, display_assigned_to, display_status, display_due_date,
is_default, display_itil_impact, display_itil_urgency, category_type,
priority_type, display_order, description
)
SELECT
tenant, board_id, board_name, display_contact_name_id, display_priority,
display_severity, display_urgency, display_impact, display_category,
display_subcategory, display_assigned_to, display_status, display_due_date,
is_default, display_itil_impact, display_itil_urgency, category_type,
priority_type, display_order, description
FROM boards
`);
// Step 3: Recreate standard_channels table
console.log('Recreating standard_channels table...');
await knex.schema.createTable('standard_channels', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.text('channel_name').notNullable().unique();
table.text('description');
table.integer('display_order').notNullable().defaultTo(0);
table.boolean('is_inactive').defaultTo(false);
table.boolean('is_default').defaultTo(false);
table.text('category_type').defaultTo('custom');
table.text('priority_type').defaultTo('custom');
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
});
await knex.raw(`
ALTER TABLE standard_channels
ADD CONSTRAINT standard_channels_category_type_check
CHECK (category_type IN ('custom', 'itil'))
`);
await knex.raw(`
ALTER TABLE standard_channels
ADD CONSTRAINT standard_channels_priority_type_check
CHECK (priority_type IN ('custom', 'itil'))
`);
await knex.raw('CREATE INDEX idx_standard_channels_category_type ON standard_channels(category_type)');
await knex.raw('CREATE INDEX idx_standard_channels_priority_type ON standard_channels(priority_type)');
// Step 4: Copy data back from standard_boards
console.log('Copying data from standard_boards back to standard_channels...');
await knex.raw(`
INSERT INTO standard_channels (
id, channel_name, description, display_order, is_inactive, is_default,
category_type, priority_type, created_at, updated_at
)
SELECT
id, board_name, description, display_order, is_inactive, is_default,
category_type, priority_type, created_at, updated_at
FROM standard_boards
`);
// Step 5: Restore channel_id columns in related tables
console.log('Restoring channel_id in categories table...');
await knex.schema.alterTable('categories', (table) => {
table.uuid('channel_id');
});
await knex.raw(`
UPDATE categories
SET channel_id = board_id
`);
await knex.schema.alterTable('categories', (table) => {
table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels');
});
console.log('Restoring channel_id in tickets table...');
await knex.schema.alterTable('tickets', (table) => {
table.uuid('channel_id');
});
await knex.raw(`
UPDATE tickets
SET channel_id = board_id
`);
await knex.schema.alterTable('tickets', (table) => {
table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels');
});
const hasTagsTable = await knex.schema.hasTable('tags');
if (hasTagsTable) {
const hasBoardId = await knex.schema.hasColumn('tags', 'board_id');
if (hasBoardId) {
console.log('Restoring channel_id in tags table...');
await knex.schema.alterTable('tags', (table) => {
table.uuid('channel_id');
});
await knex.raw(`
UPDATE tags
SET channel_id = board_id
`);
await knex.schema.alterTable('tags', (table) => {
table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels');
});
}
}
const hasTagDefinitions = await knex.schema.hasTable('tag_definitions');
if (hasTagDefinitions) {
const hasBoardId = await knex.schema.hasColumn('tag_definitions', 'board_id');
if (hasBoardId) {
console.log('Restoring channel_id in tag_definitions table...');
await knex.schema.alterTable('tag_definitions', (table) => {
table.uuid('channel_id');
});
await knex.raw(`
UPDATE tag_definitions
SET channel_id = board_id
WHERE board_id IS NOT NULL
`);
await knex.schema.alterTable('tag_definitions', (table) => {
table.foreign(['tenant', 'channel_id']).references(['tenant', 'channel_id']).inTable('channels');
});
}
}
console.log('✓ Rollback completed successfully');
console.log('Old channels tables and channel_id columns have been restored.');
};