create_project.mjs 6.7 KB

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