OurBigBook
web/models/issue.js
const assert = require('assert')

const { DataTypes } = require('sequelize')

const config = require('../front/config')
const convert = require('../convert')

module.exports = (sequelize) => {
  const Issue = sequelize.define(
    'Issue',
    {
      // OurBigBook Markup source for toplevel header title.
      titleSource: {
        type: DataTypes.TEXT,
        validate: {
          len: {
            args: [1, config.maxArticleTitleSize],
          },
        },
      },
      // OurBigBook Markup source for body withotu toplevel header title..
      bodySource: DataTypes.TEXT,
      // Rendered toplevel header title.
      titleRender: DataTypes.TEXT,
      // Full rendered output.
      render: DataTypes.TEXT,
      // User-visible numeric identifier for the issue. 1-based.
      number: DataTypes.INTEGER,
      // Upvote count.
      score: {
        type: DataTypes.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      followerCount: {
        type: DataTypes.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      commentCount: {
        type: DataTypes.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
    },
    {
      indexes: [
        {
          fields: ['articleId', 'number'],
          unique: true,
        },
        { fields: ['score'], },
        { fields: ['followerCount'], },
        { fields: ['commentCount'], },

        // Foreign key indexes https://docs.ourbigbook.com/database-guidelines
        { fields: ['authorId'], },
        { fields: ['articleId'], },
      ],
    },
  )

  Issue.prototype.getAuthor = async function() {
    if (this.author === undefined) {
      return await this.getAuthor()
    } else {
      return this.author
    }
  }

  Issue.prototype.getSlug = function() {
    return `${this.article.getSlug()}#${this.number}`
  }

  Issue.prototype.rerender = async function({ convertOptionsExtra, ignoreErrors, transaction }={}) {
    if (ignoreErrors === undefined)
      ignoreErrors = false
    await sequelize.transaction({ transaction }, async (transaction) => {
      try {
        await convert.convertIssue({
          issue: this,
          sequelize,
          transaction,
          user: this.author,
        })
      } catch(e) {
        if (ignoreErrors) {
          console.log(e)
        } else {
          throw e
        }
      }
    })
  }
  Issue.prototype.toJson = async function(loggedInUser) {
    // TODO do liked and followed with JOINs on caller, check if it is there and skip this if so.
    const [followed, liked] = await Promise.all([
      loggedInUser ? await loggedInUser.hasFollowedIssue(this.id) : false,
      loggedInUser ? await loggedInUser.hasLikedIssue(this.id) : false,
    ])
    const ret = {
      id: this.id,
      number: this.number,
      commentCount: this.commentCount,
      followerCount: this.followerCount,
      followed,
      liked,
      titleSource: this.titleSource,
      bodySource: this.bodySource,
      score: this.score,
      titleRender: this.titleRender,
      render: this.render,
      createdAt: this.createdAt.toISOString(),
      updatedAt: this.updatedAt.toISOString(),
    }
    if (this.author) {
      ret.author = await this.author.toJson(loggedInUser)
    }
    if (this.article) {
      ret.article = await this.article.toJson(loggedInUser)
    }
    return ret
  }

  Issue.createSideEffects = async function(author, article, fields, opts={}) {
    const { transaction } = opts
    return sequelize.transaction({ transaction: opts.transaction }, async (transaction) => {
      const issue = await sequelize.models.Issue.create(
        Object.assign({ articleId: article.id, authorId: author.id }, fields),
        { transaction }
      )
      await author.addIssueFollowSideEffects(issue, { transaction })
      return issue
    })
  }

  Issue.getIssue = async function ({ includeComments, number, sequelize, slug }) {
    const include = [
      {
        model: sequelize.models.Article,
        as: 'article',
        where: { slug },
        include: [{
          model: sequelize.models.File,
          as: 'file',
        }]
      },
      { model: sequelize.models.User, as: 'author' },
    ]
    let order
    if (includeComments) {
      include.push({
        model: sequelize.models.Comment,
        as: 'comments',
        include: [{ model: sequelize.models.User, as: 'author' }],
      })
      order = [[
        'comments', 'number', 'ASC'
      ]]
    }
    return await sequelize.models.Issue.findOne({
      where: { number },
      include,
      order,
      limit: config.articleLimit,
    })
  }

  Issue.getIssues = async ({
    author,
    includeArticle,
    likedBy,
    limit,
    offset,
    order,
    orderAscDesc,
    sequelize,
    transaction,
  }) => {
    assert.notStrictEqual(sequelize, undefined)
    if (orderAscDesc === undefined) {
      orderAscDesc = 'DESC'
    }
    const authorInclude = {
      model: sequelize.models.User,
      as: 'author',
      required: true,
    }
    if (author) {
      authorInclude.where = { username: author }
    }
    const include = [authorInclude]
    if (includeArticle) {
      include.push({
        model: sequelize.models.Article,
        as: 'article',
        include: [{
          model: sequelize.models.File,
          as: 'file',
          include: [{
            model: sequelize.models.User,
            as: 'author',
          }]
        }]
      })
    }
    if (likedBy) {
      include.push({
        model: sequelize.models.User,
        as: 'issueLikedBy',
        where: { username: likedBy },
      })
    }
    const orderList = []
    if (order !== undefined) {
      orderList.push([order, orderAscDesc])
    }
    if (order !== 'createdAt') {
      // To make results deterministic.
      orderList.push(['createdAt', 'DESC'])
    }
    return sequelize.models.Issue.findAndCountAll({
      include,
      limit,
      offset,
      order: orderList,
      transaction,
    })
  }

  /** Re-render multiple issues. */
  Issue.rerender = async ({ convertOptionsExtra, ignoreErrors, log }={}) => {
    if (log === undefined)
      log = false
    let offset = 0
    while (true) {
      const issues = await sequelize.models.Issue.findAll({
        include: [
          {
            model: sequelize.models.Article,
            as: 'article',
          },
          {
            model: sequelize.models.User,
            as: 'author',
          }
        ],
        offset,
        limit: config.maxArticlesInMemory,
        order: [[{model: sequelize.models.Article, as: 'article'}, 'slug', 'ASC'], ['number', 'ASC']],
      })
      if (issues.length === 0)
        break
      for (const issue of issues) {
        if (log)
          console.log(issue.getSlug())
        await issue.rerender({ convertOptionsExtra, ignoreErrors })
      }
      offset += config.maxArticlesInMemory
    }
  }

  return Issue
}