tree.mjs 6.2 KB

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