[web] add endpoint for resolving a project for writes from tpds (#35408)

* [web] add endpoint for resolving a project for writes from tpds

* [web] return historyId from new endpoint for resolving projects

* [web] add zod validation to new endpoint for resolving projects

GitOrigin-RevId: 0e607e56442117a4150a08373169c44b53dd4fdf
This commit is contained in:
Jakob Ackermann
2026-09-17 11:26:03 +00:00
committed by Copybot
parent 528e587038
commit b68a013751
7 changed files with 564 additions and 3 deletions
@@ -146,7 +146,7 @@ const ProjectGetter = {
async findUsersProjectsByName(userId, projectName) {
const allProjects = await ProjectGetter.findAllUsersProjects(
userId,
'name archived trashed'
'name archived trashed overleaf'
)
const { owned, readAndWrite } = allProjects
@@ -11,6 +11,7 @@ import ProjectCreationHandler from '../Project/ProjectCreationHandler.mjs'
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
import TpdsQueueManager from './TpdsQueueManager.mjs'
import { parseReq, z, zz } from '@overleaf/validation-tools'
async function createProject(req, res) {
const { user_id: userId } = req.params
@@ -30,6 +31,46 @@ async function createProject(req, res) {
})
}
const resolveProjectSchema = z.object({
params: z.object({
user_id: zz.objectId(),
}),
body: z
.object({
projectId: zz.objectId(),
})
.or(
z.object({
projectName: z.string().min(1),
})
),
})
// Resolve a project name (or id) to a project id, using the same
// get-or-create semantics as mergeUpdate: a blank project is created when no
// project matches the name, and duplicate names trigger the duplicate-name
// handling before rejecting the request.
async function resolveProject(req, res) {
const {
params: { user_id: userId },
body: { projectId, projectName },
} = parseReq(req, resolveProjectSchema)
const project = await TpdsUpdateHandler.promises.getOrCreateProject(
userId,
projectId,
projectName
)
if (project == null) {
return res.json({ status: 'rejected' })
}
res.json({
status: 'success',
projectId: project._id.toString(),
historyId: project.overleaf?.history?.id,
otMigrationStage: project.overleaf?.history?.otMigrationStage ?? 0,
})
}
// mergeUpdate and deleteUpdate are used by Dropbox, where the project is only
// passed as the name, as the first part of the file path. They have to check
// the project exists, find it, and create it if not. They also ignore 'noisy'
@@ -207,6 +248,7 @@ function splitPath(projectId, path) {
export default {
createProject: expressify(createProject),
resolveProject: expressify(resolveProject),
mergeUpdate: expressify(mergeUpdate),
deleteUpdate: expressify(deleteUpdate),
updateFolder: expressify(updateFolder),
@@ -94,7 +94,7 @@ async function getOrCreateProject(userId, projectId, projectName) {
async function findProjectByIdWithRWAccess(userId, projectId) {
const allProjects = await ProjectGetter.promises.findAllUsersProjects(
userId,
'name archived trashed'
'name archived trashed overleaf'
)
for (const projects of [allProjects.owned, allProjects.readAndWrite]) {
for (const project of projects) {
@@ -191,5 +191,6 @@ export default {
newUpdate,
deleteUpdate,
createFolder,
getOrCreateProject,
},
}
+5
View File
@@ -980,6 +980,11 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
AuthenticationController.requirePrivateApiAuth(),
TpdsController.createProject
)
privateApiRouter.post(
'/user/:user_id/project/resolve',
AuthenticationController.requirePrivateApiAuth(),
TpdsController.resolveProject
)
privateApiRouter.post(
'/tpds/folder-update',
AuthenticationController.requirePrivateApiAuth(),