Files
wumpfetch/lib/model/WumpRequest.js
2019-12-12 23:57:15 -07:00

254 lines
8.1 KiB
JavaScript

const zlib = require('zlib');
const http = require('http');
const https = require('https');
const { URL } = require('url');
const { stringify } = require('querystring');
const { join: pJoin } = require('path');
const wump = require('../index');
const WumpRes = require('./WumpResponse');
const metaData = require('../../package.json');
const compressions = [ 'gzip', 'deflate' ];
module.exports = class WumpRequest {
/**
* Create the initial request
*
* @param {object | string} [url={}] - The URL to send the request to or a `ReqOptions` object
* @param {object | string} [method={}] - The method to use when sending the request or a `ReqOptions` object
* @throws {Error} - Missing URL parameter
* @returns {WumpRequest} - The request class
*/
constructor (url = {}, method = {}) {
const options = (typeof url === 'string' ? method : url);
if (typeof url !== 'string' && !options.hasOwnProperty('url')) throw new Error('Missing URL parameter');
this.o = {
'm': typeof method === 'string' ? method : (options.method || 'GET'),
'url': typeof url === 'string' ? new URL(url) : typeof options.url === 'string' ? new URL(options.url) : url,
'SDA': typeof options.sendDataAs === 'string' ? options.sendDataAs : options.data && typeof options.data === 'object' ? 'json' : undefined,
'data': options.body || options.data || options.json || (options.form ? stringify(options.form) : undefined),
'parse': options.parse,
'chain': typeof options.chain === 'boolean' ? options.chain : true,
'follow': !!(options.followRedirects),
'rHeaders': typeof options.headers === 'object' ? options.headers : {},
'streamed': !!(options.streamed),
'compressed': !!(options.compressed),
'timeoutTime': typeof options.timeout === 'number' ? options.timeout : null,
'coreOptions': typeof options.coreOptions === 'object' ? options.coreOptions : {}
};
if (typeof options.coreOptions === 'object') {
for (const key of Object.keys(options.coreOptions)) {
this.option(key, options.coreOptions[key]);
}
}
if (!this.o.chain) {
return this.send();
}
return this;
}
/**
* Append the request's body
*
* @param {any} data - The data to append
* @param {string} sendAs - The way to send the data, either `json`, `form` or `buffer`
* @returns {WumpRequest} - The request class
*/
body(data, sendAs) {
this.o.SDA = typeof data === 'object' && !sendAs && !Buffer.isBuffer(data) ? 'json' : sendAs ? sendAs.toLowerCase() : 'buffer';
this.o.data = this.sendAs === 'form' ? stringify(data) : this.SDA === 'json' ? JSON.stringify(data) : data;
return this;
}
/**
* Update the path of the URL (protocol://url.domain/:path)
*
* @param {string} path - The path to append to the url
* @returns {WumpRequest} - The request class
*/
path(path) {
this.o.url.pathname = pJoin(this.o.url.pathname, path);
return this;
}
/**
* Append the request's URL with a query (?name=value)
*
* @param {object | string} name - The name of the query or an object with multiple queries
* @param {string} value - The value of the query if `name` is of type `string`
* @returns {WumpRequest} - The request class
*/
query (name, value) {
if (typeof name === 'object') {
for (const key of Object.keys(name)) {
this.o.url.searchParams.append(key, name[key]);
}
} else this.o.url.searchParams.append(name, value);
return this;
}
/**
*
* @param {string | object} name - The name of the header or an object containing multiple headers
* @param {string} value - The content of the header if `name` is type of `string`
* @returns {WumpRequest} - The request class
*/
header (name, value) {
if (typeof name === 'object') {
for (const key of Object.keys(name)) {
this.o.rHeaders[key.toLowerCase()] = name[key];
}
} else this.o.rHeaders[name.toLowerCase()] = value;
return this;
}
/**
* Change one of NodeJS' `http` library options
*
* @param {string} name - The name of the option
* @param {any} value - The value of the optiob
* @returns {WumpRequest} - The request class
*/
option(name, value) {
this.o.coreOptions[name] = value;
return this;
}
/**
* Add a request timeout
*
* @param {number} timeout - The time before timing out (ms)
* @returns {WumpRequest} - The request class
*/
timeout(timeout = 0) {
this.o.timeoutTime = timeout;
return this;
}
/**
* Keep a connection alive with the server
*
* @returns {WumpRequest} - The request class
*/
stream() {
this.o.streamed = true;
return this;
}
/**
* Accept all encodings from the server
*
* @returns {WumpRequest} - The request class
*/
compress() {
this.compressed = true;
if (!this.o.rHeaders['accept-encoding']) this.o.rHeaders['accept-encoding'] = compressions.join(', ');
return this;
}
/**
* Finish off the request by sending it to the specified URL
*
* @throws {Error} - Bad URL Protocol
* @throws {Error} - Timeout reached
* @returns {Promise} - Teh response data
*/
send () {
return new Promise((resolve, reject) => {
if (this.o.data) {
if (!this.o.rHeaders.hasOwnProperty('user-agent')) this.o.rHeaders['User-Agent'] = `${metaData.name}/${metaData.version} (${metaData.repository.url})`;
if (this.o.SDA === 'json' && !this.o.rHeaders.hasOwnProperty('content-type')) this.o.rHeaders['Content-Type'] = 'application/json';
if (this.o.SDA === 'form') {
if (!this.o.rHeaders.hasOwnProperty('content-type')) this.o.rHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
if (!this.o.rHeaders.hasOwnProperty('content-length')) this.o.rHeaders['Content-Length'] = Buffer.byteLength(this.o.data);
}
}
let req;
const options = {
'host': this.o.url.hostname,
'port': this.o.url.port,
'path': `${this.o.url.pathname}${this.o.url.search}`,
'method': this.o.m,
'headers': this.o.rHeaders,
'protocol': this.o.url.protocol,
...this.o.coreOptions
};
const handler = async (res) => {
let stream = res;
if (this.o.compressed) {
switch (res.headers['content-encoding']) {
case 'gzip': stream = res.pipe(zlib.createGunzip()); break;
case 'deflate': stream = res.pipe(zlib.createInflate()); break;
}
}
let wumpRes;
if (this.o.streamed) {
resolve(stream);
} else {
wumpRes = new WumpRes(res);
if (res.headers.hasOwnProperty('location') && this.o.follow) {
this.o.url = (new URL(res.headers['location'], this.o.url)).toString();
return await wump(this.o);
}
stream.on('error', (e) => reject(e));
stream.on('data', (c) => wumpRes._addChunk(c));
stream.on('end', () => {
if (this.o.parse) {
switch (this.o.parse) {
case 'json': wumpRes.body = JSON.parse(wumpRes.body); break;
case 'text': wumpRes.body = wumpRes.body.toString(); break;
default: wumpRes.body = wumpRes.body;
}
}
resolve(wumpRes);
});
}
};
switch (this.o.url.protocol) {
case 'http:': req = http.request(options, handler); break;
case 'https:': req = https.request(options, handler); break;
default: throw new Error(`Bad URL protocol: ${this.o.url.protocol}`);
}
if (this.o.timeoutTime) {
req.setTimeout(this.o.timeoutTime, () => {
req.abort();
if (!this.o.streamed) reject(new Error('Timeout reached'));
});
}
req.on('error', (e) => reject(e));
if (this.o.data) {
if (this.o.SDA === 'json') req.write(JSON.stringify(this.o.data));
else if (this.o.data instanceof Object) req.write(JSON.stringify(this.o.data));
else req.write(this.o.data);
}
req.end();
});
}
};