ProjectLocator.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import _ from 'lodash'
  2. import logger from '@overleaf/logger'
  3. import OError from '@overleaf/o-error'
  4. import ProjectGetter from './ProjectGetter.mjs'
  5. import Errors from '../Errors/Errors.js'
  6. import { callbackifyMultiResult } from '@overleaf/promise-utils'
  7. import { iterablePaths } from './IterablePath.mjs'
  8. function findDeep(project, predicate) {
  9. function find(value, path) {
  10. if (predicate(value)) {
  11. return { value, path: path.join('.') }
  12. }
  13. if (typeof value === 'object' && value !== null) {
  14. for (const [childKey, childVal] of Object.entries(value)) {
  15. const found = find(childVal, [...path, childKey])
  16. if (found) {
  17. return found
  18. }
  19. }
  20. }
  21. }
  22. return find(project.rootFolder, ['rootFolder'])
  23. }
  24. async function findElement(options) {
  25. const { project_id: projectId, element_id: elementId, type } = options
  26. const elementType = sanitizeTypeOfElement(type)
  27. function search(searchFolder, path) {
  28. const element = _.find(
  29. searchFolder[elementType],
  30. el => (el != null ? el._id : undefined) + '' === elementId + ''
  31. ) // need to ToString both id's for robustness
  32. if (element) {
  33. const elementPlaceInArray = getIndexOf(
  34. searchFolder[elementType],
  35. elementId
  36. )
  37. path.fileSystem += `/${element.name}`
  38. path.mongo += `.${elementType}.${elementPlaceInArray}`
  39. return { element, path, folder: searchFolder }
  40. }
  41. if (searchFolder.folders != null && searchFolder.folders.length !== 0) {
  42. for (const [index, folder] of searchFolder.folders.entries()) {
  43. if (folder == null) {
  44. continue
  45. }
  46. const newPath = {}
  47. for (const key of Object.keys(path)) {
  48. const value = path[key]
  49. newPath[key] = value
  50. } // make a value copy of the string
  51. newPath.fileSystem += `/${folder.name}`
  52. newPath.mongo += `.folders.${index}`
  53. const result = search(folder, newPath)
  54. if (result) {
  55. return result
  56. }
  57. }
  58. }
  59. }
  60. const path = { fileSystem: '', mongo: 'rootFolder.0' }
  61. const startSearch = project => {
  62. if (
  63. elementId + '' === project.rootFolder[0]._id + '' &&
  64. elementType === 'folders'
  65. ) {
  66. return { element: project.rootFolder[0], path, folder: null }
  67. }
  68. const result = search(project.rootFolder[0], path)
  69. if (!result) {
  70. logger.warn(
  71. `element ${elementId} could not be found for project ${
  72. projectId || project._id
  73. }`
  74. )
  75. throw new Errors.NotFoundError('entity not found')
  76. }
  77. return result
  78. }
  79. const project =
  80. options.project ||
  81. (await ProjectGetter.promises.getProject(projectId, {
  82. rootFolder: true,
  83. rootDoc_id: true,
  84. }))
  85. if (project == null) {
  86. throw new Errors.NotFoundError('project not found')
  87. }
  88. return startSearch(project)
  89. }
  90. async function findRootDoc(opts) {
  91. const getRootDoc = async project => {
  92. if (project.rootDoc_id == null) {
  93. return { element: null, path: null, folder: null }
  94. }
  95. try {
  96. return await findElement({
  97. project,
  98. element_id: project.rootDoc_id,
  99. type: 'docs',
  100. })
  101. } catch (err) {
  102. if (err instanceof Errors.NotFoundError) {
  103. return { element: null, path: null, folder: null }
  104. }
  105. throw err
  106. }
  107. }
  108. const { project_id: projectId } = opts
  109. const project =
  110. opts.project ||
  111. (await ProjectGetter.promises.getProject(projectId, {
  112. rootFolder: true,
  113. rootDoc_id: true,
  114. }))
  115. return await getRootDoc(project)
  116. }
  117. async function findElementByPath(options) {
  118. const { project_id: projectId, path, exactCaseMatch } = options
  119. if (path == null) {
  120. throw new Error('no path provided for findElementByPath')
  121. }
  122. const project =
  123. options.project ||
  124. (await ProjectGetter.promises.getProject(projectId, {
  125. rootFolder: true,
  126. rootDoc_id: true,
  127. }))
  128. return await _findElementByPathWithProject(project, path, exactCaseMatch)
  129. }
  130. async function _findElementByPathWithProject(
  131. project,
  132. needlePath,
  133. exactCaseMatch
  134. ) {
  135. let matchFn
  136. if (exactCaseMatch) {
  137. matchFn = (a, b) => a === b
  138. } else {
  139. matchFn = (a, b) =>
  140. (a != null ? a.toLowerCase() : undefined) ===
  141. (b != null ? b.toLowerCase() : undefined)
  142. }
  143. function getParentFolder(haystackFolder, foldersList, level) {
  144. if (foldersList.length === 0) {
  145. return haystackFolder
  146. }
  147. const needleFolderName = foldersList[level]
  148. let found = false
  149. for (const folder of haystackFolder.folders) {
  150. if (matchFn(folder.name, needleFolderName)) {
  151. found = true
  152. if (level === foldersList.length - 1) {
  153. return folder
  154. }
  155. return getParentFolder(folder, foldersList, level + 1)
  156. }
  157. }
  158. if (!found) {
  159. throw new Errors.NotFoundError('parent folder not found in project', {
  160. projectId: project._id,
  161. needlePath,
  162. needleFolderName,
  163. })
  164. }
  165. }
  166. function getEntity(folder, entityName) {
  167. let result, type
  168. if (entityName == null) {
  169. return { element: folder, type: 'folder', folder: null }
  170. }
  171. for (const file of iterablePaths(folder, 'fileRefs')) {
  172. if (matchFn(file != null ? file.name : undefined, entityName)) {
  173. result = file
  174. type = 'file'
  175. }
  176. }
  177. for (const doc of iterablePaths(folder, 'docs')) {
  178. if (matchFn(doc != null ? doc.name : undefined, entityName)) {
  179. result = doc
  180. type = 'doc'
  181. }
  182. }
  183. for (const childFolder of iterablePaths(folder, 'folders')) {
  184. if (
  185. matchFn(childFolder != null ? childFolder.name : undefined, entityName)
  186. ) {
  187. result = childFolder
  188. type = 'folder'
  189. }
  190. }
  191. if (result != null) {
  192. return { element: result, type, folder }
  193. }
  194. throw new Errors.NotFoundError('element not found in project', {
  195. projectId: project._id,
  196. needlePath,
  197. entityName,
  198. })
  199. }
  200. if (project == null) {
  201. throw new Error('Tried to find an element for a null project')
  202. }
  203. if (needlePath === '' || needlePath === '/') {
  204. return { element: project.rootFolder[0], type: 'folder', folder: null }
  205. }
  206. if (needlePath.indexOf('/') === 0) {
  207. needlePath = needlePath.substring(1)
  208. }
  209. const foldersList = needlePath.split('/')
  210. const needleName = foldersList.pop()
  211. const rootFolder = project.rootFolder[0]
  212. const parentFolder = getParentFolder(rootFolder, foldersList, 0)
  213. return getEntity(parentFolder, needleName)
  214. }
  215. function sanitizeTypeOfElement(elementType) {
  216. const lastChar = elementType.slice(-1)
  217. if (lastChar !== 's') {
  218. elementType += 's'
  219. }
  220. if (elementType === 'files') {
  221. elementType = 'fileRefs'
  222. }
  223. return elementType
  224. }
  225. function getIndexOf(searchEntity, id) {
  226. const { length } = searchEntity
  227. let count = 0
  228. while (count < length) {
  229. if (
  230. (searchEntity[count] != null ? searchEntity[count]._id : undefined) +
  231. '' ===
  232. id + ''
  233. ) {
  234. return count
  235. }
  236. count++
  237. }
  238. }
  239. /**
  240. * Follow the given Mongo path (as returned by findElement) and return the
  241. * entity at the end of it.
  242. */
  243. function findElementByMongoPath(project, mongoPath) {
  244. const components = mongoPath.split('.')
  245. let node = project
  246. for (const component of components) {
  247. const key = Array.isArray(node) ? parseInt(component, 10) : component
  248. node = node[key]
  249. if (node == null) {
  250. throw new OError('entity not found', {
  251. projectId: project._id,
  252. mongoPath,
  253. })
  254. }
  255. }
  256. return node
  257. }
  258. export default {
  259. findElement: callbackifyMultiResult(findElement, [
  260. 'element',
  261. 'path',
  262. 'folder',
  263. ]),
  264. findElementByPath: callbackifyMultiResult(findElementByPath, [
  265. 'element',
  266. 'type',
  267. 'folder',
  268. ]),
  269. findRootDoc: callbackifyMultiResult(findRootDoc, [
  270. 'element',
  271. 'path',
  272. 'folder',
  273. ]),
  274. findElementByMongoPath,
  275. findDeep,
  276. promises: {
  277. findElement,
  278. findElementByPath,
  279. findRootDoc,
  280. },
  281. }