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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
637 lines
24 KiB
JavaScript
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.');
|
|
}; |