import { MigrationInterface, QueryRunner } from 'typeorm'; interface LegacyServerRow { id: string; channels: string | null; } interface LegacyServerChannel { id: string; name: string; type: 'text' | 'voice'; position: number; } function normalizeLegacyChannels(raw: string | null): LegacyServerChannel[] { try { const parsed = JSON.parse(raw || '[]'); if (!Array.isArray(parsed)) { return []; } const seenIds = new Set(); const seenNames = new Set(); return parsed .filter((channel): channel is Record => !!channel && typeof channel === 'object') .map((channel, index) => { const id = typeof channel.id === 'string' ? channel.id.trim() : ''; const name = typeof channel.name === 'string' ? channel.name.trim().replace(/\s+/g, ' ') : ''; const type = channel.type === 'text' || channel.type === 'voice' ? channel.type : null; const position = typeof channel.position === 'number' ? channel.position : index; const nameKey = type ? `${type}:${name.toLocaleLowerCase()}` : ''; if (!id || !name || !type || seenIds.has(id) || seenNames.has(nameKey)) { return null; } seenIds.add(id); seenNames.add(nameKey); return { id, name, type, position } satisfies LegacyServerChannel; }) .filter((channel): channel is LegacyServerChannel => !!channel); } catch { return []; } } function shouldRestoreLegacyVoiceGeneral(channels: LegacyServerChannel[]): boolean { const hasTextGeneral = channels.some( (channel) => channel.type === 'text' && (channel.id === 'general' || channel.name.toLocaleLowerCase() === 'general') ); const hasVoiceAfk = channels.some( (channel) => channel.type === 'voice' && (channel.id === 'vc-afk' || channel.name.toLocaleLowerCase() === 'afk') ); const hasVoiceGeneral = channels.some( (channel) => channel.type === 'voice' && (channel.id === 'vc-general' || channel.name.toLocaleLowerCase() === 'general') ); return hasTextGeneral && hasVoiceAfk && !hasVoiceGeneral; } function repairLegacyVoiceChannels(channels: LegacyServerChannel[]): LegacyServerChannel[] { if (!shouldRestoreLegacyVoiceGeneral(channels)) { return channels; } const textChannels = channels.filter((channel) => channel.type === 'text'); const voiceChannels = channels.filter((channel) => channel.type === 'voice'); const repairedVoiceChannels = [ { id: 'vc-general', name: 'General', type: 'voice' as const, position: 0 }, ...voiceChannels ].map((channel, index) => ({ ...channel, position: index })); return [ ...textChannels, ...repairedVoiceChannels ]; } export class RepairLegacyVoiceChannels1000000000003 implements MigrationInterface { name = 'RepairLegacyVoiceChannels1000000000003'; public async up(queryRunner: QueryRunner): Promise { const rows = await queryRunner.query(`SELECT "id", "channels" FROM "servers"`) as LegacyServerRow[]; for (const row of rows) { const channels = normalizeLegacyChannels(row.channels); const repaired = repairLegacyVoiceChannels(channels); if (JSON.stringify(repaired) === JSON.stringify(channels)) { continue; } await queryRunner.query( `UPDATE "servers" SET "channels" = ? WHERE "id" = ?`, [JSON.stringify(repaired), row.id] ); } } public async down(_queryRunner: QueryRunner): Promise { // Forward-only data repair migration. } }