@r/api-client
Version:
node and browser reddit api library
352 lines (275 loc) • 8.06 kB
JavaScript
import superagent from 'superagent';
import { thingType } from '../models2/thingTypes';
import ValidationError from '../errors/validationError';
import NoModelError from '../errors/noModelError';
import NotImplementedError from '../errors/notImplementedError';
import APIResponse from './APIResponse';
const EVENTS = {
request: 'request',
response: 'response',
error: 'error',
result: 'result',
};
export default class BaseAPI {
constructor(base) {
this.config = base.config;
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;
}
}
['path', 'head', 'get', 'post', 'patch', 'put', 'del', 'move', 'copy'].forEach(method => {
this[method] = this[method].bind(this);
});
}
// 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();
}
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;
}
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;
}
parseBody(res, apiResponse/*, req, method*/) {
apiResponse.addResult(res.body);
return;
}
formatData (data) {
return data;
}
runQuery = (method, rawQuery) => {
const originalMethod = method;
const query = this.formatQuery({ ...rawQuery}, method);
query.app = this.appParameter;
let handle = this.handle;
let path = this.fullPath(method, { ...rawQuery});
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, rawQuery);
const fakeReq = { origin, path, method, query };
const req = res ? res.request : fakeReq;
handle(resolve, reject)(err, res, req, rawQuery, originalMethod);
});
});
}
rawSend(method, path, data, cb) {
const origin = this.origin;
let s = superagent[method](`${origin}/${path}`);
s.type('form');
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);
});
}
get appParameter() {
return `${this.config.appName}-${this.config.env}`;
}
save (method, data={}) {
return new Promise((resolve, reject) => {
if (!data) {
return reject(new NoModelError(this.api));
}
data = this.formatQuery(data, method);
data.app = this.appParameter;
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;
data = this.formatData(data, _method);
this.rawSend(method, path, data, (err, res, req) => {
this.handle(resolve, reject)(err, res, req, data, method);
});
});
}
head (query={}) {
return this.runQuery('head', query);
}
get (query) {
query = {
raw_json: 1,
...(query || {}),
};
return this.runQuery('get', query);
}
del (query={}) {
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, query, 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 meta;
let body;
let apiResponse;
try {
meta = this.formatMeta(res, req, method);
const start = Date.now();
apiResponse = new APIResponse(meta, query);
try {
this.parseBody(res, apiResponse, req, method);
this.parseTime = Date.now() - start;
} catch (e) {
this.event.emit(EVENTS.error, e, req);
console.trace(e);
}
if (this.formatBody) { // shim for older apis or ones were we haven't figured out normalization yet
body = this.formatBody(res, req, method);
}
} catch (e) {
if (process.env.DEBUG_API_CLIENT_BASE) {
console.trace(e);
}
return reject(e);
}
this.event.emit(EVENTS.result, body || apiResponse);
resolve(body || apiResponse);
};
}
buildResponse(body, meta) {
const { results, errors, users, links, comments, messages, subreddits } = body;
if (errors) {
return { errors, meta };
}
let response = { results, meta };
if (users) { response.users = users; }
if (links) { response.links = links; }
if (comments) { response.comments = comments; }
if (messages) { response.messages = messages; }
if (subreddits) { response.subreddits = subreddits; }
return response;
}
static thingType (id) {
return thingType(id);
}
static EVENTS = EVENTS;
}