create_project.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. // Script to create projects with sharelatex history for testing
  2. // Example:
  3. // node scripts/create_project.js --user-id=5dca84e11e71ae002ff73bd4 --name="My Test Project" --old-history
  4. const fs = require('fs')
  5. const path = require('path')
  6. const _ = require('lodash')
  7. const parseArgs = require('minimist')
  8. const OError = require('@overleaf/o-error')
  9. const { waitForDb } = require('../app/src/infrastructure/mongodb')
  10. const { User } = require('../app/src/models/User')
  11. const ProjectCreationHandler = require('../app/src/Features/Project/ProjectCreationHandler')
  12. const ProjectEntityUpdateHandler = require('../app/src/Features/Project/ProjectEntityUpdateHandler')
  13. const ProjectEntityHandler = require('../app/src/Features/Project/ProjectEntityHandler')
  14. const EditorController = require('../app/src/Features/Editor/EditorController')
  15. const argv = parseArgs(process.argv.slice(2), {
  16. string: ['user-id', 'name'],
  17. boolean: ['old-history', 'random-content'],
  18. unknown: function (arg) {
  19. console.error('unrecognised argument', arg)
  20. process.exit(1)
  21. },
  22. })
  23. console.log('argv', argv)
  24. const userId = argv['user-id']
  25. const projectName = argv.name || `Test Project ${new Date().toISOString()}`
  26. const oldHistory = argv['old-history']
  27. const randomContent = argv['random-content']
  28. console.log('userId', userId)
  29. async function _createRootDoc(project, ownerId, docLines) {
  30. try {
  31. const { doc } = await ProjectEntityUpdateHandler.promises.addDoc(
  32. project._id,
  33. project.rootFolder[0]._id,
  34. 'main.tex',
  35. docLines,
  36. ownerId,
  37. null
  38. )
  39. await ProjectEntityUpdateHandler.promises.setRootDoc(project._id, doc._id)
  40. } catch (error) {
  41. throw OError.tag(error, 'error adding root doc when creating project')
  42. }
  43. }
  44. async function _addDefaultExampleProjectFiles(ownerId, projectName, project) {
  45. const mainDocLines = await _buildTemplate(
  46. 'example-project/main.tex',
  47. ownerId,
  48. projectName
  49. )
  50. await _createRootDoc(project, ownerId, mainDocLines)
  51. const bibDocLines = await _buildTemplate(
  52. 'example-project/sample.bib',
  53. ownerId,
  54. projectName
  55. )
  56. await ProjectEntityUpdateHandler.promises.addDoc(
  57. project._id,
  58. project.rootFolder[0]._id,
  59. 'sample.bib',
  60. bibDocLines,
  61. ownerId,
  62. null
  63. )
  64. const frogPath = path.join(
  65. __dirname,
  66. '/../app/templates/project_files/example-project/frog.jpg'
  67. )
  68. await ProjectEntityUpdateHandler.promises.addFile(
  69. project._id,
  70. project.rootFolder[0]._id,
  71. 'frog.jpg',
  72. frogPath,
  73. null,
  74. ownerId,
  75. null
  76. )
  77. }
  78. async function _buildTemplate(templateName, userId, projectName) {
  79. const user = await User.findById(userId, 'first_name last_name')
  80. const templatePath = path.join(
  81. __dirname,
  82. `/../app/templates/project_files/${templateName}`
  83. )
  84. const template = fs.readFileSync(templatePath)
  85. const data = {
  86. project_name: projectName,
  87. user,
  88. year: new Date().getUTCFullYear(),
  89. month: new Date().getUTCMonth(),
  90. }
  91. const output = _.template(template.toString())(data)
  92. return output.split('\n')
  93. }
  94. // Create a project with some random content and file operations for testing history migrations
  95. // Unfortunately we cannot easily change the timestamps of the history entries, so everything
  96. // will be created at the same time.
  97. async function _pickRandomDoc(project) {
  98. const result = await ProjectEntityHandler.promises.getAllDocs(project._id)
  99. const keys = Object.keys(result)
  100. if (keys.length === 0) {
  101. return null
  102. }
  103. const filepath = _.sample(keys)
  104. result[filepath].path = filepath
  105. return result[filepath]
  106. }
  107. let COUNTER = 0
  108. // format counter as a 6 digit zero padded number
  109. function nextId() {
  110. return ('000000' + COUNTER++).slice(-6)
  111. }
  112. async function _applyRandomDocUpdate(ownerId, project) {
  113. const action = _.sample(['create', 'edit', 'delete', 'rename'])
  114. switch (action) {
  115. case 'create': // create a new doc
  116. await EditorController.promises.upsertDocWithPath(
  117. project._id,
  118. `subdir/new-doc-${nextId()}.tex`,
  119. [`This is a new doc ${new Date().toISOString()}`],
  120. 'create-project-script',
  121. ownerId
  122. )
  123. break
  124. case 'edit': {
  125. // edit an existing doc
  126. const doc = await _pickRandomDoc(project)
  127. if (!doc) {
  128. return
  129. }
  130. // pick a random line and either insert or delete a character
  131. const lines = doc.lines
  132. const index = _.random(0, lines.length - 1)
  133. let thisLine = lines[index]
  134. const pos = _.random(0, thisLine.length - 1)
  135. if (Math.random() > 0.5) {
  136. // insert a character
  137. thisLine = thisLine.slice(0, pos) + 'x' + thisLine.slice(pos)
  138. } else {
  139. // delete a character
  140. thisLine = thisLine.slice(0, pos) + thisLine.slice(pos + 1)
  141. }
  142. lines[index] = thisLine
  143. await EditorController.promises.upsertDocWithPath(
  144. project._id,
  145. doc.path,
  146. lines,
  147. 'create-project-script',
  148. ownerId
  149. )
  150. break
  151. }
  152. case 'delete': {
  153. // delete an existing doc (but not the root doc)
  154. const doc = await _pickRandomDoc(project)
  155. if (!doc || doc.path === '/main.tex') {
  156. return
  157. }
  158. await EditorController.promises.deleteEntityWithPath(
  159. project._id,
  160. doc.path,
  161. ownerId,
  162. 'create-project-script'
  163. )
  164. break
  165. }
  166. case 'rename': {
  167. // rename an existing doc (but not the root doc)
  168. const doc = await _pickRandomDoc(project)
  169. if (!doc || doc.path === '/main.tex') {
  170. return
  171. }
  172. const newName = `renamed-${nextId()}.tex`
  173. await EditorController.promises.renameEntity(
  174. project._id,
  175. doc._id,
  176. 'doc',
  177. newName,
  178. ownerId,
  179. 'create-project-script'
  180. )
  181. break
  182. }
  183. }
  184. }
  185. async function createProject() {
  186. await waitForDb()
  187. const user = await User.findById(userId)
  188. console.log('Will create project')
  189. console.log('user_id:', userId, '=>', user.email)
  190. console.log('project name:', projectName)
  191. const attributes = oldHistory ? { overleaf: {} } : {}
  192. const project = await ProjectCreationHandler.promises.createBlankProject(
  193. userId,
  194. projectName,
  195. attributes
  196. )
  197. await _addDefaultExampleProjectFiles(userId, projectName, project)
  198. if (randomContent) {
  199. for (let i = 0; i < 1000; i++) {
  200. await _applyRandomDocUpdate(userId, project)
  201. }
  202. }
  203. return project
  204. }
  205. createProject()
  206. .then(project => {
  207. console.log('Created project', project._id)
  208. process.exit()
  209. })
  210. .catch(err => {
  211. console.error(err)
  212. process.exit(1)
  213. })