fix-duplicate-lists.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env node
  2. /**
  3. * Standalone script to fix duplicate lists created by WeKan 8.10
  4. *
  5. * Usage:
  6. * node fix-duplicate-lists.js
  7. *
  8. * This script will:
  9. * 1. Connect to the MongoDB database
  10. * 2. Identify boards with duplicate lists/swimlanes
  11. * 3. Fix the duplicates by merging them
  12. * 4. Report the results
  13. */
  14. const { MongoClient } = require('mongodb');
  15. // Configuration - adjust these for your setup
  16. const MONGO_URL = process.env.MONGO_URL || 'mongodb://localhost:27017/wekan';
  17. const DB_NAME = process.env.MONGO_DB_NAME || 'wekan';
  18. class DuplicateListsFixer {
  19. constructor() {
  20. this.client = null;
  21. this.db = null;
  22. }
  23. async connect() {
  24. console.log('Connecting to MongoDB...');
  25. this.client = new MongoClient(MONGO_URL);
  26. await this.client.connect();
  27. this.db = this.client.db(DB_NAME);
  28. console.log('Connected to MongoDB');
  29. }
  30. async disconnect() {
  31. if (this.client) {
  32. await this.client.close();
  33. console.log('Disconnected from MongoDB');
  34. }
  35. }
  36. async getReport() {
  37. console.log('Analyzing boards for duplicate lists...');
  38. const boards = await this.db.collection('boards').find({}).toArray();
  39. const report = [];
  40. for (const board of boards) {
  41. const swimlanes = await this.db.collection('swimlanes').find({ boardId: board._id }).toArray();
  42. const lists = await this.db.collection('lists').find({ boardId: board._id }).toArray();
  43. // Check for duplicate swimlanes
  44. const swimlaneGroups = {};
  45. swimlanes.forEach(swimlane => {
  46. const key = swimlane.title || 'Default';
  47. if (!swimlaneGroups[key]) {
  48. swimlaneGroups[key] = [];
  49. }
  50. swimlaneGroups[key].push(swimlane);
  51. });
  52. // Check for duplicate lists
  53. const listGroups = {};
  54. lists.forEach(list => {
  55. const key = `${list.swimlaneId || 'null'}-${list.title}`;
  56. if (!listGroups[key]) {
  57. listGroups[key] = [];
  58. }
  59. listGroups[key].push(list);
  60. });
  61. const duplicateSwimlanes = Object.values(swimlaneGroups).filter(group => group.length > 1);
  62. const duplicateLists = Object.values(listGroups).filter(group => group.length > 1);
  63. if (duplicateSwimlanes.length > 0 || duplicateLists.length > 0) {
  64. report.push({
  65. boardId: board._id,
  66. boardTitle: board.title,
  67. duplicateSwimlanes: duplicateSwimlanes.length,
  68. duplicateLists: duplicateLists.length,
  69. totalSwimlanes: swimlanes.length,
  70. totalLists: lists.length
  71. });
  72. }
  73. }
  74. return {
  75. totalBoards: boards.length,
  76. boardsWithDuplicates: report.length,
  77. report
  78. };
  79. }
  80. async fixBoard(boardId) {
  81. console.log(`Fixing duplicate lists for board ${boardId}...`);
  82. // Fix duplicate swimlanes
  83. const swimlaneResult = await this.fixDuplicateSwimlanes(boardId);
  84. // Fix duplicate lists
  85. const listResult = await this.fixDuplicateLists(boardId);
  86. return {
  87. boardId,
  88. fixedSwimlanes: swimlaneResult.fixed,
  89. fixedLists: listResult.fixed,
  90. fixed: swimlaneResult.fixed + listResult.fixed
  91. };
  92. }
  93. async fixDuplicateSwimlanes(boardId) {
  94. const swimlanes = await this.db.collection('swimlanes').find({ boardId }).toArray();
  95. const swimlaneGroups = {};
  96. let fixed = 0;
  97. // Group swimlanes by title
  98. swimlanes.forEach(swimlane => {
  99. const key = swimlane.title || 'Default';
  100. if (!swimlaneGroups[key]) {
  101. swimlaneGroups[key] = [];
  102. }
  103. swimlaneGroups[key].push(swimlane);
  104. });
  105. // For each group with duplicates, keep the oldest and remove the rest
  106. for (const [title, group] of Object.entries(swimlaneGroups)) {
  107. if (group.length > 1) {
  108. // Sort by creation date, keep the oldest
  109. group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  110. const keepSwimlane = group[0];
  111. const removeSwimlanes = group.slice(1);
  112. console.log(`Found ${group.length} duplicate swimlanes with title "${title}", keeping oldest (${keepSwimlane._id})`);
  113. // Move all lists from duplicate swimlanes to the kept swimlane
  114. for (const swimlane of removeSwimlanes) {
  115. const lists = await this.db.collection('lists').find({ swimlaneId: swimlane._id }).toArray();
  116. for (const list of lists) {
  117. // Check if a list with the same title already exists in the kept swimlane
  118. const existingList = await this.db.collection('lists').findOne({
  119. boardId,
  120. swimlaneId: keepSwimlane._id,
  121. title: list.title
  122. });
  123. if (existingList) {
  124. // Move cards to existing list
  125. await this.db.collection('cards').updateMany(
  126. { listId: list._id },
  127. { $set: { listId: existingList._id } }
  128. );
  129. // Remove duplicate list
  130. await this.db.collection('lists').deleteOne({ _id: list._id });
  131. console.log(`Moved cards from duplicate list "${list.title}" to existing list in kept swimlane`);
  132. } else {
  133. // Move list to kept swimlane
  134. await this.db.collection('lists').updateOne(
  135. { _id: list._id },
  136. { $set: { swimlaneId: keepSwimlane._id } }
  137. );
  138. console.log(`Moved list "${list.title}" to kept swimlane`);
  139. }
  140. }
  141. // Remove duplicate swimlane
  142. await this.db.collection('swimlanes').deleteOne({ _id: swimlane._id });
  143. fixed++;
  144. }
  145. }
  146. }
  147. return { fixed };
  148. }
  149. async fixDuplicateLists(boardId) {
  150. const lists = await this.db.collection('lists').find({ boardId }).toArray();
  151. const listGroups = {};
  152. let fixed = 0;
  153. // Group lists by title and swimlaneId
  154. lists.forEach(list => {
  155. const key = `${list.swimlaneId || 'null'}-${list.title}`;
  156. if (!listGroups[key]) {
  157. listGroups[key] = [];
  158. }
  159. listGroups[key].push(list);
  160. });
  161. // For each group with duplicates, keep the oldest and remove the rest
  162. for (const [key, group] of Object.entries(listGroups)) {
  163. if (group.length > 1) {
  164. // Sort by creation date, keep the oldest
  165. group.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  166. const keepList = group[0];
  167. const removeLists = group.slice(1);
  168. console.log(`Found ${group.length} duplicate lists with title "${keepList.title}" in swimlane ${keepList.swimlaneId}, keeping oldest (${keepList._id})`);
  169. // Move all cards from duplicate lists to the kept list
  170. for (const list of removeLists) {
  171. await this.db.collection('cards').updateMany(
  172. { listId: list._id },
  173. { $set: { listId: keepList._id } }
  174. );
  175. // Remove duplicate list
  176. await this.db.collection('lists').deleteOne({ _id: list._id });
  177. fixed++;
  178. console.log(`Moved cards from duplicate list "${list.title}" to kept list`);
  179. }
  180. }
  181. }
  182. return { fixed };
  183. }
  184. async fixAllBoards() {
  185. console.log('Starting duplicate lists fix for all boards...');
  186. const allBoards = await this.db.collection('boards').find({}).toArray();
  187. let totalFixed = 0;
  188. let totalBoardsProcessed = 0;
  189. for (const board of allBoards) {
  190. try {
  191. const result = await this.fixBoard(board._id);
  192. totalFixed += result.fixed;
  193. totalBoardsProcessed++;
  194. if (result.fixed > 0) {
  195. console.log(`Fixed ${result.fixed} duplicate lists in board "${board.title}" (${board._id})`);
  196. }
  197. } catch (error) {
  198. console.error(`Error fixing board ${board._id}:`, error);
  199. }
  200. }
  201. console.log(`Duplicate lists fix completed. Processed ${totalBoardsProcessed} boards, fixed ${totalFixed} duplicate lists.`);
  202. return {
  203. message: `Fixed ${totalFixed} duplicate lists across ${totalBoardsProcessed} boards`,
  204. totalFixed,
  205. totalBoardsProcessed
  206. };
  207. }
  208. }
  209. // Main execution
  210. async function main() {
  211. const fixer = new DuplicateListsFixer();
  212. try {
  213. await fixer.connect();
  214. // Get report first
  215. const report = await fixer.getReport();
  216. if (report.boardsWithDuplicates === 0) {
  217. console.log('No duplicate lists found!');
  218. return;
  219. }
  220. console.log(`Found ${report.boardsWithDuplicates} boards with duplicate lists:`);
  221. report.report.forEach(board => {
  222. console.log(`- Board "${board.boardTitle}" (${board.boardId}): ${board.duplicateSwimlanes} duplicate swimlanes, ${board.duplicateLists} duplicate lists`);
  223. });
  224. // Perform the fix
  225. const result = await fixer.fixAllBoards();
  226. console.log('Fix completed:', result);
  227. } catch (error) {
  228. console.error('Error:', error);
  229. process.exit(1);
  230. } finally {
  231. await fixer.disconnect();
  232. }
  233. }
  234. // Run if called directly
  235. if (require.main === module) {
  236. main();
  237. }
  238. module.exports = DuplicateListsFixer;