Source: models/blog-post.js

'use strict';

/**
 * Model for blog posts. Blog posts can currently only be authored by teams: one
 * team can have many blog posts.
 *
 * @namespace BlogPost
 */
const thinky = require('../db');
const r = thinky.r;
const type = thinky.type;
const mlString = require('./helpers/ml-string');
const revision = require('./helpers/revision');
const isValidLanguage = require('../locales/languages').isValid;
const User = require('./user');
const TeamSlug = require('./team-slug');

/* eslint-disable newline-per-chained-call */
/* for schema readability */

let blogPostSchema = {

  id: type.string(),
  teamID: type.string().uuid(4),
  title: mlString.getSchema({
    maxLength: 100
  }),
  post: {
    text: mlString.getSchema(),
    html: mlString.getSchema()
  },
  // Track original authorship
  createdOn: type.date(),
  createdBy: type.string(),
  originalLanguage: type.string().max(4).required().validator(isValidLanguage),
  userCanEdit: type.virtual().default(false),
  userCanDelete: type.virtual().default(false)
};

/* eslint-enable newline-per-chained-call */

// Add versioning related fields
Object.assign(blogPostSchema, revision.getSchema());
let BlogPost = thinky.createModel("blog_posts", blogPostSchema);

BlogPost.ensureIndex("createdOn");
BlogPost.belongsTo(User, "creator", "createdBy", "id");

// NOTE: STATIC METHODS --------------------------------------------------------

// Standard handlers

BlogPost.createFirstRevision = revision.getFirstRevisionHandler(BlogPost);
BlogPost.getNotStaleOrDeleted = revision.getNotStaleOrDeletedGetHandler(BlogPost);

// Custom methods

/**
 * Get a blog post and the user object representing its original author
 *
 * @param {String} id
 *  blog post ID
 * @returns {BlogPost}
 *  post with populated `.creator` property
 * @async
 */
BlogPost.getWithCreator = async function(id) {
  const post = await BlogPost
    .get(id)
    .getJoin({
      creator: {
        _apply: seq => seq.without('password')
      }
    });

  if (post._revDeleted)
    throw revision.deletedError;

  if (post._oldRevOf)
    throw revision.staleError;

  return post;
};

/**
 * Get the most recent blog posts for a given team
 *
 * @param {String} teamID
 *  team ID to look up
 * @param {Object} [options]
 *  query criteria
 * @param {Number} options.limit=10
 *  number of posts to retrieve
 * @param {Date} options.offsetDate
 *  only get posts older than this date
 * @returns {BlogPosts[]}
 * @async
 */
BlogPost.getMostRecentBlogPosts = async function(teamID, {
  limit = 10,
  offsetDate = undefined
} = {}) {

  if (!teamID)
    throw new Error('We require a team ID to fetch blog posts.');

  let query = BlogPost;

  if (offsetDate && offsetDate.valueOf)
    query = query.between(r.minval, r.epochTime(offsetDate.valueOf() / 1000), {
      index: 'createdOn',
      rightBound: 'open' // Do not return previous record that exactly matches offset
    });

  query = query
    .orderBy({ index: r.desc('createdOn') })
    .filter({ teamID })
    .filter({ _revDeleted: false }, { default: true })
    .filter({ _oldRevOf: false }, { default: true })
    .limit(limit + 1) // One over limit to check if we need potentially another set
    .getJoin({
      creator: {
        _apply: seq => seq.without('password')
      }
    });

  const posts = await query;
  const result = {};

  // At least one additional document available, set offset for pagination
  if (posts.length == limit + 1) {
    result.offsetDate = posts[limit - 1].createdOn;
    posts.pop();
  }

  result.blogPosts = posts;
  return result;
};

/**
 * Get blog posts for a given team slug.
 *
 * @param {String} teamSlugName
 *  slug (short human-readable identifier) for a team
 * @param {Object} [options]
 *  options supported by {@link BlogPost.getMostRecentBlogPosts}
 * @returns {BlogPost[]}
 * @async
 */
BlogPost.getMostRecentBlogPostsBySlug = async function(teamSlugName, options) {
  const slug = await TeamSlug.get(teamSlugName);
  const posts = BlogPost.getMostRecentBlogPosts(slug.teamID, options);
  return posts;
};

// NOTE: INSTANCE METHODS ------------------------------------------------------

// Standard handlers

BlogPost.define("newRevision", revision.getNewRevisionHandler(BlogPost));
BlogPost.define("deleteAllRevisions", revision.getDeleteAllRevisionsHandler(BlogPost));

// Custom methods

BlogPost.define("populateUserInfo", populateUserInfo);

/**
 * Populate virtual permission fields for this post based on a given user
 *
 * @param {User} user
 *  the user whose permissions to check
 * @instance
 * @memberof BlogPost
 */
function populateUserInfo(user) {

  if (!user) // Keep at permissions at default (false)
    return;

  if (user.isSuperUser || user.id === this.createdBy)
    this.userCanEdit = true;

  if (user.isSuperUser || user.id === this.createdBy || user.isSiteModerator)
    this.userCanDelete = true;
}

module.exports = BlogPost;