tree.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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. locale,
  85. siteId
  86. }).first()
  87. if (parent) {
  88. return parent
  89. } else if (createIfMissing) {
  90. return WIKI.db.tree.createFolder({
  91. parentPath: parentFilter.folderPath,
  92. pathName: parentFilter.fileName,
  93. title: parentFilter.fileName,
  94. locale,
  95. siteId
  96. })
  97. } else {
  98. throw new Error('ERR_NONEXISTING_FOLDER_PATH')
  99. }
  100. }
  101. }
  102. /**
  103. * Add Page Entry
  104. *
  105. * @param {Object} args - New Page Properties
  106. * @param {string} [args.parentId] - UUID of the parent folder
  107. * @param {string} [args.parentPath] - Path of the parent folder
  108. * @param {string} args.pathName - Path name of the page to add
  109. * @param {string} args.title - Title of the page to add
  110. * @param {string} args.locale - Locale code of the page to add
  111. * @param {string} args.siteId - UUID of the site in which the page will be added
  112. */
  113. static async addPage ({ id, parentId, parentPath, fileName, title, locale, siteId, meta = {} }) {
  114. const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({
  115. parentId,
  116. parentPath,
  117. locale,
  118. siteId,
  119. createIfMissing: true
  120. }) : {
  121. folderPath: '',
  122. fileName: ''
  123. }
  124. const folderPath = commonHelper.decodeTreePath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
  125. const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName
  126. WIKI.logger.debug(`Adding page ${fullPath} to tree...`)
  127. const pageEntry = await WIKI.db.knex('tree').insert({
  128. id,
  129. folderPath,
  130. fileName,
  131. type: 'page',
  132. title: title,
  133. hash: commonHelper.generateHash(fullPath),
  134. localeCode: locale,
  135. siteId,
  136. meta
  137. }).returning('*')
  138. return pageEntry[0]
  139. }
  140. /**
  141. * Create New Folder
  142. *
  143. * @param {Object} args - New Folder Properties
  144. * @param {string} [args.parentId] - UUID of the parent folder
  145. * @param {string} [args.parentPath] - Path of the parent folder
  146. * @param {string} args.pathName - Path name of the folder to create
  147. * @param {string} args.title - Title of the folder to create
  148. * @param {string} args.locale - Locale code of the folder to create
  149. * @param {string} args.siteId - UUID of the site in which the folder will be created
  150. */
  151. static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
  152. // Validate path name
  153. if (!rePathName.test(pathName)) {
  154. throw new Error('ERR_INVALID_PATH_NAME')
  155. }
  156. // Validate title
  157. if (!reTitle.test(title)) {
  158. throw new Error('ERR_INVALID_TITLE')
  159. }
  160. parentPath = commonHelper.encodeTreePath(parentPath)
  161. WIKI.logger.debug(`Creating new folder ${pathName}...`)
  162. const parentPathParts = parentPath.split('.')
  163. const parentFilter = {
  164. folderPath: _.dropRight(parentPathParts).join('.'),
  165. fileName: _.last(parentPathParts)
  166. }
  167. // Get parent path
  168. let parent = null
  169. if (parentId) {
  170. parent = await WIKI.db.knex('tree').where('id', parentId).first()
  171. if (!parent) {
  172. throw new Error('ERR_NONEXISTING_PARENT_ID')
  173. }
  174. parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
  175. } else if (parentPath) {
  176. parent = await WIKI.db.knex('tree').where(parentFilter).first()
  177. } else {
  178. parentPath = ''
  179. }
  180. // Check for collision
  181. const existingFolder = await WIKI.db.knex('tree').where({
  182. siteId: siteId,
  183. localeCode: locale,
  184. folderPath: parentPath,
  185. fileName: pathName
  186. }).first()
  187. if (existingFolder) {
  188. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  189. }
  190. // Ensure all ancestors exist
  191. if (parentPath) {
  192. const expectedAncestors = []
  193. const existingAncestors = await WIKI.db.knex('tree').select('folderPath', 'fileName').where(builder => {
  194. const parentPathParts = parentPath.split('.')
  195. for (let i = 1; i <= parentPathParts.length; i++) {
  196. const ancestor = {
  197. folderPath: _.dropRight(parentPathParts, i).join('.'),
  198. fileName: _.nth(parentPathParts, i * -1)
  199. }
  200. expectedAncestors.push(ancestor)
  201. builder.orWhere({
  202. ...ancestor,
  203. type: 'folder'
  204. })
  205. }
  206. })
  207. for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
  208. WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
  209. const newAncestorFullPath = ancestor.folderPath ? `${commonHelper.decodeTreePath(ancestor.folderPath)}/${ancestor.fileName}` : ancestor.fileName
  210. const newAncestor = await WIKI.db.knex('tree').insert({
  211. ...ancestor,
  212. type: 'folder',
  213. title: ancestor.fileName,
  214. hash: commonHelper.generateHash(newAncestorFullPath),
  215. localeCode: locale,
  216. siteId: siteId,
  217. meta: {
  218. children: 1
  219. }
  220. }).returning('*')
  221. // Parent didn't exist until now, assign it
  222. if (!parent && ancestor.folderPath === parentFilter.folderPath && ancestor.fileName === parentFilter.fileName) {
  223. parent = newAncestor[0]
  224. }
  225. }
  226. }
  227. // Create folder
  228. const fullPath = parentPath ? `${commonHelper.decodeTreePath(parentPath)}/${pathName}` : pathName
  229. const folder = await WIKI.db.knex('tree').insert({
  230. folderPath: parentPath,
  231. fileName: pathName,
  232. type: 'folder',
  233. title: title,
  234. hash: commonHelper.generateHash(fullPath),
  235. localeCode: locale,
  236. siteId: siteId,
  237. meta: {
  238. children: 0
  239. }
  240. }).returning('*')
  241. // Update parent ancestor count
  242. if (parent) {
  243. await WIKI.db.knex('tree').where('id', parent.id).update({
  244. meta: {
  245. ...(parent.meta ?? {}),
  246. children: (parent.meta?.children || 0) + 1
  247. }
  248. })
  249. }
  250. WIKI.logger.debug(`Created folder ${folder[0].id} successfully.`)
  251. return folder[0]
  252. }
  253. /**
  254. * Rename a folder
  255. *
  256. * @param {Object} args - Rename Folder Properties
  257. * @param {string} args.folderId - UUID of the folder to rename
  258. * @param {string} args.pathName - New path name of the folder
  259. * @param {string} args.title - New title of the folder
  260. */
  261. static async renameFolder ({ folderId, pathName, title }) {
  262. // Get folder
  263. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  264. if (!folder) {
  265. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  266. }
  267. // Validate path name
  268. if (!rePathName.test(pathName)) {
  269. throw new Error('ERR_INVALID_PATH_NAME')
  270. }
  271. // Validate title
  272. if (!reTitle.test(title)) {
  273. throw new Error('ERR_INVALID_TITLE')
  274. }
  275. WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
  276. if (pathName !== folder.fileName) {
  277. // Check for collision
  278. const existingFolder = await WIKI.db.knex('tree')
  279. .whereNot('id', folder.id)
  280. .andWhere({
  281. siteId: folder.siteId,
  282. folderPath: folder.folderPath,
  283. fileName: pathName
  284. }).first()
  285. if (existingFolder) {
  286. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  287. }
  288. // Build new paths
  289. const oldFolderPath = (folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName).replaceAll('-', '_')
  290. const newFolderPath = (folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName).replaceAll('-', '_')
  291. // Update children nodes
  292. WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
  293. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', oldFolderPath).update({
  294. folderPath: newFolderPath
  295. })
  296. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', '<@', oldFolderPath).update({
  297. folderPath: WIKI.db.knex.raw(`'${newFolderPath}' || subpath(tree."folderPath", nlevel('${newFolderPath}'))`)
  298. })
  299. // Rename the folder itself
  300. const fullPath = folder.folderPath ? `${commonHelper.decodeTreePath(folder.folderPath)}/${pathName}` : pathName
  301. await WIKI.db.knex('tree').where('id', folder.id).update({
  302. fileName: pathName,
  303. title: title,
  304. hash: commonHelper.generateHash(fullPath)
  305. })
  306. } else {
  307. // Update the folder title only
  308. await WIKI.db.knex('tree').where('id', folder.id).update({
  309. title: title
  310. })
  311. }
  312. WIKI.logger.debug(`Renamed folder ${folder.id} successfully.`)
  313. }
  314. /**
  315. * Delete a folder
  316. *
  317. * @param {String} folderId Folder ID
  318. */
  319. static async deleteFolder (folderId) {
  320. // Get folder
  321. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  322. if (!folder) {
  323. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  324. }
  325. const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
  326. WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
  327. // Delete all children
  328. const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
  329. // Delete folders
  330. const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
  331. if (deletedFolders.length > 0) {
  332. WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
  333. }
  334. // Delete pages
  335. const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
  336. if (deletedPages.length > 0) {
  337. WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
  338. // TODO: Delete page
  339. }
  340. // Delete assets
  341. const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
  342. if (deletedAssets.length > 0) {
  343. WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
  344. // TODO: Delete asset
  345. }
  346. // Delete the folder itself
  347. await WIKI.db.knex('tree').where('id', folder.id).del()
  348. // Update parent children count
  349. if (folder.folderPath) {
  350. const parentPathParts = folder.folderPath.split('.')
  351. const parent = await WIKI.db.knex('tree').where({
  352. folderPath: _.dropRight(parentPathParts).join('.'),
  353. fileName: _.last(parentPathParts)
  354. }).first()
  355. await WIKI.db.knex('tree').where('id', parent.id).update({
  356. meta: {
  357. ...(parent.meta ?? {}),
  358. children: (parent.meta?.children || 1) - 1
  359. }
  360. })
  361. }
  362. WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
  363. }
  364. }