shopify-node-api
Version:
OAuth2 Module for Shopify API
334 lines (272 loc) • 11.2 kB
JavaScript
/**
* Shopify OAuth2 node.js API
*
*
*
*/
var crypto = require('crypto');
var BigJSON = require('json-bigint');
var querystring = require('querystring');
var zlib = require('zlib');
function ShopifyAPI(config) {
if (!(this instanceof ShopifyAPI)) return new ShopifyAPI(config);
if (config == null) { // == checks for null and undefined
var msg = "ShopifyAPI module expects a config object\nPlease see documentation at: https://github.com/sinechris/shopify-node-api\n";
throw new Error(msg);
}
this.config = config;
if(this.config.backoff_level){
this.config.backoff_level = parseFloat(this.config.backoff_level);
}
if (this.config.verbose !== false){
this.config.verbose = true;
}
// If any condition below is true assume the user does not want all logging
if (this.config.verbose_status === true){
this.config.verbose = false;
}
if (this.config.verbose_headers === true){
this.config.verbose = false;
}
if (this.config.verbose_api_limit === true){
this.config.verbose = false;
}
if (this.config.verbose_body === true){
this.config.verbose = false;
}
}
ShopifyAPI.prototype.buildAuthURL = function(){
var auth_url = 'https://' + this.config.shop.split(".")[0];
auth_url += ".myshopify.com/admin/oauth/authorize?";
auth_url += "client_id=" + this.config.shopify_api_key;
auth_url += "&scope=" + this.config.shopify_scope;
auth_url += "&redirect_uri=" + this.config.redirect_uri;
auth_url += "&state=" + this.config.nonce;
return auth_url;
};
ShopifyAPI.prototype.set_access_token = function(token) {
this.config.access_token = token;
};
ShopifyAPI.prototype.conditional_console_log = function(msg) {
if (this.config.verbose) {
console.log( msg );
}
// If any message type condition below is met show that message
else {
if (this.config.verbose_status === true && /^STATUS:/.test(msg) ){
console.log( msg );
}
if (this.config.verbose_headers === true && /^HEADERS:/.test(msg)){
console.log( msg );
}
if (this.config.verbose_api_limit === true && /^API_LIMIT:/.test(msg)){
console.log( msg );
}
if (this.config.verbose_body === true && /^BODY:/.test(msg)){
console.log( msg );
}
}
};
ShopifyAPI.prototype.is_valid_signature = function(params, non_state) {
if(!non_state && this.config.nonce !== params['state']){
return false;
}
var hmac = params['hmac'],
theHash = params['hmac'] || params['signature'],
secret = this.config.shopify_shared_secret,
parameters = [],
digest,
message;
for (var key in params) {
if (key !== "hmac" && key !== "signature") {
parameters.push(key + '=' + params[key]);
}
}
message = parameters.sort().join(hmac ? '&' : '');
digest = crypto
.createHmac('SHA256', secret)
.update(message)
.digest('hex');
return ( digest === theHash );
};
ShopifyAPI.prototype.exchange_temporary_token = function(query_params, callback) {
var data = {
client_id: this.config.shopify_api_key,
client_secret: this.config.shopify_shared_secret,
code: query_params['code']
},
self = this;
if (!self.is_valid_signature(query_params)) {
return callback(new Error("Signature is not authentic!"));
}
this.makeRequest('/admin/oauth/access_token', 'POST', data, function(err, body){
if (err) {
// err is either already an Error or it is a JSON object with an
// error field.
if (err.error) return callback(new Error(err.error));
return callback(err);
}
self.set_access_token(body['access_token']);
callback(null, body);
});
};
ShopifyAPI.prototype.hostname = function () {
return this.config.shop.split(".")[0] + '.myshopify.com';
};
ShopifyAPI.prototype.port = function () {
return 443;
};
ShopifyAPI.prototype.makeRequest = function(endpoint, method, data, callback, retry) {
var https = require('https'),
dataString = JSON.stringify(data),
options = {
hostname: this.hostname(),
path: endpoint,
method: method.toLowerCase() || 'get',
port: this.port(),
agent: this.config.agent,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate'
}
},
self = this;
if (this.config.access_token) {
options.headers['X-Shopify-Access-Token'] = this.config.access_token;
}
if (options.method === 'post' || options.method === 'put' || options.method === 'delete' || options.method === 'patch') {
options.headers['Content-Length'] = new Buffer(dataString).length;
}
var request = https.request(options, function(response){
self.conditional_console_log( 'STATUS: ' + response.statusCode );
self.conditional_console_log( 'HEADERS: ' + JSON.stringify(response.headers) );
if (response.headers && response.headers.http_x_shopify_shop_api_call_limit) {
self.conditional_console_log( 'API_LIMIT: ' + response.headers.http_x_shopify_shop_api_call_limit);
}
var contentEncoding = response.headers['content-encoding'];
var shouldUnzip = ['gzip', 'deflate'].indexOf(contentEncoding) !== -1;
var encoding = shouldUnzip && 'binary' || 'utf8';
var body = '';
response.setEncoding(encoding);
response.on('data', function(chunk){
self.conditional_console_log( 'BODY: ' + chunk );
body += chunk;
});
response.on('end', function(){
var delay = 0;
// If the request is being rate limited by Shopify, try again after a delay
if (response.statusCode === 429) {
return setTimeout(function() {
self.makeRequest(endpoint, method, data, callback);
}, self.config.rate_limit_delay || 10000 );
}
// If the backoff limit is reached, add a delay before executing callback function
if ((response.statusCode >= 200 || response.statusCode <= 299) && self.has_header(response, 'http_x_shopify_shop_api_call_limit')) {
var api_limit_parts = response.headers['http_x_shopify_shop_api_call_limit'].split('/');
var api_limit = parseInt(api_limit_parts[0], 10);
var api_max = parseInt(api_limit_parts[1], 10); // 40 on standard shopify accounts
var limit_rate = false;
if(self.config.backoff_level){
var used_api = api_limit / api_max;
limit_rate = (used_api > self.config.backoff_level);
self.conditional_console_log('FRACTION_USED: '+ used_api +' of '+ self.config.backoff_level);
}else limit_rate = (api_limit >= (self.config.backoff || 35));
if(limit_rate){
self.conditional_console_log('RATE DELAY: '+ api_limit +' of '+ api_max);
delay = self.config.backoff_delay || 1000; // in ms
}
}
setTimeout(function(){
var json = {}
, error;
function parseResponse(body) {
try {
if (body.trim() != '') { //on some requests, Shopify retuns an empty body (several spaces)
json = BigJSON.parse(body);
}
} catch(e) {
error = e;
}
if (response.statusCode >= 400) {
if (json && (json.hasOwnProperty('error_description') || json.hasOwnProperty('error') || json.hasOwnProperty('errors'))) {
var jsonError = (json.error_description || json.error || json.errors);
}
error = {
code: response.statusCode
, error: jsonError || response.statusMessage
};
}
return callback(error, json, response.headers, options);
}
// Use GZIP decompression if required
if (shouldUnzip) {
var unzip = contentEncoding === 'deflate' && zlib.deflate || zlib.gunzip;
return unzip(new Buffer(body, 'binary'), function(err, data) {
if (err) {
return callback(err, null, response.headers, options);
}
return parseResponse(data.toString('utf8'));
});
}
return parseResponse(body);
}, delay); // Delay the callback if we reached the backoff limit
});
});
request.on('error', function(e){
self.conditional_console_log( "Request Error: ", e );
if(self.config.retry_errors && !retry){
var delay = self.config.error_retry_delay || 10000;
self.conditional_console_log( "retrying once in " + delay + " milliseconds" );
setTimeout(function() {
self.makeRequest(endpoint, method, data, callback, true);
}, delay );
} else{
callback(e);
}
});
if (options.method === 'post' || options.method === 'put' || options.method === 'delete' || options.method === 'patch') {
request.write(dataString);
}
request.end();
};
ShopifyAPI.prototype.get = function(endpoint, data, callback) {
if (typeof data === 'function' && arguments.length < 3) {
callback = data;
data = null;
} else {
if(data){
endpoint += ((endpoint.indexOf('?') == -1) ? '?' : '&') + querystring.stringify(data);
}
data = null;
}
this.makeRequest(endpoint,'GET', data, callback);
};
ShopifyAPI.prototype.post = function(endpoint, data, callback) {
this.makeRequest(endpoint,'POST', data, callback);
};
ShopifyAPI.prototype.put = function(endpoint, data, callback) {
this.makeRequest(endpoint, 'PUT', data, callback);
};
ShopifyAPI.prototype.delete = function(endpoint, data, callback) {
if (arguments.length < 3) {
if (typeof data === 'function') {
callback = data;
data = null;
} else {
callback = new Function;
data = typeof data === 'undefined' ? null : data;
}
}
this.makeRequest(endpoint, 'DELETE', data, callback);
};
ShopifyAPI.prototype.patch = function(endpoint, data, callback) {
this.makeRequest(endpoint, 'PATCH', data, callback);
};
ShopifyAPI.prototype.has_header = function(response, header) {
return response.headers.hasOwnProperty(header) ? true : false;
};
ShopifyAPI.prototype.graphql = function(data, callback) {
this.makeRequest('/admin/api/graphql.json','POST', data, callback);
};
module.exports = ShopifyAPI;