tree.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. builder.orWhere({
  62. folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
  63. fileName: _.nth(parentPathParts, i * -1),
  64. type: 'folder'
  65. })
  66. }
  67. }
  68. // -> Include root items
  69. if (args.includeRootFolders) {
  70. builder.orWhere({
  71. folderPath: '',
  72. type: 'folder'
  73. })
  74. }
  75. // -> Filter by tags
  76. if (args.tags && args.tags.length > 0) {
  77. builder.where('tags', '@>', args.tags)
  78. }
  79. })
  80. .andWhere(builder => {
  81. // -> Limit to specific types
  82. if (args.types && args.types.length > 0) {
  83. builder.whereIn('type', args.types)
  84. }
  85. })
  86. .limit(limit)
  87. .offset(offset)
  88. .orderBy([
  89. { column: 'depth' },
  90. { column: orderBy, order: orderByDirection }
  91. ])
  92. return items.map(item => ({
  93. id: item.id,
  94. depth: item.depth,
  95. type: item.type,
  96. folderPath: decodeTreePath(decodeFolderPath(item.folderPath)),
  97. fileName: item.fileName,
  98. title: item.title,
  99. tags: item.tags ?? [],
  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
  105. },
  106. ...(item.type === 'asset') && {
  107. fileSize: item.meta?.fileSize || 0,
  108. fileExt: item.meta?.fileExt || '',
  109. mimeType: item.meta?.mimeType || ''
  110. },
  111. ...(item.type === 'page') && {
  112. description: item.meta?.description || ''
  113. }
  114. }))
  115. },
  116. /**
  117. * FETCH SINGLE FOLDER BY ID
  118. */
  119. async folderById (obj, args, context) {
  120. const folder = await WIKI.db.knex('tree')
  121. .select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
  122. .where('id', args.id)
  123. .first()
  124. if (!folder) {
  125. throw new Error('ERR_INVALID_FOLDER')
  126. }
  127. return {
  128. ...folder,
  129. folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
  130. childrenCount: folder.meta?.children || 0
  131. }
  132. },
  133. /**
  134. * FETCH SINGLE FOLDER BY PATH
  135. */
  136. async folderByPath (obj, args, context) {
  137. const parentPathParts = args.path.replaceAll('/', '.').replaceAll('-', '_').split('.')
  138. const folder = await WIKI.db.knex('tree')
  139. .select(WIKI.db.knex.raw('tree.*, nlevel(tree."folderPath") AS depth'))
  140. .where({
  141. siteId: args.siteId,
  142. locale: args.locale,
  143. folderPath: _.dropRight(parentPathParts).join('.'),
  144. fileName: _.last(parentPathParts)
  145. })
  146. .first()
  147. if (!folder) {
  148. throw new Error('ERR_INVALID_FOLDER')
  149. }
  150. return {
  151. ...folder,
  152. folderPath: folder.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
  153. childrenCount: folder.meta?.children || 0
  154. }
  155. }
  156. },
  157. Mutation: {
  158. /**
  159. * CREATE FOLDER
  160. */
  161. async createFolder (obj, args, context) {
  162. try {
  163. await WIKI.db.tree.createFolder(args)
  164. return {
  165. operation: generateSuccess('Folder created successfully')
  166. }
  167. } catch (err) {
  168. WIKI.logger.debug(`Failed to create folder: ${err.message}`)
  169. return generateError(err)
  170. }
  171. },
  172. /**
  173. * RENAME FOLDER
  174. */
  175. async renameFolder (obj, args, context) {
  176. try {
  177. await WIKI.db.tree.renameFolder(args)
  178. return {
  179. operation: generateSuccess('Folder renamed successfully')
  180. }
  181. } catch (err) {
  182. WIKI.logger.debug(`Failed to rename folder ${args.folderId}: ${err.message}`)
  183. return generateError(err)
  184. }
  185. },
  186. /**
  187. * DELETE FOLDER
  188. */
  189. async deleteFolder (obj, args, context) {
  190. try {
  191. await WIKI.db.tree.deleteFolder(args.folderId)
  192. return {
  193. operation: generateSuccess('Folder deleted successfully')
  194. }
  195. } catch (err) {
  196. WIKI.logger.debug(`Failed to delete folder ${args.folderId}: ${err.message}`)
  197. return generateError(err)
  198. }
  199. }
  200. },
  201. TreeItem: {
  202. __resolveType (obj, context, info) {
  203. return typeResolvers[obj.type] ?? null
  204. }
  205. }
  206. }