check_docs.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // @ts-check
  2. import minimist from 'minimist'
  3. import PQueue from 'p-queue'
  4. import {
  5. db,
  6. ObjectId,
  7. READ_PREFERENCE_SECONDARY,
  8. } from '../app/src/infrastructure/mongodb.js'
  9. import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js'
  10. import { NotFoundError } from '../app/src/Features/Errors/Errors.js'
  11. import { scriptRunner } from './lib/ScriptRunner.mjs'
  12. const OPTS = parseArgs()
  13. function parseArgs() {
  14. const args = minimist(process.argv.slice(2), {
  15. string: ['min-project-id', 'max-project-id', 'project-modified-since'],
  16. boolean: ['help', 'dangling-comments', 'tracked-changes'],
  17. })
  18. if (args.help) {
  19. usage()
  20. process.exit(0)
  21. }
  22. const danglingComments = Boolean(args['dangling-comments'])
  23. const trackedChanges = Boolean(args['tracked-changes'])
  24. if (!danglingComments && !trackedChanges) {
  25. console.log(
  26. 'At least one of --dangling-comments or --tracked-changes must be enabled'
  27. )
  28. process.exit(1)
  29. }
  30. return {
  31. minProjectId: args['min-project-id'] ?? null,
  32. maxProjectId: args['max-project-id'] ?? null,
  33. projectModifiedSince: args['project-modified-since']
  34. ? new Date(args['project-modified-since'])
  35. : null,
  36. danglingComments,
  37. trackedChanges,
  38. concurrency: parseInt(args.concurrency ?? '1', 10),
  39. }
  40. }
  41. function usage() {
  42. console.log(`Usage: find_dangling_comments.mjs [OPTS]
  43. Options:
  44. --min-project-id Start scanning at this project id
  45. --max-project-id Stop scanning at this project id
  46. --project-modified-since Only consider projects that were modified after the given date
  47. Example: 2020-01-01
  48. --dangling-comments Report projects with dangling comments
  49. --tracked-changes Report projects with tracked changes
  50. --concurrency How many projects can be processed in parallel
  51. `)
  52. }
  53. async function main() {
  54. const queue = new PQueue({ concurrency: OPTS.concurrency })
  55. let projectsProcessed = 0
  56. let danglingCommentsFound = 0
  57. let trackedChangesFound = 0
  58. for await (const projectId of getProjectIds()) {
  59. await queue.onEmpty()
  60. queue.add(async () => {
  61. const docs = await getDocs(projectId)
  62. if (OPTS.danglingComments) {
  63. const danglingThreadIds = await findDanglingThreadIds(projectId, docs)
  64. if (danglingThreadIds.length > 0) {
  65. console.log(
  66. `Project ${projectId} has dangling threads: ${danglingThreadIds.join(', ')}`
  67. )
  68. danglingCommentsFound += 1
  69. }
  70. }
  71. if (OPTS.trackedChanges) {
  72. if (docsHaveTrackedChanges(docs)) {
  73. console.log(`Project ${projectId} has tracked changes`)
  74. trackedChangesFound += 1
  75. }
  76. }
  77. projectsProcessed += 1
  78. if (projectsProcessed % 100000 === 0) {
  79. console.log(
  80. `${projectsProcessed} projects processed - Last project: ${projectId}`
  81. )
  82. }
  83. })
  84. }
  85. await queue.onIdle()
  86. if (OPTS.danglingComments) {
  87. console.log(
  88. `${danglingCommentsFound} projects with dangling comments found`
  89. )
  90. }
  91. if (OPTS.trackedChanges) {
  92. console.log(`${trackedChangesFound} projects with tracked changes found`)
  93. }
  94. }
  95. function getProjectIds() {
  96. const clauses = []
  97. if (OPTS.minProjectId != null) {
  98. clauses.push({ _id: { $gte: new ObjectId(OPTS.minProjectId) } })
  99. }
  100. if (OPTS.maxProjectId != null) {
  101. clauses.push({ _id: { $lte: new ObjectId(OPTS.maxProjectId) } })
  102. }
  103. if (OPTS.projectModifiedSince) {
  104. clauses.push({ lastUpdated: { $gte: OPTS.projectModifiedSince } })
  105. }
  106. const query = clauses.length > 0 ? { $and: clauses } : {}
  107. return db.projects
  108. .find(query, {
  109. projection: { _id: 1 },
  110. readPreference: READ_PREFERENCE_SECONDARY,
  111. sort: { _id: 1 },
  112. })
  113. .map(x => x._id.toString())
  114. }
  115. async function getDocs(projectId) {
  116. const mongoDocs = db.docs.find(
  117. {
  118. project_id: new ObjectId(projectId),
  119. deleted: { $ne: true },
  120. },
  121. {
  122. projection: { ranges: 1, inS3: 1 },
  123. readPreference: READ_PREFERENCE_SECONDARY,
  124. }
  125. )
  126. const docs = []
  127. for await (const mongoDoc of mongoDocs) {
  128. if (mongoDoc.inS3) {
  129. try {
  130. const archivedDoc = await DocstoreManager.promises.getDoc(
  131. projectId,
  132. mongoDoc._id,
  133. { peek: true }
  134. )
  135. docs.push({
  136. id: mongoDoc._id.toString(),
  137. ranges: archivedDoc.ranges,
  138. })
  139. } catch (err) {
  140. if (err instanceof NotFoundError) {
  141. console.warn(`Doc ${mongoDoc._id} in project ${projectId} not found`)
  142. } else {
  143. throw err
  144. }
  145. }
  146. } else {
  147. docs.push({
  148. id: mongoDoc._id.toString(),
  149. ranges: mongoDoc.ranges,
  150. })
  151. }
  152. }
  153. return docs
  154. }
  155. async function findDanglingThreadIds(projectId, docs) {
  156. const threadIds = new Set()
  157. for (const doc of docs) {
  158. const comments = doc.ranges?.comments ?? []
  159. for (const comment of comments) {
  160. threadIds.add(comment.op.t.toString())
  161. }
  162. }
  163. if (threadIds.size === 0) {
  164. return []
  165. }
  166. const rooms = await db.rooms.find(
  167. { project_id: new ObjectId(projectId), thread_id: { $exists: true } },
  168. { readPreference: READ_PREFERENCE_SECONDARY }
  169. )
  170. for await (const room of rooms) {
  171. threadIds.delete(room.thread_id.toString())
  172. if (threadIds.size === 0) {
  173. break
  174. }
  175. }
  176. return Array.from(threadIds)
  177. }
  178. function docsHaveTrackedChanges(docs) {
  179. for (const doc of docs) {
  180. const changes = doc.ranges?.changes ?? []
  181. if (changes.length > 0) {
  182. return true
  183. }
  184. }
  185. return false
  186. }
  187. try {
  188. await scriptRunner(main)
  189. process.exit(0)
  190. } catch (err) {
  191. console.error(err)
  192. process.exit(1)
  193. }