فهرست منبع

Merge pull request #11869 from overleaf/em-upgrade-mongoose-web

Upgrade Mongoose and the Mongo driver in web

GitOrigin-RevId: 2cad1aabe57eae424a9e4c68b2e0062f0e78ffaf
Eric Mc Sween 3 سال پیش
والد
کامیت
65976cb363
43فایلهای تغییر یافته به همراه653 افزوده شده و 467 حذف شده
  1. 164 43
      package-lock.json
  2. 1 1
      services/web/app/src/Features/Authentication/AuthenticationManager.js
  3. 1 1
      services/web/app/src/Features/Collaborators/CollaboratorsHandler.js
  4. 4 4
      services/web/app/src/Features/Project/ProjectHistoryHandler.js
  5. 5 11
      services/web/app/src/infrastructure/Mongoose.js
  6. 1 1
      services/web/app/src/models/DeletedFile.js
  7. 1 1
      services/web/app/src/models/DeletedProject.js
  8. 1 1
      services/web/app/src/models/DeletedSubscription.js
  9. 1 1
      services/web/app/src/models/DeletedUser.js
  10. 6 3
      services/web/app/src/models/Doc.js
  11. 1 1
      services/web/app/src/models/DocSnapshot.js
  12. 15 12
      services/web/app/src/models/Feedback.js
  13. 18 15
      services/web/app/src/models/File.js
  14. 6 3
      services/web/app/src/models/Folder.js
  15. 10 7
      services/web/app/src/models/Institution.js
  16. 1 0
      services/web/app/src/models/OauthAccessToken.js
  17. 1 0
      services/web/app/src/models/OauthApplication.js
  18. 1 0
      services/web/app/src/models/OauthAuthorizationCode.js
  19. 80 77
      services/web/app/src/models/Project.js
  20. 1 0
      services/web/app/src/models/ProjectAuditLogEntry.js
  21. 1 1
      services/web/app/src/models/ProjectHistoryFailure.js
  22. 1 0
      services/web/app/src/models/ProjectInvite.js
  23. 7 4
      services/web/app/src/models/Publisher.js
  24. 1 0
      services/web/app/src/models/SamlCache.js
  25. 1 0
      services/web/app/src/models/SamlLog.js
  26. 53 50
      services/web/app/src/models/SplitTest.js
  27. 43 40
      services/web/app/src/models/Subscription.js
  28. 1 0
      services/web/app/src/models/Survey.js
  29. 6 3
      services/web/app/src/models/SystemMessage.js
  30. 8 5
      services/web/app/src/models/Tag.js
  31. 9 6
      services/web/app/src/models/TeamInvite.js
  32. 163 154
      services/web/app/src/models/User.js
  33. 1 0
      services/web/app/src/models/UserAuditLogEntry.js
  34. 1 3
      services/web/config/settings.defaults.js
  35. 2 2
      services/web/package.json
  36. 6 2
      services/web/scripts/force_doc_flush.js
  37. 2 2
      services/web/scripts/set_tex_live_image.js
  38. 10 1
      services/web/test/unit/bootstrap.js
  39. 6 4
      services/web/test/unit/src/Authentication/AuthenticationManagerTests.js
  40. 5 3
      services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js
  41. 1 1
      services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js
  42. 1 1
      services/web/test/unit/src/User/UserAuditLogHandlerTests.js
  43. 5 3
      services/web/test/unit/src/User/UserCreatorTests.js

+ 164 - 43
package-lock.json

@@ -34519,8 +34519,8 @@
         "minimist": "^1.2.7",
         "mmmagic": "^0.5.3",
         "moment": "^2.29.4",
-        "mongodb": "~3.6.0",
-        "mongoose": "^5.13.11",
+        "mongodb": "^4.13.0",
+        "mongoose": "^6.9.1",
         "multer": "overleaf/multer#e1df247fbf8e7590520d20ae3601eaef9f3d2e9e",
         "nocache": "^2.1.0",
         "nock": "^13.1.3",
@@ -34910,6 +34910,40 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "services/web/node_modules/bson": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz",
+      "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==",
+      "dependencies": {
+        "buffer": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "services/web/node_modules/bson/node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
     "services/web/node_modules/buffer": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -35934,6 +35968,14 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "services/web/node_modules/kareem": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
+      "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "services/web/node_modules/karma-webpack": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz",
@@ -36094,41 +36136,60 @@
       }
     },
     "services/web/node_modules/mongodb": {
-      "version": "3.6.12",
-      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.12.tgz",
-      "integrity": "sha512-ErHpF4P4disEIQB8Nns2twIMVXcvmlwjpKqfVnyB/hhd/L5We48LfoBYjBjuUSiSqL6ffmcygPTgjvpy2LETRQ==",
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz",
+      "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==",
       "dependencies": {
-        "bl": "^2.2.1",
-        "bson": "^1.1.4",
-        "denque": "^1.4.1",
-        "optional-require": "^1.0.3",
-        "safe-buffer": "^5.1.2"
+        "bson": "^4.7.0",
+        "mongodb-connection-string-url": "^2.5.4",
+        "socks": "^2.7.1"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=12.9.0"
       },
       "optionalDependencies": {
-        "saslprep": "^1.0.0"
+        "@aws-sdk/credential-providers": "^3.186.0",
+        "saslprep": "^1.0.3"
+      }
+    },
+    "services/web/node_modules/mongoose": {
+      "version": "6.9.1",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.9.1.tgz",
+      "integrity": "sha512-hOz1ZWV0w6WEVLrj89Wpk7PXDYtDDF6k7/NX79lY5iKqeFtZsceBXW8xW59YFNcW5O3cH32hQ8IbDlhgyBsDMA==",
+      "dependencies": {
+        "bson": "^4.7.0",
+        "kareem": "2.5.1",
+        "mongodb": "4.13.0",
+        "mpath": "0.9.0",
+        "mquery": "4.0.3",
+        "ms": "2.1.3",
+        "sift": "16.0.1"
       },
-      "peerDependenciesMeta": {
-        "aws4": {
-          "optional": true
-        },
-        "bson-ext": {
-          "optional": true
-        },
-        "kerberos": {
-          "optional": true
-        },
-        "mongodb-client-encryption": {
-          "optional": true
-        },
-        "mongodb-extjson": {
-          "optional": true
-        },
-        "snappy": {
-          "optional": true
-        }
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mongoose"
+      }
+    },
+    "services/web/node_modules/mpath": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+      "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "services/web/node_modules/mquery": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
+      "integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
+      "dependencies": {
+        "debug": "4.x"
+      },
+      "engines": {
+        "node": ">=12.0.0"
       }
     },
     "services/web/node_modules/multer": {
@@ -36814,6 +36875,11 @@
         "stack-trace": "0.0.10"
       }
     },
