From ef1a2e73b59ef15d9d2da3de86e329f7e0a53a27 Mon Sep 17 00:00:00 2001 From: Kyle Tate Date: Sat, 5 Apr 2025 15:01:35 -0400 Subject: [PATCH] allow for afterResponse hooks to run before graphql errors are thrown --- index.js | 25 ++++++++------- test/shopify.test.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 14616cfd..12acaade 100644 --- a/index.js +++ b/index.js @@ -286,16 +286,17 @@ Shopify.prototype.graphql = function graphql(data, variables) { timeout: this.options.timeout }; - const afterResponse = (res) => { - if (res.body) { - if (res.body.extensions && res.body.extensions.cost) { - this.updateGraphqlLimits(res.body.extensions.cost); - } + const updateGqlLimits = (res) => { + if (res.body && res.body.extensions && res.body.extensions.cost) { + this.updateGraphqlLimits(res.body.extensions.cost); + } - if (Array.isArray(res.body.errors)) { - // Make Got consider this response errored and retry if needed. - throw new Error(res.body.errors[0].message); - } + return res; + }; + + const maybeError = (res) => { + if (res.body && Array.isArray(res.body.errors)) { + throw new Error(res.body.errors[0].message); } return res; @@ -303,19 +304,21 @@ Shopify.prototype.graphql = function graphql(data, variables) { if (this.options.hooks) { options.hooks = { ...this.options.hooks }; - options.hooks.afterResponse = [afterResponse]; + options.hooks.afterResponse = [updateGqlLimits]; options.hooks.beforeError = [decorateError]; if (this.options.hooks.afterResponse) { options.hooks.afterResponse.push(...this.options.hooks.afterResponse); } + options.hooks.afterResponse.push(maybeError); + if (this.options.hooks.beforeError) { options.hooks.beforeError.push(...this.options.hooks.beforeError); } } else { options.hooks = { - afterResponse: [afterResponse], + afterResponse: [updateGqlLimits, maybeError], beforeError: [decorateError] }; } diff --git a/test/shopify.test.js b/test/shopify.test.js index f512584b..a3cb2a47 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -917,6 +917,81 @@ describe('Shopify', () => { ); }); + it('can add a hook to not throw an error when the response has errors', () => { + const customerDataErrors = [ + { + message: + 'This app is not approved to use the email field. See https://partners.shopify.com/1/apps/1/customer_data for more details.', + path: ['customers', 'edges', '0', 'node', 'email'], + extensions: { + code: 'ACCESS_DENIED', + documentation: + 'https://partners.shopify.com/1/apps/1/customer_data', + requiredAccess: + 'Shopify approval is required before using the email field.' + } + }, + { + message: + 'This app is not approved to use the firstName field. See https://partners.shopify.com/1/apps/1/customer_data for more details.', + path: ['customers', 'edges', '0', 'node', 'firstName'], + extensions: { + code: 'ACCESS_DENIED', + documentation: + 'https://partners.shopify.com/1/apps/1/customer_data', + requiredAccess: + 'Shopify approval is required before using the firstName field.' + } + } + ]; + + let calledWithErrors = undefined; + + const shopify = new Shopify({ + shopName, + accessToken, + hooks: { + afterResponse: [ + (res) => { + if (res.body && res.body.errors) { + calledWithErrors = res.body.errors; + + res.body.errors = undefined; + } + + return res; + } + ] + } + }); + + scope.post('/admin/api/graphql.json').reply(200, { + data: { + customers: { + edges: [ + { + node: { + id: 'gid://shopify/Customer/1234567890', + email: null, + firstName: null + } + } + ] + } + }, + errors: customerDataErrors + }); + + return shopify.graphql('query').then((result) => { + expect(calledWithErrors).to.deep.equal(customerDataErrors); + expect(result.customers.edges[0].node.id).to.equal( + 'gid://shopify/Customer/1234567890' + ); + expect(result.customers.edges[0].node.email).to.equal(null); + expect(result.customers.edges[0].node.firstName).to.equal(null); + }); + }); + it('uses basic auth as intended', () => { const shopify = new Shopify({ shopName, apiKey, password });