HttpController.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. import logger from '@overleaf/logger'
  2. import OError from '@overleaf/o-error'
  3. import * as UpdatesProcessor from './UpdatesProcessor.js'
  4. import * as SummarizedUpdatesManager from './SummarizedUpdatesManager.js'
  5. import * as DiffManager from './DiffManager.js'
  6. import * as HistoryStoreManager from './HistoryStoreManager.js'
  7. import * as WebApiManager from './WebApiManager.js'
  8. import * as SnapshotManager from './SnapshotManager.js'
  9. import * as HealthChecker from './HealthChecker.js'
  10. import * as SyncManager from './SyncManager.js'
  11. import * as ErrorRecorder from './ErrorRecorder.js'
  12. import * as RedisManager from './RedisManager.js'
  13. import * as LabelsManager from './LabelsManager.js'
  14. import * as HistoryApiManager from './HistoryApiManager.js'
  15. import * as RetryManager from './RetryManager.js'
  16. import * as FlushManager from './FlushManager.js'
  17. import { pipeline } from 'node:stream'
  18. import { fetchNothing, RequestFailedError } from '@overleaf/fetch-utils'
  19. import { z, zz, parseReq } from '@overleaf/validation-tools'
  20. const ONE_DAY_IN_SECONDS = 24 * 60 * 60
  21. const getProjectBlobSchema = z.object({
  22. params: z.object({
  23. history_id: zz.objectId().or(z.coerce.number()),
  24. hash: z.string(),
  25. }),
  26. })
  27. export function getProjectBlob(req, res, next) {
  28. const { params } = parseReq(req, getProjectBlobSchema)
  29. const historyId = params.history_id
  30. const blobHash = params.hash
  31. HistoryStoreManager.getProjectBlobStream(
  32. historyId,
  33. blobHash,
  34. (err, stream) => {
  35. if (err != null) {
  36. if (err instanceof RequestFailedError && err.response.status === 404) {
  37. return res.status(404).end()
  38. }
  39. return next(OError.tag(err))
  40. }
  41. res.setHeader('Cache-Control', `private, max-age=${ONE_DAY_IN_SECONDS}`)
  42. pipeline(stream, res, err => {
  43. if (err) next(err)
  44. // res.end() is already called via 'end' event by pipeline.
  45. })
  46. }
  47. )
  48. }
  49. export function initializeProject(req, res, next) {
  50. const { historyId } = req.body
  51. HistoryStoreManager.initializeProject(historyId, (error, id) => {
  52. if (error != null) {
  53. return next(OError.tag(error))
  54. }
  55. res.json({ project: { id } })
  56. })
  57. }
  58. const flushProjectSchema = z.object({
  59. params: z.object({
  60. project_id: zz.objectId().or(z.coerce.number()),
  61. }),
  62. query: z.object({
  63. debug: z.stringbool().default(false),
  64. bisect: z.stringbool().default(false),
  65. }),
  66. })
  67. export function flushProject(req, res, next) {
  68. const { query, params } = parseReq(req, flushProjectSchema)
  69. const projectId = params.project_id
  70. if (query.debug) {
  71. logger.debug(
  72. { projectId },
  73. 'compressing project history in single-step mode'
  74. )
  75. UpdatesProcessor.processSingleUpdateForProject(projectId, error => {
  76. if (error != null) {
  77. return next(OError.tag(error))
  78. }
  79. res.sendStatus(204)
  80. })
  81. } else if (query.bisect) {
  82. logger.debug({ projectId }, 'compressing project history in bisect mode')
  83. UpdatesProcessor.processUpdatesForProjectUsingBisect(
  84. projectId,
  85. UpdatesProcessor.REDIS_READ_BATCH_SIZE,
  86. error => {
  87. if (error != null) {
  88. return next(OError.tag(error))
  89. }
  90. res.sendStatus(204)
  91. }
  92. )
  93. } else {
  94. logger.debug({ projectId }, 'compressing project history')
  95. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  96. if (error != null) {
  97. return next(OError.tag(error))
  98. }
  99. res.sendStatus(204)
  100. })
  101. }
  102. }
  103. const dumpProjectSchema = z.object({
  104. params: z.object({
  105. project_id: zz.objectId().or(z.coerce.number()),
  106. }),
  107. query: z.object({
  108. count: z.coerce.number().int().optional(),
  109. }),
  110. })
  111. export function dumpProject(req, res, next) {
  112. const { query, params } = parseReq(req, dumpProjectSchema)
  113. const projectId = params.project_id
  114. const batchSize = query.count || UpdatesProcessor.REDIS_READ_BATCH_SIZE
  115. logger.debug({ projectId }, 'retrieving raw updates')
  116. UpdatesProcessor.getRawUpdates(projectId, batchSize, (error, rawUpdates) => {
  117. if (error != null) {
  118. return next(OError.tag(error))
  119. }
  120. res.json(rawUpdates)
  121. })
  122. }
  123. const flushOldSchema = z.object({
  124. query: z.object({
  125. // flush projects with queued ops older than this
  126. maxAge: z.coerce
  127. .number()
  128. .int()
  129. .default(6 * 3600),
  130. // pause this amount of time between checking queues
  131. queueDelay: z.coerce.number().int().default(100),
  132. // maximum number of queues to check
  133. limit: z.coerce.number().int().default(1000),
  134. // maximum amount of time allowed
  135. timeout: z.coerce
  136. .number()
  137. .int()
  138. .default(60 * 1000),
  139. // whether to run in the background
  140. background: z.stringbool().default(false),
  141. }),
  142. })
  143. export function flushOld(req, res, next) {
  144. const { query } = parseReq(req, flushOldSchema)
  145. const { maxAge, queueDelay, limit, timeout, background } = query
  146. const options = { maxAge, queueDelay, limit, timeout, background }
  147. FlushManager.flushOldOps(options, (error, results) => {
  148. if (error != null) {
  149. return next(OError.tag(error))
  150. }
  151. res.send(results)
  152. })
  153. }
  154. const getDiffSchema = z.object({
  155. params: z.object({
  156. project_id: zz.objectId().or(z.coerce.number()),
  157. }),
  158. query: z.object({
  159. pathname: z.string(),
  160. from: z.coerce.number().int(),
  161. to: z.coerce.number().int(),
  162. }),
  163. })
  164. export function getDiff(req, res, next) {
  165. const { query, params } = parseReq(req, getDiffSchema)
  166. const { pathname, from, to } = query
  167. const projectId = params.project_id
  168. logger.debug({ projectId, pathname, from, to }, 'getting diff')
  169. DiffManager.getDiff(projectId, pathname, from, to, (error, diff) => {
  170. if (error != null) {
  171. return next(OError.tag(error))
  172. }
  173. res.json({ diff })
  174. })
  175. }
  176. const getFileTreeDiffSchema = z.object({
  177. params: z.object({
  178. project_id: zz.objectId().or(z.coerce.number()),
  179. }),
  180. query: z.object({
  181. from: z.coerce.number().int(),
  182. to: z.coerce.number().int(),
  183. }),
  184. })
  185. export function getFileTreeDiff(req, res, next) {
  186. const { query, params } = parseReq(req, getFileTreeDiffSchema)
  187. const { from, to } = query
  188. const projectId = params.project_id
  189. DiffManager.getFileTreeDiff(projectId, from, to, (error, diff) => {
  190. if (error != null) {
  191. return next(OError.tag(error))
  192. }
  193. res.json({ diff })
  194. })
  195. }
  196. const getUpdatesSchema = z.object({
  197. params: z.object({
  198. project_id: zz.objectId().or(z.coerce.number()),
  199. }),
  200. query: z.object({
  201. before: z.coerce.number().int().optional(),
  202. min_count: z.coerce.number().int().optional(),
  203. }),
  204. })
  205. export function getUpdates(req, res, next) {
  206. const { query, params } = parseReq(req, getUpdatesSchema)
  207. const projectId = params.project_id
  208. const { before, min_count: minCount } = query
  209. SummarizedUpdatesManager.getSummarizedProjectUpdates(
  210. projectId,
  211. { before, min_count: minCount },
  212. (error, updates, nextBeforeTimestamp) => {
  213. if (error != null) {
  214. return next(OError.tag(error))
  215. }
  216. for (const update of updates) {
  217. // Sets don't JSONify, so convert to arrays
  218. update.pathnames = Array.from(update.pathnames || []).sort()
  219. }
  220. res.json({
  221. updates,
  222. nextBeforeTimestamp,
  223. })
  224. }
  225. )
  226. }
  227. const getResyncPendingSchema = z.object({
  228. params: z.object({
  229. project_id: zz.objectId(),
  230. }),
  231. })
  232. export function getResyncPending(req, res, next) {
  233. const {
  234. params: { project_id: projectId },
  235. } = parseReq(req, getResyncPendingSchema)
  236. SyncManager.getResyncState(projectId, (err, state) => {
  237. if (err) return next(err)
  238. res.json({
  239. resyncPending: state.isSyncOngoing(),
  240. })
  241. })
  242. }
  243. const getDebugInfoSchema = z.object({
  244. params: z.object({
  245. project_id: zz.objectId(),
  246. }),
  247. })
  248. export function getDebugInfo(req, res, next) {
  249. const {
  250. params: { project_id: projectId },
  251. } = parseReq(req, getDebugInfoSchema)
  252. SyncManager.getResyncState(projectId, (err, state) => {
  253. if (err) return next(err)
  254. ErrorRecorder.getFailureRecord(projectId, (err, failureRecord) => {
  255. if (err) return next(err)
  256. res.json({
  257. failureRecord,
  258. syncState: {
  259. resyncPending: state.isSyncOngoing(),
  260. resyncCount: state.resyncCount,
  261. resyncPendingSince: state.resyncPendingSince,
  262. lastUpdated: state.lastUpdated,
  263. history: state.history,
  264. ...state.toRaw(),
  265. },
  266. })
  267. })
  268. })
  269. }
  270. const latestVersionSchema = z.object({
  271. params: z.object({
  272. project_id: zz.objectId().or(z.coerce.number()),
  273. }),
  274. })
  275. export function latestVersion(req, res, next) {
  276. const { params } = parseReq(req, latestVersionSchema)
  277. const projectId = params.project_id
  278. logger.debug({ projectId }, 'compressing project history and getting version')
  279. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  280. if (error != null) {
  281. return next(OError.tag(error))
  282. }
  283. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  284. if (error != null) {
  285. return next(OError.tag(error))
  286. }
  287. HistoryStoreManager.getMostRecentVersion(
  288. projectId,
  289. historyId,
  290. (error, version, projectStructureAndDocVersions, lastChange) => {
  291. if (error != null) {
  292. return next(OError.tag(error))
  293. }
  294. res.json({
  295. version,
  296. timestamp: lastChange != null ? lastChange.timestamp : undefined,
  297. v2Authors: lastChange != null ? lastChange.v2Authors : undefined,
  298. })
  299. }
  300. )
  301. })
  302. })
  303. }
  304. const getFileSnapshotSchema = z.object({
  305. params: z.object({
  306. project_id: zz.objectId().or(z.coerce.number()),
  307. version: z.coerce.number().int(),
  308. pathname: z.string(),
  309. }),
  310. })
  311. export function getFileSnapshot(req, res, next) {
  312. const { params } = parseReq(req, getFileSnapshotSchema)
  313. const { project_id: projectId, version, pathname } = params
  314. SnapshotManager.getFileSnapshotStream(
  315. projectId,
  316. version,
  317. pathname,
  318. (error, stream) => {
  319. if (error != null) {
  320. return next(OError.tag(error))
  321. }
  322. pipeline(stream, res, err => {
  323. if (err) next(err)
  324. // res.end() is already called via 'end' event by pipeline.
  325. })
  326. }
  327. )
  328. }
  329. const getRangesSnapshotSchema = z.object({
  330. params: z.object({
  331. project_id: zz.objectId().or(z.coerce.number()),
  332. version: z.coerce.number().int(),
  333. pathname: z.string(),
  334. }),
  335. })
  336. export function getRangesSnapshot(req, res, next) {
  337. const { params } = parseReq(req, getRangesSnapshotSchema)
  338. const { project_id: projectId, version, pathname } = params
  339. SnapshotManager.getRangesSnapshot(
  340. projectId,
  341. version,
  342. pathname,
  343. (err, ranges) => {
  344. if (err) {
  345. return next(OError.tag(err))
  346. }
  347. res.json(ranges)
  348. }
  349. )
  350. }
  351. const getFileMetadataSnapshotSchema = z.object({
  352. params: z.object({
  353. project_id: zz.objectId().or(z.coerce.number()),
  354. version: z.coerce.number().int(),
  355. pathname: z.string(),
  356. }),
  357. })
  358. export function getFileMetadataSnapshot(req, res, next) {
  359. const { params } = parseReq(req, getFileMetadataSnapshotSchema)
  360. const { project_id: projectId, version, pathname } = params
  361. SnapshotManager.getFileMetadataSnapshot(
  362. projectId,
  363. version,
  364. pathname,
  365. (err, data) => {
  366. if (err) {
  367. return next(OError.tag(err))
  368. }
  369. res.json(data)
  370. }
  371. )
  372. }
  373. const getLatestSnapshotSchema = z.object({
  374. params: z.object({
  375. project_id: zz.objectId().or(z.coerce.number()),
  376. }),
  377. })
  378. export function getLatestSnapshot(req, res, next) {
  379. const { params } = parseReq(req, getLatestSnapshotSchema)
  380. const { project_id: projectId } = params
  381. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  382. if (error) return next(OError.tag(error))
  383. SnapshotManager.getLatestSnapshot(
  384. projectId,
  385. historyId,
  386. (error, details) => {
  387. if (error != null) {
  388. return next(error)
  389. }
  390. const { snapshot, version } = details
  391. res.json({ snapshot: snapshot.toRaw(), version })
  392. }
  393. )
  394. })
  395. }
  396. const getChangesInChunkSinceSchema = z.object({
  397. params: z.object({
  398. project_id: zz.objectId().or(z.coerce.number()),
  399. }),
  400. query: z.object({
  401. since: z.coerce.number().int().min(0),
  402. }),
  403. })
  404. export function getChangesInChunkSince(req, res, next) {
  405. const { query, params } = parseReq(req, getChangesInChunkSinceSchema)
  406. const { project_id: projectId } = params
  407. const { since } = query
  408. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  409. if (error) return next(OError.tag(error))
  410. SnapshotManager.getChangesInChunkSince(
  411. projectId,
  412. historyId,
  413. since,
  414. (error, details) => {
  415. if (error != null) {
  416. return next(error)
  417. }
  418. const { latestStartVersion, changes } = details
  419. res.json({
  420. latestStartVersion,
  421. changes: changes.map(c => c.toRaw()),
  422. })
  423. }
  424. )
  425. })
  426. }
  427. const getProjectSnapshotSchema = z.object({
  428. params: z.object({
  429. project_id: zz.objectId().or(z.coerce.number()),
  430. version: z.coerce.number().int(),
  431. }),
  432. })
  433. export function getProjectSnapshot(req, res, next) {
  434. const { params } = parseReq(req, getProjectSnapshotSchema)
  435. const { project_id: projectId, version } = params
  436. SnapshotManager.getProjectSnapshot(
  437. projectId,
  438. version,
  439. (error, snapshotData) => {
  440. if (error != null) {
  441. return next(error)
  442. }
  443. res.json(snapshotData)
  444. }
  445. )
  446. }
  447. const getPathsAtVersionSchema = z.object({
  448. params: z.object({
  449. project_id: zz.objectId().or(z.coerce.number()),
  450. version: z.coerce.number().int(),
  451. }),
  452. })
  453. export function getPathsAtVersion(req, res, next) {
  454. const { params } = parseReq(req, getPathsAtVersionSchema)
  455. const { project_id: projectId, version } = params
  456. SnapshotManager.getPathsAtVersion(projectId, version, (error, result) => {
  457. if (error != null) {
  458. return next(error)
  459. }
  460. res.json(result)
  461. })
  462. }
  463. export function healthCheck(req, res) {
  464. HealthChecker.check(err => {
  465. if (err != null) {
  466. logger.err({ err }, 'error performing health check')
  467. res.sendStatus(500)
  468. } else {
  469. res.sendStatus(200)
  470. }
  471. })
  472. }
  473. export function checkLock(req, res) {
  474. HealthChecker.checkLock(err => {
  475. if (err != null) {
  476. logger.err({ err }, 'error performing lock check')
  477. res.sendStatus(500)
  478. } else {
  479. res.sendStatus(200)
  480. }
  481. })
  482. }
  483. const resyncProjectSchema = z.object({
  484. params: z.object({
  485. project_id: zz.objectId().or(z.coerce.number()),
  486. }),
  487. query: z.object({
  488. force: z.stringbool().default(false),
  489. }),
  490. body: z.object({
  491. force: z.boolean().default(false),
  492. origin: z
  493. .object({
  494. kind: z.string(),
  495. })
  496. .optional(),
  497. historyRangesMigration: z.enum(['forwards', 'backwards']).optional(),
  498. }),
  499. })
  500. export function resyncProject(req, res, next) {
  501. const { query, params, body } = parseReq(req, resyncProjectSchema)
  502. const projectId = params.project_id
  503. const options = {}
  504. if (body.origin) {
  505. options.origin = body.origin
  506. }
  507. if (body.historyRangesMigration) {
  508. options.historyRangesMigration = body.historyRangesMigration
  509. }
  510. if (query.force || body.force) {
  511. // this will delete the queue and clear the sync state
  512. // use if the project is completely broken
  513. SyncManager.startHardResync(projectId, options, error => {
  514. if (error != null) {
  515. return next(error)
  516. }
  517. // flush the sync operations
  518. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  519. if (error != null) {
  520. return next(error)
  521. }
  522. res.sendStatus(204)
  523. })
  524. })
  525. } else {
  526. SyncManager.startResync(projectId, options, error => {
  527. if (error != null) {
  528. return next(error)
  529. }
  530. // flush the sync operations
  531. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  532. if (error != null) {
  533. return next(error)
  534. }
  535. res.sendStatus(204)
  536. })
  537. })
  538. }
  539. }
  540. const forceDebugProjectSchema = z.object({
  541. params: z.object({
  542. project_id: zz.objectId().or(z.coerce.number()),
  543. }),
  544. query: z.object({
  545. clear: z.stringbool().default(false),
  546. }),
  547. })
  548. export function forceDebugProject(req, res, next) {
  549. const { query, params } = parseReq(req, forceDebugProjectSchema)
  550. const projectId = params.project_id
  551. // set the debug flag to true unless we see ?clear=true
  552. const state = !query.clear
  553. ErrorRecorder.setForceDebug(projectId, state, error => {
  554. if (error != null) {
  555. return next(error)
  556. }
  557. // display the failure record to help debugging
  558. ErrorRecorder.getFailureRecord(projectId, (error, result) => {
  559. if (error != null) {
  560. return next(error)
  561. }
  562. res.send(result)
  563. })
  564. })
  565. }
  566. export function getFailures(req, res, next) {
  567. ErrorRecorder.getFailures((error, result) => {
  568. if (error != null) {
  569. return next(error)
  570. }
  571. res.send({ failures: result })
  572. })
  573. }
  574. export function getQueueCounts(req, res, next) {
  575. RedisManager.getProjectIdsWithHistoryOpsCount((err, queuedProjectsCount) => {
  576. if (err != null) {
  577. return next(err)
  578. }
  579. res.send({ queuedProjects: queuedProjectsCount })
  580. })
  581. }
  582. const getLabelsSchema = z.object({
  583. params: z.object({
  584. project_id: zz.objectId().or(z.coerce.number()),
  585. }),
  586. })
  587. export function getLabels(req, res, next) {
  588. const { params } = parseReq(req, getLabelsSchema)
  589. const projectId = params.project_id
  590. HistoryApiManager.shouldUseProjectHistory(
  591. projectId,
  592. (error, shouldUseProjectHistory) => {
  593. if (error != null) {
  594. return next(error)
  595. }
  596. if (shouldUseProjectHistory) {
  597. LabelsManager.getLabels(projectId, (error, labels) => {
  598. if (error != null) {
  599. return next(error)
  600. }
  601. res.json(labels)
  602. })
  603. } else {
  604. res.sendStatus(409)
  605. }
  606. }
  607. )
  608. }
  609. const createLabelSchema = z.object({
  610. params: z.object({
  611. project_id: zz.objectId().or(z.coerce.number()),
  612. user_id: zz.objectId().optional(),
  613. }),
  614. body: z.object({
  615. version: z.number().int(),
  616. comment: z.string(),
  617. created_at: z.string().optional(),
  618. validate_exists: z.boolean().default(true),
  619. user_id: zz.objectId().nullable().optional(),
  620. }),
  621. })
  622. export function createLabel(req, res, next) {
  623. const { params, body } = parseReq(req, createLabelSchema)
  624. const { project_id: projectId, user_id: userIdParam } = params
  625. const {
  626. version,
  627. comment,
  628. user_id: userIdBody,
  629. created_at: createdAt,
  630. validate_exists: validateExists,
  631. } = body
  632. // Temporarily looking up both params and body while rolling out changes
  633. // in the router path - https://github.com/overleaf/internal/pull/20200
  634. const userId = userIdParam || userIdBody
  635. HistoryApiManager.shouldUseProjectHistory(
  636. projectId,
  637. (error, shouldUseProjectHistory) => {
  638. if (error != null) {
  639. return next(error)
  640. }
  641. if (shouldUseProjectHistory) {
  642. LabelsManager.createLabel(
  643. projectId,
  644. userId,
  645. version,
  646. comment,
  647. createdAt,
  648. validateExists,
  649. (error, label) => {
  650. if (error != null) {
  651. return next(error)
  652. }
  653. res.json(label)
  654. }
  655. )
  656. } else {
  657. logger.error(
  658. {
  659. projectId,
  660. userId,
  661. version,
  662. comment,
  663. createdAt,
  664. validateExists,
  665. },
  666. 'not using v2 history'
  667. )
  668. res.sendStatus(409)
  669. }
  670. }
  671. )
  672. }
  673. /**
  674. * This will delete a label if it is owned by the current user. If you wish to
  675. * delete a label regardless of the current user, then use `deleteLabel` instead.
  676. */
  677. const deleteLabelForUserSchema = z.object({
  678. params: z.object({
  679. project_id: zz.objectId().or(z.coerce.number()),
  680. user_id: zz.objectId(),
  681. label_id: zz.objectId(),
  682. }),
  683. })
  684. export function deleteLabelForUser(req, res, next) {
  685. const { params } = parseReq(req, deleteLabelForUserSchema)
  686. const { project_id: projectId, user_id: userId, label_id: labelId } = params
  687. LabelsManager.deleteLabelForUser(projectId, userId, labelId, error => {
  688. if (error != null) {
  689. return next(error)
  690. }
  691. res.sendStatus(204)
  692. })
  693. }
  694. const deleteLabelSchema = z.object({
  695. params: z.object({
  696. project_id: zz.objectId().or(z.coerce.number()),
  697. label_id: zz.objectId(),
  698. }),
  699. })
  700. export function deleteLabel(req, res, next) {
  701. const { params } = parseReq(req, deleteLabelSchema)
  702. const { project_id: projectId, label_id: labelId } = params
  703. LabelsManager.deleteLabel(projectId, labelId, error => {
  704. if (error != null) {
  705. return next(error)
  706. }
  707. res.sendStatus(204)
  708. })
  709. }
  710. const retryFailuresSchema = z.object({
  711. query: z.object({
  712. failureType: z.enum(['soft', 'hard']).optional(),
  713. // bail out after this time limit
  714. timeout: z.coerce.number().int().default(300),
  715. // maximum number of projects to check
  716. limit: z.coerce.number().int().default(100),
  717. callbackUrl: z.string().optional(),
  718. }),
  719. })
  720. export function retryFailures(req, res, next) {
  721. const { query } = parseReq(req, retryFailuresSchema)
  722. const { failureType, timeout, limit, callbackUrl } = query
  723. if (callbackUrl) {
  724. // send response but run in background when callbackUrl provided
  725. res.send({ retryStatus: 'running retryFailures in background' })
  726. }
  727. RetryManager.retryFailures(
  728. { failureType, timeout, limit },
  729. (error, result) => {
  730. if (callbackUrl) {
  731. // if present, notify the callbackUrl on success
  732. if (!error) {
  733. // Needs Node 12
  734. // const callbackHeaders = Object.fromEntries(Object.entries(req.headers || {}).filter(([k,v]) => k.match(/^X-CALLBACK-/i)))
  735. const callbackHeaders = {}
  736. for (const key of Object.getOwnPropertyNames(
  737. req.headers || {}
  738. ).filter(key => key.match(/^X-CALLBACK-/i))) {
  739. const found = key.match(/^X-CALLBACK-(.*)/i)
  740. callbackHeaders[found[1]] = req.headers[key]
  741. }
  742. fetchNothing(callbackUrl, { headers: callbackHeaders }).catch(err => {
  743. logger.warn({ err }, 'failed to ping callback url')
  744. })
  745. }
  746. } else {
  747. if (error != null) {
  748. return next(error)
  749. }
  750. res.send({ retryStatus: result })
  751. }
  752. }
  753. )
  754. }
  755. const transferLabelsSchema = z.object({
  756. params: z.object({
  757. from_user: zz.objectId(),
  758. to_user: zz.objectId(),
  759. }),
  760. })
  761. export function transferLabels(req, res, next) {
  762. const { params } = parseReq(req, transferLabelsSchema)
  763. const { from_user: fromUser, to_user: toUser } = params
  764. LabelsManager.transferLabels(fromUser, toUser, error => {
  765. if (error != null) {
  766. return next(error)
  767. }
  768. res.sendStatus(204)
  769. })
  770. }
  771. const deleteProjectSchema = z.object({
  772. params: z.object({
  773. project_id: zz.objectId().or(z.coerce.number()),
  774. }),
  775. })
  776. export function deleteProject(req, res, next) {
  777. const { params } = parseReq(req, deleteProjectSchema)
  778. const { project_id: projectId } = params
  779. // clear the timestamp before clearing the queue,
  780. // because the queue location is used in the migration
  781. RedisManager.clearFirstOpTimestamp(projectId, err => {
  782. if (err) {
  783. return next(err)
  784. }
  785. RedisManager.clearCachedHistoryId(projectId, err => {
  786. if (err) {
  787. return next(err)
  788. }
  789. RedisManager.destroyDocUpdatesQueue(projectId, err => {
  790. if (err) {
  791. return next(err)
  792. }
  793. SyncManager.clearResyncState(projectId, err => {
  794. if (err) {
  795. return next(err)
  796. }
  797. ErrorRecorder.clearError(projectId, err => {
  798. if (err) {
  799. return next(err)
  800. }
  801. res.sendStatus(204)
  802. })
  803. })
  804. })
  805. })
  806. })
  807. }