mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
* [web] add consistent aria-label to editing/reviewing toggle * [docstore] add endpoint for getting all docs with ranges * [history-v1] fix schema of chunkId when deleting old history chunk * [web] skip duplicate project lookup for resolving rootDocPath * [web] ignore new limits for root doc path when making debug copy * [web] allow admins to clone projects with ranges and entire history * [web] fix tests * [history-v1] re-order params for cloning project * [web] fix duplicate import of logger after merge * [project-history] re-order params for cloning project history metadata GitOrigin-RevId: 7fa35b4f90885dd453150a348d491ba0ec8de412
435 lines
11 KiB
JavaScript
435 lines
11 KiB
JavaScript
// @ts-check
|
|
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
|
|
import OError from '@overleaf/o-error'
|
|
import logger from '@overleaf/logger'
|
|
import settings from '@overleaf/settings'
|
|
import Errors from '../Errors/Errors.js'
|
|
import {
|
|
fetchJson,
|
|
fetchNothing,
|
|
RequestFailedError,
|
|
} from '@overleaf/fetch-utils'
|
|
import path from 'node:path'
|
|
|
|
/**
|
|
* @import { ObjectId } from 'mongodb'
|
|
*/
|
|
|
|
const TIMEOUT = 30 * 1000 // request timeout
|
|
|
|
/**
|
|
*
|
|
* @param {string | ObjectId} projectId
|
|
* @param {string | ObjectId} docId
|
|
* @param {string} name
|
|
* @param {Date} deletedAt
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function deleteDoc(projectId, docId, name, deletedAt) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join(
|
|
'project',
|
|
projectId.toString(),
|
|
'doc',
|
|
docId.toString()
|
|
)
|
|
const docMetaData = { deleted: true, deletedAt, name }
|
|
const options = {
|
|
json: docMetaData,
|
|
signal: AbortSignal.timeout(TIMEOUT),
|
|
method: 'PATCH',
|
|
}
|
|
try {
|
|
await fetchNothing(url, options)
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
if (error.response.status === 404) {
|
|
// maybe suppress the error when delete doc which is not present?
|
|
throw new Errors.NotFoundError({
|
|
message: 'tried to delete doc not in docstore',
|
|
info: {
|
|
projectId,
|
|
docId,
|
|
},
|
|
})
|
|
}
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
docId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} projectId
|
|
*/
|
|
async function getAllDocs(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId.toString(), 'doc')
|
|
try {
|
|
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} projectId
|
|
*/
|
|
async function getAllDocsWithRanges(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join(
|
|
'project',
|
|
projectId.toString(),
|
|
'doc-with-ranges'
|
|
)
|
|
try {
|
|
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @return {Promise<*>}
|
|
*/
|
|
async function getAllDeletedDocs(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId.toString(), 'doc-deleted')
|
|
try {
|
|
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw OError.tag(error, 'could not get deleted docs from docstore')
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @return {Promise<{_id: string, version: number}[]>}
|
|
*/
|
|
async function getAllDocVersions(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join(
|
|
'project',
|
|
projectId.toString(),
|
|
'doc-versions'
|
|
)
|
|
try {
|
|
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw OError.tag(error, 'could not get doc versions from docstore')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} projectId
|
|
*/
|
|
async function getCommentThreadIds(projectId) {
|
|
const url = `${settings.apis.docstore.url}/project/${projectId}/comment-thread-ids`
|
|
return fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
}
|
|
|
|
/**
|
|
* @param {string} projectId
|
|
*/
|
|
async function getTrackedChangesUserIds(projectId) {
|
|
const url = `${settings.apis.docstore.url}/project/${projectId}/tracked-changes-user-ids`
|
|
return fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
}
|
|
|
|
/**
|
|
* @param {string} projectId
|
|
*/
|
|
async function getAllRanges(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId, 'ranges')
|
|
try {
|
|
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string | ObjectId} projectId
|
|
* @param {string | ObjectId} docId
|
|
* @param {{ peek?: boolean, include_deleted?: boolean }} options
|
|
* @return {Promise<{lines: *, rev: *, version: *, ranges: *}>}
|
|
*/
|
|
async function getDoc(projectId, docId, options = {}) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
if (options.peek) {
|
|
url.pathname = path.posix.join(
|
|
'project',
|
|
projectId.toString(),
|
|
'doc',
|
|
docId.toString(),
|
|
'peek'
|
|
)
|
|
} else {
|
|
url.pathname = path.posix.join(
|
|
'project',
|
|
projectId.toString(),
|
|
'doc',
|
|
docId.toString()
|
|
)
|
|
}
|
|
if (options.include_deleted) {
|
|
url.searchParams.set('include_deleted', 'true')
|
|
}
|
|
try {
|
|
const doc = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
logger.debug(
|
|
{ docId, projectId, version: doc.version, rev: doc.rev },
|
|
'got doc from docstore api'
|
|
)
|
|
return {
|
|
lines: doc.lines,
|
|
rev: doc.rev,
|
|
version: doc.version,
|
|
ranges: doc.ranges,
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
if (error.response.status === 404) {
|
|
throw new Errors.NotFoundError({
|
|
message: 'doc not found in docstore',
|
|
info: {
|
|
projectId,
|
|
docId,
|
|
},
|
|
})
|
|
}
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
docId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} projectId
|
|
* @param {string} docId
|
|
* @return {Promise<boolean>}
|
|
*/
|
|
async function isDocDeleted(projectId, docId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId, 'doc', docId, 'deleted')
|
|
try {
|
|
const doc = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
return doc.deleted
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
if (error.response.status === 404) {
|
|
throw new Errors.NotFoundError({
|
|
message: 'doc does not exist in project',
|
|
info: { projectId, docId },
|
|
})
|
|
}
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
docId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} projectId
|
|
* @param {string} docId
|
|
* @param {string[]} lines
|
|
* @param {number} version
|
|
* @param ranges
|
|
* @return {Promise<{modified: *, rev: *}>}
|
|
*/
|
|
async function updateDoc(
|
|
projectId,
|
|
docId,
|
|
lines,
|
|
version,
|
|
/** @type {any} */ ranges
|
|
) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId, 'doc', docId)
|
|
try {
|
|
const result = await fetchJson(url, {
|
|
method: 'POST',
|
|
signal: AbortSignal.timeout(TIMEOUT),
|
|
json: {
|
|
lines,
|
|
version,
|
|
ranges,
|
|
},
|
|
})
|
|
logger.debug({ projectId, docId }, 'update doc in docstore url finished')
|
|
return { modified: result.modified, rev: result.rev }
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
docId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asks docstore whether any doc in the project has ranges
|
|
*
|
|
* @param {string} projectId
|
|
*/
|
|
async function projectHasRanges(projectId) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId, 'has-ranges')
|
|
try {
|
|
const body = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
|
|
return body.projectHasRanges
|
|
} catch (error) {
|
|
if (error instanceof RequestFailedError) {
|
|
throw new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: error.response.status,
|
|
})
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function archiveProject(projectId) {
|
|
await _operateOnProject(projectId, 'archive')
|
|
}
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function unarchiveProject(projectId) {
|
|
await _operateOnProject(projectId, 'unarchive')
|
|
}
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function destroyProject(projectId) {
|
|
await _operateOnProject(projectId, 'destroy')
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string|ObjectId} projectId
|
|
* @param {string} method
|
|
* @return {Promise<void>}
|
|
* @private
|
|
*/
|
|
async function _operateOnProject(projectId, method) {
|
|
const url = new URL(settings.apis.docstore.url)
|
|
url.pathname = path.posix.join('project', projectId.toString(), method)
|
|
logger.debug({ projectId }, `calling ${method} for project in docstore`)
|
|
try {
|
|
// use default timeout for archiving/unarchiving/destroying
|
|
await fetchNothing(url, {
|
|
method: 'POST',
|
|
})
|
|
} catch (err) {
|
|
if (err instanceof RequestFailedError) {
|
|
const error = new OError('docstore api responded with non-success code', {
|
|
projectId,
|
|
status: err.response.status,
|
|
})
|
|
logger.warn(
|
|
{ err: error, projectId },
|
|
`error calling ${method} project in docstore`
|
|
)
|
|
throw error
|
|
}
|
|
throw OError.tag(err, `error calling ${method} project in docstore`, {
|
|
projectId,
|
|
})
|
|
}
|
|
}
|
|
|
|
export default {
|
|
deleteDoc: callbackify(deleteDoc),
|
|
getAllDocs: callbackify(getAllDocs),
|
|
getAllDeletedDocs: callbackify(getAllDeletedDocs),
|
|
getAllRanges: callbackify(getAllRanges),
|
|
getDoc: callbackifyMultiResult(getDoc, ['lines', 'rev', 'version', 'ranges']),
|
|
getCommentThreadIds: callbackify(getCommentThreadIds),
|
|
getTrackedChangesUserIds: callbackify(getTrackedChangesUserIds),
|
|
isDocDeleted: callbackify(isDocDeleted),
|
|
updateDoc: callbackifyMultiResult(updateDoc, ['modified', 'rev']),
|
|
projectHasRanges: callbackify(projectHasRanges),
|
|
archiveProject: callbackify(archiveProject),
|
|
unarchiveProject: callbackify(unarchiveProject),
|
|
destroyProject: callbackify(destroyProject),
|
|
promises: {
|
|
deleteDoc,
|
|
getAllDocVersions,
|
|
getAllDocs,
|
|
getAllDocsWithRanges,
|
|
getAllDeletedDocs,
|
|
getAllRanges,
|
|
getDoc,
|
|
getCommentThreadIds,
|
|
getTrackedChangesUserIds,
|
|
isDocDeleted,
|
|
updateDoc,
|
|
projectHasRanges,
|
|
archiveProject,
|
|
unarchiveProject,
|
|
destroyProject,
|
|
},
|
|
}
|