+    "services/web/node_modules/sift": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
+      "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
+    },
     "services/web/node_modules/sinon": {
       "version": "7.5.0",
       "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz",
@@ -43734,8 +43800,8 @@
         "mocha-each": "^2.0.1",
         "mock-fs": "^5.1.2",
         "moment": "^2.29.4",
-        "mongodb": "~3.6.0",
-        "mongoose": "^5.13.11",
+        "mongodb": "^4.13.0",
+        "mongoose": "^6.9.1",
         "multer": "overleaf/multer#e1df247fbf8e7590520d20ae3601eaef9f3d2e9e",
         "nocache": "^2.1.0",
         "nock": "^13.1.1",
@@ -43993,6 +44059,25 @@
           "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
           "dev": true
         },
+        "bson": {
+          "version": "4.7.2",
+          "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz",
+          "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==",
+          "requires": {
+            "buffer": "^5.6.0"
+          },
+          "dependencies": {
+            "buffer": {
+              "version": "5.7.1",
+              "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+              "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+              "requires": {
+                "base64-js": "^1.3.1",
+                "ieee754": "^1.1.13"
+              }
+            }
+          }
+        },
         "buffer": {
           "version": "6.0.3",
           "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -44710,6 +44795,11 @@
             "graceful-fs": "^4.1.6"
           }
         },
+        "kareem": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
+          "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA=="
+        },
         "karma-webpack": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz",
@@ -44829,16 +44919,42 @@
           }
         },
         "mongodb": {
-          "version": "3.6.12",
-          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.12.tgz",
-          "integrity": "sha512-ErHpF4P4disEIQB8Nns2twIMVXcvmlwjpKqfVnyB/hhd/L5We48LfoBYjBjuUSiSqL6ffmcygPTgjvpy2LETRQ==",
-          "requires": {
-            "bl": "^2.2.1",
-            "bson": "^1.1.4",
-            "denque": "^1.4.1",
-            "optional-require": "^1.0.3",
-            "safe-buffer": "^5.1.2",
-            "saslprep": "^1.0.0"
+          "version": "4.13.0",
+          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz",
+          "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==",
+          "requires": {
+            "@aws-sdk/credential-providers": "^3.186.0",
+            "bson": "^4.7.0",
+            "mongodb-connection-string-url": "^2.5.4",
+            "saslprep": "^1.0.3",
+            "socks": "^2.7.1"
+          }
+        },
+        "mongoose": {
+          "version": "6.9.1",
+          "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.9.1.tgz",
+          "integrity": "sha512-hOz1ZWV0w6WEVLrj89Wpk7PXDYtDDF6k7/NX79lY5iKqeFtZsceBXW8xW59YFNcW5O3cH32hQ8IbDlhgyBsDMA==",
+          "requires": {
+            "bson": "^4.7.0",
+            "kareem": "2.5.1",
+            "mongodb": "4.13.0",
+            "mpath": "0.9.0",
+            "mquery": "4.0.3",
+            "ms": "2.1.3",
+            "sift": "16.0.1"
+          }
+        },
+        "mpath": {
+          "version": "0.9.0",
+          "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+          "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="
+        },
+        "mquery": {
+          "version": "4.0.3",
+          "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
+          "integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
+          "requires": {
+            "debug": "4.x"
           }
         },
         "multer": {
@@ -45289,6 +45405,11 @@
             "stack-trace": "0.0.10"
           }
         },
