OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
web/back/ArticlePage.ts
import { findSynonymOr404, getLoggedInUser } from 'back'
import { ArticlePageProps } from 'front/ArticlePage'
import {
  articleLimitSmall,
  convertContext,
  log,
  maxArticlesFetch,
  maxArticlesFetchToc,
} from 'front/config'
import { MyGetServerSideProps } from 'front/types'
import { idToSlug } from 'front/js'
import { IssueType } from 'front/types/IssueType'
import { UserType } from 'front/types/UserType'
import routes from 'front/routes'

import ourbigbook from 'ourbigbook'

async function getIncomingLinks(sequelize, article, { type, from, to }) {
  return sequelize.models.Article.findAll({
    attributes: ['slug', 'titleRenderWithScope'],
    order: [['slug', 'ASC']],
    include: [{
      model: sequelize.models.File,
      as: 'file',
      required: true,
      attributes: [],
      include: [{
        model: sequelize.models.Id,
        as: 'toplevelId',
        required: true,
        attributes: [],
        include: [{
          model: sequelize.models.Ref,
          as: from,
          required: true,
          where: { type: sequelize.models.Ref.Types[type] },
          attributes: [],
          include: [{
            model: sequelize.models.Id,
            as: to,
            required: true,
            attributes: [],
            include: [{
              model: sequelize.models.File,
              as: 'toplevelId',
              required: true,
              attributes: [],
              include: [{
                model: sequelize.models.Article,
                as: 'articles',
                required: true,
                attributes: [],
                where: { slug: article.slug },
              }],
            }],
          }],
        }],
      }]
    }]
  })
}

