466 lines
12 KiB
JavaScript
466 lines
12 KiB
JavaScript
const slug = require('slug')
|
|
const uuid = require('uuid')
|
|
const humps = require('humps')
|
|
const _ = require('lodash')
|
|
const comments = require('./comments-controller')
|
|
const {ValidationError} = require('lib/errors')
|
|
|
|
const {getSelect} = require('lib/utils')
|
|
const {articleFields, userFields, relationsMaps} = require('lib/relations-map')
|
|
const joinJs = require('join-js').default
|
|
|
|
module.exports = {
|
|
|
|
async bySlug (slug, ctx, next) {
|
|
if (!slug) {
|
|
ctx.throw(404)
|
|
}
|
|
|
|
const article = await ctx.app.db('articles')
|
|
.first()
|
|
.where({slug})
|
|
|
|
if (!article) {
|
|
ctx.throw(404)
|
|
}
|
|
|
|
const tagsRelations = await ctx.app.db('articles_tags')
|
|
.select()
|
|
.where({article: article.id})
|
|
|
|
let tagList = []
|
|
|
|
if (tagsRelations && tagsRelations.length > 0) {
|
|
tagList = await ctx.app.db('tags')
|
|
.select()
|
|
.whereIn('id', tagsRelations.map(r => r.tag))
|
|
|
|
tagList = tagList.map(t => t.name)
|
|
}
|
|
|
|
article.tagList = tagList
|
|
|
|
article.favorited = false
|
|
|
|
const author = await ctx.app.db('users')
|
|
.first('username', 'bio', 'image', 'id')
|
|
.where({id: article.author})
|
|
|
|
article.author = author
|
|
|
|
article.author.following = false
|
|
|
|
const {user} = ctx.state
|
|
|
|
if (user && user.username !== article.author.username) {
|
|
const res = await ctx.app.db('followers')
|
|
.where({user: article.author.id, follower: user.id})
|
|
.select()
|
|
|
|
if (res.length > 0) {
|
|
article.author.following = true
|
|
}
|
|
}
|
|
|
|
let favorites = []
|
|
|
|
if (user) {
|
|
favorites = await ctx.app.db('favorites')
|
|
.where({user: user.id, article: article.id})
|
|
.select()
|
|
|
|
if (favorites.length > 0) {
|
|
article.favorited = true
|
|
}
|
|
}
|
|
|
|
ctx.params.article = article
|
|
ctx.params.favorites = favorites
|
|
ctx.params.author = author
|
|
ctx.params.tagList = tagList
|
|
ctx.params.tagsRelations = tagsRelations
|
|
|
|
await next()
|
|
|
|
delete ctx.params.author.id
|
|
},
|
|
|
|
async get (ctx) {
|
|
const {user} = ctx.state
|
|
const {offset, limit, tag, author, favorited} = ctx.query
|
|
|
|
let articlesQuery = ctx.app.db('articles')
|
|
.select(
|
|
...getSelect('articles', 'article', articleFields),
|
|
...getSelect('users', 'author', userFields),
|
|
...getSelect('articles_tags', 'tag', ['id']),
|
|
...getSelect('tags', 'tag', ['id', 'name']),
|
|
'favorites.id as article_favorited',
|
|
'followers.id as author_following'
|
|
)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.orderBy('articles.created_at', 'desc')
|
|
|
|
let countQuery = ctx.app.db('articles').count()
|
|
|
|
if (author && author.length > 0) {
|
|
const subQuery = ctx.app.db('users')
|
|
.select('id')
|
|
.whereIn('username', author)
|
|
|
|
articlesQuery = articlesQuery.andWhere('articles.author', 'in', subQuery)
|
|
countQuery = countQuery.andWhere('articles.author', 'in', subQuery)
|
|
}
|
|
|
|
if (favorited && favorited.length > 0) {
|
|
const subQuery = ctx.app.db('favorites')
|
|
.select('article')
|
|
.whereIn(
|
|
'user',
|
|
ctx.app.db('users').select('id').whereIn('username', favorited)
|
|
)
|
|
|
|
articlesQuery = articlesQuery.andWhere('articles.id', 'in', subQuery)
|
|
countQuery = countQuery.andWhere('articles.id', 'in', subQuery)
|
|
}
|
|
|
|
if (tag && tag.length > 0) {
|
|
const subQuery = ctx.app.db('articles_tags')
|
|
.select('article')
|
|
.whereIn(
|
|
'tag',
|
|
ctx.app.db('tags').select('id').whereIn('name', tag)
|
|
)
|
|
|
|
articlesQuery = articlesQuery.andWhere('articles.id', 'in', subQuery)
|
|
countQuery = countQuery.andWhere('articles.id', 'in', subQuery)
|
|
}
|
|
|
|
articlesQuery = articlesQuery
|
|
.leftJoin('users', 'articles.author', 'users.id')
|
|
.leftJoin('articles_tags', 'articles.id', 'articles_tags.article')
|
|
.leftJoin('tags', 'articles_tags.tag', 'tags.id')
|
|
.leftJoin('favorites', function () {
|
|
this.on('articles.id', '=', 'favorites.article')
|
|
.onIn('favorites.user', [user && user.id])
|
|
})
|
|
.leftJoin('followers', function () {
|
|
this.on('articles.author', '=', 'followers.user')
|
|
.onIn('followers.follower', [user && user.id])
|
|
})
|
|
|
|
let [articles, [countRes]] = await Promise.all([articlesQuery, countQuery])
|
|
|
|
articles = joinJs
|
|
.map(articles, relationsMaps, 'articleMap', 'article_')
|
|
.map(a => {
|
|
a.favorited = Boolean(a.favorited)
|
|
a.tagList = a.tagList.map(t => t.name)
|
|
a.author.following = Boolean(a.author.following)
|
|
delete a.author.id
|
|
return a
|
|
})
|
|
|
|
let articlesCount = countRes.count || countRes['count(*)']
|
|
articlesCount = Number(articlesCount)
|
|
|
|
ctx.body = {articles, articlesCount}
|
|
},
|
|
|
|
async getOne (ctx) {
|
|
ctx.body = {article: ctx.params.article}
|
|
},
|
|
|
|
async post (ctx) {
|
|
const {body} = ctx.request
|
|
let {article} = body
|
|
let tags
|
|
const opts = {abortEarly: false}
|
|
|
|
article.id = uuid()
|
|
article.author = ctx.state.user.id
|
|
|
|
article = await ctx.app.schemas.article.validate(article, opts)
|
|
|
|
article.slug = slug(_.get(article, 'title', ''), {lower: true})
|
|
|
|
if (article.tagList && article.tagList.length > 0) {
|
|
tags = await Promise.all(
|
|
article.tagList
|
|
.map(t => ({id: uuid(), name: t}))
|
|
.map(t => ctx.app.schemas.tag.validate(t, opts))
|
|
)
|
|
}
|
|
|
|
try {
|
|
await ctx.app.db('articles')
|
|
.insert(humps.decamelizeKeys(_.omit(article, ['tagList'])))
|
|
} catch (err) {
|
|
if (Number(err.errno) === 19 || Number(err.code) === 23505) {
|
|
article.slug = article.slug + '-' + uuid().substr(-6)
|
|
|
|
await ctx.app.db('articles')
|
|
.insert(humps.decamelizeKeys(_.omit(article, ['tagList'])))
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
|
|
if (tags && tags.length) {
|
|
for (var i = 0; i < tags.length; i++) {
|
|
try {
|
|
await ctx.app.db('tags').insert(humps.decamelizeKeys(tags[i]))
|
|
} catch (err) {
|
|
if (Number(err.errno) !== 19 && Number(err.code) !== 23505) {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
tags = await ctx.app.db('tags')
|
|
.select()
|
|
.whereIn('name', tags.map(t => t.name))
|
|
|
|
const relations = tags.map(t => ({
|
|
id: uuid(),
|
|
tag: t.id,
|
|
article: article.id
|
|
}))
|
|
|
|
await ctx.app.db('articles_tags').insert(relations)
|
|
}
|
|
|
|
article.favorited = false
|
|
article.author = _.pick(ctx.state.user, ['username', 'bio', 'image'])
|
|
article.author.following = false
|
|
|
|
ctx.body = {article}
|
|
},
|
|
|
|
async put (ctx) {
|
|
const {article} = ctx.params
|
|
|
|
if (article.author.id !== ctx.state.user.id) {
|
|
ctx.throw(403, new ValidationError(['not owned by user'], '', 'article'))
|
|
}
|
|
|
|
const {body} = ctx.request
|
|
let {article: fields = {}} = body
|
|
const opts = {abortEarly: false}
|
|
|
|
let newArticle = Object.assign({}, article, fields)
|
|
newArticle.author = newArticle.author.id
|
|
newArticle = await ctx.app.schemas.article.validate(
|
|
humps.camelizeKeys(newArticle),
|
|
opts
|
|
)
|
|
|
|
if (fields.title) {
|
|
newArticle.slug = slug(_.get(newArticle, 'title', ''), {lower: true})
|
|
}
|
|
|
|
newArticle.updatedAt = new Date().toISOString()
|
|
|
|
try {
|
|
await ctx.app.db('articles')
|
|
.update(humps.decamelizeKeys(
|
|
_.pick(
|
|
newArticle,
|
|
['title', 'slug', 'body', 'description', 'updatedAt']
|
|
)
|
|
))
|
|
.where({id: article.id})
|
|
} catch (err) {
|
|
if (Number(err.errno) === 19 || Number(err.code) === 23505) {
|
|
newArticle.slug = newArticle.slug + '-' + uuid().substr(-6)
|
|
|
|
await ctx.app.db('articles')
|
|
.update(humps.decamelizeKeys(
|
|
_.pick(
|
|
newArticle,
|
|
['title', 'slug', 'body', 'description', 'updatedAt']
|
|
)
|
|
))
|
|
.where({id: article.id})
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
|
|
if (fields.tagList && fields.tagList.length === 0) {
|
|
await ctx.app.db('articles_tags')
|
|
.del()
|
|
.where({article: article.id})
|
|
}
|
|
|
|
if (fields.tagList && fields.tagList.length > 0) {
|
|
if (_.difference(article.tagList).length || _.difference(fields.tagList).length) {
|
|
await ctx.app.db('articles_tags')
|
|
.del()
|
|
.where({article: article.id})
|
|
|
|
let tags = await Promise.all(
|
|
newArticle.tagList
|
|
.map(t => ({id: uuid(), name: t}))
|
|
.map(t => ctx.app.schemas.tag.validate(t, opts))
|
|
)
|
|
|
|
for (var i = 0; i < tags.length; i++) {
|
|
try {
|
|
await ctx.app.db('tags').insert(humps.decamelizeKeys(tags[i]))
|
|
} catch (err) {
|
|
if (Number(err.errno) !== 19 && Number(err.code) !== 23505) {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
tags = await ctx.app.db('tags')
|
|
.select()
|
|
.whereIn('name', tags.map(t => t.name))
|
|
|
|
const relations = tags.map(t => ({
|
|
id: uuid(),
|
|
tag: t.id,
|
|
article: article.id
|
|
}))
|
|
|
|
await ctx.app.db('articles_tags').insert(relations)
|
|
}
|
|
}
|
|
|
|
newArticle.author = ctx.params.author
|
|
newArticle.favorited = article.favorited
|
|
ctx.body = {article: newArticle}
|
|
},
|
|
|
|
async del (ctx) {
|
|
const {article} = ctx.params
|
|
|
|
if (article.author.id !== ctx.state.user.id) {
|
|
ctx.throw(403, new ValidationError(['not owned by user'], '', 'article'))
|
|
}
|
|
|
|
await Promise.all([
|
|
ctx.app.db('favorites')
|
|
.del()
|
|
.where({user: ctx.state.user.id, article: article.id}),
|
|
ctx.app.db('articles_tags')
|
|
.del()
|
|
.where({article: article.id}),
|
|
ctx.app.db('articles')
|
|
.del()
|
|
.where({id: article.id})
|
|
])
|
|
|
|
ctx.body = {}
|
|
},
|
|
|
|
feed: {
|
|
|
|
async get (ctx) {
|
|
const {user} = ctx.state
|
|
const {offset, limit} = ctx.query
|
|
|
|
const followedQuery = ctx.app.db('followers')
|
|
.pluck('user')
|
|
.where({follower: user.id})
|
|
|
|
let [articles, [countRes]] = await Promise.all([
|
|
ctx.app.db('articles')
|
|
.select(
|
|
...getSelect('articles', 'article', articleFields),
|
|
...getSelect('users', 'author', userFields),
|
|
...getSelect('articles_tags', 'tag', ['id']),
|
|
...getSelect('tags', 'tag', ['id', 'name']),
|
|
'favorites.id as article_favorited'
|
|
)
|
|
.whereIn('articles.author', followedQuery)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.orderBy('articles.created_at', 'desc')
|
|
.leftJoin('users', 'articles.author', 'users.id')
|
|
.leftJoin('articles_tags', 'articles.id', 'articles_tags.article')
|
|
.leftJoin('tags', 'articles_tags.tag', 'tags.id')
|
|
.leftJoin('favorites', function () {
|
|
this.on('articles.id', '=', 'favorites.article')
|
|
.onIn('favorites.user', [user && user.id])
|
|
}),
|
|
|
|
ctx.app.db('articles').count().whereIn('author', followedQuery)
|
|
])
|
|
|
|
articles = joinJs
|
|
.map(articles, relationsMaps, 'articleMap', 'article_')
|
|
.map(a => {
|
|
a.favorited = Boolean(a.favorited)
|
|
a.tagList = a.tagList.map(t => t.name)
|
|
a.author.following = true
|
|
delete a.author.id
|
|
return a
|
|
})
|
|
|
|
let articlesCount = countRes.count || countRes['count(*)']
|
|
articlesCount = Number(articlesCount)
|
|
|
|
ctx.body = {articles, articlesCount}
|
|
}
|
|
|
|
},
|
|
|
|
favorite: {
|
|
|
|
async post (ctx) {
|
|
const {article} = ctx.params
|
|
|
|
if (article.favorited) {
|
|
ctx.body = {article: ctx.params.article}
|
|
return
|
|
}
|
|
|
|
await Promise.all([
|
|
ctx.app.db('favorites').insert({
|
|
id: uuid(),
|
|
user: ctx.state.user.id,
|
|
article: article.id
|
|
}),
|
|
ctx.app.db('articles')
|
|
.increment('favorites_count', 1)
|
|
.where({id: article.id})
|
|
])
|
|
|
|
article.favorited = true
|
|
article.favorites_count = Number(article.favorites_count) + 1
|
|
|
|
ctx.body = {article: ctx.params.article}
|
|
},
|
|
|
|
async del (ctx) {
|
|
const {article} = ctx.params
|
|
|
|
if (!article.favorited) {
|
|
ctx.body = {article: ctx.params.article}
|
|
return
|
|
}
|
|
|
|
await Promise.all([
|
|
ctx.app.db('favorites')
|
|
.del()
|
|
.where({user: ctx.state.user.id, article: article.id}),
|
|
ctx.app.db('articles')
|
|
.decrement('favorites_count', 1)
|
|
.where({id: article.id})
|
|
])
|
|
|
|
article.favorited = false
|
|
article.favorites_count = Number(article.favorites_count) - 1
|
|
|
|
ctx.body = {article: ctx.params.article}
|
|
}
|
|
|
|
},
|
|
|
|
comments
|
|
}
|