page.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. import _ from 'lodash-es'
  2. import { generateError, generateSuccess } from '../../helpers/graph.mjs'
  3. import { parsePath }from '../../helpers/page.mjs'
  4. export default {
  5. Query: {
  6. /**
  7. * PAGE HISTORY
  8. */
  9. async pageHistoryById (obj, args, context, info) {
  10. const page = await WIKI.db.pages.query().select('path', 'localeCode').findById(args.id)
  11. if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
  12. path: page.path,
  13. locale: page.localeCode
  14. })) {
  15. return WIKI.db.pageHistory.getHistory({
  16. pageId: args.id,
  17. offsetPage: args.offsetPage || 0,
  18. offsetSize: args.offsetSize || 100
  19. })
  20. } else {
  21. throw new WIKI.Error.PageHistoryForbidden()
  22. }
  23. },
  24. /**
  25. * PAGE VERSION
  26. */
  27. async pageVersionById (obj, args, context, info) {
  28. const page = await WIKI.db.pages.query().select('path', 'localeCode').findById(args.pageId)
  29. if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
  30. path: page.path,
  31. locale: page.localeCode
  32. })) {
  33. return WIKI.db.pageHistory.getVersion({
  34. pageId: args.pageId,
  35. versionId: args.versionId
  36. })
  37. } else {
  38. throw new WIKI.Error.PageHistoryForbidden()
  39. }
  40. },
  41. /**
  42. * SEARCH PAGES
  43. */
  44. async searchPages (obj, args, context) {
  45. if (WIKI.data.searchEngine) {
  46. const resp = await WIKI.data.searchEngine.query(args.query, args)
  47. return {
  48. ...resp,
  49. results: _.filter(resp.results, r => {
  50. return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  51. path: r.path,
  52. locale: r.locale,
  53. tags: r.tags // Tags are needed since access permissions can be limited by page tags too
  54. })
  55. })
  56. }
  57. } else {
  58. return {
  59. results: [],
  60. suggestions: [],
  61. totalHits: 0
  62. }
  63. }
  64. },
  65. /**
  66. * LIST PAGES
  67. */
  68. async pages (obj, args, context, info) {
  69. let results = await WIKI.db.pages.query().column([
  70. 'pages.id',
  71. 'path',
  72. { locale: 'localeCode' },
  73. 'title',
  74. 'description',
  75. 'isPublished',
  76. 'isPrivate',
  77. 'privateNS',
  78. 'contentType',
  79. 'createdAt',
  80. 'updatedAt'
  81. ])
  82. .withGraphJoined('tags')
  83. .modifyGraph('tags', builder => {
  84. builder.select('tag')
  85. })
  86. .modify(queryBuilder => {
  87. if (args.limit) {
  88. queryBuilder.limit(args.limit)
  89. }
  90. if (args.locale) {
  91. queryBuilder.where('localeCode', args.locale)
  92. }
  93. if (args.creatorId && args.authorId && args.creatorId > 0 && args.authorId > 0) {
  94. queryBuilder.where(function () {
  95. this.where('creatorId', args.creatorId).orWhere('authorId', args.authorId)
  96. })
  97. } else {
  98. if (args.creatorId && args.creatorId > 0) {
  99. queryBuilder.where('creatorId', args.creatorId)
  100. }
  101. if (args.authorId && args.authorId > 0) {
  102. queryBuilder.where('authorId', args.authorId)
  103. }
  104. }
  105. if (args.tags && args.tags.length > 0) {
  106. queryBuilder.whereIn('tags.tag', args.tags.map(t => _.trim(t).toLowerCase()))
  107. }
  108. const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc'
  109. switch (args.orderBy) {
  110. case 'CREATED':
  111. queryBuilder.orderBy('createdAt', orderDir)
  112. break
  113. case 'PATH':
  114. queryBuilder.orderBy('path', orderDir)
  115. break
  116. case 'TITLE':
  117. queryBuilder.orderBy('title', orderDir)
  118. break
  119. case 'UPDATED':
  120. queryBuilder.orderBy('updatedAt', orderDir)
  121. break
  122. default:
  123. queryBuilder.orderBy('pages.id', orderDir)
  124. break
  125. }
  126. })
  127. results = _.filter(results, r => {
  128. return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  129. path: r.path,
  130. locale: r.locale
  131. })
  132. }).map(r => ({
  133. ...r,
  134. tags: _.map(r.tags, 'tag')
  135. }))
  136. if (args.tags && args.tags.length > 0) {
  137. results = _.filter(results, r => _.every(args.tags, t => _.includes(r.tags, t)))
  138. }
  139. return results
  140. },
  141. /**
  142. * FETCH SINGLE PAGE BY ID
  143. */
  144. async pageById (obj, args, context, info) {
  145. const page = await WIKI.db.pages.getPageFromDb(args.id)
  146. if (page) {
  147. if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  148. path: page.path,
  149. locale: page.localeCode
  150. })) {
  151. return {
  152. ...page,
  153. ...page.config,
  154. scriptCss: page.scripts?.css,
  155. scriptJsLoad: page.scripts?.jsLoad,
  156. scriptJsUnload: page.scripts?.jsUnload,
  157. locale: page.localeCode
  158. }
  159. } else {
  160. throw new Error('ERR_FORBIDDEN')
  161. }
  162. } else {
  163. throw new Error('ERR_PAGE_NOT_FOUND')
  164. }
  165. },
  166. /**
  167. * FETCH SINGLE PAGE BY PATH
  168. */
  169. async pageByPath (obj, args, context, info) {
  170. // console.info(info)
  171. const pageArgs = parsePath(args.path)
  172. const page = await WIKI.db.pages.getPageFromDb({
  173. ...pageArgs,
  174. siteId: args.siteId
  175. })
  176. if (page) {
  177. return {
  178. ...page,
  179. ...page.config,
  180. scriptCss: page.scripts?.css,
  181. scriptJsLoad: page.scripts?.jsLoad,
  182. scriptJsUnload: page.scripts?.jsUnload,
  183. locale: page.localeCode
  184. }
  185. } else {
  186. throw new Error('ERR_PAGE_NOT_FOUND')
  187. }
  188. },
  189. /**
  190. * FETCH PATH FROM ALIAS
  191. */
  192. async pathFromAlias (obj, args, context, info) {
  193. const alias = args.alias?.trim()
  194. if (!alias) {
  195. throw new Error('ERR_ALIAS_MISSING')
  196. }
  197. if (!WIKI.sites[args.siteId]) {
  198. throw new Error('ERR_INVALID_SITE_ID')
  199. }
  200. const page = await WIKI.db.pages.query().findOne({
  201. alias: args.alias,
  202. siteId: args.siteId
  203. }).select('id', 'path', 'localeCode')
  204. if (!page) {
  205. throw new Error('ERR_ALIAS_NOT_FOUND')
  206. }
  207. return {
  208. id: page.id,
  209. path: WIKI.sites[args.siteId].config.localeNamespacing ? `${page.localeCode}/${page.path}` : page.path
  210. }
  211. },
  212. /**
  213. * FETCH TAGS
  214. */
  215. async tags (obj, args, context, info) {
  216. const pages = await WIKI.db.pages.query()
  217. .column([
  218. 'path',
  219. { locale: 'localeCode' }
  220. ])
  221. .withGraphJoined('tags')
  222. const allTags = _.filter(pages, r => {
  223. return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  224. path: r.path,
  225. locale: r.locale
  226. })
  227. }).flatMap(r => r.tags)
  228. return _.orderBy(_.uniqBy(allTags, 'id'), ['tag'], ['asc'])
  229. },
  230. /**
  231. * SEARCH TAGS
  232. */
  233. async searchTags (obj, args, context, info) {
  234. const query = _.trim(args.query)
  235. const pages = await WIKI.db.pages.query()
  236. .column([
  237. 'path',
  238. { locale: 'localeCode' }
  239. ])
  240. .withGraphJoined('tags')
  241. .modifyGraph('tags', builder => {
  242. builder.select('tag')
  243. })
  244. .modify(queryBuilder => {
  245. queryBuilder.andWhere(builderSub => {
  246. if (WIKI.config.db.type === 'postgres') {
  247. builderSub.where('tags.tag', 'ILIKE', `%${query}%`)
  248. } else {
  249. builderSub.where('tags.tag', 'LIKE', `%${query}%`)
  250. }
  251. })
  252. })
  253. const allTags = _.filter(pages, r => {
  254. return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  255. path: r.path,
  256. locale: r.locale
  257. })
  258. }).flatMap(r => r.tags).map(t => t.tag)
  259. return _.uniq(allTags).slice(0, 5)
  260. },
  261. /**
  262. * FETCH PAGE TREE
  263. */
  264. async pageTree (obj, args, context, info) {
  265. let curPage = null
  266. if (!args.locale) { args.locale = WIKI.config.lang.code }
  267. if (args.path && !args.parent) {
  268. curPage = await WIKI.db.knex('pageTree').first('parent', 'ancestors').where({
  269. path: args.path,
  270. localeCode: args.locale
  271. })
  272. if (curPage) {
  273. args.parent = curPage.parent || 0
  274. } else {
  275. return []
  276. }
  277. }
  278. const results = await WIKI.db.knex('pageTree').where(builder => {
  279. builder.where('localeCode', args.locale)
  280. switch (args.mode) {
  281. case 'FOLDERS':
  282. builder.andWhere('isFolder', true)
  283. break
  284. case 'PAGES':
  285. builder.andWhereNotNull('pageId')
  286. break
  287. }
  288. if (!args.parent || args.parent < 1) {
  289. builder.whereNull('parent')
  290. } else {
  291. builder.where('parent', args.parent)
  292. if (args.includeAncestors && curPage && curPage.ancestors.length > 0) {
  293. builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors)
  294. }
  295. }
  296. }).orderBy([{ column: 'isFolder', order: 'desc' }, 'title'])
  297. return results.filter(r => {
  298. return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
  299. path: r.path,
  300. locale: r.localeCode
  301. })
  302. }).map(r => ({
  303. ...r,
  304. parent: r.parent || 0,
  305. locale: r.localeCode
  306. }))
  307. },
  308. /**
  309. * FETCH PAGE LINKS
  310. */
  311. async pageLinks (obj, args, context, info) {
  312. let results
  313. if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {
  314. results = await WIKI.db.knex('pages')
  315. .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })
  316. .leftJoin('pageLinks', 'pages.id', 'pageLinks.pageId')
  317. .where({
  318. 'pages.localeCode': args.locale
  319. })
  320. .unionAll(
  321. WIKI.db.knex('pageLinks')
  322. .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })
  323. .leftJoin('pages', 'pageLinks.pageId', 'pages.id')
  324. .where({
  325. 'pages.localeCode': args.locale
  326. })
  327. )
  328. } else {
  329. results = await WIKI.db.knex('pages')
  330. .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' })
  331. .fullOuterJoin('pageLinks', 'pages.id', 'pageLinks.pageId')
  332. .where({
  333. 'pages.localeCode': args.locale
  334. })
  335. }
  336. return _.reduce(results, (result, val) => {
  337. // -> Check if user has access to source and linked page
  338. if (
  339. !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.path, locale: args.locale }) ||
  340. !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.link, locale: val.locale })
  341. ) {
  342. return result
  343. }
  344. const existingEntry = _.findIndex(result, ['id', val.id])
  345. if (existingEntry >= 0) {
  346. if (val.link) {
  347. result[existingEntry].links.push(`${val.locale}/${val.link}`)
  348. }
  349. } else {
  350. result.push({
  351. id: val.id,
  352. title: val.title,
  353. path: `${args.locale}/${val.path}`,
  354. links: val.link ? [`${val.locale}/${val.link}`] : []
  355. })
  356. }
  357. return result
  358. }, [])
  359. },
  360. /**
  361. * CHECK FOR EDITING CONFLICT
  362. */
  363. async checkConflicts (obj, args, context, info) {
  364. let page = await WIKI.db.pages.query().select('path', 'localeCode', 'updatedAt').findById(args.id)
  365. if (page) {
  366. if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
  367. path: page.path,
  368. locale: page.localeCode
  369. })) {
  370. return page.updatedAt > args.checkoutDate
  371. } else {
  372. throw new WIKI.Error.PageUpdateForbidden()
  373. }
  374. } else {
  375. throw new WIKI.Error.PageNotFound()
  376. }
  377. },
  378. /**
  379. * FETCH LATEST VERSION FOR CONFLICT COMPARISON
  380. */
  381. async checkConflictsLatest (obj, args, context, info) {
  382. let page = await WIKI.db.pages.getPageFromDb(args.id)
  383. if (page) {
  384. if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
  385. path: page.path,
  386. locale: page.localeCode
  387. })) {
  388. return {
  389. ...page,
  390. tags: page.tags.map(t => t.tag),
  391. locale: page.localeCode
  392. }
  393. } else {
  394. throw new WIKI.Error.PageViewForbidden()
  395. }
  396. } else {
  397. throw new WIKI.Error.PageNotFound()
  398. }
  399. }
  400. },
  401. Mutation: {
  402. /**
  403. * CREATE PAGE
  404. */
  405. async createPage(obj, args, context) {
  406. try {
  407. const page = await WIKI.db.pages.createPage({
  408. ...args,
  409. user: context.req.user
  410. })
  411. return {
  412. operation: generateSuccess('Page created successfully.'),
  413. page
  414. }
  415. } catch (err) {
  416. return generateError(err)
  417. }
  418. },
  419. /**
  420. * UPDATE PAGE
  421. */
  422. async updatePage(obj, args, context) {
  423. try {
  424. const page = await WIKI.db.pages.updatePage({
  425. ...args,
  426. user: context.req.user
  427. })
  428. return {
  429. operation: generateSuccess('Page has been updated.'),
  430. page
  431. }
  432. } catch (err) {
  433. return generateError(err)
  434. }
  435. },
  436. /**
  437. * CONVERT PAGE
  438. */
  439. async convertPage(obj, args, context) {
  440. try {
  441. await WIKI.db.pages.convertPage({
  442. ...args,
  443. user: context.req.user
  444. })
  445. return {
  446. responseResult: generateSuccess('Page has been converted.')
  447. }
  448. } catch (err) {
  449. return generateError(err)
  450. }
  451. },
  452. /**
  453. * RENAME PAGE
  454. */
  455. async renamePage(obj, args, context) {
  456. try {
  457. await WIKI.db.pages.movePage({
  458. ...args,
  459. user: context.req.user
  460. })
  461. return {
  462. responseResult: generateSuccess('Page has been moved.')
  463. }
  464. } catch (err) {
  465. return generateError(err)
  466. }
  467. },
  468. /**
  469. * DELETE PAGE
  470. */
  471. async deletePage(obj, args, context) {
  472. try {
  473. await WIKI.db.pages.deletePage({
  474. ...args,
  475. user: context.req.user
  476. })
  477. return {
  478. responseResult: generateSuccess('Page has been deleted.')
  479. }
  480. } catch (err) {
  481. return generateError(err)
  482. }
  483. },
  484. /**
  485. * DELETE TAG
  486. */
  487. async deleteTag (obj, args, context) {
  488. try {
  489. const tagToDel = await WIKI.db.tags.query().findById(args.id)
  490. if (tagToDel) {
  491. await tagToDel.$relatedQuery('pages').unrelate()
  492. await WIKI.db.tags.query().deleteById(args.id)
  493. } else {
  494. throw new Error('This tag does not exist.')
  495. }
  496. return {
  497. responseResult: generateSuccess('Tag has been deleted.')
  498. }
  499. } catch (err) {
  500. return generateError(err)
  501. }
  502. },
  503. /**
  504. * UPDATE TAG
  505. */
  506. async updateTag (obj, args, context) {
  507. try {
  508. const affectedRows = await WIKI.db.tags.query()
  509. .findById(args.id)
  510. .patch({
  511. tag: _.trim(args.tag).toLowerCase(),
  512. title: _.trim(args.title)
  513. })
  514. if (affectedRows < 1) {
  515. throw new Error('This tag does not exist.')
  516. }
  517. return {
  518. responseResult: generateSuccess('Tag has been updated successfully.')
  519. }
  520. } catch (err) {
  521. return generateError(err)
  522. }
  523. },
  524. /**
  525. * FLUSH PAGE CACHE
  526. */
  527. async flushCache(obj, args, context) {
  528. try {
  529. await WIKI.db.pages.flushCache()
  530. WIKI.events.outbound.emit('flushCache')
  531. return {
  532. responseResult: generateSuccess('Pages Cache has been flushed successfully.')
  533. }
  534. } catch (err) {
  535. return generateError(err)
  536. }
  537. },
  538. /**
  539. * MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE
  540. */
  541. async migrateToLocale(obj, args, context) {
  542. try {
  543. const count = await WIKI.db.pages.migrateToLocale(args)
  544. return {
  545. responseResult: generateSuccess('Migrated content to target locale successfully.'),
  546. count
  547. }
  548. } catch (err) {
  549. return generateError(err)
  550. }
  551. },
  552. /**
  553. * REBUILD TREE
  554. */
  555. async rebuildPageTree(obj, args, context) {
  556. try {
  557. await WIKI.db.pages.rebuildTree()
  558. return {
  559. responseResult: generateSuccess('Page tree rebuilt successfully.')
  560. }
  561. } catch (err) {
  562. return generateError(err)
  563. }
  564. },
  565. /**
  566. * RENDER PAGE
  567. */
  568. async renderPage (obj, args, context) {
  569. try {
  570. const page = await WIKI.db.pages.query().findById(args.id)
  571. if (!page) {
  572. throw new WIKI.Error.PageNotFound()
  573. }
  574. await WIKI.db.pages.renderPage(page)
  575. return {
  576. responseResult: generateSuccess('Page rendered successfully.')
  577. }
  578. } catch (err) {
  579. return generateError(err)
  580. }
  581. },
  582. /**
  583. * RESTORE PAGE VERSION
  584. */
  585. async restorePage (obj, args, context) {
  586. try {
  587. const page = await WIKI.db.pages.query().select('path', 'localeCode').findById(args.pageId)
  588. if (!page) {
  589. throw new WIKI.Error.PageNotFound()
  590. }
  591. if (!WIKI.auth.checkAccess(context.req.user, ['write:pages'], {
  592. path: page.path,
  593. locale: page.localeCode
  594. })) {
  595. throw new WIKI.Error.PageRestoreForbidden()
  596. }
  597. const targetVersion = await WIKI.db.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId })
  598. if (!targetVersion) {
  599. throw new WIKI.Error.PageNotFound()
  600. }
  601. await WIKI.db.pages.updatePage({
  602. ...targetVersion,
  603. id: targetVersion.pageId,
  604. user: context.req.user,
  605. action: 'restored'
  606. })
  607. return {
  608. responseResult: generateSuccess('Page version restored successfully.')
  609. }
  610. } catch (err) {
  611. return generateError(err)
  612. }
  613. },
  614. /**
  615. * Purge history
  616. */
  617. async purgePagesHistory (obj, args, context) {
  618. try {
  619. await WIKI.db.pageHistory.purge(args.olderThan)
  620. return {
  621. responseResult: generateSuccess('Page history purged successfully.')
  622. }
  623. } catch (err) {
  624. return generateError(err)
  625. }
  626. }
  627. },
  628. Page: {
  629. icon (obj) {
  630. return obj.icon || 'las la-file-alt'
  631. },
  632. password (obj) {
  633. return obj.password ? '********' : ''
  634. },
  635. async tags (obj) {
  636. return WIKI.db.pages.relatedQuery('tags').for(obj.id)
  637. },
  638. tocDepth (obj) {
  639. return {
  640. min: obj.extra?.tocDepth?.min ?? 1,
  641. max: obj.extra?.tocDepth?.max ?? 2
  642. }
  643. }
  644. // comments(pg) {
  645. // return pg.$relatedQuery('comments')
  646. // }
  647. }
  648. }