| 
									
										
										
										
											2018-11-29 12:12:15 +00:00
										 |  |  | import { has, pick, isEmpty } from 'lodash'; | 
					
						
							| 
									
										
										
										
											2018-08-03 11:41:17 +01:00
										 |  |  | import debug from 'debug'; | 
					
						
							| 
									
										
										
										
											2018-11-29 12:12:15 +00:00
										 |  |  | import { reportError } from '../middlewares/error-reporter'; | 
					
						
							| 
									
										
										
										
											2018-07-31 16:58:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-29 12:12:15 +00:00
										 |  |  | const log = debug('fcc:boot:news'); | 
					
						
							| 
									
										
										
										
											2018-08-03 11:41:17 +01: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 11:41:17 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-29 12:12:15 +00:00
										 |  |  | function createPopularityHandler(app) { | 
					
						
							|  |  |  |   const { Article, Popularity } = app.models; | 
					
						
							| 
									
										
										
										
											2018-08-03 11:41:17 +01: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 11:41:17 +01: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 11:41:17 +01: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 11:41:17 +01: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(); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ) | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } |