Merge pull request #28380 from overleaf/td-zod-history

Migrate history-related web back end to Zod

GitOrigin-RevId: 38ed56927bee4f5670604d7178b096e382c8cb65
This commit is contained in:
Tim Down
2025-09-17 11:22:09 +01:00
committed by Copybot
parent 9afcd88a48
commit d68e8679ee
4 changed files with 38 additions and 39 deletions

View File

@@ -25,6 +25,7 @@ import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.js
import RestoreManager from './RestoreManager.mjs'
import { prepareZipAttachment } from '../../infrastructure/Response.js'
import Features from '../../infrastructure/Features.js'
import { z, zz, validateReq } from '../../infrastructure/Validation.js'
// Number of seconds after which the browser should send a request to revalidate
// blobs
@@ -44,8 +45,19 @@ async function headBlob(req, res) {
await requestBlob('HEAD', req, res)
}
const requestBlobSchema = z.object({
params: z.object({
project_id: zz.coercedObjectId(),
hash: zz.hex().length(40),
}),
query: z.object({
fallback: zz.coercedObjectId().optional(),
}),
})
async function requestBlob(method, req, res) {
const { project_id: projectId, hash } = req.params
const { params } = validateReq(req, requestBlobSchema)
const { project_id: projectId, hash } = params
// Handle conditional GET request
if (req.get('If-None-Match') === hash) {
@@ -461,17 +473,35 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
}
}
const getLatestHistorySchema = z.object({
params: z.object({
project_id: zz.objectId(),
}),
})
async function getLatestHistory(req, res, next) {
const projectId = req.params.project_id
const { params } = validateReq(req, getLatestHistorySchema)
const projectId = params.project_id
const history = await HistoryManager.promises.getLatestHistory(projectId)
res.json(history)
}
const getChangesSchema = z.object({
params: z.object({
project_id: zz.objectId(),
}),
query: z.object({
since: z.coerce.number().int().min(0).optional(),
paginated: z.stringbool().optional(),
}),
})
async function getChanges(req, res, next) {
const projectId = req.params.project_id
let since = req.query.since
const { params, query } = validateReq(req, getChangesSchema)
const projectId = params.project_id
let since = query.since
// TODO: Transition flag; remove after a while
const paginated = req.query.paginated === 'true'
const paginated = query.paginated
if (paginated) {
const changes = await HistoryManager.promises.getChanges(projectId, {

View File

@@ -280,8 +280,8 @@ async function getLatestHistory(projectId) {
* Get history changes since a given version
*
* @param {string} projectId
* @param {object} opts
* @param {number} opts.since - The start version of changes to get
* @param {object} [opts]
* @param {number} [opts.since] - The start version of changes to get
*/
async function getChanges(projectId, opts = {}) {
const historyId = await getHistoryId(projectId)

View File

@@ -1,7 +1,6 @@
// @ts-check
import Settings from '@overleaf/settings'
import { Joi, validate } from '../../infrastructure/Validation.js'
import { RateLimiter } from '../../infrastructure/RateLimiter.js'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import AuthorizationMiddleware from '../Authorization/AuthorizationMiddleware.mjs'
@@ -29,30 +28,12 @@ function apply(webRouter, privateApiRouter) {
webRouter.head(
'/project/:project_id/blob/:hash',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
hash: Joi.string().required().hex().length(40),
}),
query: Joi.object({
fallback: Joi.objectId().optional(),
}),
}),
RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.headBlob
)
webRouter.get(
'/project/:project_id/blob/:hash',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
hash: Joi.string().required().hex().length(40),
}),
query: Joi.object({
fallback: Joi.objectId().optional(),
}),
}),
RateLimiterMiddleware.rateLimit(rateLimiters.getProjectBlob),
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getBlob
@@ -151,25 +132,12 @@ function apply(webRouter, privateApiRouter) {
webRouter.get(
'/project/:project_id/latest/history',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
}),
}),
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getLatestHistory
)
webRouter.get(
'/project/:project_id/changes',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
}),
query: Joi.object({
since: Joi.number().integer().min(0).optional(),
}),
}),
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.getChanges

View File

@@ -48,6 +48,7 @@ const zz = {
.string()
.refine(ObjectId.isValid, { message: 'invalid Mongo ObjectId' })
.transform(val => new ObjectId(val)),
hex: () => z.string().regex(/^[0-9a-f]*$/),
}
/**