page.mjs 21 KB

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