tree.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import _ from 'lodash-es'
  2. import {
  3. decodeFolderPath,
  4. encodeFolderPath,
  5. decodeTreePath,
  6. encodeTreePath
  7. } from '../../helpers/common.mjs'
  8. import { generateError, generateSuccess } from '../../helpers/graph.mjs'
  9. const typeResolvers = {
  10. folder: 'TreeItemFolder',
  11. page: 'TreeItemPage',
  12. asset: 'TreeItemAsset'
  13. }
  14. const rePathName = /^[a-z0-9-]+$/
  15. const reTitle = /^[^<>"]+$/
  16. export default {
  17. Query: {
  18. /**
  19. * FETCH TREE
  20. */
  21. async tree (obj, args, context, info) {
  22. // Offset
  23. const offset = args.offset || 0
  24. if (offset < 0) {
  25. throw new Error('Invalid Offset')
  26. }
  27. // Limit
  28. const limit = args.limit || 1000
  29. if (limit < 1 || limit > 1000) {
  30. throw new Error('Invalid Limit')
  31. }
  32. // Order By
  33. const orderByDirection = args.orderByDirection || 'asc'
  34. const orderBy = args.orderBy || 'title'
  35. // Parse depth
  36. const depth = args.depth || 0
  37. if (depth < 0 || depth > 10) {
  38. throw new Error('Invalid Depth')
  39. }
  40. const depthCondition = depth > 0 ? `*{,${depth}}` : '*{0}'
  41. // Get parent path
  42. let parentPath = ''
  43. if (args.parentId) {
  44. const parent = await WIKI.db.knex('tree').where('id', args.parentId).first()
  45. if (parent) {
  46. parentPath = (parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName)
  47. }
  48. } else if (args.parentPath) {
  49. parentPath = encodeTreePath(args.parentPath)
  50. }
  51. const folderPathCondition = parentPath ? `${encodeFolderPath(parentPath)}.${depthCondition}` : depthCondition
  52. // Fetch Items
  53. const items = await WIKI.db.knex('tree')
  54. .select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
  55. .where(builder => {
  56. builder.where('folderPath', '~', folderPathCondition)
  57. // -> Include ancestors
  58. if (args.includeAncestors) {
  59. const parentPathParts = parentPath.split('.')
  60. for (let i = 0; i <= parentPathParts.length; i++) {
  61. console.info({
  62. folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
  63. fileName: _.nth(parentPathParts, i * -1),
  64. type: 'folder'
  65. })
  66. builder.orWhere({
  67. folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
  68. fileName: _.nth(parentPathParts, i * -1),
  69. type: 'folder'
  70. })
  71. }
  72. }
  73. // -> Include root items
  74. if (args.includeRootFolders) {
  75. builder.orWhere({
  76. folderPath: '',
  77. type: 'folder'
  78. })
  79. }
  80. })
  81. .andWhere(builder => {
  82. // -> Limit to specific types
  83. if (args.types && args.types.length > 0) {
  84. builder.whereIn('type', args.types)
  85. }
  86. })
  87. .limit(limit)
  88. .offset(offset)
  89. .orderBy([
  90. { column: 'depth' },
  91. { column: orderBy, order: orderByDirection }
  92. ])
  93. return items.map(item => ({
  94. id: item.id,
  95. depth: item.depth,
  96. type: item.type,
  97. folderPath: decodeTreePath(decodeFolderPath(item.folderPath)),
  98. fileName: item.fileName,
  99. title: item.title,
  100. createdAt: item.createdAt,
  101. updatedAt: item.updatedAt,
  102. ...(item.type === 'folder') && {
  103. childrenCount: item.meta?.children || 0,
  104. isAncestor: item.folderPath.length < parentPath.length || (parentPath !== '' && item.folderPath === parentPath)
  105. },
  106. ...(item.type === 'asset') && {
  107. fileSize: item.meta?.fileSize || 0,
  108. fileExt: item.meta?.fileExt || '',
  109. mimeType: item.meta?.mimeType || ''
  110. }
  111. }))
  112. },
  113. /**
  114. * FETCH SINGLE FOLDER BY ID
  115. */
  116. async folderById (obj, args, context) {
  117. const folder = await WIKI.db.knex('tree')
  118. .select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
  119. .where('id', args.id)
  120. .first()
  121. if (!folder) {
  122. throw new Error('ERR_FOLDER_NOT_EXIST')
  123. }
  124. return {
  125. ...folder,
  126. folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
  127. childrenCount: folder.meta?.children || 0
  128. }
  129. },
  130. /**
  131. * FETCH SINGLE FOLDER BY PATH
  132. */
  133. async folderByPath (obj, args, context) {
  134. const parentPathParts = args.path.replaceAll('/', '.').replaceAll('-', '_').split('.')
  135. const folder = await WIKI.db.knex('tree')
  136. .select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
  137. .where({
  138. siteId: args.siteId,
  139. localeCode: args.locale,
  140. folderPath: _.dropRight(parentPathParts).join('.'),
  141. fileName: _.last(parentPathParts)
  142. })
  143. .first()
  144. if (!folder) {
  145. throw new Error('ERR_FOLDER_NOT_EXIST')
  146. }
  147. return {
  148. ...folder,
  149. folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
  150. childrenCount: folder.meta?.children || 0
  151. }
  152. }
  153. },
  154. Mutation: {
  155. /**
  156. * CREATE FOLDER
  157. */
  158. async createFolder (obj, args, context) {
  159. try {
  160. await WIKI.db.tree.createFolder(args)
  161. return {
  162. operation: generateSuccess('Folder created successfully')
  163. }
  164. } catch (err) {
  165. WIKI.logger.debug(`Failed to create folder: ${err.message}`)
  166. return generateError(err)
  167. }
  168. },
  169. /**
  170. * RENAME FOLDER
  171. */
  172. async renameFolder (obj, args, context) {
  173. try {
  174. await WIKI.db.tree.renameFolder(args)
  175. return {
  176. operation: generateSuccess('Folder renamed successfully')
  177. }
  178. } catch (err) {
  179. WIKI.logger.debug(`Failed to rename folder ${args.folderId}: ${err.message}`)
  180. return generateError(err)
  181. }
  182. },
  183. /**
  184. * DELETE FOLDER
  185. */
  186. async deleteFolder (obj, args, context) {
  187. try {
  188. await WIKI.db.tree.deleteFolder(args.folderId)
  189. return {
  190. operation: generateSuccess('Folder deleted successfully')
  191. }
  192. } catch (err) {
  193. WIKI.logger.debug(`Failed to delete folder ${args.folderId}: ${err.message}`)
  194. return generateError(err)
  195. }
  196. }
  197. },
  198. TreeItem: {
  199. __resolveType (obj, context, info) {
  200. return typeResolvers[obj.type] ?? null
  201. }
  202. }
  203. }