HistoryManager.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. const { callbackify } = require('util')
  2. const request = require('request-promise-native')
  3. const settings = require('@overleaf/settings')
  4. const OError = require('@overleaf/o-error')
  5. const UserGetter = require('../User/UserGetter')
  6. module.exports = {
  7. initializeProject: callbackify(initializeProject),
  8. flushProject: callbackify(flushProject),
  9. resyncProject: callbackify(resyncProject),
  10. deleteProject: callbackify(deleteProject),
  11. deleteProjectHistory: callbackify(deleteProjectHistory),
  12. injectUserDetails: callbackify(injectUserDetails),
  13. promises: {
  14. initializeProject,
  15. flushProject,
  16. resyncProject,
  17. deleteProject,
  18. injectUserDetails,
  19. deleteProjectHistory,
  20. },
  21. }
  22. async function initializeProject() {
  23. if (
  24. !(
  25. settings.apis.project_history &&
  26. settings.apis.project_history.initializeHistoryForNewProjects
  27. )
  28. ) {
  29. return
  30. }
  31. try {
  32. const body = await request.post({
  33. url: `${settings.apis.project_history.url}/project`,
  34. })
  35. const project = JSON.parse(body)
  36. const overleafId = project && project.project && project.project.id
  37. if (!overleafId) {
  38. throw new Error('project-history did not provide an id', project)
  39. }
  40. return { overleaf_id: overleafId }
  41. } catch (err) {
  42. throw OError.tag(err, 'failed to initialize project history')
  43. }
  44. }
  45. async function flushProject(projectId) {
  46. try {
  47. await request.post({
  48. url: `${settings.apis.project_history.url}/project/${projectId}/flush`,
  49. })
  50. } catch (err) {
  51. throw OError.tag(err, 'failed to flush project to project history', {
  52. projectId,
  53. })
  54. }
  55. }
  56. async function deleteProjectHistory(projectId) {
  57. try {
  58. await request.delete({
  59. url: `${settings.apis.project_history.url}/project/${projectId}`,
  60. })
  61. } catch (err) {
  62. throw OError.tag(err, 'failed to delete project history', {
  63. projectId,
  64. })
  65. }
  66. }
  67. async function resyncProject(projectId, force = false) {
  68. try {
  69. await request.post({
  70. url: `${settings.apis.project_history.url}/project/${projectId}/resync`,
  71. qs: { force }, // TODO: only send if true?
  72. })
  73. } catch (err) {
  74. throw OError.tag(err, 'failed to resync project history', { projectId })
  75. }
  76. }
  77. async function deleteProject(projectId, historyId) {
  78. try {
  79. const tasks = [
  80. request.delete(
  81. `${settings.apis.project_history.url}/project/${projectId}`
  82. ),
  83. ]
  84. if (historyId != null) {
  85. tasks.push(
  86. request.delete({
  87. url: `${settings.apis.v1_history.url}/projects/${historyId}`,
  88. auth: {
  89. user: settings.apis.v1_history.user,
  90. pass: settings.apis.v1_history.pass,
  91. },
  92. })
  93. )
  94. }
  95. await Promise.all(tasks)
  96. } catch (err) {
  97. throw OError.tag(err, 'failed to clear project history', {
  98. projectId,
  99. historyId,
  100. })
  101. }
  102. }
  103. async function injectUserDetails(data) {
  104. // data can be either:
  105. // {
  106. // diff: [{
  107. // i: "foo",
  108. // meta: {
  109. // users: ["user_id", v1_user_id, ...]
  110. // ...
  111. // }
  112. // }, ...]
  113. // }
  114. // or
  115. // {
  116. // updates: [{
  117. // pathnames: ["main.tex"]
  118. // meta: {
  119. // users: ["user_id", v1_user_id, ...]
  120. // ...
  121. // },
  122. // ...
  123. // }, ...]
  124. // }
  125. // Either way, the top level key points to an array of objects with a meta.users property
  126. // that we need to replace user_ids with populated user objects.
  127. // Note that some entries in the users arrays may be v1 ids returned by the v1 history
  128. // service. v1 ids will be `numbers`
  129. let userIds = new Set()
  130. let v1UserIds = new Set()
  131. const entries = Array.isArray(data.diff)
  132. ? data.diff
  133. : Array.isArray(data.updates)
  134. ? data.updates
  135. : []
  136. for (const entry of entries) {
  137. for (const user of (entry.meta && entry.meta.users) || []) {
  138. if (typeof user === 'string') {
  139. userIds.add(user)
  140. } else if (typeof user === 'number') {
  141. v1UserIds.add(user)
  142. }
  143. }
  144. }
  145. userIds = Array.from(userIds)
  146. v1UserIds = Array.from(v1UserIds)
  147. const projection = { first_name: 1, last_name: 1, email: 1 }
  148. const usersArray = await UserGetter.promises.getUsers(userIds, projection)
  149. const users = {}
  150. for (const user of usersArray) {
  151. users[user._id.toString()] = _userView(user)
  152. }
  153. projection.overleaf = 1
  154. const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids(
  155. v1UserIds,
  156. projection
  157. )
  158. for (const user of v1IdentifiedUsersArray) {
  159. users[user.overleaf.id] = _userView(user)
  160. }
  161. for (const entry of entries) {
  162. if (entry.meta != null) {
  163. entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => {
  164. if (typeof user === 'string' || typeof user === 'number') {
  165. return users[user]
  166. } else {
  167. return user
  168. }
  169. })
  170. }
  171. }
  172. return data
  173. }
  174. function _userView(user) {
  175. const { _id, first_name: firstName, last_name: lastName, email } = user
  176. return { first_name: firstName, last_name: lastName, email, id: _id }
  177. }