snoode
Version:
node and browser reddit api library
388 lines (304 loc) • 8.19 kB
JavaScript
import superagent from 'superagent';
import ValidationError from '../errors/validationError';
import NoModelError from '../errors/noModelError';
import NotImplementedError from '../errors/notImplementedError';
const EVENTS = {
request: 'request',
response: 'response',
error: 'error',
};
const TYPES = {
't1': 'comments',
't2': 'users',
't3': 'links',
't4': 'messages',
't5': 'subreddits',
't6': 'trophies',
't8': 'promocampaigns',
};
class BaseAPI {
constructor(base) {
this.config = base.config;
this.cache = base.cache;
this.event = base.event;
if (base.config) {
this.origin = base.config.origin;
if (base.config.origins) {
let name = this.constructor.name.toLowerCase();
this.origin = base.config.origins[name] ||
this.config.origin;
}
}
}
// Used to format/unformat for caching; `links` or `comments`, for example.
// Should match the constructor name.
get dataType () {
return this.constructor.name.toLowerCase();
}
get api () {
return this.constructor.name.toLowerCase();
}
get requestCacheRules () {
let api = this;
return {
name: api.api,
cache: {
max: 10,
maxAge: 1000 * 60 * 5,
},
format: api.formatCacheData,
unformat: api.unformatCacheData,
rules: [
api.clientCacheRule,
],
};
}
path (method, query={}) {
let basePath = this.api;
if (['string', 'number'].contains(typeof query.id)) {
basePath += `/${query.id}`;
}
return basePath;
}
fullPath (method, query={}) {
return `${this.origin}/${this.path(method, query)}`;
}
formatMeta(res) {
return res.headers;
}
formatBody(res) {
return res.body;
}
buildQueryParams(method, data) {
return [
data,
method,
data,
];
}
buildAuthHeader () {
let token = this.config.token;
if (token) {
return { Authorization: `Bearer ${token}` };
}
return {};
}
buildHeaders () {
return this.config.defaultHeaders || {};
}
formatQuery (query) {
return query;
}
formatData (data) {
return data;
}
runQuery = (method, query) => {
const originalMethod = method;
query = this.formatQuery(query, method);
let handle = this.handle;
let path = this.fullPath(method, query);
const fakeReq = { url: path, method, query };
this.event.emit(EVENTS.request, fakeReq);
method = query._method || method;
delete query._method;
return new Promise((resolve, reject) => {
let s = superagent[method](path).timeout(this.config.timeout || 5000);
if (s.redirects) {
s.redirects(0);
}
s.query(query);
s.set(this.buildAuthHeader());
s.set(this.buildHeaders());
if (query.id && !Array.isArray(query.id)) {
delete query.id;
}
s.end((err, res) => {
if (err && err.timeout) {
err.status = 504;
}
const origin = this.origin;
const path = this.path(method, query);
const fakeReq = { origin, path, method, query };
const req = res ? res.request : fakeReq;
handle(resolve, reject)(err, res, req, originalMethod);
});
});
}
rawSend(method, path, data, type='form', cb) {
const origin = this.origin;
let s = superagent[method](`${origin}/${path}`);
s.type(type);
if (this.config.token) {
s.set('Authorization', 'bearer ' + this.config.token);
}
s.send(data).end((err, res) => {
const fakeReq = {
origin,
path,
method,
query: data,
};
const req = res ? res.request : fakeReq;
cb(err, res, req);
});
}
save (method, data={}) {
return new Promise((resolve, reject) => {
if (!data) {
return reject(new NoModelError(this.api));
}
data = this.formatQuery(data, method);
if (this.model) {
let model = new this.model(data);
let keys;
// Only validate keys being sent in, if this is a patch
if (method === 'patch') {
keys = Object.keys(data);
}
const valid = model.validate(keys);
if (valid !== true) {
return reject(new ValidationError(this.api, model));
}
if (method !== 'patch') {
data = model.toJSON();
}
}
const path = this.path(method, data);
const _method = method;
method = data._method || method;
const type = data._type;
data = this.formatData(data, _method);
this.rawSend(method, path, data, type, (err, res, req) => {
if (!err && res) {
if (this.cache.dataCache[this.dataType] && this.cache.requestCache.get(this.api)) {
this.cache.resetRequests(this.api);
this.cache.resetData(this.dataType, res.body);
}
}
this.handle(resolve, reject)(err, res, req, method);
});
});
}
head (query={}) {
const headCache = this.cache.head(this.api, this.buildQueryParams('get', query));
if (headCache) {
return Promise.resolve(headCache);
}
return this.runQuery('head', query);
}
get (query) {
query = {
raw_json: 1,
...(query || {}),
};
if (query.id) {
return this.cache.getById(
this.fullPath('get', query),
query.id,
this.runQuery,
['get', query],
this.requestCacheRules
);
}
return this.cache.get(
this.runQuery,
['get', query],
this.requestCacheRules
);
}
del (query={}) {
if (this.cache.dataCache[this.dataType]) {
if (query.id) {
this.cache.deleteData(this.dataType, query.id);
} else {
this.cache.resetData(this.dataType);
}
this.cache.resetRequests(this.api);
}
return this.runQuery('del', query);
}
post (data) {
return this.save('post', data);
}
put (data) {
return this.save('put', data);
}
patch (data) {
return this.save('patch', data);
}
// Get the source, then save it, modified by data.
copy (fromId, data) {
return new Promise((resolve, reject) => {
this.get(fromId).then(oldData => {
this.save('copy', {
...oldData,
_method: data.id ? 'put' : 'post',
...data,
}).then(resolve, reject);
});
});
}
// Get the old one, save the new one, then delete the old one if save succeeded
move (fromId, toId, data) {
return new Promise((resolve, reject) => {
this.get(fromId).then(oldData => {
this.save('move', {
_method: 'put',
...oldData,
id: toId,
...data,
}).then(data => {
this.del({ id: fromId }).then(() => { resolve(data); }, reject);
}, reject);
});
});
}
notImplemented (method) {
return function() {
throw new NotImplementedError(method, this.api);
};
}
handle = (resolve, reject) => {
return (err, res, req, method) => {
// lol handle the twelve ways superagent sends request back
if (res && !req) {
req = res.request || res.req;
}
if (err || (res && !res.ok)) {
//this.event.emit(EVENTS.error, err, req);
if (this.config.defaultErrorHandler) {
return this.config.defaultErrorHandler(err || 500);
} else {
return reject(err || 500);
}
}
this.event.emit(EVENTS.response, req, res);
let headers;
let body;
try {
headers = this.formatMeta(res, req, method);
body = this.formatBody(res, req, method);
} catch (e) {
return reject(e);
}
resolve({ headers, body });
};
}
formatCacheData = (data) => {
let api = this.dataType;
return {
[api]: data,
};
}
unformatCacheData = (data) => {
return data[this.dataType];
}
clientCacheRule = (/*options*/) => {
return this.config.env === 'CLIENT';
};
static thingType (id) {
return TYPES[id.substring(0, 2)];
}
static EVENTS = EVENTS;
}
export default BaseAPI;