diff --git a/jest.config.js b/jest.config.js index e4d74660..dc7157fe 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,8 @@ module.exports = { roots: ['/src/test'], moduleNameMapper: { - '^~/(.*)$': '/src/$1' + '^~/(.*)$': '/src/$1', + '@root/(.*)': '/$1' }, verbose: true, testEnvironment: 'node', @@ -27,6 +28,10 @@ module.exports = { coverageReporters: ['html', 'lcov'], coverageDirectory: '/src/test/coverage', testTimeout: 12000, - testMatch: ['/src/test/integration/**/*.spec.js', '/src/test/unit/**/*.spec.js'], + testMatch: [ + '/src/test/integration/**/*.spec.js', + '/src/test/unit/**/*.spec.js', + '/src/test/migrations/*.spec.js' + ], testResultsProcessor: 'jest-sonar-reporter' } diff --git a/migrate-mongo-config.js b/migrate-mongo-config.js new file mode 100644 index 00000000..510065f9 --- /dev/null +++ b/migrate-mongo-config.js @@ -0,0 +1,22 @@ +require('dotenv').config({ + path: process.env.NODE_ENV === 'test' ? '.env.test.local' : '.env.local' +}) +require('dotenv').config() + +const config = { + mongodb: { + url: process.env.MONGODB_URL, + options: { + useNewUrlParser: true, + useUnifiedTopology: true + } + }, + + migrationsDir: 'migrations', + changelogCollectionName: 'changelog', + migrationFileExtension: '.js', + useFileHash: false, + moduleSystem: 'commonjs' +} + +module.exports = config diff --git a/migrations/20241203085442-remove-unique-author-target-user-index.js b/migrations/20241203085442-remove-unique-author-target-user-index.js new file mode 100644 index 00000000..51b9be4a --- /dev/null +++ b/migrations/20241203085442-remove-unique-author-target-user-index.js @@ -0,0 +1,13 @@ +const collectionName = 'reviews' + +const indexName = 'author_1_targetUserId_1' + +module.exports = { + async up(db) { + await db.collection(collectionName).dropIndex(indexName) + }, + + async down(db) { + await db.collection(collectionName).createIndex({ author: 1, targetUserId: 1 }, { unique: true }) + } +} diff --git a/package-lock.json b/package-lock.json index a057cda3..13668ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "googleapis": "^105.0.0", "iconv-lite": "^0.6.3", "jsonwebtoken": "^8.5.1", + "migrate-mongo": "^11.0.0", "module-alias": "^2.2.2", "mongodb": "^4.2.2", "mongoose": "^6.1.2", @@ -1582,6 +1583,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", @@ -3962,6 +3974,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-truncate": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", @@ -4328,6 +4389,21 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5286,6 +5362,14 @@ "integrity": "sha512-noqGuLw158+DuD9UPRKHpJ2hGxpFyDlYYrfM0mWt4XhT4n0lwzTLh70Tkdyy4kyTmyTT9Bv7bWAJqw7cgkEXDg==", "dev": true }, + "node_modules/fn-args": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fn-args/-/fn-args-5.0.0.tgz", + "integrity": "sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==", + "engines": { + "node": ">=8" + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -5322,6 +5406,19 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -5654,8 +5751,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -6963,6 +7059,17 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -7788,6 +7895,37 @@ "node": ">=8.6" } }, + "node_modules/migrate-mongo": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/migrate-mongo/-/migrate-mongo-11.0.0.tgz", + "integrity": "sha512-GB/gHzUwp/fL1w6ksNGihTyb+cSrm6NbVLlz1OSkQKaLlzAXMwH7iKK2ZS7W5v+I8vXiY2rL58WTUZSAL6QR+A==", + "dependencies": { + "cli-table3": "^0.6.1", + "commander": "^9.1.0", + "date-fns": "^2.28.0", + "fn-args": "^5.0.0", + "fs-extra": "^10.0.1", + "lodash": "^4.17.21", + "p-each-series": "^2.2.0" + }, + "bin": { + "migrate-mongo": "bin/migrate-mongo.js" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "mongodb": "^4.4.1 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/migrate-mongo/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -8364,6 +8502,17 @@ "node": ">= 0.8.0" } }, + "node_modules/p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-event": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", @@ -9056,6 +9205,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/require-at": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", @@ -10315,6 +10469,14 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 12a32ff8..15ab20f1 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "scripts": { "test": "jest --maxWorkers=1", "start": "nodemon src/app.js --legacy-watch", + "migrate:up": "migrate-mongo up", + "migrate:rollback": "migrate-mongo down", + "migrate:create": "migrate-mongo create", "lint": "eslint . --fix", "prepare": "husky install", "pre-commit": "lint-staged" @@ -36,6 +39,7 @@ "googleapis": "^105.0.0", "iconv-lite": "^0.6.3", "jsonwebtoken": "^8.5.1", + "migrate-mongo": "^11.0.0", "module-alias": "^2.2.2", "mongodb": "^4.2.2", "mongoose": "^6.1.2", diff --git a/sonar-project.properties b/sonar-project.properties index 5feec943..644219fc 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,7 @@ sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. sonar.sources=. -sonar.exclusions=**/src/test/**/*.js, node_modules/**, jest.config.js, swagger-settings.js, docs/**, src/emails/** +sonar.exclusions=**/src/test/**/*.js, node_modules/**, jest.config.js, swagger-settings.js, docs/**, src/emails/**, migrate-mongo-config.js sonar.language=js diff --git a/src/models/review.js b/src/models/review.js index e7e567fe..df8128ce 100644 --- a/src/models/review.js +++ b/src/models/review.js @@ -61,7 +61,7 @@ const reviewSchema = new Schema( } ) -reviewSchema.index({ author: 1, targetUserId: 1 }, { unique: true }) +reviewSchema.index({ author: 1, targetUserId: 1, offer: 1 }, { unique: true }) reviewSchema.statics.calcAverageRatings = async function (targetUserId, targetUserRole) { const stats = await this.aggregate([ diff --git a/src/test/migrations/20241203085442-remove-unique-author-target-user-index.spec.js b/src/test/migrations/20241203085442-remove-unique-author-target-user-index.spec.js new file mode 100644 index 00000000..b99ce638 --- /dev/null +++ b/src/test/migrations/20241203085442-remove-unique-author-target-user-index.spec.js @@ -0,0 +1,41 @@ +const { MongoClient } = require('mongodb') + +const { up, down } = require('@root/migrations/20241203085442-remove-unique-author-target-user-index') + +require('~/initialization/envSetup') +const { + config: { MONGODB_URL } +} = require('~/configs/config') + +const collectionName = 'reviews' +const indexName = 'author_1_targetUserId_1' + +const url = MONGODB_URL.slice(0, MONGODB_URL.lastIndexOf('/')) +const databaseName = MONGODB_URL.slice(MONGODB_URL.lastIndexOf('/') + 1) + +describe('20241203085442-remove-unique-author-target-user-index', () => { + let client, database + + beforeAll(() => { + client = new MongoClient(url) + database = client.db(databaseName) + }) + + afterAll(() => { + client.close() + }) + + test('should create the unique index on migrate down', async () => { + await down(database) + const indexes = await database.collection(collectionName).indexes() + const indexNames = indexes.map((index) => index.name) + expect(indexNames).toContain(indexName) + }) + + test('should remove the unique index on migrate up', async () => { + await up(database) + const indexes = await database.collection(collectionName).indexes() + const indexNames = indexes.map((index) => index.name) + expect(indexNames).not.toContain(indexName) + }) +})