SIGSEV/Starveller

View on GitHub
src/api/Repo.service.js

Summary

Maintainability
A
2 hrs
Test Coverage
import q from 'q'
import r from 'superagent'
import Vibrant from 'node-vibrant'
import moment from 'moment'

import config from 'config'
import Repo from 'api/Repo.model'
import { getSocketServer } from 'api/io'
import featuredRepos from 'data/featuredRepos'
import { initRepo } from 'api/github.worker'

let trendingRepos = []

/**
 * Fetch all repos summaries
 */
export const getAll = () =>
  q.nfcall(::Repo.find, {}, 'name summary')

/**
 * Refresh a repo if its last fetch is greater than 1 day
 */
export const checkRefresh = repoName =>
  getByName(repoName)
    .then(repo =>
        repo && moment().diff(repo.summary.lastFetch, 'days') >= 1
        ? refreshOne(repoName)
        : null)

/**
 * Refresh all the repos, scheduled each day
 */
export const refreshAll = () =>
  getAll().then(repos => repos.forEach(repo => initRepo(repo.name)))

/**
 * Refresh one repo, and perform a full refresh if boolean is true
 */
export const refreshOne = (repoName, fullRefresh) =>
  initRepo(repoName, fullRefresh)

/**
 * Retrieve the featured repos
 */
export const getFeatured = () =>
  Promise.all(featuredRepos.map(n => getByName(n, 'name summary stars')))

/**
 * Retrieve the trending repos
 */
export const getTrending = () =>
  Promise.all(trendingRepos.map(n => getByName(n, 'name summary stars')))

/**
 * Refresh the trending repos each day
 * Using Bridge API
 */
export const refreshTrending = () => {

  console.log(`Starting trending refresh.`) // eslint-disable-line

  return new Promise((resolve, reject) => {
    r.get('http://bridge.sigsev.io/github/trending')
      .end((err, res) => {
        if (err) { return reject(err) }
        const repos = res.body.slice(0, 8)
        resolve(repos.map(r => r.url.substring(1)))
      })
  })
  .then(names => Promise.all(names.map(name => initRepo(name))))
  .then(repos => {
    trendingRepos = repos.map(r => r.name)
    console.log('Finished trending refresh.') // eslint-disable-line
  })
}

/**
 * Get a full repo
 */
export const getByName = (name, projection = {}) =>
  q.nfcall(::Repo.findOne, { name }, projection)

/**
 * Get the repo ranking for badge response
 */
export const getRanking = name =>
  q.nfcall(::Repo.findOne, { name }, 'cache')
    .then(repo => repo.cache.rank)

/**
 * Get nbars for a repo
 */
export const getBars = (name, n = 20) =>
  q.nfcall(::Repo.findOne, { name }, 'stars.byDay')
    .then(({ stars: ({ byDay: stars }) }) => stars)
    .then(stars => stars.slice(-n))

/**
 * Remove a repo
 */
export const removeByName = name => q.nfcall(::Repo.remove, { name })

/**
 * Update repo
 */
export const updateByName = (name, mods) =>
  q.nfcall(::Repo.update, { name }, mods)
    .then(() => null)

/**
 * Get a populated repo, without cache
 */
export const getOnePopulated = name => q.nfcall(::Repo.findOne, { name }, '-cache')

const starProgress = {}

/**
 * Fetch github stars
 */
const fetchStarPage = (name, _id, starsCount, page, io) => {

  return q.Promise((resolve, reject) => {

    r.get(`https://api.github.com/repos/${name}/stargazers?page=${page}&per_page=100`)
      .set('Authorization', `token ${config.github}`)
      .set('Accept', 'application/vnd.github.v3.star+json')
      .end((err, res) => {

        if (err) { return reject(err) }
        // Finish once we reached the Github API limit
        if (res.statusCode === 422) { return resolve([]) }

        const stars = res.body.map(star => ({ date: star.starred_at, page }))

        starProgress[name] += res.body.length
        const value = ((starProgress[name] / starsCount) * 100).toFixed(2)

        if (io) { io.repoProgress({ _id, value }) }

        if (res.body.length === 100) {
          return fetchStarPage(name, _id, starsCount, ++page, io)
            .then(data => resolve(data.concat(stars)))
            .catch(reject)
        }

        resolve(stars)
      })

  })

}

export const fetchStars = ({ _id, name, summary: { starsCount } }, fromPage) => {
  starProgress[name] = 0
  const io = getSocketServer()
  return fetchStarPage(name, _id, starsCount, fromPage, io)
}

/**
 * Create event
 */
export const createEvent = ({ name, data: { title, link, comment } }) => {
  if (!title && !comment && !link) { return q.reject(new Error('Say something maybe?')) }
  return updateByName(name, { $push: { events: { title, link, comment } } })
}

const extractColor = url => {
  return q.Promise(resolve => {

    r.get(url)
      .end((err, res) => {
        if (err) { return resolve(null) }
        const v = new Vibrant(res.body)

        v.getPalette((err, palette) => {
          if (err || !palette.Vibrant) { return resolve(null) }
          resolve(palette.Vibrant.getHex())
        })
      })

  })
}

export const fetchRepo = name => {

  return q.Promise((resolve, reject) => {
    r.get(`https://api.github.com/repos/${name}`)
      .set('Authorization', `token ${config.github}`)
      .end((err, res) => {
        if (err) { return reject(err) }

        const src = res.body

        const picture = src.owner.avatar_url
        extractColor(picture)
          .then(mainColor => {

            resolve({
              name,
              summary: {
                picture,
                mainColor,
                lastFetch: new Date(),
                language: src.language,
                createdAt: src.created_at,
                description: src.description,
                starsCount: src.stargazers_count,
                forksCount: src.forks,
                watchersCount: src.subscribers_count
              }
            })

          })

      })
  })

}

export const ask = repoName =>
  getByName(repoName)
    .then(repo => !repo ? initRepo(repoName) : repo)