Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
coverage/
.idea/
101 changes: 69 additions & 32 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,41 @@
const transform = require('lodash/transform');
const EventEmitter = require('events');
const stopcock = require('stopcock');
const got = require('got');
const url = require('url');

const pkg = require('./package');
const resources = require('./resources');

// got is now ESM-only, so we need to use dynamic import
// We lazy load it only when needed (when a request is made)
let got;
let gotPromise;

/**
* Ensures got is loaded before use
* @private
* @return {Promise}
*/
function ensureGot() {
if (!gotPromise) {
gotPromise = import('got').then((module) => {
got = module.default;
return got;
});
}
return gotPromise;
}

/**
* Converts a URL object to a string URL for got v12+
* @param {Object} urlObj URL object with pathname, hostname, protocol, search, etc.
* @return {String} Full URL string
* @private
*/
function urlObjToString(urlObj) {
return url.format(urlObj);
}

const retryableErrorCodes = new Set([
'ETIMEDOUT',
'ECONNRESET',
Expand Down Expand Up @@ -155,7 +184,7 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) {
parseJson: this.options.parseJson,
responseType: 'json',
stringifyJson: this.options.stringifyJson,
timeout: this.options.timeout
timeout: { request: this.options.timeout }
};

const afterResponse = (res) => {
Expand Down Expand Up @@ -189,45 +218,49 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) {
statusCodes: retryableStatusCodesArray
};
} else {
options.retry = 0;
options.retry = { limit: 0 };
}

return got(uri, options).then((res) => {
const body = res.body;

if (res.statusCode === 202 && res.headers['location']) {
const retryAfter = res.headers['retry-after'] * 1000 || 0;
const { pathname, search } = url.parse(res.headers['location']);
const urlString = urlObjToString(uri);

return delay(retryAfter).then(() => {
const uri = { pathname, ...this.baseUrl };
return ensureGot()
.then(() => got(urlString, options))
.then((res) => {
const body = res.body;

if (search) uri.search = search;
if (res.statusCode === 202 && res.headers['location']) {
const retryAfter = res.headers['retry-after'] * 1000 || 0;
const { pathname, search } = url.parse(res.headers['location']);

return this.request(uri, 'GET', key);
});
}

const data = key ? body[key] : body || {};
return delay(retryAfter).then(() => {
const uri = { pathname, ...this.baseUrl };

if (res.headers.link) {
const link = parseLinkHeader(res.headers.link);
if (search) uri.search = search;

if (link.next) {
Object.defineProperties(data, {
nextPageParameters: { value: link.next.query }
return this.request(uri, 'GET', key);
});
}

if (link.previous) {
Object.defineProperties(data, {
previousPageParameters: { value: link.previous.query }
});
const data = key ? body[key] : body || {};

if (res.headers.link) {
const link = parseLinkHeader(res.headers.link);

if (link.next) {
Object.defineProperties(data, {
nextPageParameters: { value: link.next.query }
});
}

if (link.previous) {
Object.defineProperties(data, {
previousPageParameters: { value: link.previous.query }
});
}
}
}

return data;
});
return data;
});
};

/**
Expand Down Expand Up @@ -283,7 +316,7 @@ Shopify.prototype.graphql = function graphql(data, variables) {
method: 'POST',
parseJson: this.options.parseJson,
responseType: 'json',
timeout: this.options.timeout
timeout: { request: this.options.timeout }
};

const updateGqlLimits = (res) => {
Expand Down Expand Up @@ -334,10 +367,14 @@ Shopify.prototype.graphql = function graphql(data, variables) {
statusCodes: retryableStatusCodesArray
};
} else {
options.retry = 0;
options.retry = { limit: 0 };
}

return got(uri, options).then(responseData);
const urlString = urlObjToString(uri);

return ensureGot()
.then(() => got(urlString, options))
.then(responseData);
};

resources.registerAll(Shopify);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"test": "test"
},
"engines": {
"node": ">=10.0.0"
"node": ">=20.0.0"
},
"files": [
"resources",
Expand All @@ -17,7 +17,7 @@
"types/index.d.ts"
],
"dependencies": {
"got": "^11.1.4",
"got": "^14.6.4",
"lodash": "^4.17.10",
"qs": "^6.5.2",
"stopcock": "^1.0.0"
Expand Down
37 changes: 27 additions & 10 deletions test/shopify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe('Shopify', () => {
const expect = require('chai').expect;
const format = require('url').format;
const nock = require('nock');
const got = require('got');
const qs = require('qs');

const Blog = require('../resources/blog');
Expand All @@ -25,6 +24,18 @@ describe('Shopify', () => {
const shopifyWithRetries = common.shopifyWithRetries;
const shopName = common.shopName;

// got is now ESM-only, so we need to use dynamic import
let got;
before(async () => {
const gotModule = await import('got');
got = gotModule.default;
// Attach error classes to got for backward compatibility in tests
got.RequestError = gotModule.RequestError;
got.TimeoutError = gotModule.TimeoutError;
got.ParseError = gotModule.ParseError;
got.HTTPError = gotModule.HTTPError;
});

it('exports the constructor', () => {
expect(Shopify).to.be.a('function');
});
Expand Down Expand Up @@ -207,7 +218,9 @@ describe('Shopify', () => {
},
(err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal('Response code 400 (Bad Request)');
expect(err.message).to.include(
'Request failed with status code 400 (Bad Request)'
);
}
);
});
Expand Down Expand Up @@ -597,7 +610,9 @@ describe('Shopify', () => {

return shopifyWithRetries.product.get(10).catch((err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal('Response code 404 (Not Found)');
expect(err.message).to.include(
'Request failed with status code 404 (Not Found)'
);
});
});

Expand All @@ -608,8 +623,8 @@ describe('Shopify', () => {

return shopifyWithRetries.product.update(10).catch((err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal(
'Response code 422 (Unprocessable Entity)'
expect(err.message).to.include(
'Request failed with status code 422 (Unprocessable Entity)'
);
});
});
Expand All @@ -621,8 +636,8 @@ describe('Shopify', () => {

return shopifyWithRetries.product.update(10).catch((err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal(
'Response code 422 (Unprocessable Entity)'
expect(err.message).to.include(
'Request failed with status code 422 (Unprocessable Entity)'
);
});
});
Expand All @@ -636,8 +651,8 @@ describe('Shopify', () => {

return shopifyWithRetries.product.update(10).catch((err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal(
'Response code 422 (Unprocessable Entity)'
expect(err.message).to.include(
'Request failed with status code 422 (Unprocessable Entity)'
);
});
});
Expand Down Expand Up @@ -912,7 +927,9 @@ describe('Shopify', () => {
},
(err) => {
expect(err).to.be.an.instanceof(got.HTTPError);
expect(err.message).to.equal('Response code 400 (Bad Request)');
expect(err.message).to.include(
'Request failed with status code 400 (Bad Request)'
);
}
);
});
Expand Down