|
@@ -0,0 +1,307 @@
|
|
|
+const Model = require('objection').Model
|
|
|
+const _ = require('lodash')
|
|
|
+
|
|
|
+const rePathName = /^[a-z0-9-]+$/
|
|
|
+const reTitle = /^[^<>"]+$/
|
|
|
+
|
|
|
+/**
|
|
|
+ * Tree model
|
|
|
+ */
|
|
|
+module.exports = class Tree extends Model {
|
|
|
+ static get tableName() { return 'tree' }
|
|
|
+
|
|
|
+ static get jsonSchema () {
|
|
|
+ return {
|
|
|
+ type: 'object',
|
|
|
+ required: ['fileName'],
|
|
|
+
|
|
|
+ properties: {
|
|
|
+ id: {type: 'string'},
|
|
|
+ folderPath: {type: 'string'},
|
|
|
+ fileName: {type: 'string'},
|
|
|
+ type: {type: 'string'},
|
|
|
+ title: {type: 'string'},
|
|
|
+ createdAt: {type: 'string'},
|
|
|
+ updatedAt: {type: 'string'}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static get jsonAttributes() {
|
|
|
+ return ['meta']
|
|
|
+ }
|
|
|
+
|
|
|
+ static get relationMappings() {
|
|
|
+ return {
|
|
|
+ locale: {
|
|
|
+ relation: Model.BelongsToOneRelation,
|
|
|
+ modelClass: require('./locales'),
|
|
|
+ join: {
|
|
|
+ from: 'tree.localeCode',
|
|
|
+ to: 'locales.code'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ site: {
|
|
|
+ relation: Model.BelongsToOneRelation,
|
|
|
+ modelClass: require('./sites'),
|
|
|
+ join: {
|
|
|
+ from: 'tree.siteId',
|
|
|
+ to: 'sites.id'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $beforeUpdate() {
|
|
|
+ this.updatedAt = new Date().toISOString()
|
|
|
+ }
|
|
|
+ $beforeInsert() {
|
|
|
+ this.createdAt = new Date().toISOString()
|
|
|
+ this.updatedAt = new Date().toISOString()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create New Folder
|
|
|
+ *
|
|
|
+ * @param {Object} args - New Folder Properties
|
|
|
+ * @param {string} [args.parentId] - UUID of the parent folder
|
|
|
+ * @param {string} [args.parentPath] - Path of the parent folder
|
|
|
+ * @param {string} args.pathName - Path name of the folder to create
|
|
|
+ * @param {string} args.title - Title of the folder to create
|
|
|
+ * @param {string} args.locale - Locale code of the folder to create
|
|
|
+ * @param {string} args.siteId - UUID of the site in which the folder will be created
|
|
|
+ */
|
|
|
+ static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
|
|
|
+ // Validate path name
|
|
|
+ if (!rePathName.test(pathName)) {
|
|
|
+ throw new Error('ERR_INVALID_PATH_NAME')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate title
|
|
|
+ if (!reTitle.test(title)) {
|
|
|
+ throw new Error('ERR_INVALID_TITLE')
|
|
|
+ }
|
|
|
+
|
|
|
+ WIKI.logger.debug(`Creating new folder ${pathName}...`)
|
|
|
+ parentPath = parentPath?.replaceAll('/', '.')?.replaceAll('-', '_') || ''
|
|
|
+ const parentPathParts = parentPath.split('.')
|
|
|
+ const parentFilter = {
|
|
|
+ folderPath: _.dropRight(parentPathParts).join('.'),
|
|
|
+ fileName: _.last(parentPathParts)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get parent path
|
|
|
+ let parent = null
|
|
|
+ if (parentId) {
|
|
|
+ parent = await WIKI.db.knex('tree').where('id', parentId).first()
|
|
|
+ if (!parent) {
|
|
|
+ throw new Error('ERR_NONEXISTING_PARENT_ID')
|
|
|
+ }
|
|
|
+ parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
|
|
|
+ } else if (parentPath) {
|
|
|
+ parent = await WIKI.db.knex('tree').where(parentFilter).first()
|
|
|
+ } else {
|
|
|
+ parentPath = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for collision
|
|
|
+ const existingFolder = await WIKI.db.knex('tree').where({
|
|
|
+ siteId: siteId,
|
|
|
+ localeCode: locale,
|
|
|
+ folderPath: parentPath,
|
|
|
+ fileName: pathName
|
|
|
+ }).first()
|
|
|
+ if (existingFolder) {
|
|
|
+ throw new Error('ERR_FOLDER_ALREADY_EXISTS')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Ensure all ancestors exist
|
|
|
+ if (parentPath) {
|
|
|
+ const expectedAncestors = []
|
|
|
+ const existingAncestors = await WIKI.db.knex('tree').select('folderPath', 'fileName').where(builder => {
|
|
|
+ const parentPathParts = parentPath.split('.')
|
|
|
+ for (let i = 1; i <= parentPathParts.length; i++) {
|
|
|
+ const ancestor = {
|
|
|
+ folderPath: _.dropRight(parentPathParts, i).join('.'),
|
|
|
+ fileName: _.nth(parentPathParts, i * -1)
|
|
|
+ }
|
|
|
+ expectedAncestors.push(ancestor)
|
|
|
+ builder.orWhere({
|
|
|
+ ...ancestor,
|
|
|
+ type: 'folder'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
|
|
|
+ WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
|
|
|
+ const newAncestor = await WIKI.db.knex('tree').insert({
|
|
|
+ ...ancestor,
|
|
|
+ type: 'folder',
|
|
|
+ title: ancestor.fileName,
|
|
|
+ localeCode: locale,
|
|
|
+ siteId: siteId,
|
|
|
+ meta: {
|
|
|
+ children: 1
|
|
|
+ }
|
|
|
+ }).returning('*')
|
|
|
+
|
|
|
+ // Parent didn't exist until now, assign it
|
|
|
+ if (!parent && ancestor.folderPath === parentFilter.folderPath && ancestor.fileName === parentFilter.fileName) {
|
|
|
+ parent = newAncestor
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create folder
|
|
|
+ WIKI.logger.debug(`Creating new folder ${pathName} at path /${parentPath}...`)
|
|
|
+ await WIKI.db.knex('tree').insert({
|
|
|
+ folderPath: parentPath,
|
|
|
+ fileName: pathName,
|
|
|
+ type: 'folder',
|
|
|
+ title: title,
|
|
|
+ localeCode: locale,
|
|
|
+ siteId: siteId,
|
|
|
+ meta: {
|
|
|
+ children: 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Update parent ancestor count
|
|
|
+ if (parent) {
|
|
|
+ await WIKI.db.knex('tree').where('id', parent.id).update({
|
|
|
+ meta: {
|
|
|
+ ...(parent.meta ?? {}),
|
|
|
+ children: (parent.meta?.children || 0) + 1
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Rename a folder
|
|
|
+ *
|
|
|
+ * @param {Object} args - Rename Folder Properties
|
|
|
+ * @param {string} args.folderId - UUID of the folder to rename
|
|
|
+ * @param {string} args.pathName - New path name of the folder
|
|
|
+ * @param {string} args.title - New title of the folder
|
|
|
+ */
|
|
|
+ static async renameFolder ({ folderId, pathName, title }) {
|
|
|
+ // Get folder
|
|
|
+ const folder = await WIKI.db.knex('tree').where('id', folderId).first()
|
|
|
+ if (!folder) {
|
|
|
+ throw new Error('ERR_NONEXISTING_FOLDER_ID')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate path name
|
|
|
+ if (!rePathName.test(pathName)) {
|
|
|
+ throw new Error('ERR_INVALID_PATH_NAME')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate title
|
|
|
+ if (!reTitle.test(title)) {
|
|
|
+ throw new Error('ERR_INVALID_TITLE')
|
|
|
+ }
|
|
|
+
|
|
|
+ WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
|
|
|
+
|
|
|
+ if (pathName !== folder.fileName) {
|
|
|
+ // Check for collision
|
|
|
+ const existingFolder = await WIKI.db.knex('tree')
|
|
|
+ .whereNot('id', folder.id)
|
|
|
+ .andWhere({
|
|
|
+ siteId: folder.siteId,
|
|
|
+ folderPath: folder.folderPath,
|
|
|
+ fileName: pathName
|
|
|
+ }).first()
|
|
|
+ if (existingFolder) {
|
|
|
+ throw new Error('ERR_FOLDER_ALREADY_EXISTS')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build new paths
|
|
|
+ const oldFolderPath = (folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName).replaceAll('-', '_')
|
|
|
+ const newFolderPath = (folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName).replaceAll('-', '_')
|
|
|
+
|
|
|
+ // Update children nodes
|
|
|
+ WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
|
|
|
+ await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', oldFolderPath).update({
|
|
|
+ folderPath: newFolderPath
|
|
|
+ })
|
|
|
+ await WIKI.db.knex('tree').where('siteId', folder.siteId).andWhere('folderPath', '<@', oldFolderPath).update({
|
|
|
+ folderPath: WIKI.db.knex.raw(`'${newFolderPath}' || subpath(tree."folderPath", nlevel('${newFolderPath}'))`)
|
|
|
+ })
|
|
|
+
|
|
|
+ // Rename the folder itself
|
|
|
+ await WIKI.db.knex('tree').where('id', folder.id).update({
|
|
|
+ fileName: pathName,
|
|
|
+ title: title
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // Update the folder title only
|
|
|
+ await WIKI.db.knex('tree').where('id', folder.id).update({
|
|
|
+ title: title
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ WIKI.logger.debug(`Renamed folder ${folder.id} successfully.`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete a folder
|
|
|
+ *
|
|
|
+ * @param {String} folderId Folder ID
|
|
|
+ */
|
|
|
+ static async deleteFolder (folderId) {
|
|
|
+ // Get folder
|
|
|
+ const folder = await WIKI.db.knex('tree').where('id', folderId).first()
|
|
|
+ if (!folder) {
|
|
|
+ throw new Error('ERR_NONEXISTING_FOLDER_ID')
|
|
|
+ }
|
|
|
+ const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
|
|
|
+ WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
|
|
|
+
|
|
|
+ // Delete all children
|
|
|
+ const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
|
|
|
+
|
|
|
+ // Delete folders
|
|
|
+ const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
|
|
|
+ if (deletedFolders.length > 0) {
|
|
|
+ WIKI.logger.debug(`Deleted ${deletedFolders.length} children folders.`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete pages
|
|
|
+ const deletedPages = deletedNodes.filter(n => n.type === 'page').map(n => n.id)
|
|
|
+ if (deletedPages.length > 0) {
|
|
|
+ WIKI.logger.debug(`Deleting ${deletedPages.length} children pages...`)
|
|
|
+
|
|
|
+ // TODO: Delete page
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete assets
|
|
|
+ const deletedAssets = deletedNodes.filter(n => n.type === 'asset').map(n => n.id)
|
|
|
+ if (deletedAssets.length > 0) {
|
|
|
+ WIKI.logger.debug(`Deleting ${deletedPages.length} children assets...`)
|
|
|
+
|
|
|
+ // TODO: Delete asset
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete the folder itself
|
|
|
+ await WIKI.db.knex('tree').where('id', folder.id).del()
|
|
|
+
|
|
|
+ // Update parent children count
|
|
|
+ if (folder.folderPath) {
|
|
|
+ const parentPathParts = folder.folderPath.split('.')
|
|
|
+ const parent = await WIKI.db.knex('tree').where({
|
|
|
+ folderPath: _.dropRight(parentPathParts).join('.'),
|
|
|
+ fileName: _.last(parentPathParts)
|
|
|
+ }).first()
|
|
|
+ await WIKI.db.knex('tree').where('id', parent.id).update({
|
|
|
+ meta: {
|
|
|
+ ...(parent.meta ?? {}),
|
|
|
+ children: (parent.meta?.children || 1) - 1
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ WIKI.logger.debug(`Deleted folder ${folder.id} successfully.`)
|
|
|
+ }
|
|
|
+}
|