check_docs.mjs 6.3 KB

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