'use strict'; /** * Migration: Backfill project_element_defaults for existing projects * * For each existing project that doesn't have project_element_defaults, * create a snapshot of the current global element_type_defaults. */ // Default element types to ensure they exist before backfilling const DEFAULT_ELEMENT_TYPES = [ { element_type: 'navigation_next', name: 'Navigation Forward Button', sort_order: 1, settings_json: JSON.stringify({ label: 'Navigation: Forward', navLabel: 'Forward', navType: 'forward', navDisabled: false, transitionReverseMode: 'auto_reverse', transitionDurationSec: 0.7, appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'navigation_prev', name: 'Navigation Back Button', sort_order: 2, settings_json: JSON.stringify({ label: 'Navigation: Back', navLabel: 'Back', navType: 'back', navDisabled: false, transitionReverseMode: 'auto_reverse', transitionDurationSec: 0.7, appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'tooltip', name: 'Tooltip', sort_order: 3, settings_json: JSON.stringify({ label: 'Tooltip', tooltipTitle: 'Tooltip title', tooltipText: 'Tooltip text', appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'description', name: 'Description', sort_order: 4, settings_json: JSON.stringify({ label: 'Description', descriptionTitle: 'TITLE', descriptionText: '', descriptionTitleFontSize: '48px', descriptionTextFontSize: '36px', descriptionTitleFontFamily: 'inherit', descriptionTextFontFamily: 'inherit', descriptionTitleColor: '#000000', descriptionTextColor: '#4B5563', descriptionBackgroundColor: 'transparent', appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'gallery', name: 'Gallery', sort_order: 5, settings_json: JSON.stringify({ label: 'Gallery', galleryCards: [{ imageUrl: '', title: 'Card 1', description: '' }], appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'carousel', name: 'Carousel', sort_order: 6, settings_json: JSON.stringify({ label: 'Carousel', carouselSlides: [{ imageUrl: '', caption: 'Slide 1' }], carouselPrevIconUrl: '', carouselNextIconUrl: '', appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'video_player', name: 'Video Player', sort_order: 7, settings_json: JSON.stringify({ label: 'Video Player', mediaUrl: '', mediaAutoplay: true, mediaLoop: true, mediaMuted: true, appearDelaySec: 0, appearDurationSec: null, }), }, { element_type: 'audio_player', name: 'Audio Player', sort_order: 8, settings_json: JSON.stringify({ label: 'Audio Player', mediaUrl: '', mediaAutoplay: true, mediaLoop: true, mediaMuted: false, appearDelaySec: 0, appearDurationSec: null, }), }, ]; module.exports = { async up(queryInterface, Sequelize) { // First, ensure element_type_defaults has all default rows // This is needed because the API's lazy initialization won't have run yet during migration const [existingTypes] = await queryInterface.sequelize.query( `SELECT element_type FROM element_type_defaults WHERE "deletedAt" IS NULL`, { type: Sequelize.QueryTypes.SELECT } ); const existingTypeSet = new Set( Array.isArray(existingTypes) ? existingTypes.map((t) => t.element_type) : existingTypes ? [existingTypes.element_type] : [] ); // Insert missing element types for (const defaultType of DEFAULT_ELEMENT_TYPES) { if (!existingTypeSet.has(defaultType.element_type)) { await queryInterface.sequelize.query( `INSERT INTO element_type_defaults (id, element_type, name, sort_order, settings_json, "createdAt", "updatedAt") VALUES (gen_random_uuid(), :element_type, :name, :sort_order, :settings_json, NOW(), NOW())`, { replacements: { element_type: defaultType.element_type, name: defaultType.name, sort_order: defaultType.sort_order, settings_json: defaultType.settings_json, }, } ); console.log(`Created missing element_type_default: ${defaultType.element_type}`); } } // Get all existing projects const [projects] = await queryInterface.sequelize.query( `SELECT id FROM projects WHERE "deletedAt" IS NULL`, { type: Sequelize.QueryTypes.SELECT } ); if (!projects || projects.length === 0) { console.log('No projects found, skipping backfill'); return; } // Get all global element type defaults (now guaranteed to have all types) const [globalDefaults] = await queryInterface.sequelize.query( `SELECT id, element_type, name, sort_order, settings_json FROM element_type_defaults WHERE "deletedAt" IS NULL`, { type: Sequelize.QueryTypes.SELECT } ); if (!globalDefaults || globalDefaults.length === 0) { console.log('No global element type defaults found, skipping backfill'); return; } const projectIds = Array.isArray(projects) ? projects.map((p) => p.id) : [projects.id]; const globalDefaultRows = Array.isArray(globalDefaults) ? globalDefaults : [globalDefaults]; // For each project, add any missing element type defaults for (const projectId of projectIds) { // Get existing element types for this project const [existingDefaults] = await queryInterface.sequelize.query( `SELECT element_type FROM project_element_defaults WHERE "projectId" = :projectId AND "deletedAt" IS NULL`, { replacements: { projectId }, type: Sequelize.QueryTypes.SELECT, } ); const existingProjectTypes = new Set( Array.isArray(existingDefaults) ? existingDefaults.map((d) => d.element_type) : existingDefaults ? [existingDefaults.element_type] : [] ); // Create project element defaults for missing types let addedCount = 0; for (const globalDefault of globalDefaultRows) { if (existingProjectTypes.has(globalDefault.element_type)) { continue; // Already has this type } await queryInterface.sequelize.query( `INSERT INTO project_element_defaults (id, element_type, name, sort_order, settings_json, source_element_id, snapshot_version, "projectId", "createdAt", "updatedAt") VALUES ( gen_random_uuid(), :element_type, :name, :sort_order, :settings_json, :source_element_id, 1, :projectId, NOW(), NOW() )`, { replacements: { element_type: globalDefault.element_type, name: globalDefault.name, sort_order: globalDefault.sort_order, settings_json: globalDefault.settings_json, source_element_id: globalDefault.id, projectId, }, type: Sequelize.QueryTypes.INSERT, } ); addedCount++; } if (addedCount > 0) { console.log(`Backfilled ${addedCount} element defaults for project ${projectId}`); } else { console.log(`Project ${projectId} already has all element defaults`); } } console.log('Successfully backfilled project_element_defaults for existing projects'); }, async down(queryInterface, _Sequelize) { // Delete all project_element_defaults with snapshot_version = 1 // (only the ones we created during backfill) await queryInterface.sequelize.query( `DELETE FROM project_element_defaults WHERE snapshot_version = 1` ); console.log('Successfully removed backfilled project_element_defaults'); }, };