Procházet zdrojové kódy

Merge pull request #11051 from overleaf/revert-10983-revert-10901-lg-openapi-chat

Migrate chat service to OpenAPI : (Revert of the Revert)

GitOrigin-RevId: 77a34fef20f77344e01461d42b531c623cf28203
Lucie Germain před 3 roky
rodič
revize
afa66aef49

+ 280 - 8
package-lock.json

@@ -1022,6 +1022,17 @@
       "name": "@overleaf/settings",
       "name": "@overleaf/settings",
       "version": "3.0.0"
       "version": "3.0.0"
     },
     },
+    "node_modules/@apidevtools/json-schema-ref-parser": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz",
+      "integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==",
+      "dependencies": {
+        "@jsdevtools/ono": "^7.1.3",
+        "@types/json-schema": "^7.0.6",
+        "call-me-maybe": "^1.0.1",
+        "js-yaml": "^4.1.0"
+      }
+    },
     "node_modules/@arrows/array": {
     "node_modules/@arrows/array": {
       "version": "1.4.1",
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
       "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
@@ -5149,6 +5160,11 @@
         "node": ">=8"
         "node": ">=8"
       }
       }
     },
     },
+    "node_modules/@jsdevtools/ono": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+      "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
+    },
     "node_modules/@juggle/resize-observer": {
     "node_modules/@juggle/resize-observer": {
       "version": "3.3.1",
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
       "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
@@ -10222,7 +10238,6 @@
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
-      "devOptional": true,
       "dependencies": {
       "dependencies": {
         "ajv": "^8.0.0"
         "ajv": "^8.0.0"
       },
       },
@@ -10239,7 +10254,6 @@
       "version": "8.11.0",
       "version": "8.11.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
       "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
       "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
-      "devOptional": true,
       "dependencies": {
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-deep-equal": "^3.1.1",
         "json-schema-traverse": "^1.0.0",
         "json-schema-traverse": "^1.0.0",
@@ -10254,8 +10268,7 @@
     "node_modules/ajv-formats/node_modules/json-schema-traverse": {
     "node_modules/ajv-formats/node_modules/json-schema-traverse": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-      "devOptional": true
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
     },
     },
     "node_modules/ajv-keywords": {
     "node_modules/ajv-keywords": {
       "version": "3.5.2",
       "version": "3.5.2",
@@ -12203,6 +12216,11 @@
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     },
+    "node_modules/call-me-maybe": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+      "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
+    },
     "node_modules/callsites": {
     "node_modules/callsites": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -14515,6 +14533,11 @@
         "node": ">=4.0.0"
         "node": ">=4.0.0"
       }
       }
     },
     },
+    "node_modules/deep-freeze": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
+      "integrity": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg=="
+    },
     "node_modules/deep-is": {
     "node_modules/deep-is": {
       "version": "0.1.4",
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -16572,6 +16595,11 @@
         "node": ">=0.4.x"
         "node": ">=0.4.x"
       }
       }
     },
     },
+    "node_modules/events-listener": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz",
+      "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g=="
+    },
     "node_modules/execa": {
     "node_modules/execa": {
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -16628,6 +16656,96 @@
         "node": ">=4"
         "node": ">=4"
       }
       }
     },
     },
+    "node_modules/exegesis": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.1.tgz",
+      "integrity": "sha512-PvSqaMOw2absLBgsthtJyVOeCHN4lxQ1dM7ibXb6TfZZJaoXtGELoEAGJRFvdN16+u9kg8oy1okZXRk8VpimWA==",
+      "dependencies": {
+        "@apidevtools/json-schema-ref-parser": "^9.0.3",
+        "ajv": "^8.3.0",
+        "ajv-formats": "^2.1.0",
+        "body-parser": "^1.18.3",
+        "content-type": "^1.0.4",
+        "deep-freeze": "0.0.1",
+        "events-listener": "^1.1.0",
+        "glob": "^7.1.3",
+        "json-ptr": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "lodash": "^4.17.11",
+        "openapi3-ts": "^3.1.1",
+        "promise-breaker": "^6.0.0",
+        "pump": "^3.0.0",
+        "qs": "^6.6.0",
+        "raw-body": "^2.3.3",
+        "semver": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=6.0.0",
+        "npm": ">5.0.0"
+      }
+    },
+    "node_modules/exegesis-express": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz",
+      "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==",
+      "dependencies": {
+        "exegesis": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6.0.0",
+        "npm": ">5.0.0"
+      }
+    },
+    "node_modules/exegesis/node_modules/ajv": {
+      "version": "8.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/exegesis/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
+    },
+    "node_modules/exegesis/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/exegesis/node_modules/semver": {
+      "version": "7.3.8",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/exegesis/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
     "node_modules/exifr": {
     "node_modules/exifr": {
       "version": "6.3.0",
       "version": "6.3.0",
       "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz",
       "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz",
@@ -22117,6 +22235,11 @@
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "dev": true
       "dev": true
     },
     },
+    "node_modules/json-ptr": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz",
+      "integrity": "sha512-SiSJQ805W1sDUCD1+/t1/1BIrveq2Fe9HJqENxZmMCILmrPI7WhS/pePpIOx85v6/H2z1Vy7AI08GV2TzfXocg=="
+    },
     "node_modules/json-refs": {
     "node_modules/json-refs": {
       "version": "3.0.15",
       "version": "3.0.15",
       "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz",
       "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz",
@@ -26372,6 +26495,22 @@
         "url": "https://github.com/sponsors/sindresorhus"
         "url": "https://github.com/sponsors/sindresorhus"
       }
       }
     },
     },
