Pārlūkot izejas kodu

Merge pull request #29593 from overleaf/mfb-from-joi-to-zod-analytics

[Analytics service] Migrate from JOI to ZOD

GitOrigin-RevId: 5f6abc23c5359ca8599ef4b7d660d5f08551d247
Maria Florencia Besteiro Gonzalez 8 mēneši atpakaļ
vecāks
revīzija
e861e28296

+ 2 - 0
libraries/validation-tools/index.js

@@ -2,11 +2,13 @@ const { ParamsError } = require('./Errors')
 const { z } = require('zod')
 const { zz } = require('./zodHelpers')
 const { validateReq } = require('./validateReq')
+const { validateSchema } = require('./validateSchema')
 const { handleValidationError } = require('./handleValidationError')
 
 module.exports = {
   z,
   zz,
+  validateSchema,
   validateReq,
   handleValidationError,
   ParamsError,

+ 60 - 0
libraries/validation-tools/validateSchema.js

@@ -0,0 +1,60 @@
+// @ts-check
+const { isZodErrorLike } = require('zod-validation-error')
+
+/**
+ * @typedef {import('zod').ZodType} ZodType
+ */
+/**
+ * @template T
+ * @typedef {import('zod').output<T>} output<T>
+ */
+
+/**
+ * A helper function to safely get a nested value from an object
+ * using a path array (e.g., ["query", "resource_type"])
+ */
+function getPathValue(data, path) {
+  let current = data
+  for (const key of path) {
+    if (current === null || typeof current !== 'object') {
+      return undefined
+    }
+    current = current[key]
+  }
+  return current
+}
+
+const isRequiredError = (issue, value) =>
+  value === undefined &&
+  (issue.code === 'invalid_type' || issue.code === 'invalid_union')
+
+/**
+ * Validates data against a Zod schema and throws a user-friendly error.
+ *
+ * @template {ZodType} T
+ * @param {T} schema - The Zod schema
+ * @param {unknown} data - The data to validate
+ * @returns {output<T>} The validated (and transformed) data
+ */
+function validateSchema(schema, data) {
+  try {
+    return schema.parse(data)
+  } catch (err) {
+    if (isZodErrorLike(err)) {
+      const errorMessages = err.issues.map(issue => {
+        const value = getPathValue(data, issue.path)
+        const fieldName = String(issue.path[issue.path.length - 1])
+        if (isRequiredError(issue, value)) {
+          return `"${fieldName}" is required`
+        }
+        return `"${fieldName}" - ` + issue.message
+      })
+
+      throw new Error(errorMessages.join('; '))
+    }
+
+    throw err
+  }
+}
+
+module.exports = { validateSchema }

+ 15 - 0
libraries/validation-tools/zodHelpers.js

@@ -3,6 +3,13 @@ const mongodb = require('mongodb')
 
 const { ObjectId } = mongodb
 
+const dateWithTransform = (schema, allowNull = false) => {
+  return schema.transform(dt => {
+    if (allowNull && !dt) return null
+    return dt instanceof Date ? dt : new Date(dt)
+  })
+}
+
 const zz = {
   objectId: () =>
     z.string().refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' }),
@@ -12,6 +19,14 @@ const zz = {
       .refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' })
       .transform(val => new ObjectId(val)),
   hex: () => z.string().regex(/^[0-9a-f]*$/),
+  datetime: () => dateWithTransform(z.union([z.iso.datetime(), z.date()])),
+  datetimeNullable: () =>
+    dateWithTransform(z.union([z.iso.datetime(), z.date(), z.null()]), true),
+  datetimeNullish: () =>
+    dateWithTransform(
+      z.union([z.iso.datetime(), z.date(), z.null(), z.undefined()]),
+      true
+    ),
 }
 
 module.exports = { zz }

+ 1 - 2
package-lock.json

@@ -53543,15 +53543,14 @@
         "@overleaf/redis-wrapper": "*",
         "@overleaf/settings": "*",
         "@overleaf/stream-utils": "^0.1.0",
+        "@overleaf/validation-tools": "*",
         "bluebird": "^3.7.2",
         "body-parser": "^1.20.3",
         "bull": "^3.18.0",
         "camelcase-keys": "^4.2.0",
-        "celebrate": "^15.0.3",
         "csv": "^5.4.0",
         "east": "^2.0.3",
         "express": "^4.21.2",
-        "joi": "^17.12.0",
         "json2csv": "^4.5.4",
         "lodash": "^4.17.21",
         "minimist": "^1.2.7",