+        "sift": {
+          "version": "16.0.1",
+          "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
+          "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
+        },
         "sinon": {
           "version": "7.5.0",
           "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz",

+ 1 - 1
services/web/app/src/Features/Authentication/AuthenticationManager.js

@@ -109,7 +109,7 @@ const AuthenticationManager = {
             if (err) {
               return callback(err)
             }
-            if (result.nModified !== 1) {
+            if (result.modifiedCount !== 1) {
               return callback(new ParallelLoginError())
             }
             if (!match) {

+ 1 - 1
services/web/app/src/Features/Collaborators/CollaboratorsHandler.js

@@ -234,7 +234,7 @@ async function setCollaboratorPrivilegeLevel(
     }
   }
   const mongoResponse = await Project.updateOne(query, update).exec()
-  if (mongoResponse.n === 0) {
+  if (mongoResponse.matchedCount === 0) {
     throw new Errors.NotFoundError('project or collaborator not found')
   }
 }

+ 4 - 4
services/web/app/src/Features/Project/ProjectHistoryHandler.js

@@ -18,7 +18,7 @@ const ProjectHistoryHandler = {
         if (err) {
           return callback(err)
         }
-        if (result.n === 0) {
+        if (result.matchedCount === 0) {
           return callback(new Error('history exists'))
         }
         callback()
@@ -57,7 +57,7 @@ const ProjectHistoryHandler = {
           return callback(err)
         }
         // return an error if overleaf.history.id wasn't present
-        if (result.n === 0) {
+        if (result.matchedCount === 0) {
           return callback(new Error('history not upgraded'))
         }
         callback()
@@ -76,7 +76,7 @@ const ProjectHistoryHandler = {
         if (err) {
           return callback(err)
         }
-        if (result.n === 0) {
+        if (result.matchedCount === 0) {
           return callback(new Error('history not downgraded'))
         }
         callback()
@@ -94,7 +94,7 @@ const ProjectHistoryHandler = {
         if (err) {
           return callback(err)
         }
-        if (result.n === 0) {
+        if (result.matchedCount === 0) {
           return callback(new Error('migration flag not set'))
         }
         callback()

+ 5 - 11
services/web/app/src/infrastructure/Mongoose.js

@@ -12,20 +12,14 @@ if (
   )
 }
 
+mongoose.set('autoIndex', false)
+mongoose.set('strictQuery', false)
+
 const connectionPromise = mongoose.connect(
   Settings.mongo.url,
-  Object.assign(
-    {
-      // mongoose specific config
-      config: { autoIndex: false },
-      // mongoose defaults to false, native driver defaults to true
-      useNewUrlParser: true,
-      // use the equivalent `findOneAndUpdate` methods of the native driver
-      useFindAndModify: false,
-    },
-    Settings.mongo.options
-  )
+  Settings.mongo.options
 )
+
 addConnectionDrainer('mongoose', async () => {
   await connectionPromise
   await mongoose.disconnect()

+ 1 - 1
services/web/app/src/models/DeletedFile.js

@@ -14,7 +14,7 @@ const DeletedFileSchema = new Schema(
     },
     deletedAt: { type: Date },
   },
-  { collection: 'deletedFiles' }
+  { collection: 'deletedFiles', minimize: false }
 )
 
 exports.DeletedFile = mongoose.model('DeletedFile', DeletedFileSchema)

+ 1 - 1
services/web/app/src/models/DeletedProject.js

@@ -26,7 +26,7 @@ const DeletedProjectSchema = new Schema(
     deleterData: DeleterDataSchema,
     project: ProjectSchema,
   },
-  { collection: 'deletedProjects' }
+  { collection: 'deletedProjects', minimize: false }
 )
 
 exports.DeletedProject = mongoose.model('DeletedProject', DeletedProjectSchema)

+ 1 - 1
services/web/app/src/models/DeletedSubscription.js

@@ -23,7 +23,7 @@ const DeletedSubscriptionSchema = new Schema(
     deleterData: DeleterDataSchema,
     subscription: SubscriptionSchema,
   },
-  { collection: 'deletedSubscriptions' }
+  { collection: 'deletedSubscriptions', minimize: false }
 )
 
 exports.DeletedSubscription = mongoose.model(

+ 1 - 1
services/web/app/src/models/DeletedUser.js

@@ -23,7 +23,7 @@ const DeletedUserSchema = new Schema(
     deleterData: DeleterDataSchema,
     user: UserSchema,
   },
-  { collection: 'deletedUsers' }
+  { collection: 'deletedUsers', minimize: false }
 )
 
 exports.DeletedUser = mongoose.model('DeletedUser', DeletedUserSchema)

+ 6 - 3
services/web/app/src/models/Doc.js

@@ -2,9 +2,12 @@ const mongoose = require('../infrastructure/Mongoose')
 
 const { Schema } = mongoose
 
-const DocSchema = new Schema({
-  name: { type: String, default: 'new doc' },
-})
+const DocSchema = new Schema(
+  {
+    name: { type: String, default: 'new doc' },
+  },
+  { minimize: false }
+)
 
 exports.Doc = mongoose.model('Doc', DocSchema)
 

+ 1 - 1
services/web/app/src/models/DocSnapshot.js

@@ -12,7 +12,7 @@ const DocSnapshotSchema = new Schema(
     ranges: Schema.Types.Mixed,
     ts: Date,
   },
-  { collection: 'docSnapshots' }
+  { collection: 'docSnapshots', minimize: false }
 )
 
 exports.DocSnapshot = mongoose.model('DocSnapshot', DocSnapshotSchema)

+ 15 - 12
services/web/app/src/models/Feedback.js

@@ -2,20 +2,23 @@ const mongoose = require('../infrastructure/Mongoose')
 const { Schema } = mongoose
 const { ObjectId } = Schema
 
-const FeedbackSchema = new Schema({
-  userId: {
-    type: ObjectId,
-    ref: 'User',
-  },
-  source: String,
-  createdAt: {
-    type: Date,
-    default() {
-      return new Date()
+const FeedbackSchema = new Schema(
+  {
+    userId: {
+      type: ObjectId,
+      ref: 'User',
+    },
+    source: String,
+    createdAt: {
+      type: Date,
+      default() {
+        return new Date()
+      },
     },
+    data: {},
   },
-  data: {},
-})
+  { minimize: false }
+)
 
 exports.Feedback = mongoose.model('Feedback', FeedbackSchema)
 exports.FeedbackSchema = FeedbackSchema

+ 18 - 15
services/web/app/src/models/File.js

@@ -2,23 +2,26 @@ const mongoose = require('../infrastructure/Mongoose')
 
 const { Schema } = mongoose
 
-const FileSchema = new Schema({
-  name: {
-    type: String,
-    default: '',
-  },
-  created: {
-    type: Date,
-    default() {
-      return new Date()
+const FileSchema = new Schema(
+  {
+    name: {
+      type: String,
+      default: '',
+    },
+    created: {
+      type: Date,
+      default() {
+        return new Date()
+      },
+    },
+    rev: { type: Number, default: 0 },
+    linkedFileData: { type: Schema.Types.Mixed },
+    hash: {
+      type: String,
     },
   },
-  rev: { type: Number, default: 0 },
-  linkedFileData: { type: Schema.Types.Mixed },
-  hash: {
-    type: String,
-  },
-})
+  { minimize: false }
+)
 
 exports.File = mongoose.model('File', FileSchema)
 exports.FileSchema = FileSchema

+ 6 - 3
services/web/app/src/models/Folder.js

@@ -4,9 +4,12 @@ const { FileSchema } = require('./File')
 
 const { Schema } = mongoose
 
-const FolderSchema = new Schema({
-  name: { type: String, default: 'new folder' },
-})
+const FolderSchema = new Schema(
+  {
+    name: { type: String, default: 'new folder' },
+  },
+  { minimize: false }
+)
 
 FolderSchema.add({
   docs: [DocSchema],

+ 10 - 7
services/web/app/src/models/Institution.js

@@ -5,14 +5,17 @@ const settings = require('@overleaf/settings')
 const logger = require('@overleaf/logger')
 const request = require('request')
 
-const InstitutionSchema = new Schema({
-  v1Id: { type: Number, required: true },
-  managerIds: [{ type: ObjectId, ref: 'User' }],
-  metricsEmail: {
-    optedOutUserIds: [{ type: ObjectId, ref: 'User' }],
-    lastSent: { type: Date },
+const InstitutionSchema = new Schema(
+  {
+    v1Id: { type: Number, required: true },
+    managerIds: [{ type: ObjectId, ref: 'User' }],
+    metricsEmail: {
+      optedOutUserIds: [{ type: ObjectId, ref: 'User' }],
+      lastSent: { type: Date },
+    },
   },
-})
+  { minimize: false }
+)
 
 // fetch institution's data from v1 API. Errors are ignored
 InstitutionSchema.method('fetchV1Data', function (callback) {

+ 1 - 0
services/web/app/src/models/OauthAccessToken.js

@@ -15,6 +15,7 @@ const OauthAccessTokenSchema = new Schema(
   },
   {
     collection: 'oauthAccessTokens',
+    minimize: false,
   }
 )
 

+ 1 - 0
services/web/app/src/models/OauthApplication.js

@@ -13,6 +13,7 @@ const OauthApplicationSchema = new Schema(
   },
   {
     collection: 'oauthApplications',
+    minimize: false,
   }
 )
 

+ 1 - 0
services/web/app/src/models/OauthAuthorizationCode.js

@@ -14,6 +14,7 @@ const OauthAuthorizationCodeSchema = new Schema(
   },
   {
     collection: 'oauthAuthorizationCodes',
+    minimize: false,
   }
 )
 

+ 80 - 77
services/web/app/src/models/Project.js

@@ -24,92 +24,95 @@ const DeletedFileSchema = new Schema({
   deletedAt: { type: Date },
 })
 
-const ProjectSchema = new Schema({
-  name: { type: String, default: 'new project' },
-  lastUpdated: {
-    type: Date,
-    default() {
-      return new Date()
-    },
-  },
-  lastUpdatedBy: { type: ObjectId, ref: 'User' },
-  lastOpened: { type: Date },
-  active: { type: Boolean, default: true },
-  owner_ref: { type: ObjectId, ref: 'User' },
-  collaberator_refs: [{ type: ObjectId, ref: 'User' }],
-  readOnly_refs: [{ type: ObjectId, ref: 'User' }],
-  rootDoc_id: { type: ObjectId },
-  rootFolder: [FolderSchema],
-  version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
-  publicAccesLevel: { type: String, default: 'private' },
-  compiler: { type: String, default: 'pdflatex' },
-  spellCheckLanguage: { type: String, default: 'en' },
-  deletedByExternalDataSource: { type: Boolean, default: false },
-  description: { type: String, default: '' },
-  archived: { type: Schema.Types.Mixed },
-  trashed: [{ type: ObjectId, ref: 'User' }],
-  deletedDocs: [DeletedDocSchema],
-  deletedFiles: [DeletedFileSchema],
-  imageName: { type: String },
-  brandVariationId: { type: String },
-  track_changes: { type: Object },
-  tokens: {
-    readOnly: {
-      type: String,
-      index: {
-        unique: true,
-        partialFilterExpression: { 'tokens.readOnly': { $exists: true } },
+const ProjectSchema = new Schema(
+  {
+    name: { type: String, default: 'new project' },
+    lastUpdated: {
+      type: Date,
+      default() {
+        return new Date()
       },
     },
-    readAndWrite: {
-      type: String,
-      index: {
-        unique: true,
-        partialFilterExpression: { 'tokens.readAndWrite': { $exists: true } },
+    lastUpdatedBy: { type: ObjectId, ref: 'User' },
+    lastOpened: { type: Date },
+    active: { type: Boolean, default: true },
+    owner_ref: { type: ObjectId, ref: 'User' },
+    collaberator_refs: [{ type: ObjectId, ref: 'User' }],
+    readOnly_refs: [{ type: ObjectId, ref: 'User' }],
+    rootDoc_id: { type: ObjectId },
+    rootFolder: [FolderSchema],
+    version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
+    publicAccesLevel: { type: String, default: 'private' },
+    compiler: { type: String, default: 'pdflatex' },
+    spellCheckLanguage: { type: String, default: 'en' },
+    deletedByExternalDataSource: { type: Boolean, default: false },
+    description: { type: String, default: '' },
+    archived: { type: Schema.Types.Mixed },
+    trashed: [{ type: ObjectId, ref: 'User' }],
+    deletedDocs: [DeletedDocSchema],
+    deletedFiles: [DeletedFileSchema],
+    imageName: { type: String },
+    brandVariationId: { type: String },
+    track_changes: { type: Object },
+    tokens: {
+      readOnly: {
+        type: String,
+        index: {
+          unique: true,
+          partialFilterExpression: { 'tokens.readOnly': { $exists: true } },
+        },
       },
-    },
-    readAndWritePrefix: {
-      type: String,
-      index: {
-        unique: true,
-        partialFilterExpression: {
-          'tokens.readAndWritePrefix': { $exists: true },
+      readAndWrite: {
+        type: String,
+        index: {
+          unique: true,
+          partialFilterExpression: { 'tokens.readAndWrite': { $exists: true } },
+        },
+      },
+      readAndWritePrefix: {
+        type: String,
+        index: {
+          unique: true,
+          partialFilterExpression: {
+            'tokens.readAndWritePrefix': { $exists: true },
+          },
         },
       },
     },
-  },
-  tokenAccessReadOnly_refs: [{ type: ObjectId, ref: 'User' }],
-  tokenAccessReadAndWrite_refs: [{ type: ObjectId, ref: 'User' }],
-  fromV1TemplateId: { type: Number },
-  fromV1TemplateVersionId: { type: Number },
-  overleaf: {
-    id: { type: Number },
-    imported_at_ver_id: { type: Number },
-    token: { type: String },
-    read_token: { type: String },
-    history: {
-      id: { type: Schema.Types.Mixed },
-      display: { type: Boolean },
-      upgradedAt: { type: Date },
-      allowDowngrade: { type: Boolean },
-      zipFileArchivedInProject: { type: Boolean },
+    tokenAccessReadOnly_refs: [{ type: ObjectId, ref: 'User' }],
+    tokenAccessReadAndWrite_refs: [{ type: ObjectId, ref: 'User' }],
+    fromV1TemplateId: { type: Number },
+    fromV1TemplateVersionId: { type: Number },
+    overleaf: {
+      id: { type: Number },
+      imported_at_ver_id: { type: Number },
+      token: { type: String },
+      read_token: { type: String },
+      history: {
+        id: { type: Schema.Types.Mixed },
+        display: { type: Boolean },
+        upgradedAt: { type: Date },
+        allowDowngrade: { type: Boolean },
+        zipFileArchivedInProject: { type: Boolean },
+      },
     },
-  },
-  collabratecUsers: [
-    {
-      user_id: { type: ObjectId, ref: 'User' },
-      collabratec_document_id: { type: String },
-      collabratec_privategroup_id: { type: String },
-      added_at: {
-        type: Date,
-        default() {
-          return new Date()
+    collabratecUsers: [
+      {
+        user_id: { type: ObjectId, ref: 'User' },
+        collabratec_document_id: { type: String },
+        collabratec_privategroup_id: { type: String },
+        added_at: {
+          type: Date,
+          default() {
+            return new Date()
+          },
         },
       },
-    },
-  ],
-  deferredTpdsFlushCounter: { type: Number },
-})
+    ],
+    deferredTpdsFlushCounter: { type: Number },
+  },
+  { minimize: false }
+)
 
 ProjectSchema.statics.getProject = function (projectOrId, fields, callback) {
   if (projectOrId._id != null) {

+ 1 - 0
services/web/app/src/models/ProjectAuditLogEntry.js

@@ -11,6 +11,7 @@ const ProjectAuditLogEntrySchema = new Schema(
   },
   {
     collection: 'projectAuditLogEntries',
+    minimize: false,
   }
 )
 

+ 1 - 1
services/web/app/src/models/ProjectHistoryFailure.js

@@ -15,7 +15,7 @@ const ProjectHistoryFailureSchema = new Schema(
     resyncAttempts: Number,
     requestCount: Number,
   },
-  { collection: 'projectHistoryFailures' }
+  { collection: 'projectHistoryFailures', minimize: false }
 )
 
 exports.ProjectHistoryFailure = mongoose.model(

+ 1 - 0
services/web/app/src/models/ProjectInvite.js

@@ -27,6 +27,7 @@ const ProjectInviteSchema = new Schema(
   },
   {
     collection: 'projectInvites',
+    minimize: false,
   }
 )
 

+ 7 - 4
services/web/app/src/models/Publisher.js

@@ -5,10 +5,13 @@ const settings = require('@overleaf/settings')
 const logger = require('@overleaf/logger')
 const request = require('request')
 
-const PublisherSchema = new Schema({
-  slug: { type: String, required: true },
-  managerIds: [{ type: ObjectId, ref: 'User' }],
-})
+const PublisherSchema = new Schema(
+  {
+    slug: { type: String, required: true },
+    managerIds: [{ type: ObjectId, ref: 'User' }],
+  },
+  { minimize: false }
+)
 
 // fetch publisher's (brand on v1) data from v1 API. Errors are ignored
 PublisherSchema.method('fetchV1Data', function (callback) {

+ 1 - 0
services/web/app/src/models/SamlCache.js

@@ -8,6 +8,7 @@ const SamlCacheSchema = new Schema(
   },
   {
     collection: 'samlCache',
+    minimize: false,
   }
 )
 

+ 1 - 0
services/web/app/src/models/SamlLog.js

@@ -14,6 +14,7 @@ const SamlLogSchema = new Schema(
   },
   {
     collection: 'samlLogs',
+    minimize: false,
   }
 )
 

+ 53 - 50
services/web/app/src/models/SplitTest.js

@@ -96,59 +96,62 @@ const VersionSchema = new Schema(
   { _id: false }
 )
 
-const SplitTestSchema = new Schema({
-  name: {
-    type: String,
-    minLength: MIN_NAME_LENGTH,
-    maxlength: MAX_NAME_LENGTH,
-    required: true,
-    unique: true,
-    validate: {
-      validator: function (input) {
-        return input !== null && NAME_REGEX.test(input)
+const SplitTestSchema = new Schema(
+  {
+    name: {
+      type: String,
+      minLength: MIN_NAME_LENGTH,
+      maxlength: MAX_NAME_LENGTH,
+      required: true,
+      unique: true,
+      validate: {
+        validator: function (input) {
+          return input !== null && NAME_REGEX.test(input)
+        },
+        message: `invalid, must match: ${NAME_REGEX}`,
       },
-      message: `invalid, must match: ${NAME_REGEX}`,
+    },
+    versions: [VersionSchema],
+    forbidReleasePhase: {
+      type: Boolean,
+      required: false,
+    },
+    description: {
+      type: String,
+      required: false,
+    },
+    expectedEndDate: {
+      type: Date,
+      required: false,
+    },
+    ticketUrl: {
+      type: String,
+      required: false,
+    },
+    reportsUrls: {
+      type: [String],
+      required: false,
+      default: [],
+    },
+    winningVariant: {
+      type: String,
+      required: false,
+    },
+    archived: {
+      type: Boolean,
+      required: false,
+    },
+    archivedAt: {
+      type: Date,
+      required: false,
+    },
+    badgeInfo: {
+      type: BadgeInfoSchema,
+      required: false,
     },
   },
-  versions: [VersionSchema],
-  forbidReleasePhase: {
-    type: Boolean,
-    required: false,
-  },
-  description: {
-    type: String,
-    required: false,
-  },
-  expectedEndDate: {
-    type: Date,
-    required: false,
-  },
-  ticketUrl: {
-    type: String,
-    required: false,
-  },
-  reportsUrls: {
-    type: [String],
-    required: false,
-    default: [],
-  },
-  winningVariant: {
-    type: String,
-    required: false,
-  },
-  archived: {
-    type: Boolean,
-    required: false,
-  },
-  archivedAt: {
-    type: Date,
-    required: false,
-  },
-  badgeInfo: {
-    type: BadgeInfoSchema,
-    required: false,
-  },
-})
+  { minimize: false }
+)
 
 module.exports = {
   SplitTest: mongoose.model('SplitTest', SplitTestSchema),

+ 43 - 40
services/web/app/src/models/Subscription.js

@@ -4,52 +4,55 @@ const { TeamInviteSchema } = require('./TeamInvite')
 const { Schema } = mongoose
 const { ObjectId } = Schema
 
-const SubscriptionSchema = new Schema({
-  admin_id: {
-    type: ObjectId,
-    ref: 'User',
-    index: { unique: true, dropDups: true },
-  },
-  manager_ids: {
-    type: [ObjectId],
-    ref: 'User',
-    required: true,
-    validate: function (managers) {
-      // require at least one manager
-      return !!managers.length
+const SubscriptionSchema = new Schema(
+  {
+    admin_id: {
+      type: ObjectId,
+      ref: 'User',
+      index: { unique: true, dropDups: true },
     },
-  },
-  member_ids: [{ type: ObjectId, ref: 'User' }],
-  invited_emails: [String],
-  teamInvites: [TeamInviteSchema],
-  recurlySubscription_id: String,
-  teamName: { type: String },
-  teamNotice: { type: String },
-  planCode: { type: String },
-  groupPlan: { type: Boolean, default: false },
-  membersLimit: { type: Number, default: 0 },
-  customAccount: Boolean,
-  overleaf: {
-    id: {
-      type: Number,
-      index: {
-        unique: true,
-        partialFilterExpression: { 'overleaf.id': { $exists: true } },
+    manager_ids: {
+      type: [ObjectId],
+      ref: 'User',
+      required: true,
+      validate: function (managers) {
+        // require at least one manager
+        return !!managers.length
       },
     },
-  },
-  recurlyStatus: {
-    state: {
-      type: String,
-    },
-    trialStartedAt: {
-      type: Date,
+    member_ids: [{ type: ObjectId, ref: 'User' }],
+    invited_emails: [String],
+    teamInvites: [TeamInviteSchema],
+    recurlySubscription_id: String,
+    teamName: { type: String },
+    teamNotice: { type: String },
+    planCode: { type: String },
+    groupPlan: { type: Boolean, default: false },
+    membersLimit: { type: Number, default: 0 },
+    customAccount: Boolean,
+    overleaf: {
+      id: {
+        type: Number,
+        index: {
+          unique: true,
+          partialFilterExpression: { 'overleaf.id': { $exists: true } },
+        },
+      },
     },
-    trialEndsAt: {
-      type: Date,
+    recurlyStatus: {
+      state: {
+        type: String,
+      },
+      trialStartedAt: {
+        type: Date,
+      },
+      trialEndsAt: {
+        type: Date,
+      },
     },
   },
-})
+  { minimize: false }
+)
 
 // Subscriptions have no v1 data to fetch
 SubscriptionSchema.method('fetchV1Data', function (callback) {

+ 1 - 0
services/web/app/src/models/Survey.js

@@ -40,6 +40,7 @@ const SurveySchema = new Schema(
   },
   {
     collection: 'surveys',
+    minimize: false,
   }
 )
 

+ 6 - 3
services/web/app/src/models/SystemMessage.js

@@ -2,8 +2,11 @@ const mongoose = require('../infrastructure/Mongoose')
 
 const { Schema } = mongoose
 
-const SystemMessageSchema = new Schema({
-  content: { type: String, default: '' },
-})
+const SystemMessageSchema = new Schema(
+  {
+    content: { type: String, default: '' },
+  },
+  { minimize: false }
+)
 
 exports.SystemMessage = mongoose.model('SystemMessage', SystemMessageSchema)

+ 8 - 5
services/web/app/src/models/Tag.js

@@ -4,11 +4,14 @@ const { Schema } = mongoose
 // Note that for legacy reasons, user_id and project_ids are plain strings,
 // not ObjectIds.
 
-const TagSchema = new Schema({
-  user_id: { type: String, required: true },
-  name: { type: String, required: true },
-  project_ids: [String],
-})
+const TagSchema = new Schema(
+  {
+    user_id: { type: String, required: true },
+    name: { type: String, required: true },
+    project_ids: [String],
+  },
+  { minimize: false }
+)
 
 exports.Tag = mongoose.model('Tag', TagSchema)
 exports.TagSchema = TagSchema

+ 9 - 6
services/web/app/src/models/TeamInvite.js

@@ -2,12 +2,15 @@ const mongoose = require('../infrastructure/Mongoose')
 
 const { Schema } = mongoose
 
-const TeamInviteSchema = new Schema({
-  email: { type: String, required: true },
-  token: { type: String },
-  inviterName: { type: String },
-  sentAt: { type: Date },
-})
+const TeamInviteSchema = new Schema(
+  {
+    email: { type: String, required: true },
+    token: { type: String },
+    inviterName: { type: String },
+    sentAt: { type: Date },
+  },
+  { minimize: false }
+)
 
 exports.TeamInvite = mongoose.model('TeamInvite', TeamInviteSchema)
 exports.TeamInviteSchema = TeamInviteSchema

+ 163 - 154
services/web/app/src/models/User.js

@@ -7,173 +7,182 @@ const { ObjectId } = Schema
 // See https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/574698#574698
 const MAX_EMAIL_LENGTH = 254
 
-const UserSchema = new Schema({
-  email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH },
-  emails: [
-    {
-      email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH },
-      reversedHostname: { type: String, default: '' },
-      createdAt: {
-        type: Date,
-        default() {
-          return new Date()
+const UserSchema = new Schema(
+  {
+    email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH },
+    emails: [
+      {
+        email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH },
+        reversedHostname: { type: String, default: '' },
+        createdAt: {
+          type: Date,
+          default() {
+            return new Date()
+          },
         },
+        confirmedAt: { type: Date },
+        samlProviderId: { type: String },
+        affiliationUnchecked: { type: Boolean },
+        reconfirmedAt: { type: Date },
       },
-      confirmedAt: { type: Date },
-      samlProviderId: { type: String },
-      affiliationUnchecked: { type: Boolean },
-      reconfirmedAt: { type: Date },
+    ],
+    first_name: { type: String, default: '' },
+    last_name: { type: String, default: '' },
+    role: { type: String, default: '' },
+    institution: { type: String, default: '' },
+    hashedPassword: String,
+    isAdmin: { type: Boolean, default: false },
+    staffAccess: {
+      publisherMetrics: { type: Boolean, default: false },
+      publisherManagement: { type: Boolean, default: false },
+      institutionMetrics: { type: Boolean, default: false },
+      institutionManagement: { type: Boolean, default: false },
+      groupMetrics: { type: Boolean, default: false },
+      groupManagement: { type: Boolean, default: false },
+      adminMetrics: { type: Boolean, default: false },
+      splitTestMetrics: { type: Boolean, default: false },
+      splitTestManagement: { type: Boolean, default: false },
     },
-  ],
-  first_name: { type: String, default: '' },
-  last_name: { type: String, default: '' },
-  role: { type: String, default: '' },
-  institution: { type: String, default: '' },
-  hashedPassword: String,
-  isAdmin: { type: Boolean, default: false },
-  staffAccess: {
-    publisherMetrics: { type: Boolean, default: false },
-    publisherManagement: { type: Boolean, default: false },
-    institutionMetrics: { type: Boolean, default: false },
-    institutionManagement: { type: Boolean, default: false },
-    groupMetrics: { type: Boolean, default: false },
-    groupManagement: { type: Boolean, default: false },
-    adminMetrics: { type: Boolean, default: false },
-    splitTestMetrics: { type: Boolean, default: false },
-    splitTestManagement: { type: Boolean, default: false },
-  },
-  signUpDate: {
-    type: Date,
-    default() {
-      return new Date()
+    signUpDate: {
+      type: Date,
+      default() {
+        return new Date()
+      },
     },
-  },
-  loginEpoch: { type: Number },
-  lastActive: { type: Date },
-  lastFailedLogin: { type: Date },
-  lastLoggedIn: { type: Date },
-  lastLoginIp: { type: String, default: '' },
-  lastPrimaryEmailCheck: { type: Date },
-  loginCount: { type: Number, default: 0 },
-  holdingAccount: { type: Boolean, default: false },
-  ace: {
-    mode: { type: String, default: 'none' },
-    theme: { type: String, default: 'textmate' },
-    overallTheme: { type: String, default: '' },
-    fontSize: { type: Number, default: '12' },
-    autoComplete: { type: Boolean, default: true },
-    autoPairDelimiters: { type: Boolean, default: true },
-    spellCheckLanguage: { type: String, default: 'en' },
-    pdfViewer: { type: String, default: 'pdfjs' },
-    syntaxValidation: { type: Boolean },
-    fontFamily: { type: String },
-    lineHeight: { type: String },
-  },
-  features: {
-    collaborators: {
-      type: Number,
-      default: Settings.defaultFeatures.collaborators,
+    loginEpoch: { type: Number },
+    lastActive: { type: Date },
+    lastFailedLogin: { type: Date },
+    lastLoggedIn: { type: Date },
+    lastLoginIp: { type: String, default: '' },
+    lastPrimaryEmailCheck: { type: Date },
+    loginCount: { type: Number, default: 0 },
+    holdingAccount: { type: Boolean, default: false },
+    ace: {
+      mode: { type: String, default: 'none' },
+      theme: { type: String, default: 'textmate' },
+      overallTheme: { type: String, default: '' },
+      fontSize: { type: Number, default: '12' },
+      autoComplete: { type: Boolean, default: true },
+      autoPairDelimiters: { type: Boolean, default: true },
+      spellCheckLanguage: { type: String, default: 'en' },
+      pdfViewer: { type: String, default: 'pdfjs' },
+      syntaxValidation: { type: Boolean },
+      fontFamily: { type: String },
+      lineHeight: { type: String },
     },
-    versioning: { type: Boolean, default: Settings.defaultFeatures.versioning },
-    dropbox: { type: Boolean, default: Settings.defaultFeatures.dropbox },
-    github: { type: Boolean, default: Settings.defaultFeatures.github },
-    gitBridge: { type: Boolean, default: Settings.defaultFeatures.gitBridge },
-    compileTimeout: {
-      type: Number,
-      default: Settings.defaultFeatures.compileTimeout,
+    features: {
+      collaborators: {
+        type: Number,
+        default: Settings.defaultFeatures.collaborators,
+      },
+      versioning: {
+        type: Boolean,
+        default: Settings.defaultFeatures.versioning,
+      },
+      dropbox: { type: Boolean, default: Settings.defaultFeatures.dropbox },
+      github: { type: Boolean, default: Settings.defaultFeatures.github },
+      gitBridge: { type: Boolean, default: Settings.defaultFeatures.gitBridge },
+      compileTimeout: {
+        type: Number,
+        default: Settings.defaultFeatures.compileTimeout,
+      },
+      compileGroup: {
+        type: String,
+        default: Settings.defaultFeatures.compileGroup,
+      },
+      templates: { type: Boolean, default: Settings.defaultFeatures.templates },
+      references: {
+        type: Boolean,
+        default: Settings.defaultFeatures.references,
+      },
+      trackChanges: {
+        type: Boolean,
+        default: Settings.defaultFeatures.trackChanges,
+      },
+      mendeley: { type: Boolean, default: Settings.defaultFeatures.mendeley },
+      zotero: { type: Boolean, default: Settings.defaultFeatures.zotero },
+      referencesSearch: {
+        type: Boolean,
+        default: Settings.defaultFeatures.referencesSearch,
+      },
+      symbolPalette: {
+        type: Boolean,
+        default: Settings.defaultFeatures.symbolPalette,
+      },
     },
-    compileGroup: {
+    featuresOverrides: [
+      {
+        createdAt: {
+          type: Date,
+          default() {
+            return new Date()
+          },
+        },
+        expiresAt: { type: Date },
+        note: { type: String },
+        features: {
+          collaborators: { type: Number },
+          versioning: { type: Boolean },
+          dropbox: { type: Boolean },
+          github: { type: Boolean },
+          gitBridge: { type: Boolean },
+          compileTimeout: { type: Number },
+          compileGroup: { type: String },
+          templates: { type: Boolean },
+          trackChanges: { type: Boolean },
+          mendeley: { type: Boolean },
+          zotero: { type: Boolean },
+          referencesSearch: { type: Boolean },
+          symbolPalette: { type: Boolean },
+        },
+      },
+    ],
+    featuresUpdatedAt: { type: Date },
+    featuresEpoch: {
       type: String,
-      default: Settings.defaultFeatures.compileGroup,
     },
-    templates: { type: Boolean, default: Settings.defaultFeatures.templates },
-    references: { type: Boolean, default: Settings.defaultFeatures.references },
-    trackChanges: {
-      type: Boolean,
-      default: Settings.defaultFeatures.trackChanges,
+    // when auto-merged from SL and must-reconfirm is set, we may end up using
+    // `sharelatexHashedPassword` to recover accounts...
+    sharelatexHashedPassword: String,
+    must_reconfirm: { type: Boolean, default: false },
+    referal_id: {
+      type: String,
+      default() {
+        return TokenGenerator.generateReferralId()
+      },
     },
-    mendeley: { type: Boolean, default: Settings.defaultFeatures.mendeley },
-    zotero: { type: Boolean, default: Settings.defaultFeatures.zotero },
-    referencesSearch: {
-      type: Boolean,
-      default: Settings.defaultFeatures.referencesSearch,
+    refered_users: [{ type: ObjectId, ref: 'User' }],
+    refered_user_count: { type: Number, default: 0 },
+    refProviders: {
+      // The actual values are managed by third-party-references.
+      mendeley: Schema.Types.Mixed,
+      zotero: Schema.Types.Mixed,
     },
-    symbolPalette: {
-      type: Boolean,
-      default: Settings.defaultFeatures.symbolPalette,
+    alphaProgram: { type: Boolean, default: false }, // experimental features
+    betaProgram: { type: Boolean, default: false },
+    labsProgram: { type: Boolean, default: false },
+    labsProgramGalileo: { type: Boolean, default: false },
+    overleaf: {
+      id: { type: Number },
+      accessToken: { type: String },
+      refreshToken: { type: String },
     },
-  },
-  featuresOverrides: [
-    {
-      createdAt: {
-        type: Date,
-        default() {
-          return new Date()
-        },
-      },
-      expiresAt: { type: Date },
-      note: { type: String },
-      features: {
-        collaborators: { type: Number },
-        versioning: { type: Boolean },
-        dropbox: { type: Boolean },
-        github: { type: Boolean },
-        gitBridge: { type: Boolean },
-        compileTimeout: { type: Number },
-        compileGroup: { type: String },
-        templates: { type: Boolean },
-        trackChanges: { type: Boolean },
-        mendeley: { type: Boolean },
-        zotero: { type: Boolean },
-        referencesSearch: { type: Boolean },
-        symbolPalette: { type: Boolean },
-      },
-    },
-  ],
-  featuresUpdatedAt: { type: Date },
-  featuresEpoch: {
-    type: String,
-  },
-  // when auto-merged from SL and must-reconfirm is set, we may end up using
-  // `sharelatexHashedPassword` to recover accounts...
-  sharelatexHashedPassword: String,
-  must_reconfirm: { type: Boolean, default: false },
-  referal_id: {
-    type: String,
-    default() {
-      return TokenGenerator.generateReferralId()
+    awareOfV2: { type: Boolean, default: false },
+    samlIdentifiers: { type: Array, default: [] },
+    thirdPartyIdentifiers: { type: Array, default: [] },
+    migratedAt: { type: Date },
+    twoFactorAuthentication: {
+      createdAt: { type: Date },
+      enrolledAt: { type: Date },
+      secretEncrypted: { type: String },
     },
+    onboardingEmailSentAt: { type: Date },
+    splitTests: Schema.Types.Mixed,
+    analyticsId: { type: String },
+    surveyResponses: Schema.Types.Mixed,
   },
-  refered_users: [{ type: ObjectId, ref: 'User' }],
-  refered_user_count: { type: Number, default: 0 },
-  refProviders: {
-    // The actual values are managed by third-party-references.
-    mendeley: Schema.Types.Mixed,
-    zotero: Schema.Types.Mixed,
-  },
-  alphaProgram: { type: Boolean, default: false }, // experimental features
-  betaProgram: { type: Boolean, default: false },
-  labsProgram: { type: Boolean, default: false },
-  labsProgramGalileo: { type: Boolean, default: false },
-  overleaf: {
-    id: { type: Number },
-    accessToken: { type: String },
-    refreshToken: { type: String },
-  },
-  awareOfV2: { type: Boolean, default: false },
-  samlIdentifiers: { type: Array, default: [] },
-  thirdPartyIdentifiers: { type: Array, default: [] },
-  migratedAt: { type: Date },
-  twoFactorAuthentication: {
-    createdAt: { type: Date },
-    enrolledAt: { type: Date },
-    secretEncrypted: { type: String },
-  },
-  onboardingEmailSentAt: { type: Date },
-  splitTests: Schema.Types.Mixed,
-  analyticsId: { type: String },
-  surveyResponses: Schema.Types.Mixed,
-})
+  { minimize: false }
+)
 
 function formatSplitTestsSchema(next) {
   if (this.splitTests) {

+ 1 - 0
services/web/app/src/models/UserAuditLogEntry.js

@@ -12,6 +12,7 @@ const UserAuditLogEntrySchema = new Schema(
   },
   {
     collection: 'userAuditLogEntries',
+    minimize: false,
   }
 )
 

+ 1 - 3
services/web/config/settings.defaults.js

@@ -88,9 +88,7 @@ module.exports = {
   mongo: {
     options: {
       appname: 'web',
-      useUnifiedTopology:
-        (process.env.MONGO_USE_UNIFIED_TOPOLOGY || 'true') === 'true',
-      poolSize: parseInt(process.env.MONGO_POOL_SIZE, 10) || 10,
+      maxPoolSize: parseInt(process.env.MONGO_POOL_SIZE, 10) || 100,
       serverSelectionTimeoutMS:
         parseInt(process.env.MONGO_SERVER_SELECTION_TIMEOUT, 10) || 60000,
       socketTimeoutMS: parseInt(process.env.MONGO_SOCKET_TIMEOUT, 10) || 60000,

+ 2 - 2
services/web/package.json

@@ -189,8 +189,8 @@
     "minimist": "^1.2.7",
     "mmmagic": "^0.5.3",
     "moment": "^2.29.4",
-    "mongodb": "~3.6.0",
-    "mongoose": "^5.13.11",
+    "mongodb": "^4.13.0",
+    "mongoose": "^6.9.1",
     "multer": "overleaf/multer#e1df247fbf8e7590520d20ae3601eaef9f3d2e9e",
     "nocache": "^2.1.0",
     "nock": "^13.1.3",

+ 6 - 2
services/web/scripts/force_doc_flush.js

@@ -27,7 +27,7 @@ async function main() {
   })
   if (!DRY_RUN) {
     console.log(`updating doc ${DOC_ID} in mongo for project ${PROJECT_ID}`)
-    const { result } = await db.docs.updateOne(
+    const result = await db.docs.updateOne(
       { _id: ObjectId(DOC_ID), project_id: ObjectId(PROJECT_ID) },
       {
         $set: { lines, version, ranges },
@@ -38,7 +38,11 @@ async function main() {
       }
     )
     console.log('mongo result', result)
-    if (result.n !== 1 || result.nModified !== 1 || result.ok !== 1) {
+    if (
+      result.matchedCount !== 1 ||
+      result.modifiedCount !== 1 ||
+      !result.acknowledged
+    ) {
       throw new Error('unexpected result from mongo update')
     }
     console.log(`deleting doc ${DOC_ID} from redis for project ${PROJECT_ID}`)

+ 2 - 2
services/web/scripts/set_tex_live_image.js

@@ -54,8 +54,8 @@ async function updateImage(image, projectIds) {
     { _id: { $in: projectIds.map(ObjectId) } },
     { $set: { imageName: `quay.io/sharelatex/${image}` } }
   ).exec()
-  console.log(`Found ${res.n} out of ${projectIds.length} projects`)
-  console.log(`Modified ${res.nModified} projects`)
+  console.log(`Found ${res.matchedCount} out of ${projectIds.length} projects`)
+  console.log(`Modified ${res.modifiedCount} projects`)
 }
 
 main()

+ 10 - 1
services/web/test/unit/bootstrap.js

@@ -46,7 +46,16 @@ const SandboxedModule = require('sandboxed-module')
 SandboxedModule.configure({
   ignoreMissing: true,
   requires: getSandboxedModuleRequires(),
-  globals: { AbortSignal, Buffer, Promise, console, process, URL },
+  globals: {
+    AbortSignal,
+    Buffer,
+    Promise,
+    console,
+    process,
+    URL,
+    TextEncoder,
+    TextDecoder,
+  },
 })
 
 function getSandboxedModuleRequires() {

+ 6 - 4
services/web/test/unit/src/Authentication/AuthenticationManagerTests.js

@@ -17,7 +17,7 @@ describe('AuthenticationManager', function () {
       requires: {
         '../../models/User': {
           User: (this.User = {
-            updateOne: sinon.stub().callsArgWith(3, null, { nModified: 1 }),
+            updateOne: sinon.stub().callsArgWith(3, null, { modifiedCount: 1 }),
           }),
         },
         '../../infrastructure/mongodb': {
@@ -99,7 +99,7 @@ describe('AuthenticationManager', function () {
         })
 
         it('should return the user', function () {
-          this.callback.calledWith(null, this.user).should.equal(true)
+          this.callback.should.have.been.calledWith(null, this.user)
         })
 
         it('should send metrics', function () {
@@ -147,7 +147,7 @@ describe('AuthenticationManager', function () {
         beforeEach(function () {
           this.User.updateOne = sinon
             .stub()
-            .callsArgWith(3, null, { nModified: 0 })
+            .callsArgWith(3, null, { modifiedCount: 0 })
         })
 
         describe('correct password', function () {
@@ -171,7 +171,9 @@ describe('AuthenticationManager', function () {
 
         describe('bad password', function () {
           beforeEach(function (done) {
-            this.User.updateOne = sinon.stub().yields(null, { nModified: 0 })
+            this.User.updateOne = sinon
+              .stub()
+              .yields(null, { modifiedCount: 0 })
             this.AuthenticationManager.authenticate(
               { email: this.email },
               'notthecorrectpassword',

+ 5 - 3
services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js

@@ -506,7 +506,7 @@ describe('CollaboratorsHandler', function () {
           }
         )
         .chain('exec')
-        .resolves({ n: 1 })
+        .resolves({ matchedCount: 1 })
       await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
         this.projectId,
         this.userId,
@@ -530,7 +530,7 @@ describe('CollaboratorsHandler', function () {
           }
         )
         .chain('exec')
-        .resolves({ n: 1 })
+        .resolves({ matchedCount: 1 })
       await this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
         this.projectId,
         this.userId,
@@ -539,7 +539,9 @@ describe('CollaboratorsHandler', function () {
     })
 
     it('throws a NotFoundError if the project or collaborator does not exist', async function () {
-      this.ProjectMock.expects('updateOne').chain('exec').resolves({ n: 0 })
+      this.ProjectMock.expects('updateOne')
+        .chain('exec')
+        .resolves({ matchedCount: 0 })
       await expect(
         this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
           this.projectId,

+ 1 - 1
services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js

@@ -75,7 +75,7 @@ describe('ProjectHistoryHandler', function () {
           .callsArgWith(1, null, this.project)
         this.ProjectModel.updateOne = sinon
           .stub()
-          .callsArgWith(2, null, { n: 1 })
+          .callsArgWith(2, null, { matchedCount: 1 })
         return this.ProjectHistoryHandler.ensureHistoryExistsForProject(
           project_id,
           this.callback

+ 1 - 1
services/web/test/unit/src/User/UserAuditLogHandlerTests.js

@@ -40,7 +40,7 @@ describe('UserAuditLogHandler', function () {
       beforeEach(function () {
         this.dbUpdate = this.UserAuditLogEntryMock.expects('create')
           .chain('exec')
-          .resolves({ nModified: 1 })
+          .resolves({ modifiedCount: 1 })
       })
       it('writes a log', async function () {
         await this.UserAuditLogHandler.promises.addEntry(

+ 5 - 3
services/web/test/unit/src/User/UserCreatorTests.js

@@ -35,9 +35,11 @@ describe('UserCreator', function () {
         }),
         './UserUpdater': (this.UserUpdater = {
           promises: {
-            addAffiliationForNewUser: sinon
-              .stub()
-              .resolves({ n: 1, nModified: 1, ok: 1 }),
+            addAffiliationForNewUser: sinon.stub().resolves({
+              matchedCount: 1,
+              modifiedCount: 1,
+              acknowledged: true,
+            }),
             updateUser: sinon.stub().resolves(),
           },
         }),