Переглянути джерело

[docstore] replace remaining Joi usage with zod (#34603)

Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: 41ca0c35d59bf6accd619f6b5149be14a69e1078
Copilot 1 місяць тому
батько
коміт
f40cebf60b

+ 2 - 0
services/docstore/Dockerfile

@@ -29,6 +29,7 @@ COPY libraries/object-persistor/package.json /overleaf/libraries/object-persisto
 COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/package.json
 COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
 COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
+COPY libraries/validation-tools/package.json /overleaf/libraries/validation-tools/package.json
 COPY services/docstore/package.json /overleaf/services/docstore/package.json
 COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
 COPY .yarn/patches/ /overleaf/.yarn/patches/
@@ -45,6 +46,7 @@ COPY libraries/object-persistor/ /overleaf/libraries/object-persistor/
 COPY libraries/promise-utils/ /overleaf/libraries/promise-utils/
 COPY libraries/settings/ /overleaf/libraries/settings/
 COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/
+COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
 COPY services/docstore/ /overleaf/services/docstore/
 COPY tools/migrations/ /overleaf/tools/migrations/
 

+ 1 - 0
services/docstore/Makefile

@@ -26,6 +26,7 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
 	$(MONOREPO)/libraries/promise-utils/package.json \
 	$(MONOREPO)/libraries/settings/package.json \
 	$(MONOREPO)/libraries/stream-utils/package.json \
+	$(MONOREPO)/libraries/validation-tools/package.json \
 	$(MONOREPO)/services/docstore/package.json \
 	$(MONOREPO)/tools/migrations/package.json \
 	$(MONOREPO)/.yarn/patches/* \

+ 2 - 13
services/docstore/app.js

@@ -7,11 +7,7 @@ import Settings from '@overleaf/settings'
 import logger from '@overleaf/logger'
 import express from 'express'
 import bodyParser from 'body-parser'
-import {
-  celebrate as validate,
-  Joi,
-  errors as handleValidationErrors,
-} from 'celebrate'
+import { handleValidationError } from '@overleaf/validation-tools'
 import mongodb from './app/js/mongodb.js'
 import Errors from './app/js/Errors.js'
 import HttpController from './app/js/HttpController.js'
@@ -79,13 +75,6 @@ app.post(
 app.patch(
   '/project/:project_id/doc/:doc_id',
   bodyParser.json(),
-  validate({
-    body: {
-      deleted: Joi.boolean(),
-      name: Joi.string().when('deleted', { is: true, then: Joi.required() }),
-      deletedAt: Joi.date().when('deleted', { is: true, then: Joi.required() }),
-    },
-  }),
   HttpController.patchDoc
 )
 app.delete('/project/:project_id/doc/:doc_id', (req, res) => {
@@ -101,7 +90,7 @@ app.get('/health_check', HttpController.healthCheck)
 
 app.get('/status', (req, res) => res.send('docstore is alive'))
 
-app.use(handleValidationErrors())
+app.use(handleValidationError)
 app.use(function (error, req, res, next) {
   if (error instanceof Errors.NotFoundError) {
     logger.warn({ req }, 'not found')

+ 12 - 10
services/docstore/app/js/HttpController.js

@@ -5,6 +5,17 @@ import HealthChecker from './HealthChecker.js'
 import Errors from './Errors.js'
 import Settings from '@overleaf/settings'
 import { expressify } from '@overleaf/promise-utils'
+import { parseReq, z } from '@overleaf/validation-tools'
+
+const patchDocSchema = z.object({
+  body: z
+    .object({
+      deleted: z.literal(true),
+      deletedAt: z.coerce.date(),
+      name: z.string(),
+    })
+    .strict(),
+})
 
 async function getDoc(req, res) {
   const { doc_id: docId, project_id: projectId } = req.params
@@ -182,18 +193,9 @@ async function updateDoc(req, res) {
 }
 
 async function patchDoc(req, res) {
+  const { body: meta } = parseReq(req, patchDocSchema)
   const { doc_id: docId, project_id: projectId } = req.params
   logger.debug({ projectId, docId }, 'patching doc')
-
-  const allowedFields = ['deleted', 'deletedAt', 'name']
-  const meta = {}
-  Object.entries(req.body).forEach(([field, value]) => {
-    if (allowedFields.includes(field)) {
-      meta[field] = value
-    } else {
-      logger.fatal({ field }, 'joi validation for pathDoc is broken')
-    }
-  })
   await DocManager.patchDoc(projectId, docId, meta)
   res.sendStatus(204)
 }

+ 1 - 1
services/docstore/package.json

@@ -25,10 +25,10 @@
     "@overleaf/promise-utils": "workspace:*",
     "@overleaf/settings": "workspace:*",
     "@overleaf/stream-utils": "^0.1.0",
+    "@overleaf/validation-tools": "workspace:*",
     "async": "^3.2.5",
     "body-parser": "1.20.4",
     "bunyan": "^1.8.15",
-    "celebrate": "^15.0.3",
     "express": "4.22.1",
     "lodash": "^4.18.1",
     "mongodb-legacy": "6.1.3",

+ 16 - 13
services/docstore/test/unit/js/HttpController.test.js

@@ -486,7 +486,11 @@ describe('HttpController', () => {
         project_id: ctx.projectId,
         doc_id: ctx.docId,
       }
-      ctx.req.body = { name: 'foo.tex' }
+      ctx.req.body = {
+        deleted: true,
+        deletedAt: '2026-06-15T00:00:00Z',
+        name: 'foo.tex',
+      }
       ctx.DocManager.patchDoc = sinon.stub().resolves()
       await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next)
     })
@@ -494,7 +498,12 @@ describe('HttpController', () => {
     it('should delete the document', ctx => {
       expect(ctx.DocManager.patchDoc).to.have.been.calledWith(
         ctx.projectId,
-        ctx.docId
+        ctx.docId,
+        {
+          deleted: true,
+          deletedAt: new Date('2026-06-15T00:00:00Z'),
+          name: 'foo.tex',
+        }
       )
     })
 
@@ -510,19 +519,13 @@ describe('HttpController', () => {
         await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next)
       })
 
-      it('should log a message', ctx => {
-        expect(ctx.logger.fatal).to.have.been.calledWith(
-          { field: 'cannot' },
-          'joi validation for pathDoc is broken'
-        )
+      it('should pass a validation error to next', ctx => {
+        expect(ctx.next).to.have.been.calledOnce
+        expect(ctx.next.firstCall.args[0].name).to.equal('InvalidRequestError')
       })
 
-      it('should not pass the invalid field along', ctx => {
-        expect(ctx.DocManager.patchDoc).to.have.been.calledWith(
-          ctx.projectId,
-          ctx.docId,
-          {}
-        )
+      it('should not patch the document', ctx => {
+        expect(ctx.DocManager.patchDoc).not.to.have.been.called
       })
     })
   })

+ 3 - 14
yarn.lock

@@ -6585,10 +6585,10 @@ __metadata:
     "@overleaf/promise-utils": "workspace:*"
     "@overleaf/settings": "workspace:*"
     "@overleaf/stream-utils": "npm:^0.1.0"
+    "@overleaf/validation-tools": "workspace:*"
     async: "npm:^3.2.5"
     body-parser: "npm:1.20.4"
     bunyan: "npm:^1.8.15"
-    celebrate: "npm:^15.0.3"
     chai: "npm:^4.3.6"
     chai-as-promised: "npm:^7.1.1"
     express: "npm:4.22.1"
@@ -14578,17 +14578,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"celebrate@npm:^15.0.3":
-  version: 15.0.3
-  resolution: "celebrate@npm:15.0.3"
-  dependencies:
-    escape-html: "npm:1.0.3"
-    joi: "npm:17.x.x"
-    lodash: "npm:4.17.x"
-  checksum: 10c0/f1fdbb3b4eb84d9caef64345763809ece8b3bdf2ade752b91eee3bfba0a174377c0843d52db686b4d8f426eea51c55bc0a9ee2f8f989a9a2898158a1d63595c9
-  languageName: node
-  linkType: hard
-
 "chai-as-promised@npm:^7.1.1":
   version: 7.1.2
   resolution: "chai-as-promised@npm:7.1.2"
@@ -17664,7 +17653,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"escape-html@npm:1.0.3, escape-html@npm:^1.0.3, escape-html@npm:~1.0.3":
+"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3":
   version: 1.0.3
   resolution: "escape-html@npm:1.0.3"
   checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3
@@ -22028,7 +22017,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"joi@npm:17.x.x, joi@npm:^17.6.4":
+"joi@npm:^17.6.4":
   version: 17.13.3
   resolution: "joi@npm:17.13.3"
   dependencies: