39948-vm/backend/src/db/migrations/20260326000004-backfill-project-element-defaults.js
2026-03-26 21:19:18 +04:00

267 lines
8.1 KiB
JavaScript

'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');
},
};