+    "node_modules/openapi3-ts": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.2.tgz",
+      "integrity": "sha512-S8fijNOqe/ut0kEDAwHZnI7sVYqb8Q3XnISmSyXmK76jgrcf4ableI75KTY1qdksd9EI/t39Vi5M4VYKrkNKfQ==",
+      "dependencies": {
+        "yaml": "^2.1.3"
+      }
+    },
+    "node_modules/openapi3-ts/node_modules/yaml": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz",
+      "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/opentracing": {
     "node_modules/opentracing": {
       "version": "0.14.7",
       "version": "0.14.7",
       "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
       "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
@@ -27805,6 +27944,11 @@
         "asap": "~2.0.3"
         "asap": "~2.0.3"
       }
       }
     },
     },
+    "node_modules/promise-breaker": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz",
+      "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA=="
+    },
     "node_modules/promise.prototype.finally": {
     "node_modules/promise.prototype.finally": {
       "version": "3.1.3",
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz",
       "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz",
@@ -35147,12 +35291,15 @@
     },
     },
     "services/chat": {
     "services/chat": {
       "name": "@overleaf/chat",
       "name": "@overleaf/chat",
+      "version": "1.0.0",
+      "license": "AGPL-3.0",
       "dependencies": {
       "dependencies": {
         "@overleaf/logger": "*",
         "@overleaf/logger": "*",
         "@overleaf/metrics": "*",
         "@overleaf/metrics": "*",
         "@overleaf/settings": "*",
         "@overleaf/settings": "*",
         "async": "^3.2.2",
         "async": "^3.2.2",
         "body-parser": "^1.19.0",
         "body-parser": "^1.19.0",
+        "exegesis-express": "^4.0.0",
         "express": "4.17.1",
         "express": "4.17.1",
         "mongodb": "^4.11.0"
         "mongodb": "^4.11.0"
       },
       },
@@ -40973,6 +41120,17 @@
     }
     }
   },
   },
   "dependencies": {
   "dependencies": {
+    "@apidevtools/json-schema-ref-parser": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.0.tgz",
+      "integrity": "sha512-teB30tFooE3iQs2HQIKJ02D8UZA1Xy1zaczzhUjJs0CymYxeC0g+y5rCY2p8NHBM6DBUVoR8rSM4kHLj1WE9mQ==",
+      "requires": {
+        "@jsdevtools/ono": "^7.1.3",
+        "@types/json-schema": "^7.0.6",
+        "call-me-maybe": "^1.0.1",
+        "js-yaml": "^4.1.0"
+      }
+    },
     "@arrows/array": {
     "@arrows/array": {
       "version": "1.4.1",
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
       "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
@@ -44177,6 +44335,11 @@
       "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "dev": true
       "dev": true
     },
     },
+    "@jsdevtools/ono": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+      "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
+    },
     "@juggle/resize-observer": {
     "@juggle/resize-observer": {
       "version": "3.3.1",
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
       "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
@@ -46074,6 +46237,7 @@
         "body-parser": "^1.19.0",
         "body-parser": "^1.19.0",
         "chai": "^4.3.6",
         "chai": "^4.3.6",
         "chai-as-promised": "^7.1.1",
         "chai-as-promised": "^7.1.1",
+        "exegesis-express": "^4.0.0",
         "express": "4.17.1",
         "express": "4.17.1",
         "mocha": "^8.4.0",
         "mocha": "^8.4.0",
         "mongodb": "^4.11.0",
         "mongodb": "^4.11.0",
@@ -52725,7 +52889,6 @@
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
-      "devOptional": true,
       "requires": {
       "requires": {
         "ajv": "^8.0.0"
         "ajv": "^8.0.0"
       },
       },
@@ -52734,7 +52897,6 @@
           "version": "8.11.0",
           "version": "8.11.0",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
           "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
           "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
           "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
-          "devOptional": true,
           "requires": {
           "requires": {
             "fast-deep-equal": "^3.1.1",
             "fast-deep-equal": "^3.1.1",
             "json-schema-traverse": "^1.0.0",
             "json-schema-traverse": "^1.0.0",
@@ -52745,8 +52907,7 @@
         "json-schema-traverse": {
         "json-schema-traverse": {
           "version": "1.0.0",
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
           "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-          "devOptional": true
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
         }
         }
       }
       }
     },
     },
@@ -54279,6 +54440,11 @@
         "get-intrinsic": "^1.0.2"
         "get-intrinsic": "^1.0.2"
       }
       }
     },
     },
+    "call-me-maybe": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+      "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
+    },
     "callsites": {
     "callsites": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -56101,6 +56267,11 @@
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
       "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
       "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
     },
     },
+    "deep-freeze": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz",
+      "integrity": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg=="
+    },
     "deep-is": {
     "deep-is": {
       "version": "0.1.4",
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -57676,6 +57847,11 @@
       "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
       "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
       "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
     },
     },
+    "events-listener": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz",
+      "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g=="
+    },
     "execa": {
     "execa": {
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -57716,6 +57892,77 @@
         "pify": "^2.2.0"
         "pify": "^2.2.0"
       }
       }
     },
     },
+    "exegesis": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.1.tgz",
+      "integrity": "sha512-PvSqaMOw2absLBgsthtJyVOeCHN4lxQ1dM7ibXb6TfZZJaoXtGELoEAGJRFvdN16+u9kg8oy1okZXRk8VpimWA==",
+      "requires": {
+        "@apidevtools/json-schema-ref-parser": "^9.0.3",
+        "ajv": "^8.3.0",
+        "ajv-formats": "^2.1.0",
+        "body-parser": "^1.18.3",
+        "content-type": "^1.0.4",
+        "deep-freeze": "0.0.1",
+        "events-listener": "^1.1.0",
+        "glob": "^7.1.3",
+        "json-ptr": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "lodash": "^4.17.11",
+        "openapi3-ts": "^3.1.1",
+        "promise-breaker": "^6.0.0",
+        "pump": "^3.0.0",
+        "qs": "^6.6.0",
+        "raw-body": "^2.3.3",
+        "semver": "^7.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.12.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+          "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.3.8",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+          "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
+    "exegesis-express": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz",
+      "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==",
+      "requires": {
+        "exegesis": "^4.1.0"
+      }
+    },
     "exifr": {
     "exifr": {
       "version": "6.3.0",
       "version": "6.3.0",
       "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz",
       "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz",
@@ -62037,6 +62284,11 @@
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "dev": true
       "dev": true
     },
     },
+    "json-ptr": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz",
+      "integrity": "sha512-SiSJQ805W1sDUCD1+/t1/1BIrveq2Fe9HJqENxZmMCILmrPI7WhS/pePpIOx85v6/H2z1Vy7AI08GV2TzfXocg=="
+    },
     "json-refs": {
     "json-refs": {
       "version": "3.0.15",
       "version": "3.0.15",
       "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz",
       "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz",
@@ -65474,6 +65726,21 @@
         "is-wsl": "^2.2.0"
         "is-wsl": "^2.2.0"
       }
       }
     },
     },
+    "openapi3-ts": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.2.tgz",
+      "integrity": "sha512-S8fijNOqe/ut0kEDAwHZnI7sVYqb8Q3XnISmSyXmK76jgrcf4ableI75KTY1qdksd9EI/t39Vi5M4VYKrkNKfQ==",
+      "requires": {
+        "yaml": "^2.1.3"
+      },
+      "dependencies": {
+        "yaml": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz",
+          "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw=="
+        }
+      }
+    },
     "opentracing": {
     "opentracing": {
       "version": "0.14.7",
       "version": "0.14.7",
       "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
       "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz",
@@ -67053,6 +67320,11 @@
         "asap": "~2.0.3"
         "asap": "~2.0.3"
       }
       }
     },
     },
