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

[Analytics service] Migrate from JOI to ZOD

GitOrigin-RevId: 5f6abc23c5359ca8599ef4b7d660d5f08551d247
This commit is contained in:
Maria Florencia Besteiro Gonzalez
2025-11-20 16:29:18 +01:00
committed by Copybot
parent 51639030f0
commit e861e28296
4 changed files with 78 additions and 2 deletions

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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 }

3
package-lock.json generated
View File

@@ -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",