2018-11-29 12:12:15 +00:00
|
|
|
import { has, pick, isEmpty } from 'lodash';
|
2018-08-03 10:41:17 +00:00
|
|
|
import debug from 'debug';
|
2018-11-29 12:12:15 +00:00
|
|
|
import { reportError } from '../middlewares/error-reporter';
|
2018-07-31 15:58:04 +00:00
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
const log = debug('fcc:boot:news');
|
2018-08-03 10:41:17 +00:00
|
|
|
|
|
|
|
export default function newsBoot(app) {
|
2018-11-29 12:12:15 +00:00
|
|
|
const api = app.loopback.Router();
|
|
|
|
|
|
|
|
api.get('/n/:shortId', createShortLinkHandler(app));
|
|
|
|
|
|
|
|
api.post('/p', createPopularityHandler(app));
|
|
|
|
|
|
|
|
app.use('/internal', api);
|
|
|
|
}
|
|
|
|
|
|
|
|
function createShortLinkHandler(app) {
|
|
|
|
const { Article } = app.models;
|
|
|
|
|
|
|
|
const referralHandler = createReferralHandler(app);
|
|
|
|
|
|
|
|
return function shortLinkHandler(req, res, next) {
|
|
|
|
const { query, user } = req;
|
|
|
|
const { shortId } = req.params;
|
|
|
|
|
|
|
|
// We manually report the error here as it should not affect this request
|
|
|
|
referralHandler(query, shortId, !!user).catch(err => reportError(err));
|
|
|
|
|
|
|
|
if (!shortId) {
|
|
|
|
return res.sendStatus(400);
|
|
|
|
}
|
|
|
|
return Article.findOne(
|
|
|
|
{
|
|
|
|
where: {
|
|
|
|
or: [{ shortId }, { slugPart: shortId }]
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(err, article) => {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
}
|
|
|
|
if (!article) {
|
|
|
|
return res.status(404).send('Could not find article by shortId');
|
|
|
|
}
|
|
|
|
const {
|
|
|
|
slugPart,
|
|
|
|
shortId,
|
|
|
|
author: { username }
|
|
|
|
} = article;
|
|
|
|
const slug = `/news/${username}/${slugPart}--${shortId}`;
|
|
|
|
const articleData = {
|
|
|
|
...pick(article, [
|
|
|
|
'author',
|
|
|
|
'renderableContent',
|
|
|
|
'firstPublishedDate',
|
|
|
|
'viewCount',
|
|
|
|
'title',
|
|
|
|
'featureImage',
|
|
|
|
'slugPart',
|
|
|
|
'shortId',
|
|
|
|
'meta'
|
|
|
|
]),
|
|
|
|
slug
|
|
|
|
};
|
|
|
|
return res.json(articleData);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
2018-08-03 10:41:17 +00:00
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
function createPopularityHandler(app) {
|
|
|
|
const { Article, Popularity } = app.models;
|
2018-08-03 10:41:17 +00:00
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
function findArticleByShortId(shortId) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
Article.findOne({ where: { shortId } }, (err, article) => {
|
|
|
|
if (err) {
|
|
|
|
log('Error returned from Article.findOne(shortId)');
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('article found');
|
|
|
|
return resolve(article);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2018-08-03 10:41:17 +00:00
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
function findPopularityByShortId(shortId) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
Popularity.findOne(
|
|
|
|
{ where: { articleId: shortId } },
|
|
|
|
(err, popularity) => {
|
|
|
|
if (err) {
|
|
|
|
log('Error returned from Popularity.findOne(shortId)');
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('popularity found');
|
|
|
|
return resolve(popularity);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2018-08-03 10:41:17 +00:00
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
function createPopularity(popularityUpdate, shortId) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
Popularity.create(
|
|
|
|
{
|
|
|
|
events: [popularityUpdate],
|
|
|
|
articleId: shortId
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('poulartiy created');
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function updatePopularity(popularity, popularityUpdate) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
popularity.updateAttribute(
|
|
|
|
'events',
|
|
|
|
[popularityUpdate, ...popularity.events],
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
log('Error returned from popularity.updateAttribute()');
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('poplarity updated');
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function incrementArticleViews(article) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
article.updateAttributes({ $inc: { viewCount: 1 } }, err => {
|
|
|
|
if (err) {
|
|
|
|
log(err);
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('article views updated');
|
|
|
|
return resolve();
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return async function handlePopularityStats(req, res, next) {
|
|
|
|
const { body, user } = req;
|
|
|
|
|
|
|
|
if (
|
|
|
|
!has(body, 'event') ||
|
|
|
|
!has(body, 'timestamp') ||
|
|
|
|
!has(body, 'shortId')
|
|
|
|
) {
|
|
|
|
console.warn('Popularity event recieved from client is malformed');
|
|
|
|
console.log(JSON.stringify(body, null, 2));
|
|
|
|
// sending 200 because the client shouldn't care for this
|
|
|
|
return res.sendStatus(200);
|
|
|
|
}
|
|
|
|
const { shortId } = body;
|
|
|
|
log('shortId', shortId);
|
|
|
|
|
|
|
|
const articlePromise = findArticleByShortId(shortId);
|
|
|
|
const popularityPromise = findPopularityByShortId(shortId);
|
|
|
|
|
|
|
|
const [article, popularity] = await Promise.all([
|
|
|
|
articlePromise,
|
|
|
|
popularityPromise
|
|
|
|
]).catch(err => {
|
|
|
|
log('find catch');
|
|
|
|
return next(err);
|
|
|
|
});
|
|
|
|
if (!article || isEmpty(article)) {
|
|
|
|
log('No article found to handle the populartity update');
|
|
|
|
// sending 200 because the client shouldn't care for this
|
|
|
|
return res.sendStatus(200);
|
|
|
|
}
|
|
|
|
|
|
|
|
const populartiyUpdate = {
|
|
|
|
...body,
|
|
|
|
byAuthenticatedUser: !!user
|
|
|
|
};
|
|
|
|
|
|
|
|
const populartiyUpdateOrCreatePromise = isEmpty(popularity)
|
|
|
|
? createPopularity(populartiyUpdate, shortId)
|
|
|
|
: updatePopularity(popularity, populartiyUpdate);
|
|
|
|
const maybeUpdateArticlePromise =
|
|
|
|
body.event === 'view' ? incrementArticleViews(article) : null;
|
|
|
|
return Promise.all([
|
|
|
|
populartiyUpdateOrCreatePromise,
|
|
|
|
maybeUpdateArticlePromise
|
|
|
|
])
|
|
|
|
.then(() => res.sendStatus(200))
|
|
|
|
.catch(err => {
|
|
|
|
log('updates catch');
|
|
|
|
return next(err);
|
|
|
|
});
|
|
|
|
};
|
2018-08-03 10:41:17 +00:00
|
|
|
}
|
|
|
|
|
2018-11-29 12:12:15 +00:00
|
|
|
function createReferralHandler(app) {
|
|
|
|
const { Popularity } = app.models;
|
|
|
|
|
|
|
|
return function referralHandler(query, shortId, byAuthenticatedUser) {
|
|
|
|
if (!query.refsource) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
const eventUpdate = {
|
|
|
|
event: `referral - ${query.refsource}`,
|
|
|
|
timestamp: new Date(Date.now()),
|
|
|
|
byAuthenticatedUser
|
|
|
|
};
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
Popularity.findOne(
|
|
|
|
{ where: { articleId: shortId } },
|
|
|
|
(err, popularity) => {
|
|
|
|
if (err) {
|
|
|
|
console.error(
|
|
|
|
'Failed finding a `Popularity` in a referral handler',
|
|
|
|
err
|
|
|
|
);
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (popularity) {
|
|
|
|
return popularity.updateAttribute(
|
|
|
|
'events',
|
|
|
|
[eventUpdate, ...popularity.events],
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('populartiy updated');
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return Popularity.create(
|
|
|
|
{
|
|
|
|
events: [eventUpdate],
|
|
|
|
articleId: shortId
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
log('poulartiy created');
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|