+    "promise-breaker": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz",
+      "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA=="
+    },
     "promise.prototype.finally": {
     "promise.prototype.finally": {
       "version": "3.1.3",
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz",
       "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz",

+ 3 - 2
services/chat/app.js

@@ -1,13 +1,14 @@
 import logger from '@overleaf/logger'
 import logger from '@overleaf/logger'
 import settings from '@overleaf/settings'
 import settings from '@overleaf/settings'
 import { mongoClient } from './app/js/mongodb.js'
 import { mongoClient } from './app/js/mongodb.js'
-import { server } from './app/js/server.js'
+import { createServer } from './app/js/server.js'
 
 
 const port = settings.internal.chat.port
 const port = settings.internal.chat.port
 const host = settings.internal.chat.host
 const host = settings.internal.chat.host
 mongoClient
 mongoClient
   .connect()
   .connect()
-  .then(() => {
+  .then(async () => {
+    const { server } = await createServer()
     server.listen(port, host, function (err) {
     server.listen(port, host, function (err) {
       if (err) {
       if (err) {
         logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`)
         logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`)

+ 124 - 40
services/chat/app/js/Features/Messages/MessageHttpController.js

@@ -3,24 +3,105 @@ import * as MessageManager from './MessageManager.js'
 import * as MessageFormatter from './MessageFormatter.js'
 import * as MessageFormatter from './MessageFormatter.js'
 import * as ThreadManager from '../Threads/ThreadManager.js'
 import * as ThreadManager from '../Threads/ThreadManager.js'
 import { ObjectId } from '../../mongodb.js'
 import { ObjectId } from '../../mongodb.js'
-import { expressify } from '../../util/promises.js'
 
 
 const DEFAULT_MESSAGE_LIMIT = 50
 const DEFAULT_MESSAGE_LIMIT = 50
 const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
 const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
 
 
-export const getGlobalMessages = expressify(async (req, res) => {
+async function readContext(context, req) {
+  req.body = context.requestBody
+  req.params = context.params.path
+  req.query = context.params.query
+  if (typeof req.params.projectId !== 'undefined') {
+    if (!ObjectId.isValid(req.params.projectId)) {
+      context.res.status(400).setBody('Invalid projectId')
+    }
+  }
+  if (typeof req.params.threadId !== 'undefined') {
+    if (!ObjectId.isValid(req.params.threadId)) {
+      context.res.status(400).setBody('Invalid threadId')
+    }
+  }
+}
+
+export async function callMessageHttpController(context, ControllerMethod) {
+  const req = {}
+  readContext(context, req)
+  if (context.res.statusCode !== 400) {
+    return await ControllerMethod(req, context.res)
+  } else {
+    return context.res.body
+  }
+}
+
+export async function getGlobalMessages(context) {
+  return await callMessageHttpController(context, _getGlobalMessages)
+}
+
+export async function sendGlobalMessage(context) {
+  return await callMessageHttpController(context, _sendGlobalMessage)
+}
+
+export async function sendMessage(context) {
+  return await callMessageHttpController(context, _sendThreadMessage)
+}
+
+export async function getThreads(context) {
+  return await callMessageHttpController(context, _getAllThreads)
+}
+
+export async function resolveThread(context) {
+  return await callMessageHttpController(context, _resolveThread)
+}
+
+export async function reopenThread(context) {
+  return await callMessageHttpController(context, _reopenThread)
+}
+
+export async function deleteThread(context) {
+  return await callMessageHttpController(context, _deleteThread)
+}
+
+export async function editMessage(context) {
+  return await callMessageHttpController(context, _editMessage)
+}
+
+export async function deleteMessage(context) {
+  return await callMessageHttpController(context, _deleteMessage)
+}
+
+export async function destroyProject(context) {
+  return await callMessageHttpController(context, _destroyProject)
+}
+
+export async function getStatus(context) {
+  const message = 'chat is alive'
+  context.res.status(200).setBody(message)
+  return message
+}
+
+const _getGlobalMessages = async (req, res) => {
   await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
   await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
-})
+}
 
 
-export const sendGlobalMessage = expressify(async (req, res) => {
-  await _sendMessage(ThreadManager.GLOBAL_THREAD, req, res)
-})
+async function _sendGlobalMessage(req, res) {
+  const { user_id: userId, content } = req.body
+  const { projectId } = req.params
+  return await _sendMessage(
+    userId,
+    projectId,
+    content,
+    ThreadManager.GLOBAL_THREAD,
+    res
+  )
+}
 
 
-export const sendThreadMessage = expressify(async (req, res) => {
-  await _sendMessage(req.params.threadId, req, res)
-})
+async function _sendThreadMessage(req, res) {
+  const { user_id: userId, content } = req.body
+  const { projectId, threadId } = req.params
+  return await _sendMessage(userId, projectId, content, threadId, res)
+}
 
 
-export const getAllThreads = expressify(async (req, res) => {
+const _getAllThreads = async (req, res) => {
   const { projectId } = req.params
   const { projectId } = req.params
   logger.debug({ projectId }, 'getting all threads')
   logger.debug({ projectId }, 'getting all threads')
   const rooms = await ThreadManager.findAllThreadRooms(projectId)
   const rooms = await ThreadManager.findAllThreadRooms(projectId)
@@ -28,32 +109,32 @@ export const getAllThreads = expressify(async (req, res) => {
   const messages = await MessageManager.findAllMessagesInRooms(roomIds)
   const messages = await MessageManager.findAllMessagesInRooms(roomIds)
   const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
   const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
   res.json(threads)
   res.json(threads)
-})
+}
 
 
-export const resolveThread = expressify(async (req, res) => {
+const _resolveThread = async (req, res) => {
   const { projectId, threadId } = req.params
   const { projectId, threadId } = req.params
   const { user_id: userId } = req.body
   const { user_id: userId } = req.body
   logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
   logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
   await ThreadManager.resolveThread(projectId, threadId, userId)
   await ThreadManager.resolveThread(projectId, threadId, userId)
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-export const reopenThread = expressify(async (req, res) => {
+const _reopenThread = async (req, res) => {
   const { projectId, threadId } = req.params
   const { projectId, threadId } = req.params
   logger.debug({ projectId, threadId }, 'reopening thread')
   logger.debug({ projectId, threadId }, 'reopening thread')
   await ThreadManager.reopenThread(projectId, threadId)
   await ThreadManager.reopenThread(projectId, threadId)
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-export const deleteThread = expressify(async (req, res) => {
+const _deleteThread = async (req, res) => {
   const { projectId, threadId } = req.params
   const { projectId, threadId } = req.params
   logger.debug({ projectId, threadId }, 'deleting thread')
   logger.debug({ projectId, threadId }, 'deleting thread')
   const roomId = await ThreadManager.deleteThread(projectId, threadId)
   const roomId = await ThreadManager.deleteThread(projectId, threadId)
   await MessageManager.deleteAllMessagesInRoom(roomId)
   await MessageManager.deleteAllMessagesInRoom(roomId)
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-export const editMessage = expressify(async (req, res) => {
+const _editMessage = async (req, res) => {
   const { content, userId } = req.body
   const { content, userId } = req.body
   const { projectId, threadId, messageId } = req.params
   const { projectId, threadId, messageId } = req.params
   logger.debug({ projectId, threadId, messageId, content }, 'editing message')
   logger.debug({ projectId, threadId, messageId, content }, 'editing message')
@@ -66,20 +147,21 @@ export const editMessage = expressify(async (req, res) => {
     Date.now()
     Date.now()
   )
   )
   if (!found) {
   if (!found) {
-    return res.sendStatus(404)
+    res.status(404)
+    return
   }
   }
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-export const deleteMessage = expressify(async (req, res) => {
+const _deleteMessage = async (req, res) => {
   const { projectId, threadId, messageId } = req.params
   const { projectId, threadId, messageId } = req.params
   logger.debug({ projectId, threadId, messageId }, 'deleting message')
   logger.debug({ projectId, threadId, messageId }, 'deleting message')
   const room = await ThreadManager.findOrCreateThread(projectId, threadId)
   const room = await ThreadManager.findOrCreateThread(projectId, threadId)
   await MessageManager.deleteMessage(room._id, messageId)
   await MessageManager.deleteMessage(room._id, messageId)
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-export const destroyProject = expressify(async (req, res) => {
+const _destroyProject = async (req, res) => {
   const { projectId } = req.params
   const { projectId } = req.params
   logger.debug({ projectId }, 'destroying project')
   logger.debug({ projectId }, 'destroying project')
   const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
   const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
@@ -88,22 +170,24 @@ export const destroyProject = expressify(async (req, res) => {
   await MessageManager.deleteAllMessagesInRooms(roomIds)
   await MessageManager.deleteAllMessagesInRooms(roomIds)
   logger.debug({ projectId }, 'deleting all threads in project')
   logger.debug({ projectId }, 'deleting all threads in project')
   await ThreadManager.deleteAllThreadsInProject(projectId)
   await ThreadManager.deleteAllThreadsInProject(projectId)
-  res.sendStatus(204)
-})
+  res.status(204)
+}
 
 
-async function _sendMessage(clientThreadId, req, res) {
-  const { user_id: userId, content } = req.body
-  const { projectId } = req.params
+async function _sendMessage(userId, projectId, content, clientThreadId, res) {
   if (!ObjectId.isValid(userId)) {
   if (!ObjectId.isValid(userId)) {
-    return res.status(400).send('Invalid userId')
+    const message = 'Invalid userId'
+    res.status(400).setBody(message)
+    return message
   }
   }
   if (!content) {
   if (!content) {
-    return res.status(400).send('No content provided')
+    const message = 'No content provided'
+    res.status(400).setBody(message)
+    return message
   }
   }
   if (content.length > MAX_MESSAGE_LENGTH) {
   if (content.length > MAX_MESSAGE_LENGTH) {
-    return res
-      .status(400)
-      .send(`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`)
+    const message = `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
+    res.status(400).setBody(message)
+    return message
   }
   }
   logger.debug(
   logger.debug(
     { clientThreadId, projectId, userId, content },
     { clientThreadId, projectId, userId, content },
@@ -121,7 +205,7 @@ async function _sendMessage(clientThreadId, req, res) {
   )
   )
   message = MessageFormatter.formatMessageForClientSide(message)
   message = MessageFormatter.formatMessageForClientSide(message)
   message.room_id = projectId
   message.room_id = projectId
-  res.status(201).send(message)
+  res.status(201).setBody(message)
 }
 }
 
 
 async function _getMessages(clientThreadId, req, res) {
 async function _getMessages(clientThreadId, req, res) {
@@ -153,5 +237,5 @@ async function _getMessages(clientThreadId, req, res) {
   let messages = await MessageManager.getMessages(threadObjectId, limit, before)
   let messages = await MessageManager.getMessages(threadObjectId, limit, before)
   messages = MessageFormatter.formatMessagesForClientSide(messages)
   messages = MessageFormatter.formatMessagesForClientSide(messages)
   logger.debug({ projectId, messages }, 'got messages')
   logger.debug({ projectId, messages }, 'got messages')
-  res.status(200).send(messages)
+  res.status(200).setBody(messages)
 }
 }

+ 0 - 65
services/chat/app/js/router.js

@@ -1,65 +0,0 @@
-import * as MessageHttpController from './Features/Messages/MessageHttpController.js'
-import { ObjectId } from './mongodb.js'
-
-export function route(app) {
-  app.param('projectId', function (req, res, next, projectId) {
-    if (ObjectId.isValid(projectId)) {
-      next()
-    } else {
-      res.status(400).send('Invalid projectId')
-    }
-  })
-
-  app.param('threadId', function (req, res, next, threadId) {
-    if (ObjectId.isValid(threadId)) {
-      next()
-    } else {
-      res.status(400).send('Invalid threadId')
-    }
-  })
-
-  // These are for backwards compatibility
-  app.get('/room/:projectId/messages', MessageHttpController.getGlobalMessages)
-  app.post('/room/:projectId/messages', MessageHttpController.sendGlobalMessage)
-
-  app.get(
-    '/project/:projectId/messages',
-    MessageHttpController.getGlobalMessages
-  )
-  app.post(
-    '/project/:projectId/messages',
-    MessageHttpController.sendGlobalMessage
-  )
-
-  app.post(
-    '/project/:projectId/thread/:threadId/messages',
-    MessageHttpController.sendThreadMessage
-  )
-  app.get('/project/:projectId/threads', MessageHttpController.getAllThreads)
-
-  app.post(
-    '/project/:projectId/thread/:threadId/messages/:messageId/edit',
-    MessageHttpController.editMessage
-  )
-  app.delete(
-    '/project/:projectId/thread/:threadId/messages/:messageId',
-    MessageHttpController.deleteMessage
-  )
-
-  app.post(
-    '/project/:projectId/thread/:threadId/resolve',
-    MessageHttpController.resolveThread
-  )
-  app.post(
-    '/project/:projectId/thread/:threadId/reopen',
-    MessageHttpController.reopenThread
-  )
-  app.delete(
-    '/project/:projectId/thread/:threadId',
-    MessageHttpController.deleteThread
-  )
-
-  app.delete('/project/:projectId', MessageHttpController.destroyProject)
-
-  app.get('/status', (req, res, next) => res.send('chat is alive'))
-}

+ 36 - 9
services/chat/app/js/server.js

@@ -2,18 +2,45 @@ import http from 'http'
 import metrics from '@overleaf/metrics'
 import metrics from '@overleaf/metrics'
 import logger from '@overleaf/logger'
 import logger from '@overleaf/logger'
 import express from 'express'
 import express from 'express'
-import bodyParser from 'body-parser'
-import * as Router from './router.js'
+import exegesisExpress from 'exegesis-express'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import * as messagesController from './Features/Messages/MessageHttpController.js'
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url))
 
 
 metrics.initialize('chat')
 metrics.initialize('chat')
 logger.initialize('chat')
 logger.initialize('chat')
 
 
-export const app = express()
-export const server = http.createServer(app)
+export async function createServer() {
+  const app = express()
+
+  // See https://github.com/exegesis-js/exegesis/blob/master/docs/Options.md
+  const options = {
+    controllers: { messagesController },
+    ignoreServers: true,
+    allowMissingControllers: false,
+  }
+
+  // const exegesisMiddleware = await exegesisExpress.middleware(
+  const exegesisMiddleware = await exegesisExpress.middleware(
+    path.resolve(__dirname, '../../chat.yaml'),
+    options
+  )
+
+  // If you have any body parsers, this should go before them.
+  app.use(exegesisMiddleware)
+
+  // Return a 404
+  app.use((req, res) => {
+    res.status(404).json({ message: `Not found` })
+  })
 
 
-app.use(bodyParser.json())
-app.use(bodyParser.urlencoded({ extended: true }))
-app.use(metrics.http.monitor(logger))
-metrics.injectMetricsRoute(app)
+  // Handle any unexpected errors
+  app.use((err, req, res, next) => {
+    res.status(500).json({ message: `Internal error: ${err.message}` })
+  })
 
 
-Router.route(app)
+  const server = http.createServer(app)
+  return { app, server }
+}

+ 317 - 0
services/chat/chat.yaml

@@ -0,0 +1,317 @@
+openapi: 3.1.0
+x-stoplight:
+  id: okoe8mh50pjec
+info:
+  title: chat
+  version: '1.0'
+servers:
+  - url: 'http://chat:3010'
+x-exegesis-controller: messagesController
+paths:
+  '/project/{projectId}/messages':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+    get:
+      summary: Get Global messages
+      tags: []
+      responses:
+        '201':
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Message'
+      operationId: getGlobalMessages
+      description: Get global messages for the project with Project ID provided
+      parameters:
+        - schema:
+            type: string
+          in: query
+          name: before
+        - schema:
+            type: string
+          in: query
+          name: limit
+    post:
+      summary: Send Global message
+      operationId: sendGlobalMessage
+      responses:
+        '201':
+          description: OK
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Message'
+            examples:
+              example-1:
+                value:
+                  user_id: string
+                  content: string
+        description: 'UserID and Content of the message to be posted. '
+      description: Send global message  for the project with Project ID provided
+  '/project/{projectId}/thread/{threadId}/messages':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+    post:
+      summary: Send message
+      operationId: sendMessage
+      responses:
+        '201':
+          description: Created
+      description: Add a message to the thread with thread ID provided from the Project with Project ID provided.
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Message'
+        description: |-
+          JSON object with :
+          - user_id: Id of the user 
+          - content: Content of the message
+  '/project/{projectId}/threads':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+    get:
+      summary: Get Threads
+      tags: []
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Thread'
+              examples: {}
+        '404':
+          description: Not Found
+      operationId: getThreads
+      description: Get the list of threads for the project with Project ID provided
+  '/project/{projectId}/thread/{threadId}/messages/{messageId}/edit':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: messageId
+        in: path
+        required: true
+    post:
+      summary: Edit message
+      operationId: editMessage
+      responses:
+        '204':
+          description: No Content
+        '404':
+          description: Not Found
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                content:
+                  type: string
+                user_id:
+                  type: string
+                  readOnly: true
+              required:
+                - content
+            examples: {}
+        description: |-
+          JSON object with :
+          - content: Content of the message to edit
+          - user_id: Id of the user (optional)
+      description: |
+        Update message with Message ID provided from the Thread ID and Project ID provided
+  '/project/{projectId}/thread/{threadId}/messages/{messageId}':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: messageId
+        in: path
+        required: true
+    delete:
+      summary: Delete message
+      operationId: deleteMessage
+      responses:
+        '204':
+          description: No Content
+      description: 'Delete message with Message ID provided, from the Thread with ThreadID and ProjectID provided'
+  '/project/{projectId}/thread/{threadId}/resolve':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+    post:
+      summary: Resolve Thread
+      operationId: resolveThread
+      responses:
+        '204':
+          description: No Content
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                user_id:
+                  type: string
+              required:
+                - user_id
+        description: |-
+          JSON object with : 
+          - user_id: Id of the user.
+      description: Mark Thread with ThreadID and ProjectID provided owned by the user with UserID provided as resolved.
+  '/project/{projectId}/thread/{threadId}/reopen':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+    post:
+      summary: Reopen Thread
+      operationId: reopenThread
+      responses:
+        '204':
+          description: No Content
+      description: |-
+        Reopen Thread with ThreadID and ProjectID provided. 
+        i.e unmark it as resolved.
+  '/project/{projectId}/thread/{threadId}':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+      - schema:
+          type: string
+        name: threadId
+        in: path
+        required: true
+    delete:
+      summary: Delete thread
+      operationId: deleteThread
+      responses:
+        '204':
+          description: No Content
+      description: Delete thread with ThreadID and ProjectID provided
+  '/project/{projectId}':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+    delete:
+      summary: Destroy project
+      operationId: destroyProject
+      responses:
+        '204':
+          description: No Content
+      description: 'Delete all threads from Project with Project ID provided, and all messages in those threads.'
+  /status:
+    get:
+      summary: Check status
+      tags: []
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: string
+                description: chat is alive
+      operationId: getStatus
+      description: Check that the Chat service is alive
+    head:
+      summary: Check status
+      tags: []
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: string
+                description: chat is alive
+      operationId: getStatus
+      description: Check that the Chat service is alive
+components:
+  schemas:
+    Message:
+      title: Message
+      x-stoplight:
+        id: ue9n1vvezlutw
+      type: object
+      examples:
+        - user_id: string
+        - content: string
+      properties:
+        user_id:
+          type: string
+        content:
+          type: string
+      required:
+        - user_id
+        - content
+    Thread:
+      title: Thread
+      x-stoplight:
+        id: 0ppt3jw4h5bua
+      type: array
+      items:
+        $ref: '#/components/schemas/Message'

+ 9 - 1
services/chat/package.json

@@ -22,6 +22,7 @@
     "@overleaf/settings": "*",
     "@overleaf/settings": "*",
     "async": "^3.2.2",
     "async": "^3.2.2",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
+    "exegesis-express": "^4.0.0",
     "express": "4.17.1",
     "express": "4.17.1",
     "mongodb": "^4.11.0"
     "mongodb": "^4.11.0"
   },
   },
@@ -35,5 +36,12 @@
     "sandboxed-module": "^2.0.4",
     "sandboxed-module": "^2.0.4",
     "sinon": "^9.2.4",
     "sinon": "^9.2.4",
     "timekeeper": "^2.2.0"
     "timekeeper": "^2.2.0"
-  }
+  },
+  "version": "1.0.0",
+  "directories": {
+    "test": "test"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "AGPL-3.0"
 }
 }

+ 3 - 0
services/chat/test/acceptance/js/GettingMessagesTests.js

@@ -28,6 +28,9 @@ describe('Getting messages', async function () {
         content2
         content2
       )
       )
       expect(response2.statusCode).to.equal(201)
       expect(response2.statusCode).to.equal(201)
+      const { response: response3, body } = await ChatClient.checkStatus()
+      expect(response3.statusCode).to.equal(200)
+      expect(body).to.equal('chat is alive')
     })
     })
 
 
     it('should contain the messages and populated users when getting the messages', async function () {
     it('should contain the messages and populated users when getting the messages', async function () {

+ 3 - 1
services/chat/test/acceptance/js/SendingAMessageTests.js

@@ -122,7 +122,9 @@ describe('Sending a message', async function () {
           null
           null
         )
         )
         expect(response.statusCode).to.equal(400)
         expect(response.statusCode).to.equal(400)
-        expect(body).to.equal('No content provided')
+        // Exegesis is responding with validation errors. I can´t find a way to choose the validation error yet.
+        // expect(body).to.equal('No content provided')
+        expect(body.message).to.equal('Validation errors')
       })
       })
     })
     })
 
 

+ 5 - 10
services/chat/test/acceptance/js/helpers/ChatApp.js

@@ -1,20 +1,15 @@
-import { server } from '../../../../app/js/server.js'
+import { createServer } from '../../../../app/js/server.js'
+import { promisify } from 'util'
 
 
 export { db } from '../../../../app/js/mongodb.js'
 export { db } from '../../../../app/js/mongodb.js'
 
 
 let serverPromise = null
 let serverPromise = null
-function startServer(resolve, reject) {
-  server.listen(3010, 'localhost', error => {
-    if (error) {
-      return reject(error)
-    }
-    resolve()
-  })
-}
 
 
 export async function ensureRunning() {
 export async function ensureRunning() {
   if (!serverPromise) {
   if (!serverPromise) {
-    serverPromise = new Promise(startServer)
+    const { app } = await createServer()
+    const startServer = promisify(app.listen.bind(app))
+    serverPromise = startServer(3010, 'localhost')
   }
   }
   return serverPromise
   return serverPromise
 }
 }

+ 22 - 14
services/chat/test/acceptance/js/helpers/ChatClient.js

@@ -64,20 +64,6 @@ export async function resolveThread(projectId, threadId, userId) {
   })
   })
 }
 }
 
 
-export async function reopenThread(projectId, threadId) {
-  return asyncRequest({
-    method: 'post',
-    url: `/project/${projectId}/thread/${threadId}/reopen`,
-  })
-}
-
-export async function deleteThread(projectId, threadId) {
-  return asyncRequest({
-    method: 'delete',
-    url: `/project/${projectId}/thread/${threadId}`,
-  })
-}
-
 export async function editMessage(projectId, threadId, messageId, content) {
 export async function editMessage(projectId, threadId, messageId, content) {
   return asyncRequest({
   return asyncRequest({
     method: 'post',
     method: 'post',
@@ -105,6 +91,28 @@ export async function editMessageWithUser(
   })
   })
 }
 }
 
 
+export async function checkStatus() {
+  return asyncRequest({
+    method: 'get',
+    url: `/status`,
+    json: true,
+  })
+}
+
+export async function reopenThread(projectId, threadId) {
+  return asyncRequest({
+    method: 'post',
+    url: `/project/${projectId}/thread/${threadId}/reopen`,
+  })
+}
+
+export async function deleteThread(projectId, threadId) {
+  return asyncRequest({
+    method: 'delete',
+    url: `/project/${projectId}/thread/${threadId}`,
+  })
+}
+
 export async function deleteMessage(projectId, threadId, messageId) {
 export async function deleteMessage(projectId, threadId, messageId) {
   return asyncRequest({
   return asyncRequest({
     method: 'delete',
     method: 'delete',