export const getServerSidePropsArticleHoc = ({
  includeIssues=false,
  loggedInUserCache,
}:
  {
    includeIssues?: boolean,
    loggedInUserCache?: UserType,
  }
={}): MyGetServerSideProps => {
  return async function getServerSidePropsArticle({ params: { slug }, req, res }) {
    let t0
    if (log.perf) {
      t0 = performance.now()
    }
    if (slug instanceof Array) {
      const slugString = slug.join('/')
      const sequelize = req.sequelize
      const { Article, File, Issue, Id, Ref } = sequelize.models
      const limit = articleLimitSmall
      const [article, articleTopIssues, loggedInUser] = await Promise.all([
        Article.getArticle({
          limit,
          includeIssues,
          sequelize,
          slug: slugString,
        }),
        //// TODO benchmark the effect of this monstrous query on article pages.
        //// If very slow, we could move it to after page load.
        //// TODO don't run this on split pages? But it requires doing a separate query step, which
        //// would possibly slow things down more than this actual query?
        //Article.getArticlesInSamePage({
        //  sequelize,
        //  slug: slugString,
        //  loggedInUser,
        //}),
        Article.getArticle({
          includeIssues,
          includeIssuesOrder: 'score',
          limit,
          sequelize,
          slug: slugString,
        }),
        getLoggedInUser(req, res, loggedInUserCache),
      ])
      if (!article) {
        return await findSynonymOr404(sequelize, slugString, routes.article)
      }
      const isIndex = article.isIndex()
      const [
        ancestors,
        articleJson,
        [articlesInSamePage, articlesInSamePageCount],
        [articlesInSamePageForToc, articlesInSamePageForTocCount],
        articleInTopicByLoggedInUser,
        h1ArticlesInSamePage,
        incomingLinks,
        issuesCount,
        otherArticlesInTopic,
        synonymIds,
        latestIssues,
        tagged,
        topIssues
      ] = await Promise.all([
        // ancestors
        article.treeFindAncestors({ attributes: ['slug', 'titleRender', 'titleRenderPlaintext'] }),
        article.toJson(loggedInUser),
        // articlesInSamePage
        Article.getArticlesInSamePage({
          article,
          getCount: true,
          getTagged: true,
          loggedInUser,
          limit: maxArticlesFetch,
          list: true,
          sequelize,
          toplevelId: true,
        }),
        // articlesInSamePageForToc
        Article.getArticlesInSamePage({
          article,
          getCount: true,
          loggedInUser,
          // This 10x made this be the dominating query on /wikibot when we last benchmarked.
          // (lots or empty articles) On /cirosantilli it didn't matter as much.
          limit: maxArticlesFetchToc,
          list: true,
          // Fundamental optimization to alleviate the 10x.
          toc: true,
          sequelize,
        }),
        Article.getArticleJsonInTopicBy(loggedInUser, article.topicId),
        // h1ArticlesInSamePage
        Article.getArticlesInSamePage({
          article,
          loggedInUser,
          list: undefined,
          h1: true,
          sequelize,
        }),
        getIncomingLinks(sequelize, article, { type: ourbigbook.REFS_TABLE_X, from: 'from', to: 'to' }),
        includeIssues ? Issue.count({ where: { articleId: article.id } }) : null,
        isIndex
          ? { rows: [] }
          : Article.getArticles({
              excludeIds: [article.id],
              limit,
              offset: 0,
              order: 'score',
              sequelize,
              topicId: article.topicId,
            })
        ,
        // synonymIds
        Id.findAll({
          include: [{
            model: Ref,
            as: 'from',
            required: true,
            where: { type: Ref.Types[ourbigbook.REFS_TABLE_SYNONYM] },
            attributes: [],
            include: [{
              model: Id,
              as: 'to',
              required: true,
              attributes: [],
              include: [{
                model: File,
                as: 'toplevelId',
                required: true,
                attributes: [],
                include: [{
                  model: Article,
                  as: 'articles',
                  required: true,
                  attributes: [],
                  where: {
                    slug: article.slug
                  },
                }],
              }],
            }]
          }]
        }),
        includeIssues ? Promise.all(article.issues.map(issue => issue.toJson(loggedInUser))) as Promise<IssueType[]> : null,
        // Tagged.
        getIncomingLinks(sequelize, article, { type: ourbigbook.REFS_TABLE_X_CHILD, from: 'to', to: 'from' }),
        includeIssues ? Promise.all(articleTopIssues.issues.map(issue => issue.toJson(loggedInUser))) as Promise<IssueType[]> : null,
      ])
      const h1ArticleInSamePage = h1ArticlesInSamePage[0]
      if (
        // False for Index pages, I think because they have no associated topic.
        // Which is correct.
        h1ArticleInSamePage
      ) {
        articleJson.topicCount = h1ArticleInSamePage.topicCount
        articleJson.hasSameTopic = h1ArticleInSamePage.hasSameTopic
      }
      const props: ArticlePageProps = {
        ancestors: ancestors.map((a, i) => {
          return {
            hasScope: i !== 0 && ourbigbook.AstNode.fromJSON(a.file.toplevelId.ast_json, convertContext)
              .validation_output.scope.given,
            slug: a.slug,
            titleRender: a.titleRender,
            titleRenderPlaintext: a.titleRenderPlaintext,
          }
        }),
        article: articleJson,
        articleInTopicByLoggedInUser,
        articlesInSamePage,
        articlesInSamePageCount,
        articlesInSamePageForToc,
        articlesInSamePageForTocCount,
        incomingLinks: incomingLinks.map(a => { return { slug: a.slug, titleRenderWithScope: a.titleRenderWithScope } }),
        isIndex,
        loggedInUser,
        otherArticlesInTopic: await Promise.all(otherArticlesInTopic.rows.map(article => article.toJson(loggedInUser))),
        synonymLinks: synonymIds.map(i => { return {
          slug: idToSlug(i.idid),
          titleRenderWithScope: idToSlug(i.idid),
          // TODO does not blow up, but returns empty.
          // https://docs.ourbigbook.com/todo/list-synonyms-on-metadata-section
          //titleRender: ourbigbook.renderAstFromOpts(i.ast_json, getConvertOpts({ render: true, sequelize })),
        }}),
        tagged: tagged.map(a => { return { slug: a.slug, titleRenderWithScope: a.titleRenderWithScope } }),
      }
      if (loggedInUser) {
        props.loggedInUser = await loggedInUser.toJson(loggedInUser)
      }
      if (includeIssues) {
        props.latestIssues = latestIssues
        props.topIssues = topIssues
        props.issuesCount = issuesCount
      }
      if (log.perf) {
        console.error(`perf: getServerSidePropsArticle: ${performance.now() - t0} ms`)
      }
      return { props }
    } else {
      return { notFound: true }
    }
  }
}