mirror of
https://github.com/wekan/wekan.git
synced 2025-12-30 22:28:49 +01:00
286 lines
8.2 KiB
JavaScript
286 lines
8.2 KiB
JavaScript
/**
|
|
* Migration: Ensure all entities have valid swimlaneId
|
|
*
|
|
* This migration ensures that:
|
|
* 1. All cards have a valid swimlaneId
|
|
* 2. All lists have a valid swimlaneId (if applicable)
|
|
* 3. Orphaned entities (without valid swimlaneId) are moved to a "Rescued Data" swimlane
|
|
*
|
|
* This is similar to the existing rescue migration but specifically for swimlaneId validation
|
|
*/
|
|
|
|
// Helper collection to track migrations - must be defined first
|
|
const Migrations = new Mongo.Collection('migrations');
|
|
|
|
Meteor.startup(() => {
|
|
// Only run on server
|
|
if (!Meteor.isServer) return;
|
|
|
|
const MIGRATION_NAME = 'ensure-valid-swimlane-ids';
|
|
const MIGRATION_VERSION = 1;
|
|
|
|
// Check if migration already ran
|
|
const existingMigration = Migrations.findOne({ name: MIGRATION_NAME });
|
|
if (existingMigration && existingMigration.version >= MIGRATION_VERSION) {
|
|
return;
|
|
}
|
|
|
|
console.log(`Running migration: ${MIGRATION_NAME} v${MIGRATION_VERSION}`);
|
|
|
|
/**
|
|
* Get or create a "Rescued Data" swimlane for a board
|
|
*/
|
|
function getOrCreateRescuedSwimlane(boardId) {
|
|
const board = Boards.findOne(boardId);
|
|
if (!board) return null;
|
|
|
|
// Look for existing rescued data swimlane
|
|
let rescuedSwimlane = Swimlanes.findOne({
|
|
boardId,
|
|
title: { $regex: /rescued.*data/i },
|
|
});
|
|
|
|
if (!rescuedSwimlane) {
|
|
// Create a new rescued data swimlane
|
|
const swimlaneId = Swimlanes.insert({
|
|
title: 'Rescued Data (Missing Swimlane)',
|
|
boardId,
|
|
archived: false,
|
|
sort: 9999999, // Put at the end
|
|
type: 'swimlane',
|
|
color: 'red',
|
|
});
|
|
|
|
rescuedSwimlane = Swimlanes.findOne(swimlaneId);
|
|
|
|
Activities.insert({
|
|
userId: 'migration',
|
|
type: 'swimlane',
|
|
activityType: 'createSwimlane',
|
|
boardId,
|
|
swimlaneId,
|
|
title: 'Created rescued data swimlane during migration',
|
|
});
|
|
}
|
|
|
|
return rescuedSwimlane;
|
|
}
|
|
|
|
/**
|
|
* Validate and fix cards without valid swimlaneId
|
|
*/
|
|
function fixCardsWithoutSwimlaneId() {
|
|
let fixedCount = 0;
|
|
let rescuedCount = 0;
|
|
|
|
const cardsWithoutSwimlane = Cards.find({
|
|
$or: [
|
|
{ swimlaneId: { $exists: false } },
|
|
{ swimlaneId: null },
|
|
{ swimlaneId: '' },
|
|
],
|
|
}).fetch();
|
|
|
|
console.log(`Found ${cardsWithoutSwimlane.length} cards without swimlaneId`);
|
|
|
|
cardsWithoutSwimlane.forEach(card => {
|
|
const board = Boards.findOne(card.boardId);
|
|
if (!board) {
|
|
console.warn(`Card ${card._id} has invalid boardId: ${card.boardId}`);
|
|
return;
|
|
}
|
|
|
|
// Try to get default swimlane
|
|
let defaultSwimlane = Swimlanes.findOne({
|
|
boardId: card.boardId,
|
|
type: { $ne: 'template-swimlane' },
|
|
archived: false,
|
|
}, { sort: { sort: 1 } });
|
|
|
|
if (!defaultSwimlane) {
|
|
// No swimlanes at all - create default
|
|
const swimlaneId = Swimlanes.insert({
|
|
title: 'Default',
|
|
boardId: card.boardId,
|
|
archived: false,
|
|
sort: 0,
|
|
type: 'swimlane',
|
|
});
|
|
defaultSwimlane = Swimlanes.findOne(swimlaneId);
|
|
}
|
|
|
|
if (defaultSwimlane) {
|
|
Cards.update(card._id, {
|
|
$set: { swimlaneId: defaultSwimlane._id },
|
|
});
|
|
fixedCount++;
|
|
} else {
|
|
console.warn(`Could not find or create default swimlane for card ${card._id}`);
|
|
}
|
|
});
|
|
|
|
return { fixedCount, rescuedCount };
|
|
}
|
|
|
|
/**
|
|
* Validate and fix lists without valid swimlaneId
|
|
*/
|
|
function fixListsWithoutSwimlaneId() {
|
|
let fixedCount = 0;
|
|
|
|
const listsWithoutSwimlane = Lists.find({
|
|
$or: [
|
|
{ swimlaneId: { $exists: false } },
|
|
{ swimlaneId: null },
|
|
],
|
|
}).fetch();
|
|
|
|
console.log(`Found ${listsWithoutSwimlane.length} lists without swimlaneId`);
|
|
|
|
listsWithoutSwimlane.forEach(list => {
|
|
// Set to empty string for backward compatibility
|
|
// (lists can be shared across swimlanes)
|
|
Lists.update(list._id, {
|
|
$set: { swimlaneId: '' },
|
|
});
|
|
fixedCount++;
|
|
});
|
|
|
|
return { fixedCount };
|
|
}
|
|
|
|
/**
|
|
* Find and rescue orphaned cards (swimlaneId points to non-existent swimlane)
|
|
*/
|
|
function rescueOrphanedCards() {
|
|
let rescuedCount = 0;
|
|
|
|
const allCards = Cards.find({}).fetch();
|
|
|
|
allCards.forEach(card => {
|
|
if (!card.swimlaneId) return; // Handled by fixCardsWithoutSwimlaneId
|
|
|
|
// Check if swimlane exists
|
|
const swimlane = Swimlanes.findOne(card.swimlaneId);
|
|
if (!swimlane) {
|
|
// Orphaned card - swimlane doesn't exist
|
|
const rescuedSwimlane = getOrCreateRescuedSwimlane(card.boardId);
|
|
|
|
if (rescuedSwimlane) {
|
|
Cards.update(card._id, {
|
|
$set: { swimlaneId: rescuedSwimlane._id },
|
|
});
|
|
rescuedCount++;
|
|
|
|
Activities.insert({
|
|
userId: 'migration',
|
|
type: 'card',
|
|
activityType: 'moveCard',
|
|
boardId: card.boardId,
|
|
cardId: card._id,
|
|
swimlaneId: rescuedSwimlane._id,
|
|
listId: card.listId,
|
|
title: `Rescued card from deleted swimlane`,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return { rescuedCount };
|
|
}
|
|
|
|
/**
|
|
* Ensure all swimlaneId references are always saved in all operations
|
|
* This adds a global hook to validate swimlaneId before insert/update
|
|
*/
|
|
function addSwimlaneIdValidationHooks() {
|
|
// Card insert hook
|
|
Cards.before.insert(function(userId, doc) {
|
|
if (!doc.swimlaneId) {
|
|
const board = Boards.findOne(doc.boardId);
|
|
if (board) {
|
|
const defaultSwimlane = Swimlanes.findOne({
|
|
boardId: doc.boardId,
|
|
type: { $ne: 'template-swimlane' },
|
|
archived: false,
|
|
}, { sort: { sort: 1 } });
|
|
|
|
if (defaultSwimlane) {
|
|
doc.swimlaneId = defaultSwimlane._id;
|
|
} else {
|
|
console.warn('No default swimlane found for new card, creating one');
|
|
const swimlaneId = Swimlanes.insert({
|
|
title: 'Default',
|
|
boardId: doc.boardId,
|
|
archived: false,
|
|
sort: 0,
|
|
type: 'swimlane',
|
|
});
|
|
doc.swimlaneId = swimlaneId;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Card update hook - ensure swimlaneId is never removed
|
|
Cards.before.update(function(userId, doc, fieldNames, modifier) {
|
|
if (modifier.$unset && modifier.$unset.swimlaneId) {
|
|
delete modifier.$unset.swimlaneId;
|
|
console.warn('Prevented removal of swimlaneId from card', doc._id);
|
|
}
|
|
|
|
if (modifier.$set && modifier.$set.swimlaneId === null) {
|
|
const defaultSwimlane = Swimlanes.findOne({
|
|
boardId: doc.boardId,
|
|
type: { $ne: 'template-swimlane' },
|
|
archived: false,
|
|
}, { sort: { sort: 1 } });
|
|
|
|
if (defaultSwimlane) {
|
|
modifier.$set.swimlaneId = defaultSwimlane._id;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Run all fix operations
|
|
const cardResults = fixCardsWithoutSwimlaneId();
|
|
const listResults = fixListsWithoutSwimlaneId();
|
|
const rescueResults = rescueOrphanedCards();
|
|
|
|
console.log('Migration results:');
|
|
console.log(`- Fixed ${cardResults.fixedCount} cards without swimlaneId`);
|
|
console.log(`- Fixed ${listResults.fixedCount} lists without swimlaneId`);
|
|
console.log(`- Rescued ${rescueResults.rescuedCount} orphaned cards`);
|
|
|
|
// Record migration completion
|
|
Migrations.upsert(
|
|
{ name: MIGRATION_NAME },
|
|
{
|
|
$set: {
|
|
name: MIGRATION_NAME,
|
|
version: MIGRATION_VERSION,
|
|
completedAt: new Date(),
|
|
results: {
|
|
cardsFixed: cardResults.fixedCount,
|
|
listsFixed: listResults.fixedCount,
|
|
cardsRescued: rescueResults.rescuedCount,
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
|
|
} catch (error) {
|
|
console.error(`Migration ${MIGRATION_NAME} failed:`, error);
|
|
}
|
|
|
|
// Add validation hooks (outside try-catch to ensure they run even if migration failed)
|
|
try {
|
|
addSwimlaneIdValidationHooks();
|
|
console.log('SwimlaneId validation hooks installed');
|
|
} catch (error) {
|
|
console.error('Failed to install swimlaneId validation hooks:', error);
|
|
}
|
|
});
|