tree.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. const Model = require('objection').Model
  2. const _ = require('lodash')
  3. const rePathName = /^[a-z0-9-]+$/
  4. const reTitle = /^[^<>"]+$/
  5. /**
  6. * Tree model
  7. */
  8. module.exports = class Tree extends Model {
  9. static get tableName() { return 'tree' }
  10. static get jsonSchema () {
  11. return {
  12. type: 'object',
  13. required: ['fileName'],
  14. properties: {
  15. id: {type: 'string'},
  16. folderPath: {type: 'string'},
  17. fileName: {type: 'string'},
  18. type: {type: 'string'},
  19. title: {type: 'string'},
  20. createdAt: {type: 'string'},
  21. updatedAt: {type: 'string'}
  22. }
  23. }
  24. }
  25. static get jsonAttributes() {
  26. return ['meta']
  27. }
  28. static get relationMappings() {
  29. return {
  30. locale: {
  31. relation: Model.BelongsToOneRelation,
  32. modelClass: require('./locales'),
  33. join: {
  34. from: 'tree.localeCode',
  35. to: 'locales.code'
  36. }
  37. },
  38. site: {
  39. relation: Model.BelongsToOneRelation,
  40. modelClass: require('./sites'),
  41. join: {
  42. from: 'tree.siteId',
  43. to: 'sites.id'
  44. }
  45. }
  46. }
  47. }
  48. $beforeUpdate() {
  49. this.updatedAt = new Date().toISOString()
  50. }
  51. $beforeInsert() {
  52. this.createdAt = new Date().toISOString()
  53. this.updatedAt = new Date().toISOString()
  54. }
  55. /**
  56. * Create New Folder
  57. *
  58. * @param {Object} args - New Folder Properties
  59. * @param {string} [args.parentId] - UUID of the parent folder
  60. * @param {string} [args.parentPath] - Path of the parent folder
  61. * @param {string} args.pathName - Path name of the folder to create
  62. * @param {string} args.title - Title of the folder to create
  63. * @param {string} args.locale - Locale code of the folder to create
  64. * @param {string} args.siteId - UUID of the site in which the folder will be created
  65. */
  66. static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
  67. // Validate path name
  68. if (!rePathName.test(pathName)) {
  69. throw new Error('ERR_INVALID_PATH_NAME')
  70. }
  71. // Validate title
  72. if (!reTitle.test(title)) {
  73. throw new Error('ERR_INVALID_TITLE')
  74. }
  75. WIKI.logger.debug(`Creating new folder ${pathName}...`)
  76. parentPath = parentPath?.replaceAll('/', '.')?.replaceAll('-', '_') || ''
  77. const parentPathParts = parentPath.split('.')
  78. const parentFilter = {
  79. folderPath: _.dropRight(parentPathParts).join('.'),
  80. fileName: _.last(parentPathParts)
  81. }
  82. // Get parent path
  83. let parent = null
  84. if (parentId) {
  85. parent = await WIKI.db.knex('tree').where('id', parentId).first()
  86. if (!parent) {
  87. throw new Error('ERR_NONEXISTING_PARENT_ID')
  88. }
  89. parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
  90. } else if (parentPath) {
  91. parent = await WIKI.db.knex('tree').where(parentFilter).first()
  92. } else {
  93. parentPath = ''
  94. }
  95. // Check for collision
  96. const existingFolder = await WIKI.db.knex('tree').where({
  97. siteId: siteId,
  98. localeCode: locale,
  99. folderPath: parentPath,
  100. fileName: pathName
  101. }).first()
  102. if (existingFolder) {
  103. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  104. }
  105. // Ensure all ancestors exist
  106. if (parentPath) {
  107. const expectedAncestors = []
  108. const existingAncestors = await WIKI.db.knex('tree').select('folderPath', 'fileName').where(builder => {
  109. const parentPathParts = parentPath.split('.')
  110. for (let i = 1; i <= parentPathParts.length; i++) {
  111. const ancestor = {
  112. folderPath: _.dropRight(parentPathParts, i).join('.'),
  113. fileName: _.nth(parentPathParts, i * -1)
  114. }
  115. expectedAncestors.push(ancestor)
  116. builder.orWhere({
  117. ...ancestor,
  118. type: 'folder'
  119. })
  120. }
  121. })
  122. for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
  123. WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
  124. const newAncestor = await WIKI.db.knex('tree').insert({
  125. ...ancestor,
  126. type: 'folder',
  127. title: ancestor.fileName,
  128. localeCode: locale,
  129. siteId: siteId,
  130. meta: {
  131. children: 1
  132. }
  133. }).returning('*')
  134. // Parent didn't exist until now, assign it
  135. if (!parent && ancestor.folderPath === parentFilter.folderPath && ancestor.fileName === parentFilter.fileName) {
  136. parent = newAncestor
  137. }
  138. }
  139. }
  140. // Create folder
  141. WIKI.logger.debug(`Creating new folder ${pathName} at path /${parentPath}...`)
  142. await WIKI.db.knex('tree').insert({
  143. folderPath: parentPath,
  144. fileName: pathName,
  145. type: 'folder',
  146. title: title,
  147. localeCode: locale,
  148. siteId: siteId,
  149. meta: {
  150. children: 0
  151. }
  152. })
  153. // Update parent ancestor count
  154. if (parent) {
  155. await WIKI.db.knex('tree').where('id', parent.id).update({
  156. meta: {
  157. ...(parent.meta ?? {}),
  158. children: (parent.meta?.children || 0) + 1
  159. }
  160. })
  161. }
  162. }
  163. /**
  164. * Rename a folder
  165. *
  166. * @param {Object} args - Rename Folder Properties
  167. * @param {string} args.folderId - UUID of the folder to rename
  168. * @param {string} args.pathName - New path name of the folder
  169. * @param {string} args.title - New title of the folder
  170. */
  171. static async renameFolder ({ folderId, pathName, title }) {
  172. // Get folder
  173. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  174. if (!folder) {
  175. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  176. }
  177. // Validate path name
  178. if (!rePathName.test(pathName)) {
  179. throw new Error('ERR_INVALID_PATH_NAME')
  180. }
  181. // Validate title
  182. if (!reTitle.test(title)) {
  183. throw new Error('ERR_INVALID_TITLE')
  184. }
  185. WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
  186. if (pathName !== folder.fileName) {
  187. // Check for collision
  188. const existingFolder = await WIKI.db.knex('tree')
  189. .whereNot('id', folder.id)
  190. .andWhere({
  191. siteId: folder.siteId,
  192. folderPath: folder.folderPath,
  193. fileName: pathName
  194. }).first()
  195. if (existingFolder) {
  196. throw new Error('ERR_FOLDER_ALREADY_EXISTS')
  197. }
  198. // Build new paths
  199. const oldFolderPath = (folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName).replaceAll('-', '_')
  200. const newFolderPath = (folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName).replaceAll('-', '_')
  201. // Update children nodes
  202. WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
  203. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', oldFolderPath).update({
  204. folderPath: newFolderPath
  205. })
  206. await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', '<@', oldFolderPath).update({
  207. folderPath: WIKI.db.knex.raw(`'${newFolderPath}' || subpath(tree."folderPath", nlevel('${newFolderPath}'))`)
  208. })
  209. // Rename the folder itself
  210. await WIKI.db.knex('tree').where('id', folder.id).update({
  211. fileName: pathName,
  212. title: title
  213. })
  214. } else {
  215. // Update the folder title only
  216. await WIKI.db.knex('tree').where('id', folder.id).update({
  217. title: title
  218. })
  219. }
  220. WIKI.logger.debug(`Renamed folder ${folder.id} successfully.`)
  221. }
  222. /**
  223. * Delete a folder
  224. *
  225. * @param {String} folderId Folder ID
  226. */
  227. static async deleteFolder (folderId) {
  228. // Get folder
  229. const folder = await WIKI.db.knex('tree').where('id', folderId).first()
  230. if (!folder) {
  231. throw new Error('ERR_NONEXISTING_FOLDER_ID')
  232. }
  233. const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
  234. WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
  235. // Delete all children
  236. const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
  237. // Delete folders
  238. const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
  239. if (deletedFolders.length > 0) {
  240. WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
  241. }
  242. // Delete pages
  243. const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
  244. if (deletedPages.length > 0) {
  245. WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
  246. // TODO: Delete page
  247. }
  248. // Delete assets
  249. const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
  250. if (deletedAssets.length > 0) {
  251. WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
  252. // TODO: Delete asset
  253. }
  254. // Delete the folder itself
  255. await WIKI.db.knex('tree').where('id', folder.id).del()
  256. // Update parent children count
  257. if (folder.folderPath) {
  258. const parentPathParts = folder.folderPath.split('.')
  259. const parent = await WIKI.db.knex('tree').where({
  260. folderPath: _.dropRight(parentPathParts).join('.'),
  261. fileName: _.last(parentPathParts)
  262. }).first()
  263. await WIKI.db.knex('tree').where('id', parent.id).update({
  264. meta: {
  265. ...(parent.meta ?? {}),
  266. children: (parent.meta?.children || 1) - 1
  267. }
  268. })
  269. }
  270. WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
  271. }
  272. }