2
0

tree.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. const Model = require('objection').Model
  2. const _ = require('lodash')
  3. const commonHelper = require('../helpers/common')
  4. const rePathName = /^[a-z0-9-]+$/
  5. const reTitle = /^[^<>"]+$/
  6. /**
  7. * Tree model
  8. */
  9. module.exports = class Tree extends Model {
  10. static get tableName() { return 'tree' }
  11. static get jsonSchema () {
  12. return {
  13. type: 'object',
  14. required: ['fileName'],
  15. properties: {
  16. id: {type: 'string'},
  17. folderPath: {type: 'string'},
  18. fileName: {type: 'string'},
  19. type: {type: 'string'},
  20. title: {type: 'string'},
  21. createdAt: {type: 'string'},
  22. updatedAt: {type: 'string'}
  23. }
  24. }
  25. }
  26. static get jsonAttributes() {
  27. return ['meta']
  28. }
  29. static get relationMappings() {
  30. return {
  31. locale: {
  32. relation: Model.BelongsToOneRelation,
  33. modelClass: require('./locales'),
  34. join: {
  35. from: 'tree.localeCode',
  36. to: 'locales.code'
  37. }
  38. },
  39. site: {
  40. relation: Model.BelongsToOneRelation,
  41. modelClass: require('./sites'),
  42. join: {
  43. from: 'tree.siteId',
  44. to: 'sites.id'
  45. }
  46. }
  47. }
  48. }
  49. $beforeUpdate() {
  50. this.updatedAt = new Date().toISOString()
  51. }
  52. $beforeInsert() {
  53. this.createdAt = new Date().toISOString()
  54. this.updatedAt = new Date().toISOString()
  55. }
  56. /**
  57. * Get a Folder
  58. *
  59. * @param {Object} args - Fetch Properties
  60. * @param {string} [args.id] - UUID of the folder
  61. * @param {string} [args.path] - Path of the folder
  62. * @param {string} [args.locale] - Locale code of the folder (when using path)
  63. * @param {string} [args.siteId] - UUID of the site in which the folder is (when using path)
  64. * @param {boolean} [args.createIfMissing] - Create the folder and its ancestor if it's missing (when using path)
  65. */
  66. static async getFolder ({ id, path, locale, siteId, createIfMissing = false }) {
  67. // Get by ID
  68. if (id) {
  69. const parent = await WIKI.db.knex('tree').where('id', id).first()
  70. if (!parent) {
  71. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  72. }
  73. return parent
  74. } else {
  75. // Get by path
  76. const parentPath = commonHelper.encodeTreePath(path)
  77. const parentPathParts = parentPath.split('.')
  78. const parentFilter = {
  79. folderPath: _.dropRight(parentPathParts).join('.'),
  80. fileName: _.last(parentPathParts)
  81. }
  82. const parent = await WIKI.db.knex('tree').where({
  83. ...parentFilter,
  84. type: 'folder',
  85. localeCode: locale,
  86. siteId
  87. }).first()
  88. if (parent) {
  89. return parent
  90. } else if (createIfMissing) {
  91. return WIKI.db.tree.createFolder({
  92. parentPath: parentFilter.folderPath,
  93. pathName: parentFilter.fileName,
  94. title: parentFilter.fileName,
  95. locale,
  96. siteId
  97. })
  98. } else {
  99. throw new Error('ERR_NONEXISTING_FOLDER_PATH')
  100. }
  101. }
  102. }
  103. /**
  104. * Add Page Entry
  105. *
  106. * @param {Object} args - New Page Properties
  107. * @param {string} [args.parentId] - UUID of the parent folder
  108. * @param {string} [args.parentPath] - Path of the parent folder
  109. * @param {string} args.pathName - Path name of the page to add
  110. * @param {string} args.title - Title of the page to add
  111. * @param {string} args.locale - Locale code of the page to add
  112. * @param {string} args.siteId - UUID of the site in which the page will be added
  113. * @param {Object} [args.meta] - Extra metadata
  114. */
  115. static async addPage ({ id, parentId, parentPath, fileName, title, locale, siteId, meta = {} }) {
  116. const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({
  117. parentId,
  118. parentPath,
  119. locale,
  120. siteId,
  121. createIfMissing: true
  122. }) : {
  123. folderPath: '',
  124. fileName: ''
  125. }
  126. const folderPath = commonHelper.decodeTreePath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
  127. const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName
  128. WIKI.logger.debug(`Adding page ${fullPath} to tree...`)
  129. const pageEntry = await WIKI.db.knex('tree').insert({
  130. id,
  131. folderPath,
  132. fileName,
  133. type: 'page',
  134. title: title,
  135. hash: commonHelper.generateHash(fullPath),
  136. localeCode: locale,
  137. siteId,
  138. meta
  139. }).returning('*')
  140. return pageEntry[0]
  141. }
  142. /**
  143. * Add Asset Entry
  144. *
  145. * @param {Object} args - New Asset Properties
  146. * @param {string} [args.parentId] - UUID of the parent folder
  147. * @param {string} [args.parentPath] - Path of the parent folder
  148. * @param {string} args.pathName - Path name of the asset to add
  149. * @param {string} args.title - Title of the asset to add
  150. * @param {string} args.locale - Locale code of the asset to add
  151. * @param {string} args.siteId - UUID of the site in which the asset will be added
  152. * @param {Object} [args.meta] - Extra metadata
  153. */
  154. static async addAsset ({ id, parentId, parentPath, fileName, title, locale, siteId, meta = {} }) {
  155. const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({
  156. id: parentId,
  157. path: parentPath,
  158. locale,
  159. siteId,
  160. createIfMissing: true
  161. }) : {
  162. folderPath: '',
  163. fileName: ''
  164. }
  165. const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
  166. const decodedFolderPath = commonHelper.decodeTreePath(folderPath)
  167. const fullPath = decodedFolderPath ? `${decodedFolderPath}/${fileName}` : fileName
  168. WIKI.logger.debug(`Adding asset ${fullPath} to tree...`)
  169. const assetEntry = await WIKI.db.knex('tree').insert({
  170. id,
  171. folderPath,
  172. fileName,
  173. type: 'asset',
  174. title: title,
  175. hash: commonHelper.generateHash(fullPath),
  176. localeCode: locale,
  177. siteId,
  178. meta
  179. }).returning('*')
  180. return assetEntry[0]
  181. }
  182. /**
  183. * Create New Folder
  184. *
  185. * @param {Object} args - New Folder Properties
  186. * @param {string} [args.parentId] - UUID of the parent folder
  187. * @param {string} [args.parentPath] - Path of the parent folder
  188. * @param {string} args.pathName - Path name of the folder to create
  189. * @param {string} args.title - Title of the folder to create
  190. * @param {string} args.locale - Locale code of the folder to create
  191. * @param {string} args.siteId - UUID of the site in which the folder will be created
  192. */
  193. static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
  194. // Validate path name
  195. if (!rePathName.test(pathName)) {
  196. throw new Error('ERR_INVALID_PATH_NAME')
  197. }
  198. // Validate title
  199. if (!reTitle.test(title)) {
  200. throw new Error('ERR_INVALID_TITLE')
  201. }
  202. parentPath = commonHelper.encodeTreePath(parentPath)
  203. WIKI.logger.debug(`Creating new folder ${pathName}...`)
  204. const parentPathParts = parentPath.split('.')
  205. const parentFilter = {
  206. folderPath: _.dropRight(parentPathParts).join('.'),
  207. fileName: _.last(parentPathParts)
  208. }
  209. // Get parent path
  210. let parent = null
  211. if (parentId) {
  212. parent = await WIKI.db.knex('tree').where('id', parentId).first()
  213. if (!parent) {
  214. throw new Error('ERR_NONEXISTING_PARENT_ID')
  215. }
  216. parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
  217. } else if (parentPath) {
  218. parent = await WIKI.db.knex('tree').where(parentFilter).first()
  219. } else {
  220. parentPath = ''
  221. }
  222. // Check for collision
  223. const existingFolder = await WIKI.db.knex('tree').where({
  224. siteId: siteId,
  225. localeCode: locale,
  226. folderPath: parentPath,
  227. fileName: pathName
  228. }).first()
  229. if (existingFolder) {
  230. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  231. }
  232. // Ensure all ancestors exist
  233. if (parentPath) {
  234. const expectedAncestors = []
  235. const existingAncestors = await WIKI.db.knex('tree').select('folderPath', 'fileName').where(builder => {
  236. const parentPathParts = parentPath.split('.')
  237. for (let i = 1; i <= parentPathParts.length; i++) {
  238. const ancestor = {
  239. folderPath: _.dropRight(parentPathParts, i).join('.'),
  240. fileName: _.nth(parentPathParts, i * -1)
  241. }
  242. expectedAncestors.push(ancestor)
  243. builder.orWhere({
  244. ...ancestor,
  245. type: 'folder'
  246. })
  247. }
  248. })
  249. for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
  250. WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
  251. const newAncestorFullPath = ancestor.folderPath ? `${commonHelper.decodeTreePath(ancestor.folderPath)}/${ancestor.fileName}` : ancestor.fileName
  252. const newAncestor = await WIKI.db.knex('tree').insert({
  253. ...ancestor,
  254. type: 'folder',
  255. title: ancestor.fileName,
  256. hash: commonHelper.generateHash(newAncestorFullPath),
  257. localeCode: locale,
  258. siteId: siteId,
  259. meta: {
  260. children: 1
  261. }
  262. }).returning('*')
  263. // Parent didn't exist until now, assign it
  264. if (!parent && ancestor.folderPath === parentFilter.folderPath && ancestor.fileName === parentFilter.fileName) {
  265. parent = newAncestor[0]
  266. }
  267. }
  268. }
  269. // Create folder
  270. const fullPath = parentPath ? `${commonHelper.decodeTreePath(parentPath)}/${pathName}` : pathName
  271. const folder = await WIKI.db.knex('tree').insert({
  272. folderPath: parentPath,
  273. fileName: pathName,
  274. type: 'folder',
  275. title: title,
  276. hash: commonHelper.generateHash(fullPath),
  277. localeCode: locale,
  278. siteId: siteId,
  279. meta: {
  280. children: 0
  281. }
  282. }).returning('*')
  283. // Update parent ancestor count
  284. if (parent) {
  285. await WIKI.db.knex('tree').where('id', parent.id).update({
  286. meta: {
  287. ...(parent.meta ?? {}),
  288. children: (parent.meta?.children || 0) + 1
  289. }
  290. })
  291. }
  292. WIKI.logger.debug(`Created folder ${folder[0].id} successfully.`)
  293. return folder[0]
  294. }
  295. /**
  296. * Rename a folder
  297. *
  298. * @param {Object} args - Rename Folder Properties
  299. * @param {string} args.folderId - UUID of the folder to rename
  300. * @param {string} args.pathName - New path name of the folder
  301. * @param {string} args.title - New title of the folder
  302. */
  303. static async renameFolder ({ folderId, pathName, title }) {
  304. // Get folder
  305. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  306. if (!folder) {
  307. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  308. }
  309. // Validate path name
  310. if (!rePathName.test(pathName)) {
  311. throw new Error('ERR_INVALID_PATH_NAME')
  312. }
  313. // Validate title
  314. if (!reTitle.test(title)) {
  315. throw new Error('ERR_INVALID_TITLE')
  316. }
  317. WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
  318. if (pathName !== folder.fileName) {
  319. // Check for collision
  320. const existingFolder = await WIKI.db.knex('tree')
  321. .whereNot('id', folder.id)
  322. .andWhere({
  323. siteId: folder.siteId,
  324. folderPath: folder.folderPath,
  325. fileName: pathName
  326. }).first()
  327. if (existingFolder) {
  328. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  329. }
  330. // Build new paths
  331. const oldFolderPath = (folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName).replaceAll('-', '_')
  332. const newFolderPath = (folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName).replaceAll('-', '_')
  333. // Update children nodes
  334. WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
  335. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', oldFolderPath).update({
  336. folderPath: newFolderPath
  337. })
  338. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', '<@', oldFolderPath).update({
  339. folderPath: WIKI.db.knex.raw(`'${newFolderPath}' || subpath(tree."folderPath", nlevel('${newFolderPath}'))`)
  340. })
  341. // Rename the folder itself
  342. const fullPath = folder.folderPath ? `${commonHelper.decodeTreePath(folder.folderPath)}/${pathName}` : pathName
  343. await WIKI.db.knex('tree').where('id', folder.id).update({
  344. fileName: pathName,
  345. title: title,
  346. hash: commonHelper.generateHash(fullPath)
  347. })
  348. } else {
  349. // Update the folder title only
  350. await WIKI.db.knex('tree').where('id', folder.id).update({
  351. title: title
  352. })
  353. }
  354. WIKI.logger.debug(`Renamed folder ${folder.id} successfully.`)
  355. }
  356. /**
  357. * Delete a folder
  358. *
  359. * @param {String} folderId Folder ID
  360. */
  361. static async deleteFolder (folderId) {
  362. // Get folder
  363. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  364. if (!folder) {
  365. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  366. }
  367. const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
  368. WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
  369. // Delete all children
  370. const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
  371. // Delete folders
  372. const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
  373. if (deletedFolders.length > 0) {
  374. WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
  375. }
  376. // Delete pages
  377. const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
  378. if (deletedPages.length > 0) {
  379. WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
  380. // TODO: Delete page
  381. }
  382. // Delete assets
  383. const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
  384. if (deletedAssets.length > 0) {
  385. WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
  386. // TODO: Delete asset
  387. }
  388. // Delete the folder itself
  389. await WIKI.db.knex('tree').where('id', folder.id).del()
  390. // Update parent children count
  391. if (folder.folderPath) {
  392. const parentPathParts = folder.folderPath.split('.')
  393. const parent = await WIKI.db.knex('tree').where({
  394. folderPath: _.dropRight(parentPathParts).join('.'),
  395. fileName: _.last(parentPathParts)
  396. }).first()
  397. await WIKI.db.knex('tree').where('id', parent.id).update({
  398. meta: {
  399. ...(parent.meta ?? {}),
  400. children: (parent.meta?.children || 1) - 1
  401. }
  402. })
  403. }
  404. WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
  405. }
  406. }