create_project.js 6.8 KB

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