ProjectLocator.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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 Error(
  160. `not found project: ${project._id} search path: ${needlePath}, folder ${foldersList[level]} could not be found`
  161. )
  162. }
  163. }
  164. function getEntity(folder, entityName) {
  165. let result, type
  166. if (entityName == null) {
  167. return { element: folder, type: 'folder', folder: null }
  168. }
  169. for (const file of iterablePaths(folder, 'fileRefs')) {
  170. if (matchFn(file != null ? file.name : undefined, entityName)) {
  171. result = file
  172. type = 'file'
  173. }
  174. }
  175. for (const doc of iterablePaths(folder, 'docs')) {
  176. if (matchFn(doc != null ? doc.name : undefined, entityName)) {
  177. result = doc
  178. type = 'doc'
  179. }
  180. }
  181. for (const childFolder of iterablePaths(folder, 'folders')) {
  182. if (
  183. matchFn(childFolder != null ? childFolder.name : undefined, entityName)
  184. ) {
  185. result = childFolder
  186. type = 'folder'
  187. }
  188. }
  189. if (result != null) {
  190. return { element: result, type, folder }
  191. }
  192. throw new Error(
  193. `not found project: ${project._id} search path: ${needlePath}, entity ${entityName} could not be found`
  194. )
  195. }
  196. if (project == null) {
  197. throw new Error('Tried to find an element for a null project')
  198. }
  199. if (needlePath === '' || needlePath === '/') {
  200. return { element: project.rootFolder[0], type: 'folder', folder: null }
  201. }
  202. if (needlePath.indexOf('/') === 0) {
  203. needlePath = needlePath.substring(1)
  204. }
  205. const foldersList = needlePath.split('/')
  206. const needleName = foldersList.pop()
  207. const rootFolder = project.rootFolder[0]
  208. const parentFolder = getParentFolder(rootFolder, foldersList, 0)
  209. return getEntity(parentFolder, needleName)
  210. }
  211. function sanitizeTypeOfElement(elementType) {
  212. const lastChar = elementType.slice(-1)
  213. if (lastChar !== 's') {
  214. elementType += 's'
  215. }
  216. if (elementType === 'files') {
  217. elementType = 'fileRefs'
  218. }
  219. return elementType
  220. }
  221. function getIndexOf(searchEntity, id) {
  222. const { length } = searchEntity
  223. let count = 0
  224. while (count < length) {
  225. if (
  226. (searchEntity[count] != null ? searchEntity[count]._id : undefined) +
  227. '' ===
  228. id + ''
  229. ) {
  230. return count
  231. }
  232. count++
  233. }
  234. }
  235. /**
  236. * Follow the given Mongo path (as returned by findElement) and return the
  237. * entity at the end of it.
  238. */
  239. function findElementByMongoPath(project, mongoPath) {
  240. const components = mongoPath.split('.')
  241. let node = project
  242. for (const component of components) {
  243. const key = Array.isArray(node) ? parseInt(component, 10) : component
  244. node = node[key]
  245. if (node == null) {
  246. throw new OError('entity not found', {
  247. projectId: project._id,
  248. mongoPath,
  249. })
  250. }
  251. }
  252. return node
  253. }
  254. export default {
  255. findElement: callbackifyMultiResult(findElement, [
  256. 'element',
  257. 'path',
  258. 'folder',
  259. ]),
  260. findElementByPath: callbackifyMultiResult(findElementByPath, [
  261. 'element',
  262. 'type',
  263. 'folder',
  264. ]),
  265. findRootDoc: callbackifyMultiResult(findRootDoc, [
  266. 'element',
  267. 'path',
  268. 'folder',
  269. ]),
  270. findElementByMongoPath,
  271. findDeep,
  272. promises: {
  273. findElement,
  274. findElementByPath,
  275. findRootDoc,
  276. },
  277. }