rollodeqc-gh-search-users
Version:
RoLLodeQc module to search GitHub users.
184 lines (158 loc) • 5.72 kB
JavaScript
/*
RoLLodeQc module to search GitHub users.
Copyright 2016 Robin Millette <http://robin.millette.info/>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the
[GNU Affero General Public License](LICENSE.md)
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// core
const qs = require('querystring')
// npm
const flow = require('lodash.flow')
// const omitBy = require('lodash.omitby')
const deburr = require('lodash.deburr')
const flatten = require('lodash.flatten')
const uniq = require('lodash.uniq')
const partial = require('lodash.partial')
// own
const utils = require('rollodeqc-gh-utils')
// data
const packageJson = require('./package.json')
const userAgent = [packageJson.repository, packageJson.version].join(' v')
const earlyReject = function (query) {
if (typeof query !== 'object' && typeof query !== 'string') {
throw new Error('`query` required (string or object)')
}
if (typeof query === 'string') {
if (query.slice(0, 4) === 'http') {
const f = query.slice(4, 5)
let i
if (f === ':') { i = 4 } else if (f === 's') { i = 5 }
if (i && query.slice(i, i + 3) === '://') { query = { u: query } }
}
if (!query.u) { query = { q: query } }
}
if (!query.q && !query.o && !query.u) {
throw new Error('either `query.q` or `query.o` should be provided and not empty')
}
if ([undefined, 'followers', 'repositories', 'joined'].indexOf(query.sort) === -1) {
throw new Error('`query.sort` should be one of "followers", "repositories" or "joined"')
}
if ([undefined, 'asc', 'desc'].indexOf(query.order) === -1) {
throw new Error('`query.order` should be one of "asc" or "desc"')
}
return query
}
const locationQuery = function (query, location, not) {
const loc = not ? '-location' : 'location'
if (typeof location === 'string') {
location = [location]
} else if (typeof location !== 'object') {
throw new Error('location must be a string or an array of strings')
}
uniq(flatten(location
.map((i) => i.toLowerCase().replace(' ', '-'))
.map((i) => [i, deburr(i)])
)).forEach((i) => { query.q.push(loc + ':' + i) })
return query
}
const notQuery = function (query) {
if (query.n && query.n.location) {
query.q = [query.q]
query = locationQuery(query, query.n.location, true)
query.q = query.q.join(' ')
}
return query
}
const rangeQuery = function (field, query) {
if (!query.o[field]) { return query }
const str = field === 'created'
? '>=2016-03-25; <=2016-03-30; 2016-03-25..2016-03-30'
: '>=5; <=10; 5..10'
if (typeof query.o[field] === 'string') {
query.q.push(field + ':' + query.o[field])
} else {
throw new Error('`query.o.' + field + '` should be a string (' + str + ')')
}
return query
}
const inQuery = function (query) {
if (!query.o.in) { return query }
const good = ['email', 'login', 'fullname']
if (typeof query.o.in === 'string') {
query.o.in = query.o.in === 'all' ? good.slice() : [query.o.in]
}
if (typeof query.o.in !== 'object') {
throw new Error('`query.o.in` should be a string or an array of "email", "login" or "fullname"')
}
let bad = false
query.o.in.forEach((i) => {
if (good.indexOf(i) === -1) {
bad = true
} else {
query.q.push('in:' + i)
}
})
if (bad) {
throw new Error('`query.o.in` should be a string or an array of "email", "login" or "fullname"')
}
return query
}
const beginDoQuery = flow(inQuery, partial(rangeQuery, 'repos'),
partial(rangeQuery, 'followers'), partial(rangeQuery, 'created'))
const doQuery = function (query) {
if (query.u || query.q) { return query }
query.q = []
if (query.o.string) { query.q.push(query.o.string) }
if (query.o.type) { query.q.push('type:' + query.o.type) }
query = beginDoQuery(query)
if (query.o.location) { query = locationQuery(query, query.o.location) }
if (query.o.language) {
if (typeof query.o.language === 'string') {
query.o.language = [query.o.language]
} else if (typeof query.o.language !== 'object') {
throw new Error('`query.o.language` must be a string or an array of strings')
}
query.o.language.forEach((i) => { query.q.push('language:' + i.toLowerCase().replace(' ', '-')) })
}
if (query.q.length) {
query.q = query.q.join(' ')
} else {
throw new Error('either `query.q` or `query.o` should be provided and not empty')
}
return query
}
const begin = flow(earlyReject, doQuery, notQuery)
/*
const itemsOmitter = (value, key) =>
key === 'gravatar_id' || key === 'url' || key.slice(-4) === '_url'
const chosenFields = (i) => omitBy(i, itemsOmitter)
*/
module.exports = function (query, token) {
try { query = begin(query) } catch (e) { return Promise.reject(e) }
if (!query.per_page) { query.per_page = 100 }
const opts = {
headers: { 'user-agent': userAgent },
retry: {
retries: 5,
statusCodes: [
403, 408, 413, 429, 500, 502, 503, 504
]
}
}
if (token) { opts.token = token }
return utils.got(query.u ? query.u : 'search/users?' + qs.stringify(query), opts)
.then((body) => {
body.items = body.items.map((i) => utils.chosenFields(i))
return body
})
}