Repository: ita-social-projects/SpaceToStudy-BackEnd
Pull request #1350: Missing down Method in Migration delete-old-fields-in-cooperatives-and-add-new-ones #1136
Author: login: danikua
Labels: name: bug
description: Something isn't working
name: Backend
Created: 2025-05-08T13:27:38Z | Merged: 2025-05-10T11:15:11Z

**Description:**
This PR enhances the `delete-old-fields-in-cooperatives-and-add-new-ones` migration by introducing:

* A backup mechanism that creates a `cooperation_backup` collection before applying changes.
* A `down` method that restores the original `cooperation` collection from the backup, ensuring safe rollback.
* Safeguards to prevent rollback if the backup is missing.
* Added tests for down method

These additions ensure data integrity and provide a reliable way to revert changes if necessary.



Diff:
diff --git a/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js b/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
index fd427d63..95ffca13 100644
--- a/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
+++ b/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
@@ -15,17 +15,14 @@ function transformSection(section) {
     }
 
     if (activity.resourceType) transformedResource.resourceType = activity.resourceType
-    if (activity.resource.availability) {
+    if (activity.resource?.availability) {
       transformedResource.availability = {
         status: activity.resource.availability.status,
         date: activity.resource.availability.date
       }
     }
 
-    if (activity.completionStatus) transformedResource.completionStatus = activity.completionStatus
-    else {
-      transformedResource.completionStatus = 'active'
-    }
+    transformedResource.completionStatus = activity.completionStatus || 'active'
 
     return transformedResource
   })
@@ -56,6 +53,11 @@ async function processSections(db, cooperation) {
 
 module.exports = {
   async up(db) {
+    await db
+      .collection('cooperation')
+      .aggregate([{ $match: {} }, { $out: 'cooperation_backup' }])
+      .toArray()
+
     const allUserIds = new Set(
       (
         await db
@@ -71,14 +73,12 @@ module.exports = {
         await db.collection('cooperation').deleteOne({ _id: cooperation._id })
       }
 
-      if (Array.isArray(cooperation.proficiencyLevel)) {
-        if (cooperation.proficiencyLevel.length > 0) {
-          const averageProficiencyLevelIndex = cooperation.proficiencyLevel.length / 2
-          const averageProficiencyLevel = cooperation.proficiencyLevel[Math.ceil(averageProficiencyLevelIndex)]
-          await db
-            .collection('cooperation')
-            .updateOne({ _id: cooperation._id }, { $set: { proficiencyLevel: averageProficiencyLevel } })
-        }
+      if (Array.isArray(cooperation.proficiencyLevel) && cooperation.proficiencyLevel.length > 0) {
+        const averageProficiencyLevelIndex = cooperation.proficiencyLevel.length / 2
+        const averageProficiencyLevel = cooperation.proficiencyLevel[Math.ceil(averageProficiencyLevelIndex)]
+        await db
+          .collection('cooperation')
+          .updateOne({ _id: cooperation._id }, { $set: { proficiencyLevel: averageProficiencyLevel } })
       }
 
       if (!cooperation.title) {
@@ -93,6 +93,7 @@ module.exports = {
       }
       await processSections(db, cooperation)
     }
+
     await db.collection('cooperation').updateMany(
       {
         $or: [{ category: { $exists: true } }, { subject: { $exists: true } }, { languages: { $exists: true } }]
@@ -117,6 +118,21 @@ module.exports = {
       }
     )
   },
-  down() {},
+
+  async down(db) {
+    const backupExists = await db.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (!backupExists) {
+      throw new Error("Backup 'cooperation_backup' not found. Rollback is not possible.")
+    }
+
+    await db.collection('cooperation').drop()
+
+    await db
+      .collection('cooperation_backup')
+      .aggregate([{ $match: {} }, { $out: 'cooperation' }])
+      .toArray()
+
+    await db.collection('cooperation_backup').drop()
+  },
   transformSection
 }
diff --git a/package-lock.json b/package-lock.json
index b9d29ef2..46b7f444 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8131,12 +8131,6 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
-    "node_modules/openapi-types": {
-      "version": "12.1.3",
-      "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
-      "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
-      "peer": true
-    },
     "node_modules/natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -8435,6 +8429,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/openapi-types": {
+      "version": "12.1.3",
+      "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+      "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+      "peer": true
+    },
     "node_modules/optional-require": {
       "version": "1.1.8",
       "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
diff --git a/src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js b/src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js
index 229e311a..8bb0b70a 100644
--- a/src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js
+++ b/src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js
@@ -257,4 +257,38 @@ describe('20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones', ()
 
     expect(updatedCooperation.sections).toEqual([])
   })
+  test('down should restore the cooperation collection from backup if it exists', async () => {
+    const backupDoc = {
+      _id: new ObjectId(),
+      initiator: validCooperation.initiator,
+      receiver: validCooperation.receiver,
+      title: 'Restored from Backup',
+      proficiencyLevel: 'Advanced',
+      sections: [],
+      additionalInfo: 'Restored info'
+    }
+    await database.collection('cooperation_backup').insertOne(backupDoc)
+
+    await database.collection(cooperationCollection).drop()
+
+    await require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+
+    const restoredDoc = await database.collection(cooperationCollection).findOne({ _id: backupDoc._id })
+    expect(restoredDoc).toBeDefined()
+    expect(restoredDoc.title).toBe('Restored from Backup')
+
+    const backupStillExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    expect(backupStillExists).toBe(false)
+  })
+
+  test('down should throw an error if the backup does not exist', async () => {
+    const backupExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (backupExists) {
+      await database.collection('cooperation_backup').drop()
+    }
+
+    await expect(
+      require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+    ).rejects.toThrow("Backup 'cooperation_backup' not found. Rollback is not possible.")
+  })
 })


Reviews:
user: login: Iarynovskyi
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T20:41:02Z
user: login: danikua
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:12:46Z
user: login: danikua
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:12:50Z
user: login: luvthenika
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:31:14Z
user: login: Iarynovskyi
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:51:35Z
user: login: IrynaKhylchuk
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:10:19Z

Review comments:
diff_hunk: @@ -117,6 +118,23 @@ module.exports = {
       }
     )
   },
-  down() {},
+
+  async down(db) {
+    const backupExists = await db.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (!backupExists) {
+      console.error("Резервная копия 'cooperation_backup' не найдена. Откат невозможен.")
path: migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
user: login: Iarynovskyi
body: Please write it in English
created_at: 2025-05-08T20:17:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,6 +118,23 @@ module.exports = {
       }
     )
   },
-  down() {},
+
+  async down(db) {
+    const backupExists = await db.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (!backupExists) {
+      console.error("Резервная копия 'cooperation_backup' не найдена. Откат невозможен.")
path: migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
user: login: Iarynovskyi
body: I thought about it, and in this error case, replace the `return` with `throw new Error()` than migration process fails explicitly and prevents data loss
created_at: 2025-05-08T20:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -257,4 +257,42 @@ describe('20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones', ()
 
     expect(updatedCooperation.sections).toEqual([])
   })
+  test('down should restore the cooperation collection from backup if it exists', async () => {
+    const backupDoc = {
+      _id: new ObjectId(),
+      initiator: validCooperation.initiator,
+      receiver: validCooperation.receiver,
+      title: 'Restored from Backup',
+      proficiencyLevel: 'Advanced',
+      sections: [],
+      additionalInfo: 'Restored info'
+    }
+    await database.collection('cooperation_backup').insertOne(backupDoc)
+
+    await database.collection(cooperationCollection).drop()
+
+    await require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+
+    const restoredDoc = await database.collection(cooperationCollection).findOne({ _id: backupDoc._id })
+    expect(restoredDoc).toBeDefined()
+    expect(restoredDoc.title).toBe('Restored from Backup')
+
+    const backupStillExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    expect(backupStillExists).toBe(false)
+  })
+
+  test('down should not fail and log an error if the backup does not exist', async () => {
+    const backupExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (backupExists) {
+      await database.collection('cooperation_backup').drop()
+    }
+
+    const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
+
+    await require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+
+    expect(consoleSpy).toHaveBeenCalledWith("Резервная копия 'cooperation_backup' не найдена. Откат невозможен.")
path: src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js
user: login: Iarynovskyi
body: In English please, too
created_at: 2025-05-08T20:30:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,6 +118,23 @@ module.exports = {
       }
     )
   },
-  down() {},
+
+  async down(db) {
+    const backupExists = await db.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (!backupExists) {
+      console.error("Резервная копия 'cooperation_backup' не найдена. Откат невозможен.")
path: migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.js
user: login: danikua
body: fixed

created_at: 2025-05-09T14:12:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -257,4 +257,42 @@ describe('20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones', ()
 
     expect(updatedCooperation.sections).toEqual([])
   })
+  test('down should restore the cooperation collection from backup if it exists', async () => {
+    const backupDoc = {
+      _id: new ObjectId(),
+      initiator: validCooperation.initiator,
+      receiver: validCooperation.receiver,
+      title: 'Restored from Backup',
+      proficiencyLevel: 'Advanced',
+      sections: [],
+      additionalInfo: 'Restored info'
+    }
+    await database.collection('cooperation_backup').insertOne(backupDoc)
+
+    await database.collection(cooperationCollection).drop()
+
+    await require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+
+    const restoredDoc = await database.collection(cooperationCollection).findOne({ _id: backupDoc._id })
+    expect(restoredDoc).toBeDefined()
+    expect(restoredDoc.title).toBe('Restored from Backup')
+
+    const backupStillExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    expect(backupStillExists).toBe(false)
+  })
+
+  test('down should not fail and log an error if the backup does not exist', async () => {
+    const backupExists = await database.listCollections({ name: 'cooperation_backup' }).hasNext()
+    if (backupExists) {
+      await database.collection('cooperation_backup').drop()
+    }
+
+    const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
+
+    await require('@root/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones').down(database)
+
+    expect(consoleSpy).toHaveBeenCalledWith("Резервная копия 'cooperation_backup' не найдена. Откат невозможен.")
path: src/test/migrations/20241220213859-delete-old-fields-in-cooperatives-and-add-new-ones.spec.js
user: login: danikua
body: fixed

created_at: 2025-05-09T14:12:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-08T13:36:03Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [91.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T13:36:08Z
body: # ✅ CI Report - 14:12 10/05/2025

## 📝 Lint Report

```
✅ No lint issues found.
```

## 🧪 Test Results

**🔹 Node.js Version:** 22.x
**🔹 MongoDB Version:** 8.0

**Test Summary:**
```
Test Suites: 81 passed, 81 total
Tests:       672 passed, 672 total
Snapshots:   0 total
Time:        336.368 s
```


## 🐳 Docker Health Check

**Health Check Results:**
```
✅ No containerization issues detected.
```
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-10T11:12:31Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [85.4% Coverage on New Code](https://sonarcloud.io/component_measures?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ita-social-projects_SpaceToStudy-BackEnd&pullRequest=1350)


author_association: NONE