From 8493c36be27074f23d66a2e8f70a04b573bd3a6c Mon Sep 17 00:00:00 2001 From: Ronn <12510911+zronn@users.noreply.github.com> Date: Thu, 3 Feb 2022 20:59:35 +0100 Subject: [PATCH 001/110] [ts] Add id to IOrderShippingLine interface (#525) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index ede727e8..bd405bb5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2178,6 +2178,7 @@ declare namespace Shopify { } interface IOrderShippingLine { + id: number; code: string; discounted_price: string; discounted_price_set: IMoneySet; From a7f20a2d6c4c931ea356ac33654862ad1f69631a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 3 Feb 2022 21:01:04 +0100 Subject: [PATCH 002/110] [dist] 3.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10653a12..d0930467 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.8.1", + "version": "3.8.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From baadd6ea53e8a015f5f32b2bc2557a5087b6b286 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 2 Mar 2022 21:40:03 +0100 Subject: [PATCH 003/110] [ci] Update actions/setup-node action to v3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05d4214c..7dbe316f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - 17 steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - run: npm install From 76779d6e1b8c32f0ca794dcfb613b289b7e8524c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 9 Mar 2022 20:02:01 +0100 Subject: [PATCH 004/110] [ci] Update actions/checkout action to v3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dbe316f..015aac71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - 16 - 17 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} From 0bcb125ba777fbede74a286229ed463b0d61a1aa Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 30 Mar 2022 21:05:43 +0200 Subject: [PATCH 005/110] [api] Add DeprecatedApiCall resource Fixes: https://github.com/MONEI/Shopify-api-node/issues/531 --- README.md | 2 ++ index.d.ts | 14 +++++++++++ resources/deprecated-api-call.js | 23 +++++++++++++++++++ resources/index.js | 1 + test/deprecated-api-call.test.js | 23 +++++++++++++++++++ test/fixtures/deprecated-api-call/index.js | 3 +++ .../fixtures/deprecated-api-call/res/index.js | 3 +++ .../deprecated-api-call/res/list.json | 15 ++++++++++++ 8 files changed, 84 insertions(+) create mode 100644 resources/deprecated-api-call.js create mode 100644 test/deprecated-api-call.test.js create mode 100644 test/fixtures/deprecated-api-call/index.js create mode 100644 test/fixtures/deprecated-api-call/res/index.js create mode 100644 test/fixtures/deprecated-api-call/res/list.json diff --git a/README.md b/README.md index bae14b6c..1eee8958 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,8 @@ This feature is only available on version 2.24.0 and above. - `get(id[, params])` - `list([params])` - `update(id, params)` +- deprecatedApiCall + - `list()` - discountCode - `create(priceRuleId, params)` - `delete(priceRuleId, id)` diff --git a/index.d.ts b/index.d.ts index bd405bb5..3c99ba4d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -250,6 +250,9 @@ declare class Shopify { ) => Promise>; update: (id: number, params: any) => Promise; }; + deprecatedApiCall: { + list: () => Promise; + }; discountCode: { create: ( priceRuleId: number, @@ -1418,6 +1421,17 @@ declare namespace Shopify { custom_message: string; } + interface IDeprecatedApiCall { + api_type: string; + description: string; + documentation_url: string; + endpoint: string; + graphql_schema_name: string | null; + last_call_at: string; + migration_deadline: string; + version: string; + } + type AllocationMethod = 'across' | 'each' | 'one'; type TargetSelection = 'all' | 'entitled' | 'explicit'; type TargetType = 'line_item' | 'shipping_line'; diff --git a/resources/deprecated-api-call.js b/resources/deprecated-api-call.js new file mode 100644 index 00000000..10ad20b2 --- /dev/null +++ b/resources/deprecated-api-call.js @@ -0,0 +1,23 @@ +'use strict'; + +const assign = require('lodash/assign'); +const pick = require('lodash/pick'); + +const base = require('../mixins/base'); + +/** + * Creates a DeprecatedApiCall instance. + * + * @param {Shopify} shopify Reference to the Shopify instance + * @constructor + * @public + */ +function DeprecatedApiCall(shopify) { + this.shopify = shopify; + + this.name = 'deprecated_api_calls'; +} + +assign(DeprecatedApiCall.prototype, pick(base, ['buildUrl', 'list'])); + +module.exports = DeprecatedApiCall; diff --git a/resources/index.js b/resources/index.js index 2295f6ab..d82d5d6b 100644 --- a/resources/index.js +++ b/resources/index.js @@ -22,6 +22,7 @@ const map = { customer: 'customer', customerAddress: 'customer-address', customerSavedSearch: 'customer-saved-search', + deprecatedApiCall: 'deprecated-api-call', discountCode: 'discount-code', discountCodeCreationJob: 'discount-code-creation-job', dispute: 'dispute', diff --git a/test/deprecated-api-call.test.js b/test/deprecated-api-call.test.js new file mode 100644 index 00000000..2f3068ba --- /dev/null +++ b/test/deprecated-api-call.test.js @@ -0,0 +1,23 @@ +describe('Shopify#deprecatedApiCall', () => { + 'use strict'; + + const expect = require('chai').expect; + + const fixtures = require('./fixtures/deprecated-api-call'); + const common = require('./common'); + + const shopify = common.shopify; + const scope = common.scope; + + afterEach(() => expect(scope.isDone()).to.be.true); + + it('gets a list of deprecated API calls', () => { + const output = fixtures.res.list; + + scope.get('/admin/deprecated_api_calls.json').reply(200, output); + + return shopify.deprecatedApiCall + .list() + .then((data) => expect(data).to.deep.equal(output.deprecated_api_calls)); + }); +}); diff --git a/test/fixtures/deprecated-api-call/index.js b/test/fixtures/deprecated-api-call/index.js new file mode 100644 index 00000000..38c8b447 --- /dev/null +++ b/test/fixtures/deprecated-api-call/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.res = require('./res'); diff --git a/test/fixtures/deprecated-api-call/res/index.js b/test/fixtures/deprecated-api-call/res/index.js new file mode 100644 index 00000000..c157715b --- /dev/null +++ b/test/fixtures/deprecated-api-call/res/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.list = require('./list'); diff --git a/test/fixtures/deprecated-api-call/res/list.json b/test/fixtures/deprecated-api-call/res/list.json new file mode 100644 index 00000000..8af307be --- /dev/null +++ b/test/fixtures/deprecated-api-call/res/list.json @@ -0,0 +1,15 @@ +{ + "data_updated_at": "2020-10-13T00:15:30Z", + "deprecated_api_calls": [ + { + "api_type": "REST", + "description": "The page filter has been removed from multiple endpoints. Use cursor-based pagination instead.", + "documentation_url": "https://shopify.dev/tutorials/make-paginated-requests-to-rest-admin-api", + "endpoint": "Product", + "last_call_at": "2020-06-12T03:46:18Z", + "migration_deadline": "2020-07-02T13:00:00Z", + "graphql_schema_name": null, + "version": "2019-07" + } + ] +} From 0d4b6c91d5d879740ee70630836d1308edd82e61 Mon Sep 17 00:00:00 2001 From: Kristian PD Date: Sat, 2 Apr 2022 14:52:56 -0400 Subject: [PATCH 006/110] [ts] Add private_metafield_namespaces property to webhook types (#530) --- index.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.d.ts b/index.d.ts index 3c99ba4d..1874f8c0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3088,6 +3088,7 @@ declare namespace Shopify { format: WebhookFormat; id: number; metafield_namespaces: string[]; + private_metafield_namespaces: string[]; topic: WebhookTopic; updated_at: string; } @@ -3097,6 +3098,7 @@ declare namespace Shopify { fields?: string[]; format?: WebhookFormat; metafield_namespaces?: string[]; + private_metafield_namespaces?: string[]; topic: WebhookTopic; } @@ -3105,6 +3107,7 @@ declare namespace Shopify { fields?: string[]; format?: WebhookFormat; metafield_namespaces?: string[]; + private_metafield_namespaces?: string[]; topic: WebhookTopic; } From d03abc8a40b0acee99c7133268b7c2013987bb1c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 2 Apr 2022 20:55:04 +0200 Subject: [PATCH 007/110] [dist] 3.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0930467..cea81991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.8.2", + "version": "3.9.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From aaa94479bf6145116487808248cc04a149b89a5d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 2 May 2022 06:56:04 +0200 Subject: [PATCH 008/110] [ci] Do not test on node 17 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 015aac71..0909e3be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: - 12 - 14 - 16 - - 17 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From 2fd1eba45ef778a56122962262045a4e9272a747 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 2 May 2022 06:56:37 +0200 Subject: [PATCH 009/110] [ci] Test on node 18 --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0909e3be..aa696304 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: - 12 - 14 - 16 + - 18 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -21,9 +22,9 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: matrix.node == 16 + if: matrix.node == 18 - run: npm test - uses: coverallsapp/github-action@1.1.3 - if: matrix.node == 16 + if: matrix.node == 18 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 608345ec2ee7234906ae0c71d451ff2bf9504370 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 16 May 2022 10:52:42 +0200 Subject: [PATCH 010/110] [pkg] Update husky to version 8.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cea81991..c519606e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint": "^8.0.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^4.0.0", - "husky": "^7.0.2", + "husky": "^8.0.1", "json-bigint": "^1.0.0", "lint-staged": "^12.1.2", "mocha": "^8.0.1", From 498c2a79bafe59c9742881e68b5bd074e2938a5f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 2 Jun 2022 06:54:22 +0200 Subject: [PATCH 011/110] [pkg] Update lint-staged to version 13.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c519606e..3dc923bc 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "eslint-plugin-prettier": "^4.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", - "lint-staged": "^12.1.2", + "lint-staged": "^13.0.0", "mocha": "^8.0.1", "nock": "^13.0.1", "prettier": "^2.0.2" From 1da591864bd3836425d4104539e3fcadc0d3f809 Mon Sep 17 00:00:00 2001 From: tkalliom Date: Tue, 21 Jun 2022 22:19:09 +0300 Subject: [PATCH 012/110] [ts] Fix up ITransaction interface (#540) - Use correct type: `device_id`, and `source_name`. - Add missing fields: `authorization_expires_at`, `extended_authorization_attributes`, `payment_details`, and `payments_refund_attributes`. - Mark fields as nullable: `authorization`, `currency_exchange_adjustment`, `error_code`, `location_id`, `message`, `parent_id`, and `user_id`. --- index.d.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1874f8c0..d549f0fb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2914,31 +2914,45 @@ declare namespace Shopify { | 'refund' | 'sale' | 'void'; - type TransactionSourceName = 'android' | 'iphone' | 'pos' | 'web'; type TransactionStatus = 'error' | 'failure' | 'pending' | 'success'; + interface IExtendedAuthorizationAttributes { + standard_authorization_expires_at: string; + extended_authorization_expires_at: string; + } + + interface IPaymentsRefund { + status: TransactionStatus; + acquirer_reference_number: string; + } + interface ITransaction { amount: string; - authorization: string; + authorization: string | null; + authorization_expires_at?: string | null; created_at: string; currency: string; - currency_exchange_adjustment: ICurrencyExchangeAdjustment; - device_id: string; - error_code: TransactionErrorCode; + currency_exchange_adjustment?: ICurrencyExchangeAdjustment | null; + device_id: number | null; + error_code: TransactionErrorCode | null; + extended_authorization_attributes?: + | IExtendedAuthorizationAttributes + | Record; gateway: string; id: number; kind: TransactionKind; - location_id: number; - message: string; + location_id: number | null; + message: string | null; order_id: number; - payment_details: IPaymentDetails; - parent_id: number; + parent_id: number | null; + payment_details?: IPaymentDetails; + payments_refund_attributes?: IPaymentsRefund; processed_at: string; receipt: Record; - source_name: TransactionSourceName; + source_name: string; status: TransactionStatus; test: boolean; - user_id: number; + user_id: number | null; } interface IUsageCharge { From 0afb707d7f9b6163b91acb1d483f4398eed71074 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 21 Jun 2022 21:23:04 +0200 Subject: [PATCH 013/110] [dist] 3.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3dc923bc..194c24ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.9.0", + "version": "3.9.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 279671a9075b8de43429856057b15c8228389d6e Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Thu, 23 Jun 2022 14:20:03 -0400 Subject: [PATCH 014/110] [test] Use a more revealing assertion for leftover nock mocks (#542) --- test/access-scope.test.js | 2 +- test/api-permission.test.js | 2 +- test/application-charge.test.js | 2 +- test/application-credit.test.js | 2 +- test/article.test.js | 2 +- test/asset.test.js | 2 +- test/balance.test.js | 2 +- test/blog.test.js | 2 +- test/cancellation-request.test.js | 2 +- test/carrier-service.test.js | 2 +- test/checkout.test.js | 2 +- test/collect.test.js | 2 +- test/collection-listing.test.js | 2 +- test/collection.test.js | 2 +- test/comment.test.js | 2 +- test/country.test.js | 2 +- test/currency.test.js | 2 +- test/custom-collection.test.js | 2 +- test/customer-address.test.js | 2 +- test/customer-saved-search.test.js | 2 +- test/customer.test.js | 2 +- test/deprecated-api-call.test.js | 2 +- test/discount-code-creation-job.test.js | 2 +- test/discount-code.test.js | 2 +- test/dispute.test.js | 2 +- test/draft-order.test.js | 2 +- test/event.test.js | 2 +- test/fulfillment-event.test.js | 2 +- test/fulfillment-order.test.js | 2 +- test/fulfillment-request.test.js | 2 +- test/fulfillment-service.test.js | 2 +- test/fulfillment.test.js | 2 +- test/gift-card-adjustment.test.js | 2 +- test/gift-card.test.js | 2 +- test/inventory-item.test.js | 2 +- test/inventory-level.test.js | 2 +- test/location.test.js | 2 +- test/marketing-event.test.js | 2 +- test/metafield.test.js | 2 +- test/order-risk.test.js | 2 +- test/order.test.js | 2 +- test/page.test.js | 2 +- test/payment.test.js | 2 +- test/payout.test.js | 2 +- test/policy.test.js | 2 +- test/price-rule.test.js | 2 +- test/product-image.test.js | 2 +- test/product-listing.test.js | 4 ++-- test/product-resource-feedback.test.js | 2 +- test/product-variant.test.js | 4 ++-- test/product.test.js | 4 ++-- test/province.test.js | 2 +- test/recurring-application-charge.test.js | 2 +- test/redirect.test.js | 2 +- test/refund.test.js | 2 +- test/report.test.js | 2 +- test/resource-feedback.test.js | 2 +- test/script-tag.test.js | 2 +- test/shipping-zone.test.js | 2 +- test/shop.test.js | 2 +- test/shopify.test.js | 4 ++-- test/smart-collection.test.js | 2 +- test/storefront-access-token.test.js | 2 +- test/tender-transaction.test.js | 2 +- test/theme.test.js | 2 +- test/transaction.test.js | 2 +- test/usage-charge.test.js | 2 +- test/user.test.js | 2 +- test/webhook.test.js | 2 +- 69 files changed, 73 insertions(+), 73 deletions(-) diff --git a/test/access-scope.test.js b/test/access-scope.test.js index f2a9a6ff..a16832d9 100644 --- a/test/access-scope.test.js +++ b/test/access-scope.test.js @@ -9,7 +9,7 @@ describe('Shopify#accessScope', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of access scopes associated to the access token', () => { const output = fixtures.res.list; diff --git a/test/api-permission.test.js b/test/api-permission.test.js index 1fa95f5a..2178c99f 100644 --- a/test/api-permission.test.js +++ b/test/api-permission.test.js @@ -8,7 +8,7 @@ describe('Shopify#apiPermission', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('deletes an api permission', () => { scope.delete('/admin/api_permissions/current.json').reply(200); diff --git a/test/application-charge.test.js b/test/application-charge.test.js index cfb386a7..f7a014c7 100644 --- a/test/application-charge.test.js +++ b/test/application-charge.test.js @@ -9,7 +9,7 @@ describe('Shopify#applicationCharge', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('create a new one-time application charge', () => { const input = fixtures.req.create; diff --git a/test/application-credit.test.js b/test/application-credit.test.js index 9cf6400c..d3140f00 100644 --- a/test/application-credit.test.js +++ b/test/application-credit.test.js @@ -9,7 +9,7 @@ describe('Shopify#applicationCredit', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates an application credit', () => { const input = fixtures.req.create; diff --git a/test/article.test.js b/test/article.test.js index aa724564..158a8d7a 100644 --- a/test/article.test.js +++ b/test/article.test.js @@ -9,7 +9,7 @@ describe('Shopify#article', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all articles from a certain blog (1/2)', () => { const output = fixtures.res.list; diff --git a/test/asset.test.js b/test/asset.test.js index 7dd9244f..1a78e4ca 100644 --- a/test/asset.test.js +++ b/test/asset.test.js @@ -10,7 +10,7 @@ describe('Shopify#asset', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all theme assets (1/2)', () => { const output = fixtures.res.list; diff --git a/test/balance.test.js b/test/balance.test.js index 9e109e86..e93d25e2 100644 --- a/test/balance.test.js +++ b/test/balance.test.js @@ -9,7 +9,7 @@ describe('Shopify#balance', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets the current balance', () => { const output = fixtures.res.list; diff --git a/test/blog.test.js b/test/blog.test.js index f9442f55..cb38755b 100644 --- a/test/blog.test.js +++ b/test/blog.test.js @@ -9,7 +9,7 @@ describe('Shopify#blog', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all blogs (1/2)', () => { const output = fixtures.res.list; diff --git a/test/cancellation-request.test.js b/test/cancellation-request.test.js index 6af58163..dc9f0ee4 100644 --- a/test/cancellation-request.test.js +++ b/test/cancellation-request.test.js @@ -9,7 +9,7 @@ describe('Shopify#cancellation-request', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('sends a cancellation request (1/2)', () => { const output = fixtures.res.create; diff --git a/test/carrier-service.test.js b/test/carrier-service.test.js index de976986..f2559913 100644 --- a/test/carrier-service.test.js +++ b/test/carrier-service.test.js @@ -9,7 +9,7 @@ describe('Shopify#carrierService', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a carrier service', () => { const input = fixtures.req.create; diff --git a/test/checkout.test.js b/test/checkout.test.js index 9cc4c70b..4fa33f7f 100644 --- a/test/checkout.test.js +++ b/test/checkout.test.js @@ -9,7 +9,7 @@ describe('Shopify#checkout', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a count of all checkout (1/2)', () => { scope.get('/admin/checkouts/count.json').reply(200, { count: 5 }); diff --git a/test/collect.test.js b/test/collect.test.js index 38aa248e..6b3e0ca8 100644 --- a/test/collect.test.js +++ b/test/collect.test.js @@ -9,7 +9,7 @@ describe('Shopify#collect', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('adds a product to collection', () => { const input = fixtures.req.create; diff --git a/test/collection-listing.test.js b/test/collection-listing.test.js index 1b698327..eafc6147 100644 --- a/test/collection-listing.test.js +++ b/test/collection-listing.test.js @@ -9,7 +9,7 @@ describe('Shopify#collectionListing', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets collection listings published to an application (1/2)', () => { const output = fixtures.res.list; diff --git a/test/collection.test.js b/test/collection.test.js index b225f6ad..4ef7ad0a 100644 --- a/test/collection.test.js +++ b/test/collection.test.js @@ -9,7 +9,7 @@ describe('Shopify#collection', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a collection by its ID (1/2)', () => { const output = fixtures.res.get; diff --git a/test/comment.test.js b/test/comment.test.js index ee708e1a..d1b22385 100644 --- a/test/comment.test.js +++ b/test/comment.test.js @@ -9,7 +9,7 @@ describe('Shopify#comment', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all comments (1/2)', () => { const output = fixtures.res.list; diff --git a/test/country.test.js b/test/country.test.js index f173e559..45587939 100644 --- a/test/country.test.js +++ b/test/country.test.js @@ -9,7 +9,7 @@ describe('Shopify#country', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all countries (1/2)', () => { const output = fixtures.res.list; diff --git a/test/currency.test.js b/test/currency.test.js index a22d5732..4042d09b 100644 --- a/test/currency.test.js +++ b/test/currency.test.js @@ -9,7 +9,7 @@ describe('Shopify#currency', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of currencies enabled on a shop', () => { const output = fixtures.res.list; diff --git a/test/custom-collection.test.js b/test/custom-collection.test.js index e3f2c611..034f4b13 100644 --- a/test/custom-collection.test.js +++ b/test/custom-collection.test.js @@ -9,7 +9,7 @@ describe('Shopify#customCollection', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all custom collections (1/2)', () => { const output = fixtures.res.list; diff --git a/test/customer-address.test.js b/test/customer-address.test.js index a9f6a1c4..2101ffcd 100644 --- a/test/customer-address.test.js +++ b/test/customer-address.test.js @@ -10,7 +10,7 @@ describe('Shopify#customerAddress', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all addresses from a customer (1/2)', () => { const output = fixtures.res.list; diff --git a/test/customer-saved-search.test.js b/test/customer-saved-search.test.js index b4df13ca..dbb2b1f4 100644 --- a/test/customer-saved-search.test.js +++ b/test/customer-saved-search.test.js @@ -9,7 +9,7 @@ describe('Shopify#customerSavedSearch', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all customers saved searches (1/2)', () => { const output = fixtures.res.list; diff --git a/test/customer.test.js b/test/customer.test.js index 9ce527a2..e2fac835 100644 --- a/test/customer.test.js +++ b/test/customer.test.js @@ -10,7 +10,7 @@ describe('Shopify#customer', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all customers (1/2)', () => { const output = fixtures.res.list; diff --git a/test/deprecated-api-call.test.js b/test/deprecated-api-call.test.js index 2f3068ba..82e83e6c 100644 --- a/test/deprecated-api-call.test.js +++ b/test/deprecated-api-call.test.js @@ -9,7 +9,7 @@ describe('Shopify#deprecatedApiCall', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of deprecated API calls', () => { const output = fixtures.res.list; diff --git a/test/discount-code-creation-job.test.js b/test/discount-code-creation-job.test.js index de6cfb26..637ed68e 100644 --- a/test/discount-code-creation-job.test.js +++ b/test/discount-code-creation-job.test.js @@ -9,7 +9,7 @@ describe('Shopify#discountCodeCreationJob', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a new discount code creation job', () => { const input = fixtures.req.create; diff --git a/test/discount-code.test.js b/test/discount-code.test.js index c6995f38..55db6700 100644 --- a/test/discount-code.test.js +++ b/test/discount-code.test.js @@ -9,7 +9,7 @@ describe('Shopify#discountCode', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a new discount code', () => { const input = fixtures.req.create; diff --git a/test/dispute.test.js b/test/dispute.test.js index f81cbefe..856dbe0a 100644 --- a/test/dispute.test.js +++ b/test/dispute.test.js @@ -9,7 +9,7 @@ describe('Shopify#dispute', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list disputes (1/2)', () => { const output = fixtures.res.list; diff --git a/test/draft-order.test.js b/test/draft-order.test.js index 508325a5..5f8b6bbd 100644 --- a/test/draft-order.test.js +++ b/test/draft-order.test.js @@ -9,7 +9,7 @@ describe('Shopify#draftOrder', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of draft orders (1/2)', () => { const output = fixtures.res.list; diff --git a/test/event.test.js b/test/event.test.js index 0ef02ea9..41c6dbb2 100644 --- a/test/event.test.js +++ b/test/event.test.js @@ -9,7 +9,7 @@ describe('Shopify#event', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of events (1/2)', () => { const output = fixtures.res.list; diff --git a/test/fulfillment-event.test.js b/test/fulfillment-event.test.js index ab4665d4..b1513252 100644 --- a/test/fulfillment-event.test.js +++ b/test/fulfillment-event.test.js @@ -13,7 +13,7 @@ describe('Shopify#fulfillmentEvent', () => { const shopify = common.shopify; const shopName = common.shopName; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all fulfillments events for a fulfillment (1/2)', () => { const output = fixtures.res.list; diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index 93d30946..abd127a6 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -9,7 +9,7 @@ describe('Shopify#fulfillmentOrder', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of fulfillment orders on a shop for a specific app', () => { const output = fixtures.res.list; diff --git a/test/fulfillment-request.test.js b/test/fulfillment-request.test.js index 880b0c01..cef713cd 100644 --- a/test/fulfillment-request.test.js +++ b/test/fulfillment-request.test.js @@ -9,7 +9,7 @@ describe('Shopify#fulfillment-request', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('sends a fulfillment request', () => { const input = fixtures.req.create; diff --git a/test/fulfillment-service.test.js b/test/fulfillment-service.test.js index 4cd474a2..ad2d1299 100644 --- a/test/fulfillment-service.test.js +++ b/test/fulfillment-service.test.js @@ -9,7 +9,7 @@ describe('Shopify#fulfillmentService', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all fulfillment services', () => { const output = fixtures.res.list; diff --git a/test/fulfillment.test.js b/test/fulfillment.test.js index 177a4a6d..cc57347f 100644 --- a/test/fulfillment.test.js +++ b/test/fulfillment.test.js @@ -13,7 +13,7 @@ describe('Shopify#fulfillment', () => { const shopify = common.shopify; const shopName = common.shopName; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all fulfillments for an order (1/2)', () => { const output = fixtures.res.list; diff --git a/test/gift-card-adjustment.test.js b/test/gift-card-adjustment.test.js index e30b68d9..f2006d4e 100644 --- a/test/gift-card-adjustment.test.js +++ b/test/gift-card-adjustment.test.js @@ -9,7 +9,7 @@ describe('Shopify#giftCardAdjustment', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all gift card adjustments', () => { const output = fixtures.res.list; diff --git a/test/gift-card.test.js b/test/gift-card.test.js index 15c9e298..7af809db 100644 --- a/test/gift-card.test.js +++ b/test/gift-card.test.js @@ -9,7 +9,7 @@ describe('Shopify#giftCard', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all gift cards (1/2)', () => { const output = fixtures.res.list; diff --git a/test/inventory-item.test.js b/test/inventory-item.test.js index e0762cd4..3fb9e5b0 100644 --- a/test/inventory-item.test.js +++ b/test/inventory-item.test.js @@ -10,7 +10,7 @@ describe('Shopify#inventoryItem', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of inventory items', () => { const query = { ids: '808950810,39072856,457924702' }; diff --git a/test/inventory-level.test.js b/test/inventory-level.test.js index f4cca492..3de9de80 100644 --- a/test/inventory-level.test.js +++ b/test/inventory-level.test.js @@ -10,7 +10,7 @@ describe('Shopify#inventoryLevel', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of inventory levels', () => { const query = { inventory_item_ids: 808950810 }; diff --git a/test/location.test.js b/test/location.test.js index 0586f1e2..b9d83cff 100644 --- a/test/location.test.js +++ b/test/location.test.js @@ -9,7 +9,7 @@ describe('Shopify#location', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all locations', () => { const output = fixtures.res.list; diff --git a/test/marketing-event.test.js b/test/marketing-event.test.js index 68854461..cf2c779f 100644 --- a/test/marketing-event.test.js +++ b/test/marketing-event.test.js @@ -9,7 +9,7 @@ describe('Shopify#marketingEvent', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all marketing events (1/2)', () => { const output = fixtures.res.list; diff --git a/test/metafield.test.js b/test/metafield.test.js index 7e0bd15c..c28a072d 100644 --- a/test/metafield.test.js +++ b/test/metafield.test.js @@ -9,7 +9,7 @@ describe('Shopify#metafield', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of metafields that belong to a store (1/2)', () => { const output = fixtures.res.list; diff --git a/test/order-risk.test.js b/test/order-risk.test.js index 7ab2c7b4..8fd80dca 100644 --- a/test/order-risk.test.js +++ b/test/order-risk.test.js @@ -9,7 +9,7 @@ describe('Shopify#orderRisk', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('create a new order risk for an order', () => { const input = fixtures.req.create; diff --git a/test/order.test.js b/test/order.test.js index c8e1e38e..aa9bd281 100644 --- a/test/order.test.js +++ b/test/order.test.js @@ -9,7 +9,7 @@ describe('Shopify#order', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all orders (1/2)', () => { const output = fixtures.res.list; diff --git a/test/page.test.js b/test/page.test.js index e89e38be..688d782c 100644 --- a/test/page.test.js +++ b/test/page.test.js @@ -9,7 +9,7 @@ describe('Shopify#page', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all pages (1/2)', () => { const output = fixtures.res.list; diff --git a/test/payment.test.js b/test/payment.test.js index 73492fd2..4438a97b 100644 --- a/test/payment.test.js +++ b/test/payment.test.js @@ -9,7 +9,7 @@ describe('Shopify#payment', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of payments on a particular checkout', () => { const output = fixtures.res.list; diff --git a/test/payout.test.js b/test/payout.test.js index 0623e614..a97cef39 100644 --- a/test/payout.test.js +++ b/test/payout.test.js @@ -13,7 +13,7 @@ describe('Shopify#payout', () => { const shopify = common.shopify; const shopName = common.shopName; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list payouts (1/2)', () => { const output = fixtures.res.list; diff --git a/test/policy.test.js b/test/policy.test.js index 10f847b6..8e193444 100644 --- a/test/policy.test.js +++ b/test/policy.test.js @@ -9,7 +9,7 @@ describe('Shopify#policy', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all policies (1/2)', () => { const output = fixtures.res.list; diff --git a/test/price-rule.test.js b/test/price-rule.test.js index 856052e5..2bfa6dd1 100644 --- a/test/price-rule.test.js +++ b/test/price-rule.test.js @@ -9,7 +9,7 @@ describe('Shopify#priceRule', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a new price rule', () => { const input = fixtures.req.create; diff --git a/test/product-image.test.js b/test/product-image.test.js index fef2c0f0..28f53ddc 100644 --- a/test/product-image.test.js +++ b/test/product-image.test.js @@ -9,7 +9,7 @@ describe('Shopify#productImage', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all product images for a product (1/2)', () => { const output = fixtures.res.list; diff --git a/test/product-listing.test.js b/test/product-listing.test.js index c9e11f12..df5b7b7f 100644 --- a/test/product-listing.test.js +++ b/test/product-listing.test.js @@ -12,8 +12,8 @@ describe('Shopify#productListing', () => { const presentmentApiScope = common.presentmentApiScope; afterEach(() => { - expect(presentmentApiScope.isDone()).to.be.true; - expect(standardScope.isDone()).to.be.true; + expect(presentmentApiScope.pendingMocks()).to.deep.equal([]); + expect(standardScope.pendingMocks()).to.deep.equal([]); }); it('gets product listings published to an application (1/4)', () => { diff --git a/test/product-resource-feedback.test.js b/test/product-resource-feedback.test.js index a211a233..8de8ace0 100644 --- a/test/product-resource-feedback.test.js +++ b/test/product-resource-feedback.test.js @@ -9,7 +9,7 @@ describe('Shopify#productResourceFeedback', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a resource feedback', () => { const input = fixtures.req.create; diff --git a/test/product-variant.test.js b/test/product-variant.test.js index 76392dec..a667880f 100644 --- a/test/product-variant.test.js +++ b/test/product-variant.test.js @@ -12,8 +12,8 @@ describe('Shopify#productVariant', () => { const presentmentApiScope = common.presentmentApiScope; afterEach(() => { - expect(presentmentApiScope.isDone()).to.be.true; - expect(standardScope.isDone()).to.be.true; + expect(presentmentApiScope.pendingMocks()).to.deep.equal([]); + expect(standardScope.pendingMocks()).to.deep.equal([]); }); it('gets a list of all product variants for a product (1/4)', () => { diff --git a/test/product.test.js b/test/product.test.js index 7357cb0e..43073b98 100644 --- a/test/product.test.js +++ b/test/product.test.js @@ -12,8 +12,8 @@ describe('Shopify#product', () => { const presentmentApiScope = common.presentmentApiScope; afterEach(() => { - expect(presentmentApiScope.isDone()).to.be.true; - expect(standardScope.isDone()).to.be.true; + expect(presentmentApiScope.pendingMocks()).to.deep.equal([]); + expect(standardScope.pendingMocks()).to.deep.equal([]); }); it('gets a list of all products (1/4)', () => { diff --git a/test/province.test.js b/test/province.test.js index 49d72fc3..ae8aaed9 100644 --- a/test/province.test.js +++ b/test/province.test.js @@ -9,7 +9,7 @@ describe('Shopify#province', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all provinces for a country (1/2)', () => { const output = fixtures.res.list; diff --git a/test/recurring-application-charge.test.js b/test/recurring-application-charge.test.js index 1922a815..cd62031a 100644 --- a/test/recurring-application-charge.test.js +++ b/test/recurring-application-charge.test.js @@ -9,7 +9,7 @@ describe('Shopify#recurringApplicationCharge', () => { const resource = common.shopify.recurringApplicationCharge; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a recurring application charge', () => { const input = fixtures.req.create; diff --git a/test/redirect.test.js b/test/redirect.test.js index e4790bb9..0fd3114a 100644 --- a/test/redirect.test.js +++ b/test/redirect.test.js @@ -9,7 +9,7 @@ describe('Shopify#redirect', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all redirects (1/2)', () => { const output = fixtures.res.list; diff --git a/test/refund.test.js b/test/refund.test.js index 402ea80a..7397d8b1 100644 --- a/test/refund.test.js +++ b/test/refund.test.js @@ -9,7 +9,7 @@ describe('Shopify#refund', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of refunds for an order (1/2)', () => { const output = fixtures.res.list; diff --git a/test/report.test.js b/test/report.test.js index 457fd6d8..a64841fa 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -9,7 +9,7 @@ describe('Shopify#report', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all reports (1/2)', () => { const output = fixtures.res.list; diff --git a/test/resource-feedback.test.js b/test/resource-feedback.test.js index 38ab356a..4e510350 100644 --- a/test/resource-feedback.test.js +++ b/test/resource-feedback.test.js @@ -9,7 +9,7 @@ describe('Shopify#resourceFeedback', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a resource feedback', () => { const input = fixtures.req.create; diff --git a/test/script-tag.test.js b/test/script-tag.test.js index 56b7ae81..6cd6e6a5 100644 --- a/test/script-tag.test.js +++ b/test/script-tag.test.js @@ -9,7 +9,7 @@ describe('Shopify#scriptTag', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all script tags (1/2)', () => { const output = fixtures.res.list; diff --git a/test/shipping-zone.test.js b/test/shipping-zone.test.js index 446a9d92..a04018dd 100644 --- a/test/shipping-zone.test.js +++ b/test/shipping-zone.test.js @@ -9,7 +9,7 @@ describe('Shopify#shippingZone', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all shipping zones (1/2)', () => { const output = fixtures.res.list; diff --git a/test/shop.test.js b/test/shop.test.js index 8ad7da8f..80b40423 100644 --- a/test/shop.test.js +++ b/test/shop.test.js @@ -13,7 +13,7 @@ describe('Shopify#shop', () => { const shopify = common.shopify; const shopName = common.shopName; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets the configuration of the shop (1/2)', () => { const output = fixtures.res.get; diff --git a/test/shopify.test.js b/test/shopify.test.js index 901f161a..e1da698a 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -123,7 +123,7 @@ describe('Shopify', () => { const url = { pathname: '/test', ...shopify.baseUrl }; const scope = common.scope; - afterEach(() => expect(nock.isDone()).to.be.true); + afterEach(() => expect(nock.pendingMocks()).to.deep.equal([])); it('returns a RequestError when the request fails', () => { const message = 'Something wrong happened'; @@ -539,7 +539,7 @@ describe('Shopify', () => { } }); - afterEach(() => expect(nock.isDone()).to.be.true); + afterEach(() => expect(nock.pendingMocks()).to.deep.equal([])); it('returns a RequestError when the request fails', () => { const message = 'Something wrong happened'; diff --git a/test/smart-collection.test.js b/test/smart-collection.test.js index ddaed257..419ed29c 100644 --- a/test/smart-collection.test.js +++ b/test/smart-collection.test.js @@ -10,7 +10,7 @@ describe('Shopify#smartCollection', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all smart collections (1/2)', () => { const output = fixtures.res.list; diff --git a/test/storefront-access-token.test.js b/test/storefront-access-token.test.js index 35ca33bc..8216f84e 100644 --- a/test/storefront-access-token.test.js +++ b/test/storefront-access-token.test.js @@ -9,7 +9,7 @@ describe('Shopify#storefrontAccessToken', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a storefront access token', () => { const input = fixtures.req.create; diff --git a/test/tender-transaction.test.js b/test/tender-transaction.test.js index 4d307ec3..cf829705 100644 --- a/test/tender-transaction.test.js +++ b/test/tender-transaction.test.js @@ -9,7 +9,7 @@ describe('Shopify#tenderTransaction', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of tender transactions (1/2)', () => { const output = fixtures.res.list; diff --git a/test/theme.test.js b/test/theme.test.js index fb458df5..6b66ea17 100644 --- a/test/theme.test.js +++ b/test/theme.test.js @@ -9,7 +9,7 @@ describe('Shopify#theme', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all themes (1/2)', () => { const output = fixtures.res.list; diff --git a/test/transaction.test.js b/test/transaction.test.js index acdcbfe0..2ff82aec 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -9,7 +9,7 @@ describe('Shopify#transaction', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all transactions for an order (1/2)', () => { const output = fixtures.res.list; diff --git a/test/usage-charge.test.js b/test/usage-charge.test.js index 0553ee92..2088d11b 100644 --- a/test/usage-charge.test.js +++ b/test/usage-charge.test.js @@ -10,7 +10,7 @@ describe('Shopify#usageCharge', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('creates a new usage charge', () => { const input = fixtures.req.create; diff --git a/test/user.test.js b/test/user.test.js index 97ff43f5..544a839b 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -9,7 +9,7 @@ describe('Shopify#user', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all users', () => { const output = fixtures.res.list; diff --git a/test/webhook.test.js b/test/webhook.test.js index 39909502..b76b52bc 100644 --- a/test/webhook.test.js +++ b/test/webhook.test.js @@ -10,7 +10,7 @@ describe('Shopify#webhook', () => { const shopify = common.shopify; const scope = common.scope; - afterEach(() => expect(scope.isDone()).to.be.true); + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); it('gets a list of all webhooks (1/2)', () => { const output = fixtures.res.list; From 69cac592add2b8fcde0681d11595110f2ed57134 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 24 Jun 2022 21:31:39 +0200 Subject: [PATCH 015/110] [doc] Remove broken links --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 1eee8958..1ad802e2 100644 --- a/README.md +++ b/README.md @@ -676,8 +676,6 @@ shopify ## Become a master of the Shopify ecosystem by: -- [Becoming a Shopify App Developer][becoming-a-shopify-app-developer] -- [Checking out the roots][checking-out-the-roots] - [Talking To Other Masters][talking-to-other-masters] - [Reading API Docs][reading-api-docs] - [Learning from others][learning-from-others] @@ -727,9 +725,6 @@ Used in our live products: [MoonMail][moonmail] & [MONEI][monei] [api-call-limit]: https://help.shopify.com/api/guides/api-call-limit/?ref=microapps [api-versioning]: https://help.shopify.com/en/api/versioning -[becoming-a-shopify-app-developer]: - https://app.shopify.com/services/partners/signup?ref=microapps -[checking-out-the-roots]: https://help.shopify.com/api/guides?ref=microapps [talking-to-other-masters]: https://ecommerce.shopify.com/c/shopify-apps?ref=microapps [reading-api-docs]: https://help.shopify.com/api/reference/?ref=microapps From cbf9e10f1a0ff2c1e24863040038a45b8c5bc1c0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 24 Jun 2022 21:41:04 +0200 Subject: [PATCH 016/110] [doc] Update Shopify URLs to avoid redirects --- README.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1ad802e2..831e9e94 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ This feature is only available on version 2.24.0 and above. - `list([params])` - `search(params)` - `update(id, params)` -- [giftCardAdjustment](https://help.shopify.com/en/api/reference/plus/gift_card_adjustment) +- [giftCardAdjustment](https://shopify.dev/api/admin-rest/2022-04/resources/gift-card-adjustment) - `create(giftCardId, params)` - `get(giftCardId, id)` - `list(giftCardId)` @@ -547,7 +547,7 @@ This feature is only available on version 2.24.0 and above. - `get(productId)` - `list([params])` - `productIds([params])` -- [productResourceFeedback](https://help.shopify.com/en/api/reference/sales-channels/productresourcefeedback) +- [productResourceFeedback](https://shopify.dev/api/admin-rest/2022-04/resources/product-resourcefeedback) - `create(productId[, params])` - `list(productId)` - productVariant @@ -643,8 +643,8 @@ This feature is only available on version 2.24.0 and above. - `list([params])` - `update(id, params)` -where `params` is a plain JavaScript object. See -https://help.shopify.com/api/reference?ref=microapps for parameters details. +where `params` is a plain JavaScript object. See the [Rest Admin API +reference][reading-api-docs] for parameters details. ## GraphQL @@ -719,18 +719,15 @@ Used in our live products: [MoonMail][moonmail] & [MONEI][monei] https://img.shields.io/coveralls/MONEI/Shopify-api-node/master.svg [coverage-shopify-api-node]: https://coveralls.io/github/MONEI/Shopify-api-node [generate-private-app-credentials]: - https://help.shopify.com/api/guides/api-credentials#generate-private-app-credentials?ref=microapps -[oauth]: https://help.shopify.com/api/guides/authentication/oauth?ref=microapps + https://shopify.dev/apps/auth/basic-http#step-2-generate-api-credentials +[oauth]: https://shopify.dev/apps/auth/oauth [shopify-token]: https://github.com/lpinca/shopify-token -[api-call-limit]: - https://help.shopify.com/api/guides/api-call-limit/?ref=microapps -[api-versioning]: https://help.shopify.com/en/api/versioning -[talking-to-other-masters]: - https://ecommerce.shopify.com/c/shopify-apps?ref=microapps -[reading-api-docs]: https://help.shopify.com/api/reference/?ref=microapps +[api-call-limit]: https://shopify.dev/api/usage/rate-limits +[api-versioning]: https://shopify.dev/api/usage/versioning +[talking-to-other-masters]: https://community.shopify.com/ +[reading-api-docs]: https://shopify.dev/api/admin-rest [learning-from-others]: https://stackoverflow.com/questions/tagged/shopify -[paginated-rest-results]: - https://help.shopify.com/en/api/guides/paginated-rest-results +[paginated-rest-results]: https://shopify.dev/api/usage/pagination-rest [polaris]: https://polaris.shopify.com/?ref=microapps [microapps]: http://microapps.com/?utm_source=shopify-api-node-module-repo-readme&utm_medium=click&utm_campaign=github From be39fd45e0e157e22983be66d7ea4ebb96cc53d8 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 28 Jun 2022 05:41:10 -0400 Subject: [PATCH 017/110] [api] Introduce the `maxRetries` option (#541) Add the `maxRetries` option to enable automatic retries. Fixes: https://github.com/MONEI/Shopify-api-node/issues/251 Refs: https://github.com/MONEI/Shopify-api-node/issues/199 Refs: https://github.com/MONEI/Shopify-api-node/issues/324 Refs: https://github.com/MONEI/Shopify-api-node/pull/377 --- README.md | 38 +++++- index.js | 251 ++++++++++++++++++++++++++++++---------- test/shopify.test.js | 270 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 497 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 831e9e94..cb18d482 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Creates a new `Shopify` instance. bucket size. For example `{ calls: 2, interval: 1000, bucketSize: 35 }` specifies a limit of 2 requests per second with a burst of 35 requests. When set to `true` requests are limited as specified in the above example. Defaults - to `false`. + to `false`. Mutually exclusive with the `maxRetries` option. - `parseJson` - Optional - The function used to parse JSON. The function is passed a single argument. This option allows the use of a custom JSON parser that might be needed to properly handle long integer IDs. Defaults to @@ -66,6 +66,13 @@ Creates a new `Shopify` instance. - `timeout` - Optional - The number of milliseconds before the request times out. If the request takes longer than `timeout`, it will be aborted. Defaults to `60000`, or 1 minute. +- `maxRetries` - Optional -- The number of times to attempt to make the request + to Shopify before giving up. Defaults to 0, which means no automatic retries. + If set to a value greater than 0, `shopify-api-node` will make up to that many + retries. `shopify-api-node` will respect the `Retry-After` header for requests + to the REST API, and the throttled cost data for requests to the GraphQL API, + and retry the request after that time has elapsed. Mutually exclusive with the + `autoLimit` option. #### Return value @@ -256,6 +263,35 @@ parameters needed to fetch the next and previous page of results. This feature is only available on version 2.24.0 and above. +## Shopify rate limit avoidance + +`shopify-api-node` has two optional mechanisms for avoiding requests failing +with `429 Rate Limit Exceeded` errors from Shopify. + +The `autoLimit` option implements a client side leaky bucket algorithm for +delaying requests until Shopify is likely to accept them. When `autoLimit` is +on, each `Shopify` instance will track how many requests have been made, and +delay sending subsequent requests if the rate limit has been exceeded. +`autoLimit` is very efficient because it almost entirely avoids sending requests +which will return 429 errors, but, it does not coordinate between multiple +`Shopify` instances or across multiple processes. If you're using +`shopify-api-node` in many different processes, `autoLimit` will not correctly +avoid 429 errors. + +The `maxRetries` option implements a retry based strategy for getting requests +to Shopify, where when a 429 error occurs, the request is automatically retried +after waiting. Shopify usually replies with a `Retry-After:` header indicating +to the client when the rate limit is available, and so `shopify-api-node` will +wait that long before retrying. If you are using `shopify-api-node` in many +different processes, they will all be competing to use the same rate limit +shopify enforces, so there is no guarantee that retrying after the `Retry-After` +header delay will work. It is recommended to set `maxRetries` to a high value +like `10` if you are making many concurrent requests in many processes to ensure +each request is retried for long enough to succeed. + +`autoLimit` and `maxRetries` can't be used simultaneously. Both are off by +default. + ## Available resources and methods - accessScope diff --git a/index.js b/index.js index 1dd6872c..9d4825cd 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,21 @@ const url = require('url'); const pkg = require('./package'); const resources = require('./resources'); +const RetryableErrorCodes = new Set([ + 'ETIMEDOUT', + 'ECONNRESET', + 'EADDRINUSE', + 'ECONNREFUSED', + 'EPIPE', + 'ENOTFOUND', + 'ENETUNREACH', + 'EAI_AGAIN' +]); + +const RetryableStatusCodes = new Set([ + 408, 413, 429, 500, 502, 503, 504, 521, 522, 524 +]); + /** * Creates a Shopify instance. * @@ -25,6 +40,8 @@ const resources = require('./resources'); * @param {Function} [options.parseJson] The function used to parse JSON * @param {Function} [options.stringifyJson] The function used to serialize to * JSON + * @param {Number} [options.maxRetries] Maximum number of automatic request + * retries, defaults to 0 (no retries) * @constructor * @public */ @@ -34,7 +51,8 @@ function Shopify(options) { !options || !options.shopName || (!options.accessToken && (!options.apiKey || !options.password)) || - (options.accessToken && (options.apiKey || options.password)) + (options.accessToken && (options.apiKey || options.password)) || + (options.autoLimit && options.maxRetries > 0) ) { throw new Error('Missing or invalid options'); } @@ -44,6 +62,7 @@ function Shopify(options) { parseJson: JSON.parse, stringifyJson: JSON.stringify, timeout: 60000, + maxRetries: 0, ...options }; @@ -134,61 +153,66 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { parseJson: this.options.parseJson, timeout: this.options.timeout, responseType: 'json', - retry: 0, - method + retry: + this.options.maxRetries > 0 + ? { + limit: this.options.maxRetries, + // Don't clamp Shopify retry-after header values too low. + maxRetryAfter: Infinity, + calculateDelay + } + : 0, + method, + hooks: { + afterResponse: [ + (res) => { + this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); + return res; + } + ] + } }; if (data) { options.json = key ? { [key]: data } : data; } - return got(uri, options).then( - (res) => { - const body = res.body; - - this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); + 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']); + if (res.statusCode === 202 && res.headers['location']) { + const retryAfter = res.headers['retry-after'] * 1000 || 0; + const { pathname, search } = url.parse(res.headers['location']); - return delay(retryAfter).then(() => { - const uri = { pathname, ...this.baseUrl }; + return delay(retryAfter).then(() => { + const uri = { pathname, ...this.baseUrl }; - if (search) uri.search = search; + if (search) uri.search = search; - return this.request(uri, 'GET', key); - }); - } + return this.request(uri, 'GET', key); + }); + } - const data = key ? body[key] : body || {}; + const data = key ? body[key] : body || {}; - if (res.headers.link) { - const link = parseLinkHeader(res.headers.link); + 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 } - }); - } + if (link.next) { + Object.defineProperties(data, { + nextPageParameters: { value: link.next.query } + }); } - return data; - }, - (err) => { - this.updateLimits( - err.response && err.response.headers['x-shopify-shop-api-call-limit'] - ); - - return Promise.reject(err); + if (link.previous) { + Object.defineProperties(data, { + previousPageParameters: { value: link.previous.query } + }); + } } - ); + + return data; + }); }; /** @@ -243,34 +267,54 @@ Shopify.prototype.graphql = function graphql(data, variables) { parseJson: this.options.parseJson, timeout: this.options.timeout, responseType: 'json', - retry: 0, method: 'POST', - body: json ? this.options.stringifyJson({ query: data, variables }) : data - }; - - return got(uri, options).then((res) => { - if (res.body.extensions && res.body.extensions.cost) { - this.updateGraphqlLimits(res.body.extensions.cost); - } - - if (res.body.errors) { - const first = res.body.errors[0]; - const err = new Error(first.message); - - err.locations = first.locations; - err.path = first.path; - err.extensions = first.extensions; - err.response = res; - - throw err; + body: json ? this.options.stringifyJson({ query: data, variables }) : data, + retry: + this.options.maxRetries > 0 + ? { + limit: this.options.maxRetries, + // Don't clamp Shopify retry-after header values too low. + maxRetryAfter: Infinity, + calculateDelay + } + : 0, + hooks: { + afterResponse: [ + (response) => { + if (response.body) { + if (response.body.extensions && response.body.extensions.cost) { + this.updateGraphqlLimits(response.body.extensions.cost); + } + + if (response.body.errors) { + // Make got consider this response errored and retry if needed + throw new Error(response.body.errors[0].message); + } + } + + return response; + } + ], + beforeError: [errorForGraphQLError] } + }; - return res.body.data || {}; - }); + return got(uri, options).then(returnResponseData); }; resources.registerAll(Shopify); +/** + * Return the data of a GraphQL response object + * + * @param {Response} res Got response object + * @return {Object} The data + * @private + */ +function returnResponseData(res) { + return res.body.data; +} + /** * Returns a promise that resolves after a given amount of time. * @@ -313,4 +357,89 @@ function reducer(acc, cur) { return acc; } +/** + * Given an error from got, see if Shopify told us how long to wait before + * retrying. Return a duration in ms if we can, and otherwise return null. + * + * @param {Object} error Error object from got call + * @return {Boolean | null} + * @private + **/ +function maybeRetryMS(error) { + // for simplicity, retry network connectivity issues after a hardcoded 1s + if (RetryableErrorCodes.has(error.code)) { + return 1000; + } + + const response = error.response; + + if (response.headers && response.headers['retry-after']) { + const value = parseFloat(response.headers['retry-after']); + if (isFinite(value)) { + return value * 1000; + } + + // We got a retry-after header but don't know how to parse it, + // assume retrying is unsafe as something has changed + return null; + } + + if (RetryableStatusCodes.has(response.statusCode)) { + // Arbitrary 2 seconds, in case we get a 429 without a Retry-After + // response header, or 4xx/5xx series error that matches the got retry + // defaults + return 2 * 1000; + } + + // detect graphql request throttling + if (response.body && typeof response.body === 'object') { + const body = response.body; + + if ( + body.errors && + body.errors[0].extensions && + body.errors[0].extensions.code == 'THROTTLED' + ) { + const costData = body.extensions.cost; + return ( + ((costData.requestedQueryCost - + costData.throttleStatus.currentlyAvailable) / + costData.throttleStatus.restoreRate) * + 1000 + ); + } + } + + return null; +} + +/** + * Decorate a ResponseError object thrown by got with details from + * GraphQL errors in the response body + * @param {ResponseError} error + * @returns ResponseError + * @private + */ +function errorForGraphQLError(error) { + if (error.response && error.response.body.errors) { + const first = error.response.body.errors[0]; + + error.locations = first.locations; + error.path = first.path; + error.extensions = first.extensions; + } + return error; +} + +/** + * Got `calculateDelay` hook function passed to decide how long to wait before retrying + * + * @param {Object} retryObject got's input for the retry logic + * @return {Number} + * @private + */ +function calculateDelay(retryObject) { + return maybeRetryMS(retryObject.error) || retryObject.computedValue; +} + module.exports = Shopify; diff --git a/test/shopify.test.js b/test/shopify.test.js index e1da698a..1824095c 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -38,6 +38,10 @@ describe('Shopify', () => { expect(() => new Shopify({ password })).to.throw(Error, msg); expect(() => new Shopify({ accessToken, apiKey })).to.throw(Error, msg); expect(() => new Shopify({ accessToken, password })).to.throw(Error, msg); + expect( + () => + new Shopify({ accessToken, password, maxRetries: 1, autoLimit: true }) + ).to.throw(Error, msg); }); it('makes the new operator optional', () => { @@ -529,9 +533,153 @@ describe('Shopify', () => { expect(res).to.deep.equal(data); }); }); + + const addWorkingRESTRequestMock = (scope) => { + scope.get('/admin/shop.json').reply(200, { + shop: { + name: 'My Cool Test Shop', + id: '1' + } + }); + }; + + const shopifyWithRetries = new Shopify({ + accessToken, + parseJson, + shopName, + stringifyJson, + maxRetries: 3 + }); + + it('retries 429 errors from Shopify according to the header', () => { + scope + .get('/admin/shop.json') + .reply(429, 'too many requests', { 'Retry-After': '0.5' }) + .get('/admin/shop.json') + .reply(429, 'too many requests', { 'Retry-After': '0.5' }); + + addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }); + + it("retries 429 errors from Shopify that don't have a header", () => { + scope + .get('/admin/shop.json') + .reply(429, 'too many requests') + .get('/admin/shop.json') + .reply(429, 'too many requests'); + + addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }).timeout(15000); + + it('retries 429 errors from Shopify that have broken header values', () => { + scope + .get('/admin/shop.json') + .reply(429, 'too many requests', { 'Retry-After': 'foobar' }); + + addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }).timeout(5000); + + it("doesn't retry 404 errors", () => { + scope.get('/admin/products/10.json').reply(404, { + error: 'not found' + }); + + return shopifyWithRetries.product.get('10').catch((err) => { + expect(err).to.be.an.instanceof(got.HTTPError); + expect(err.message).to.include('Response code 404 (Not Found)'); + }); + }); + + it('retries 500 errors from shopify', () => { + scope.get('/admin/shop.json').reply(500, { + error: 'something went wrong' + }); + + addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }).timeout(4000); + + it('retries network system level errors immediately', () => { + scope.get('/admin/shop.json').replyWithError({ + message: 'the network is broken', + code: 'ECONNRESET' + }); + + addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }); + + it('retries a variety of errors in order', () => { + const shopify = new Shopify({ + accessToken, + parseJson, + shopName, + stringifyJson, + maxRetries: 5, + timeout: 200 + }); + + scope + .get('/admin/shop.json') + .replyWithError({ + message: 'the network is broken', + code: 'ECONNRESET' + }) + .get('/admin/shop.json') + .reply(429, 'too many requests', { 'Retry-After': '1' }) + .get('/admin/shop.json') + .reply(429, 'too many requests', { 'Retry-After': '0.2' }) + .get('/admin/shop.json') + .reply(500, 'sorry its broken') + .get('/admin/shop.json') + .delay(500) // longer than API client configured timeout option + .reply(200, { + shop: { + name: 'My Cool Test Shop', + id: '1' + } + }); + + addWorkingRESTRequestMock(scope); + return shopify.shop.get().then((result) => { + expect(result.name).equal('My Cool Test Shop'); + }); + }).timeout(7000); }); describe('Shopify#graphql', () => { + const addWorkingGraphQLRequestMock = (scope) => { + scope.post('/admin/api/graphql.json').reply(200, { + data: { + shop: { + name: 'My Cool Test Shop', + id: '1' + } + } + }); + }; + + const shopifyWithRetries = new Shopify({ + accessToken, + parseJson, + shopName, + stringifyJson, + maxRetries: 3 + }); + const scope = nock(`https://${shopName}.myshopify.com`, { reqheaders: { 'User-Agent': `${pkg.name}/${pkg.version}`, @@ -835,5 +983,127 @@ describe('Shopify', () => { expect(res).to.deep.equal(data); }); }); + + it("doesn't retry errors from broken graphql queries", () => { + scope.post('/admin/api/graphql.json').reply(200, { + errors: [ + { + message: 'Parse error on "}" (RCURLY) at [4, 1]', + locations: [ + { + line: 4, + column: 1 + } + ] + } + ] + }); + return shopifyWithRetries.graphql('query { shop ').catch((err) => { + expect(err.message).to.include('Parse error on "}" (RCURLY) at [4, 1]'); + }); + }); + + it('retries 500 errors from Shopify', () => { + scope.post('/admin/api/graphql.json').reply(500, 'something went wrong'); + + addWorkingGraphQLRequestMock(scope); + + return shopifyWithRetries + .graphql('query { shop { id name } }') + .then((result) => { + expect(result.shop.name).equal('My Cool Test Shop'); + }); + }).timeout(3000); + + it('retries timeout errors from Shopify', () => { + const shopify = new Shopify({ + accessToken, + parseJson, + shopName, + stringifyJson, + maxRetries: 3, + timeout: 900 + }); + scope + .post('/admin/api/graphql.json') + .delay(1000) + .reply(500, () => 'something went wrong'); + + addWorkingGraphQLRequestMock(scope); + return shopify.graphql('query { shop { id name } }').then((result) => { + expect(result.shop.name).equal('My Cool Test Shop'); + }); + }); + + it('retries network system level errors immediately', () => { + scope.post('/admin/api/graphql.json').replyWithError({ + message: 'the network is broken', + code: 'ECONNRESET' + }); + + addWorkingGraphQLRequestMock(scope); + return shopifyWithRetries + .graphql('query { shop { id name } }') + .then((result) => { + expect(result.shop.name).equal('My Cool Test Shop'); + }); + }); + + it('retries graphql cost limit exceeded errors', () => { + scope + .post('/admin/api/graphql.json') + .reply(200, { + errors: [ + { + message: 'Throttled', + extensions: { + code: 'THROTTLED', + documentation: 'https://shopify.dev/api/usage/rate-limits' + } + } + ], + extensions: { + cost: { + requestedQueryCost: 732, + actualQueryCost: null, + throttleStatus: { + maximumAvailable: 1000, + currentlyAvailable: 728, + restoreRate: 50 + } + } + } + }) + .post('/admin/api/graphql.json') + .reply(200, { + errors: [ + { + message: 'Throttled', + extensions: { + code: 'THROTTLED', + documentation: 'https://shopify.dev/api/usage/rate-limits' + } + } + ], + extensions: { + cost: { + requestedQueryCost: 732, + actualQueryCost: null, + throttleStatus: { + maximumAvailable: 1000, + currentlyAvailable: 675, + restoreRate: 50 + } + } + } + }); + + addWorkingGraphQLRequestMock(scope); + return shopifyWithRetries + .graphql('query { shop { id name } }') + .then((result) => { + expect(result.shop.name).equal('My Cool Test Shop'); + }); + }).timeout(3000); }); }); From 0188f1f2296f57abc51fd7861d409b376679901b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 28 Jun 2022 11:04:36 +0200 Subject: [PATCH 018/110] [minor] Fix nits --- README.md | 30 ++++---- index.js | 168 +++++++++++++++++++++---------------------- test/common.js | 40 +++++++++++ test/shopify.test.js | 104 +++++++++------------------ 4 files changed, 171 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index cb18d482..491c2d90 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,13 @@ Creates a new `Shopify` instance. - `timeout` - Optional - The number of milliseconds before the request times out. If the request takes longer than `timeout`, it will be aborted. Defaults to `60000`, or 1 minute. -- `maxRetries` - Optional -- The number of times to attempt to make the request - to Shopify before giving up. Defaults to 0, which means no automatic retries. - If set to a value greater than 0, `shopify-api-node` will make up to that many - retries. `shopify-api-node` will respect the `Retry-After` header for requests - to the REST API, and the throttled cost data for requests to the GraphQL API, - and retry the request after that time has elapsed. Mutually exclusive with the - `autoLimit` option. +- `maxRetries` - Optional - The number of times to attempt to make the request + to Shopify before giving up. Defaults to `0`, which means no automatic + retries. If set to a value greater than `0`, `shopify-api-node` will make up + to that many retries. `shopify-api-node` will respect the `Retry-After` header + for requests to the REST API, and the throttled cost data for requests to the + GraphQL API, and retry the request after that time has elapsed. Mutually + exclusive with the `autoLimit` option. #### Return value @@ -280,14 +280,14 @@ avoid 429 errors. The `maxRetries` option implements a retry based strategy for getting requests to Shopify, where when a 429 error occurs, the request is automatically retried -after waiting. Shopify usually replies with a `Retry-After:` header indicating -to the client when the rate limit is available, and so `shopify-api-node` will -wait that long before retrying. If you are using `shopify-api-node` in many -different processes, they will all be competing to use the same rate limit -shopify enforces, so there is no guarantee that retrying after the `Retry-After` -header delay will work. It is recommended to set `maxRetries` to a high value -like `10` if you are making many concurrent requests in many processes to ensure -each request is retried for long enough to succeed. +after waiting. Shopify usually replies with a `Retry-After` header indicating to +the client when the rate limit is available, and so `shopify-api-node` will wait +that long before retrying. If you are using `shopify-api-node` in many different +processes, they will all be competing to use the same rate limit shopify +enforces, so there is no guarantee that retrying after the `Retry-After` header +delay will work. It is recommended to set `maxRetries` to a high value like `10` +if you are making many concurrent requests in many processes to ensure each +request is retried for long enough to succeed. `autoLimit` and `maxRetries` can't be used simultaneously. Both are off by default. diff --git a/index.js b/index.js index 9d4825cd..eedeb8f5 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const url = require('url'); const pkg = require('./package'); const resources = require('./resources'); -const RetryableErrorCodes = new Set([ +const retryableErrorCodes = new Set([ 'ETIMEDOUT', 'ECONNRESET', 'EADDRINUSE', @@ -20,7 +20,7 @@ const RetryableErrorCodes = new Set([ 'EAI_AGAIN' ]); -const RetryableStatusCodes = new Set([ +const retryableStatusCodes = new Set([ 408, 413, 429, 500, 502, 503, 504, 521, 522, 524 ]); @@ -41,7 +41,7 @@ const RetryableStatusCodes = new Set([ * @param {Function} [options.stringifyJson] The function used to serialize to * JSON * @param {Number} [options.maxRetries] Maximum number of automatic request - * retries, defaults to 0 (no retries) + * retries * @constructor * @public */ @@ -52,7 +52,7 @@ function Shopify(options) { !options.shopName || (!options.accessToken && (!options.apiKey || !options.password)) || (options.accessToken && (options.apiKey || options.password)) || - (options.autoLimit && options.maxRetries > 0) + (options.autoLimit && options.maxRetries) ) { throw new Error('Missing or invalid options'); } @@ -157,7 +157,7 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { this.options.maxRetries > 0 ? { limit: this.options.maxRetries, - // Don't clamp Shopify retry-after header values too low. + // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, calculateDelay } @@ -273,125 +273,108 @@ Shopify.prototype.graphql = function graphql(data, variables) { this.options.maxRetries > 0 ? { limit: this.options.maxRetries, - // Don't clamp Shopify retry-after header values too low. + // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, calculateDelay } : 0, hooks: { afterResponse: [ - (response) => { - if (response.body) { - if (response.body.extensions && response.body.extensions.cost) { - this.updateGraphqlLimits(response.body.extensions.cost); + (res) => { + if (res.body) { + if (res.body.extensions && res.body.extensions.cost) { + this.updateGraphqlLimits(res.body.extensions.cost); } - if (response.body.errors) { - // Make got consider this response errored and retry if needed - throw new Error(response.body.errors[0].message); + if (res.body.errors) { + // Make Got consider this response errored and retry if needed. + throw new Error(res.body.errors[0].message); } } - return response; + return res; } ], - beforeError: [errorForGraphQLError] + beforeError: [decorateError] } }; - return got(uri, options).then(returnResponseData); + return got(uri, options).then(responseData); }; resources.registerAll(Shopify); /** - * Return the data of a GraphQL response object + * Got `calculateDelay` hook function passed to decide how long to wait before + * retrying. * - * @param {Response} res Got response object - * @return {Object} The data + * @param {Object} retryObject Got's input for the retry logic + * @return {Number} The delay * @private */ -function returnResponseData(res) { - return res.body.data; +function calculateDelay(retryObject) { + return maybeRetryMS(retryObject.error) || retryObject.computedValue; } /** - * Returns a promise that resolves after a given amount of time. + * Decorates an `Error` object with details from GraphQL errors in the response + * body. * - * @param {Number} ms Amount of milliseconds to wait - * @return {Promise} Promise that resolves after `ms` milliseconds + * @param {Error} error The error to decorate + * @return {Error} The decorated error * @private */ -function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +function decorateError(error) { + if (error.response && error.response.body.errors) { + const first = error.response.body.errors[0]; -/** - * Parses the `Link` header into an object. - * - * @param {String} header The field value of the header - * @return {Object} The parsed header - * @private - */ -function parseLinkHeader(header) { - return header.split(',').reduce(reducer, {}); + error.locations = first.locations; + error.path = first.path; + error.extensions = first.extensions; + } + + return error; } /** - * The callback function for `Array.prototype.reduce()` used by - * `parseLinkHeader()`. + * Returns a promise that resolves after a given amount of time. * - * @param {Array} acc The accumulator - * @param {Object} cur The current element being processed in the array - * @return {Object} The accumulator + * @param {Number} ms Amount of milliseconds to wait + * @return {Promise} Promise that resolves after `ms` milliseconds * @private */ -function reducer(acc, cur) { - const pieces = cur.trim().split(';'); - const link = url.parse(pieces[0].trim().slice(1, -1), true); - const rel = pieces[1].trim().slice(4); - - if (rel === '"next"') acc.next = link; - else acc.previous = link; - - return acc; +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); } /** - * Given an error from got, see if Shopify told us how long to wait before - * retrying. Return a duration in ms if we can, and otherwise return null. + * Given an error from Got, see if Shopify told us how long to wait before + * retrying. * - * @param {Object} error Error object from got call - * @return {Boolean | null} + * @param {Object} error Error object from Got call + * @return {(Number|null)} The duration in ms, or `null` * @private **/ function maybeRetryMS(error) { - // for simplicity, retry network connectivity issues after a hardcoded 1s - if (RetryableErrorCodes.has(error.code)) { + // For simplicity, retry network connectivity issues after a hardcoded 1s. + if (retryableErrorCodes.has(error.code)) { return 1000; } const response = error.response; if (response.headers && response.headers['retry-after']) { - const value = parseFloat(response.headers['retry-after']); - if (isFinite(value)) { - return value * 1000; - } - - // We got a retry-after header but don't know how to parse it, - // assume retrying is unsafe as something has changed - return null; + return response.headers['retry-after'] * 1000 || null; } - if (RetryableStatusCodes.has(response.statusCode)) { - // Arbitrary 2 seconds, in case we get a 429 without a Retry-After - // response header, or 4xx/5xx series error that matches the got retry - // defaults + if (retryableStatusCodes.has(response.statusCode)) { + // Arbitrary 2 seconds, in case we get a 429 without a `Retry-After` + // response header, or 4xx/5xx series error that matches the Got retry + // defaults. return 2 * 1000; } - // detect graphql request throttling + // Detect GraphQL request throttling. if (response.body && typeof response.body === 'object') { const body = response.body; @@ -414,32 +397,45 @@ function maybeRetryMS(error) { } /** - * Decorate a ResponseError object thrown by got with details from - * GraphQL errors in the response body - * @param {ResponseError} error - * @returns ResponseError + * Parses the `Link` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed header * @private */ -function errorForGraphQLError(error) { - if (error.response && error.response.body.errors) { - const first = error.response.body.errors[0]; +function parseLinkHeader(header) { + return header.split(',').reduce(reducer, {}); +} - error.locations = first.locations; - error.path = first.path; - error.extensions = first.extensions; - } - return error; +/** + * The callback function for `Array.prototype.reduce()` used by + * `parseLinkHeader()`. + * + * @param {Array} acc The accumulator + * @param {Object} cur The current element being processed in the array + * @return {Object} The accumulator + * @private + */ +function reducer(acc, cur) { + const pieces = cur.trim().split(';'); + const link = url.parse(pieces[0].trim().slice(1, -1), true); + const rel = pieces[1].trim().slice(4); + + if (rel === '"next"') acc.next = link; + else acc.previous = link; + + return acc; } /** - * Got `calculateDelay` hook function passed to decide how long to wait before retrying + * Returns the data of a GraphQL response object. * - * @param {Object} retryObject got's input for the retry logic - * @return {Number} + * @param {Response} res Got response object + * @return {Object} The data * @private */ -function calculateDelay(retryObject) { - return maybeRetryMS(retryObject.error) || retryObject.computedValue; +function responseData(res) { + return res.body.data; } module.exports = Shopify; diff --git a/test/common.js b/test/common.js index da097024..860bfdbd 100644 --- a/test/common.js +++ b/test/common.js @@ -17,6 +17,11 @@ const shopifyWithPresentmentOption = new Shopify({ presentmentPrices: true, shopName }); +const shopifyWithRetries = new Shopify({ + accessToken, + shopName, + maxRetries: 3 +}); const scope = nock(`https://${shopName}.myshopify.com`, { reqheaders: { @@ -37,8 +42,42 @@ const presentmentApiScope = nock(`https://${shopName}.myshopify.com`, { badheaders: ['Authorization'] }); +/** + * Add a working (200 status code) mock for the REST Admin API. + * + * @param {Scope} scope An instance of the `Scope` class of the nock module. + * @public + */ +function addWorkingRESTRequestMock(scope) { + scope.get('/admin/shop.json').reply(200, { + shop: { + id: 1, + name: 'My Cool Test Shop' + } + }); +} + +/** + * Add a working (200 status code) mock for the GraphQL Admin API. + * + * @param {Scope} scope An instance of the `Scope` class of the nock module. + * @public + */ +function addWorkingGraphQLRequestMock(scope) { + scope.post('/admin/api/graphql.json').reply(200, { + data: { + shop: { + id: 1, + name: 'My Cool Test Shop' + } + } + }); +} + module.exports = { accessToken, + addWorkingGraphQLRequestMock, + addWorkingRESTRequestMock, apiKey, apiVersion, password, @@ -46,5 +85,6 @@ module.exports = { scope, shopify, shopifyWithPresentmentOption, + shopifyWithRetries, shopName }; diff --git a/test/shopify.test.js b/test/shopify.test.js index 1824095c..4da14f7b 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -22,6 +22,7 @@ describe('Shopify', () => { const apiVersion = common.apiVersion; const password = common.password; const shopify = common.shopify; + const shopifyWithRetries = common.shopifyWithRetries; const shopName = common.shopName; it('exports the constructor', () => { @@ -38,10 +39,9 @@ describe('Shopify', () => { expect(() => new Shopify({ password })).to.throw(Error, msg); expect(() => new Shopify({ accessToken, apiKey })).to.throw(Error, msg); expect(() => new Shopify({ accessToken, password })).to.throw(Error, msg); - expect( - () => - new Shopify({ accessToken, password, maxRetries: 1, autoLimit: true }) - ).to.throw(Error, msg); + expect(() => { + new Shopify({ accessToken, password, maxRetries: 1, autoLimit: true }); + }).to.throw(Error, msg); }); it('makes the new operator optional', () => { @@ -125,6 +125,7 @@ describe('Shopify', () => { describe('Shopify#request', () => { const url = { pathname: '/test', ...shopify.baseUrl }; + const addWorkingRESTRequestMock = common.addWorkingRESTRequestMock; const scope = common.scope; afterEach(() => expect(nock.pendingMocks()).to.deep.equal([])); @@ -178,9 +179,7 @@ describe('Shopify', () => { }, (err) => { expect(err).to.be.an.instanceof(got.TimeoutError); - expect(err.message).to.include( - "Timeout awaiting 'request' for 100ms" - ); + expect(err.message).to.equal("Timeout awaiting 'request' for 100ms"); } ); }); @@ -534,23 +533,6 @@ describe('Shopify', () => { }); }); - const addWorkingRESTRequestMock = (scope) => { - scope.get('/admin/shop.json').reply(200, { - shop: { - name: 'My Cool Test Shop', - id: '1' - } - }); - }; - - const shopifyWithRetries = new Shopify({ - accessToken, - parseJson, - shopName, - stringifyJson, - maxRetries: 3 - }); - it('retries 429 errors from Shopify according to the header', () => { scope .get('/admin/shop.json') @@ -559,6 +541,7 @@ describe('Shopify', () => { .reply(429, 'too many requests', { 'Retry-After': '0.5' }); addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); @@ -572,10 +555,11 @@ describe('Shopify', () => { .reply(429, 'too many requests'); addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); - }).timeout(15000); + }).timeout(8000); it('retries 429 errors from Shopify that have broken header values', () => { scope @@ -583,19 +567,20 @@ describe('Shopify', () => { .reply(429, 'too many requests', { 'Retry-After': 'foobar' }); addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); - }).timeout(5000); + }); it("doesn't retry 404 errors", () => { scope.get('/admin/products/10.json').reply(404, { error: 'not found' }); - return shopifyWithRetries.product.get('10').catch((err) => { + return shopifyWithRetries.product.get(10).catch((err) => { expect(err).to.be.an.instanceof(got.HTTPError); - expect(err.message).to.include('Response code 404 (Not Found)'); + expect(err.message).to.equal('Response code 404 (Not Found)'); }); }); @@ -605,6 +590,7 @@ describe('Shopify', () => { }); addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); @@ -617,6 +603,7 @@ describe('Shopify', () => { }); addWorkingRESTRequestMock(scope); + return shopifyWithRetries.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); @@ -625,9 +612,7 @@ describe('Shopify', () => { it('retries a variety of errors in order', () => { const shopify = new Shopify({ accessToken, - parseJson, shopName, - stringifyJson, maxRetries: 5, timeout: 200 }); @@ -645,47 +630,25 @@ describe('Shopify', () => { .get('/admin/shop.json') .reply(500, 'sorry its broken') .get('/admin/shop.json') - .delay(500) // longer than API client configured timeout option + .delay(500) // Longer than API client configured timeout option. .reply(200, { shop: { - name: 'My Cool Test Shop', - id: '1' + id: 1, + name: 'My Cool Test Shop' } }); addWorkingRESTRequestMock(scope); + return shopify.shop.get().then((result) => { expect(result.name).equal('My Cool Test Shop'); }); - }).timeout(7000); + }).timeout(10000); }); describe('Shopify#graphql', () => { - const addWorkingGraphQLRequestMock = (scope) => { - scope.post('/admin/api/graphql.json').reply(200, { - data: { - shop: { - name: 'My Cool Test Shop', - id: '1' - } - } - }); - }; - - const shopifyWithRetries = new Shopify({ - accessToken, - parseJson, - shopName, - stringifyJson, - maxRetries: 3 - }); - - const scope = nock(`https://${shopName}.myshopify.com`, { - reqheaders: { - 'User-Agent': `${pkg.name}/${pkg.version}`, - 'X-Shopify-Access-Token': accessToken - } - }); + const addWorkingGraphQLRequestMock = common.addWorkingGraphQLRequestMock; + const scope = common.scope; afterEach(() => expect(nock.pendingMocks()).to.deep.equal([])); @@ -782,9 +745,7 @@ describe('Shopify', () => { }, (err) => { expect(err).to.be.an.instanceof(got.TimeoutError); - expect(err.message).to.include( - "Timeout awaiting 'request' for 100ms" - ); + expect(err.message).to.equal("Timeout awaiting 'request' for 100ms"); } ); }); @@ -984,7 +945,7 @@ describe('Shopify', () => { }); }); - it("doesn't retry errors from broken graphql queries", () => { + it("doesn't retry errors from broken GraphQL queries", () => { scope.post('/admin/api/graphql.json').reply(200, { errors: [ { @@ -998,8 +959,9 @@ describe('Shopify', () => { } ] }); + return shopifyWithRetries.graphql('query { shop ').catch((err) => { - expect(err.message).to.include('Parse error on "}" (RCURLY) at [4, 1]'); + expect(err.message).to.equal('Parse error on "}" (RCURLY) at [4, 1]'); }); }); @@ -1013,27 +975,27 @@ describe('Shopify', () => { .then((result) => { expect(result.shop.name).equal('My Cool Test Shop'); }); - }).timeout(3000); + }).timeout(4000); it('retries timeout errors from Shopify', () => { const shopify = new Shopify({ accessToken, - parseJson, shopName, - stringifyJson, maxRetries: 3, timeout: 900 }); + scope .post('/admin/api/graphql.json') .delay(1000) - .reply(500, () => 'something went wrong'); + .reply(500, 'something went wrong'); addWorkingGraphQLRequestMock(scope); + return shopify.graphql('query { shop { id name } }').then((result) => { expect(result.shop.name).equal('My Cool Test Shop'); }); - }); + }).timeout(4000); it('retries network system level errors immediately', () => { scope.post('/admin/api/graphql.json').replyWithError({ @@ -1042,6 +1004,7 @@ describe('Shopify', () => { }); addWorkingGraphQLRequestMock(scope); + return shopifyWithRetries .graphql('query { shop { id name } }') .then((result) => { @@ -1049,7 +1012,7 @@ describe('Shopify', () => { }); }); - it('retries graphql cost limit exceeded errors', () => { + it('retries GraphQL cost limit exceeded errors', () => { scope .post('/admin/api/graphql.json') .reply(200, { @@ -1099,6 +1062,7 @@ describe('Shopify', () => { }); addWorkingGraphQLRequestMock(scope); + return shopifyWithRetries .graphql('query { shop { id name } }') .then((result) => { From 603a7c06850d41006a7e61e05ddb8454bbc8cf30 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 28 Jun 2022 18:43:59 +0200 Subject: [PATCH 019/110] [dist] 3.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 194c24ce..9061e32c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.9.1", + "version": "3.10.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 32d17cbedbb46735963ee0568bbe049a63752629 Mon Sep 17 00:00:00 2001 From: Luis Enrique Date: Fri, 8 Jul 2022 22:22:07 +0300 Subject: [PATCH 020/110] [ts] Add maxRetries to I{Private,Public}ShopifyConfig (#546) --- index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.d.ts b/index.d.ts index d549f0fb..ec18daab 100644 --- a/index.d.ts +++ b/index.d.ts @@ -762,6 +762,7 @@ declare namespace Shopify { accessToken: string; apiVersion?: string; autoLimit?: boolean | IAutoLimit; + maxRetries?: number; presentmentPrices?: boolean; shopName: string; timeout?: number; @@ -771,6 +772,7 @@ declare namespace Shopify { apiKey: string; apiVersion?: string; autoLimit?: boolean | IAutoLimit; + maxRetries?: number; password: string; presentmentPrices?: boolean; shopName: string; From 49a52f778529632887e717848fe4214ac2d97cc6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 8 Jul 2022 21:24:15 +0200 Subject: [PATCH 021/110] [dist] 3.10.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9061e32c..c5bb531e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.10.0", + "version": "3.10.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From f390c9caf5205d7444e4406773232e8d69ccf98d Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Thu, 14 Jul 2022 10:25:35 -0400 Subject: [PATCH 022/110] [fix] Inspect extensions only if body.errors is an array (#548) Fixes: https://github.com/MONEI/Shopify-api-node/issues/547 --- index.js | 1 + test/shopify.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/index.js b/index.js index eedeb8f5..d56cd872 100644 --- a/index.js +++ b/index.js @@ -380,6 +380,7 @@ function maybeRetryMS(error) { if ( body.errors && + Array.isArray(body.errors) && body.errors[0].extensions && body.errors[0].extensions.code == 'THROTTLED' ) { diff --git a/test/shopify.test.js b/test/shopify.test.js index 4da14f7b..22cdb053 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -584,6 +584,47 @@ describe('Shopify', () => { }); }); + it("doesn't retry 422 errors that return an error string", () => { + scope.put('/admin/products/10.json').reply(422, { + error: 'the product was invalid' + }); + + 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)' + ); + }); + }); + + it("doesn't retry 422 errors that return an errors array", () => { + scope.put('/admin/products/10.json').reply(422, { + errors: ['the product was invalid'] + }); + + 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)' + ); + }); + }); + + it("doesn't retry 422 errors that return an errors object", () => { + scope.put('/admin/products/10.json').reply(422, { + errors: { + title: 'is required' + } + }); + + 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)' + ); + }); + }); + it('retries 500 errors from shopify', () => { scope.get('/admin/shop.json').reply(500, { error: 'something went wrong' From b4f059ba53aa0f1c6e135219ae02b95632c75f9a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 14 Jul 2022 16:27:09 +0200 Subject: [PATCH 023/110] [minor] Fix nit --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index d56cd872..6fcef5f5 100644 --- a/index.js +++ b/index.js @@ -379,7 +379,6 @@ function maybeRetryMS(error) { const body = response.body; if ( - body.errors && Array.isArray(body.errors) && body.errors[0].extensions && body.errors[0].extensions.code == 'THROTTLED' From 419b2ce11d4d1e368f77988a67554bea9967b232 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 14 Jul 2022 16:28:06 +0200 Subject: [PATCH 024/110] [dist] 3.10.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5bb531e..9067a72a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.10.1", + "version": "3.10.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 89c48b89db71e27d3b6614f77fdba164b35bfed8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 21 Jul 2022 15:28:03 +0200 Subject: [PATCH 025/110] [api] Add setFulfillmentOrdersDeadline action to FulfillmentOrder --- README.md | 1 + index.d.ts | 8 ++++++++ resources/fulfillment-order.js | 15 +++++++++++++++ test/fixtures/fulfillment-order/req/index.js | 1 + .../req/set-fulfillment-orders-deadline.json | 4 ++++ test/fulfillment-order.test.js | 17 +++++++++++++++++ 6 files changed, 46 insertions(+) create mode 100644 test/fixtures/fulfillment-order/req/set-fulfillment-orders-deadline.json diff --git a/README.md b/README.md index 491c2d90..4811fe43 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,7 @@ default. - `list([params])` - `locationsForMove(id)` - `move(id, locationId)` + - `setFulfillmentOrdersDeadline(params)` - fulfillmentRequest - `accept(fulfillmentOrderId[, message])` - `create(fulfillmentOrderId, params)` diff --git a/index.d.ts b/index.d.ts index ec18daab..d804e3bd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -367,6 +367,9 @@ declare class Shopify { id: number, locationId: number ) => Promise; + setFulfillmentOrdersDeadline: ( + params: Shopify.ISetFulfillmentOrdersDeadline + ) => Promise; }; fulfillmentRequest: { accept: ( @@ -1806,6 +1809,11 @@ declare namespace Shopify { message: string; } + interface ISetFulfillmentOrdersDeadline { + fulfillment_deadline: string; + fulfillment_order_ids: number[]; + } + interface ICreateFulfillmentRequestFulfillmentOrderLineItem { id: number; quantity: number; diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 14808381..419d8581 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -100,4 +100,19 @@ FulfillmentOrder.prototype.locationsForMove = function locationsForMove(id) { return this.shopify.request(url, 'GET', 'locations_for_move'); }; +/** + * Sets the latest date and time by which the fulfillment orders need to be + * fulfilled. + * + * @param {Object} params An object containing the new fulfillment deadline and + * and the IDs of the fulfillment orders for which the deadline is being set + * @return {Promise} Promise that resolves with the result + * @public + */ +FulfillmentOrder.prototype.setFulfillmentOrdersDeadline = + function setFulfillmentOrdersDeadline(params) { + const url = this.buildUrl('set_fulfillment_orders_deadline'); + return this.shopify.request(url, 'POST', undefined, params); + }; + module.exports = FulfillmentOrder; diff --git a/test/fixtures/fulfillment-order/req/index.js b/test/fixtures/fulfillment-order/req/index.js index cb386990..54a1d09a 100644 --- a/test/fixtures/fulfillment-order/req/index.js +++ b/test/fixtures/fulfillment-order/req/index.js @@ -3,3 +3,4 @@ exports.cancel = require('./cancel'); exports.close = require('./close'); exports.move = require('./move'); +exports.setFulfillmentOrdersDeadline = require('./set-fulfillment-orders-deadline'); diff --git a/test/fixtures/fulfillment-order/req/set-fulfillment-orders-deadline.json b/test/fixtures/fulfillment-order/req/set-fulfillment-orders-deadline.json new file mode 100644 index 00000000..085f9942 --- /dev/null +++ b/test/fixtures/fulfillment-order/req/set-fulfillment-orders-deadline.json @@ -0,0 +1,4 @@ +{ + "fulfillment_order_ids": [1046000789], + "fulfillment_deadline": "2021-05-26T10:00:00-04:00" +} diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index abd127a6..0f36137f 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -105,4 +105,21 @@ describe('Shopify#fulfillmentOrder', () => { expect(data).to.deep.equal(output.original_fulfillment_order); }); }); + + it('reschedules the fulfill_at time of a scheduled fulfillment order', () => { + const input = fixtures.req.setFulfillmentOrdersDeadline; + + scope + .post( + '/admin/fulfillment_orders/set_fulfillment_orders_deadline.json', + input + ) + .reply(200, {}); + + return shopify.fulfillmentOrder + .setFulfillmentOrdersDeadline(input) + .then((data) => { + expect(data).to.deep.equal({}); + }); + }); }); From c936f047c37b071c8f8fa1fd36b697720053d719 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 21 Jul 2022 17:05:27 +0200 Subject: [PATCH 026/110] [api] Add DisputeEvidence resource --- README.md | 3 + index.d.ts | 61 ++++++++++++++++ mixins/shopify-payments-child.js | 43 +++++++++++ resources/dispute-evidence.js | 45 ++++++++++++ resources/index.js | 1 + test/dispute-evidence.test.js | 73 +++++++++++++++++++ test/fixtures/dispute-evidence/index.js | 4 + test/fixtures/dispute-evidence/req/index.js | 3 + .../fixtures/dispute-evidence/req/update.json | 5 ++ test/fixtures/dispute-evidence/res/get.json | 62 ++++++++++++++++ test/fixtures/dispute-evidence/res/index.js | 4 + .../fixtures/dispute-evidence/res/update.json | 62 ++++++++++++++++ 12 files changed, 366 insertions(+) create mode 100644 mixins/shopify-payments-child.js create mode 100644 resources/dispute-evidence.js create mode 100644 test/dispute-evidence.test.js create mode 100644 test/fixtures/dispute-evidence/index.js create mode 100644 test/fixtures/dispute-evidence/req/index.js create mode 100644 test/fixtures/dispute-evidence/req/update.json create mode 100644 test/fixtures/dispute-evidence/res/get.json create mode 100644 test/fixtures/dispute-evidence/res/index.js create mode 100644 test/fixtures/dispute-evidence/res/update.json diff --git a/README.md b/README.md index 4811fe43..07867edb 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,9 @@ default. - dispute - `get(id)` - `list([params])` +- disputeEvidence + - `get(disputeId)` + - `update(disputeId, params)` - draftOrder - `complete(id[, params])` - `count()` diff --git a/index.d.ts b/index.d.ts index d804e3bd..8c134293 100644 --- a/index.d.ts +++ b/index.d.ts @@ -286,6 +286,13 @@ declare class Shopify { get: (id: number) => Promise; list: (params?: any) => Promise>; }; + disputeEvidence: { + get: (disputeId: number) => Promise; + update: ( + disputeId: number, + params: Shopify.IUpdateDisputeEvidence + ) => Promise; + }; draftOrder: { complete: (id: number, params?: any) => Promise; count: () => Promise; @@ -1515,6 +1522,60 @@ declare namespace Shopify { finalized_on: string; } + interface IDisputeEvidenceAddress { + address1: string; + address2: string; + city: string; + country: string; + country_code: string; + id: number; + province: string; + province_code: string; + zip: string; + } + + interface IDisputeEvidenceFulfillment { + shipping_carrier: number; + shipping_date: string; + shipping_tracking_number: string; + } + + interface IDisputeEvidenceFiles { + cancellation_policy_file_id: number | null; + customer_communication_file_id: number | null; + customer_signature_file_id: number | null; + refund_policy_file_id: number | null; + service_documentation_file_id: number | null; + shipping_documentation_file_id: number | null; + uncategorized_file_id: number | null; + } + + interface IDisputeEvidence { + access_activity_log: string | null; + billing_address: IDisputeEvidenceAddress; + cancellation_policy_disclosure: string | null; + cancellation_rebuttal: string | null; + created_at: string; + customer_email_address: string; + customer_first_name: string; + customer_last_name: string; + dispute_evidence_files: IDisputeEvidenceFiles; + fulfillments: IDisputeEvidenceFulfillment[]; + id: number; + payments_dispute_id: number; + product_description: string; + refund_policy_disclosure: string | null; + refund_refusal_explanation: string | null; + shipping_address: IDisputeEvidenceAddress; + submitted_by_merchant_on: string | null; + uncategorized_text: string; + updated_at: string; + } + + interface IUpdateDisputeEvidence { + refund_refusal_explanation: string; + } + interface IDraftOrderNoteAttribute { name: string; value: string; diff --git a/mixins/shopify-payments-child.js b/mixins/shopify-payments-child.js new file mode 100644 index 00000000..5045818a --- /dev/null +++ b/mixins/shopify-payments-child.js @@ -0,0 +1,43 @@ +'use strict'; + +const qs = require('qs'); + +/** + * This provides methods used by the Shopify Payments resources. It's not meant + * to be used directly. + * + * @mixin + */ +const shopifyPaymentsChild = { + /** + * Builds the request URL. + * + * @param {Number} parentId Parent record ID + * @param {Number|String} [id] Record ID + * @param {Object} [query] Query parameters + * @return {Object} URL object + * @private + */ + buildUrl(parentId, id, query) { + id || id === 0 || (id = ''); + + let pathname = '/admin'; + + if (this.shopify.options.apiVersion) { + pathname += `/api/${this.shopify.options.apiVersion}`; + } + + pathname += `/shopify_payments/${this.parentName}/${parentId}/${this.name}/${id}`; + pathname = pathname.replace(/\/+/g, '/').replace(/\/$/, '') + '.json'; + + const url = { pathname, ...this.shopify.baseUrl }; + + if (query) { + url.search = '?' + qs.stringify(query, { arrayFormat: 'brackets' }); + } + + return url; + } +}; + +module.exports = shopifyPaymentsChild; diff --git a/resources/dispute-evidence.js b/resources/dispute-evidence.js new file mode 100644 index 00000000..f17e6ec3 --- /dev/null +++ b/resources/dispute-evidence.js @@ -0,0 +1,45 @@ +'use strict'; + +const shopifyPaymentsChild = require('../mixins/shopify-payments-child'); +const baseChild = require('../mixins/base-child'); + +/** + * Creates a DisputeEvidence instance. + * + * @param {Shopify} shopify Reference to the Shopify instance + * @constructor + * @public + */ +function DisputeEvidence(shopify) { + this.shopify = shopify; + + this.parentName = 'disputes'; + this.name = 'dispute_evidences'; + this.key = 'dispute_evidence'; +} + +DisputeEvidence.prototype.buildUrl = shopifyPaymentsChild.buildUrl; + +/** + * Returns the dispute evidence associated with the dispute ID + * + * @param {Number} disputeId The dispute ID + * @param {Object} [params] Query parameters + * @return {Promise} Promise that resolves with the result + */ +DisputeEvidence.prototype.get = function get(disputeId, params) { + return baseChild.get.call(this, disputeId, undefined, params); +}; + +/** + * Updates the dispute evidence associated with dispute ID + * + * @param {Number} disputeId The dispute ID + * @param {Object} params An object containing the refund refusal explanation + * @return {Promise} Promise that resolves with the result + */ +DisputeEvidence.prototype.update = function update(disputeId, params) { + return baseChild.update.call(this, disputeId, undefined, params); +}; + +module.exports = DisputeEvidence; diff --git a/resources/index.js b/resources/index.js index d82d5d6b..f06544dc 100644 --- a/resources/index.js +++ b/resources/index.js @@ -26,6 +26,7 @@ const map = { discountCode: 'discount-code', discountCodeCreationJob: 'discount-code-creation-job', dispute: 'dispute', + disputeEvidence: 'dispute-evidence', draftOrder: 'draft-order', event: 'event', fulfillment: 'fulfillment', diff --git a/test/dispute-evidence.test.js b/test/dispute-evidence.test.js new file mode 100644 index 00000000..eb630fb7 --- /dev/null +++ b/test/dispute-evidence.test.js @@ -0,0 +1,73 @@ +describe('Shopify#disputeEvidence', () => { + 'use strict'; + + const expect = require('chai').expect; + + const fixtures = require('./fixtures/dispute-evidence'); + const common = require('./common'); + const Shopify = require('..'); + + const accessToken = common.accessToken; + const scope = common.scope; + const shopify = common.shopify; + const shopName = common.shopName; + + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); + + it('get the dispute evidence associated with the dispute ID (1/2)', () => { + const output = fixtures.res.get; + + scope + .get('/admin/shopify_payments/disputes/598735659/dispute_evidences.json') + .reply(200, output); + + return shopify.disputeEvidence + .get(598735659) + .then((data) => expect(data).to.deep.equal(output.dispute_evidence)); + }); + + it('get the dispute evidence associated with the dispute ID (2/2)', () => { + const output = fixtures.res.get; + const params = { foo: 'bar' }; + + scope + .get('/admin/shopify_payments/disputes/598735659/dispute_evidences.json') + .query(params) + .reply(200, output); + + return shopify.disputeEvidence + .get(598735659, params) + .then((data) => expect(data).to.deep.equal(output.dispute_evidence)); + }); + + it('updates the dispute evidence associated with dispute ID', () => { + const input = fixtures.req.update; + const output = fixtures.res.update; + + scope + .put( + '/admin/shopify_payments/disputes/598735659/dispute_evidences.json', + input + ) + .reply(200, output); + + return shopify.disputeEvidence + .update(598735659, input.dispute_evidence) + .then((data) => expect(data).to.deep.equal(output.dispute_evidence)); + }); + + it('injects the api version to the request path if provided', () => { + const output = fixtures.res.get; + const apiVersion = '2022-07'; + const shopify = new Shopify({ shopName, accessToken, apiVersion }); + const pathname = + `/admin/api/${apiVersion}/shopify_payments` + + '/disputes/598735659/dispute_evidences.json'; + + scope.get(pathname).reply(200, output); + + return shopify.disputeEvidence + .get(598735659) + .then((data) => expect(data).to.deep.equal(output.dispute_evidence)); + }); +}); diff --git a/test/fixtures/dispute-evidence/index.js b/test/fixtures/dispute-evidence/index.js new file mode 100644 index 00000000..dd1de7d4 --- /dev/null +++ b/test/fixtures/dispute-evidence/index.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.req = require('./req'); +exports.res = require('./res'); diff --git a/test/fixtures/dispute-evidence/req/index.js b/test/fixtures/dispute-evidence/req/index.js new file mode 100644 index 00000000..01a36fc6 --- /dev/null +++ b/test/fixtures/dispute-evidence/req/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.update = require('./update'); diff --git a/test/fixtures/dispute-evidence/req/update.json b/test/fixtures/dispute-evidence/req/update.json new file mode 100644 index 00000000..262bbc2e --- /dev/null +++ b/test/fixtures/dispute-evidence/req/update.json @@ -0,0 +1,5 @@ +{ + "dispute_evidence": { + "refund_refusal_explanation": "Product must have receipt of proof of purchase" + } +} diff --git a/test/fixtures/dispute-evidence/res/get.json b/test/fixtures/dispute-evidence/res/get.json new file mode 100644 index 00000000..7c693ff0 --- /dev/null +++ b/test/fixtures/dispute-evidence/res/get.json @@ -0,0 +1,62 @@ +{ + "dispute_evidence": { + "id": 819974671, + "payments_dispute_id": 598735659, + "access_activity_log": null, + "billing_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "cancellation_policy_disclosure": null, + "cancellation_rebuttal": null, + "customer_email_address": "example@email.com", + "customer_first_name": "Kermit", + "customer_last_name": "the Frog", + "product_description": "Product name: Draft\nTitle: 151cm\nPrice: $10.00\nQuantity: 1\nProduct Description: good board", + "refund_policy_disclosure": null, + "refund_refusal_explanation": null, + "shipping_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "uncategorized_text": "Sample uncategorized text", + "created_at": "2022-06-07T14:30:15-04:00", + "updated_at": "2022-06-07T14:30:33-04:00", + "submitted_by_merchant_on": null, + "fulfillments": [ + { + "shipping_carrier": "UPS", + "shipping_tracking_number": "1234", + "shipping_date": "2017-01-01" + }, + { + "shipping_carrier": "FedEx", + "shipping_tracking_number": "4321", + "shipping_date": "2017-01-02" + } + ], + "dispute_evidence_files": { + "cancellation_policy_file_id": null, + "customer_communication_file_id": 539650252, + "customer_signature_file_id": 799719586, + "refund_policy_file_id": null, + "service_documentation_file_id": null, + "shipping_documentation_file_id": 799719586, + "uncategorized_file_id": 567271523 + } + } +} diff --git a/test/fixtures/dispute-evidence/res/index.js b/test/fixtures/dispute-evidence/res/index.js new file mode 100644 index 00000000..2e3b6b9a --- /dev/null +++ b/test/fixtures/dispute-evidence/res/index.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.get = require('./get'); +exports.update = require('./update'); diff --git a/test/fixtures/dispute-evidence/res/update.json b/test/fixtures/dispute-evidence/res/update.json new file mode 100644 index 00000000..8af370a2 --- /dev/null +++ b/test/fixtures/dispute-evidence/res/update.json @@ -0,0 +1,62 @@ +{ + "dispute_evidence": { + "id": 819974671, + "payments_dispute_id": 598735659, + "access_activity_log": null, + "billing_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "cancellation_policy_disclosure": null, + "cancellation_rebuttal": null, + "customer_email_address": "example@email.com", + "customer_first_name": "Kermit", + "customer_last_name": "the Frog", + "product_description": "Product name: Draft\nTitle: 151cm\nPrice: $10.00\nQuantity: 1\nProduct Description: good board", + "refund_policy_disclosure": null, + "refund_refusal_explanation": "Product must have receipt of proof of purchase", + "shipping_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "uncategorized_text": "Sample uncategorized text", + "created_at": "2022-06-07T14:30:15-04:00", + "updated_at": "2022-06-07T14:30:42-04:00", + "submitted_by_merchant_on": null, + "fulfillments": [ + { + "shipping_carrier": "UPS", + "shipping_tracking_number": "1234", + "shipping_date": "2017-01-01" + }, + { + "shipping_carrier": "FedEx", + "shipping_tracking_number": "4321", + "shipping_date": "2017-01-02" + } + ], + "dispute_evidence_files": { + "cancellation_policy_file_id": null, + "customer_communication_file_id": 539650252, + "customer_signature_file_id": 799719586, + "refund_policy_file_id": null, + "service_documentation_file_id": null, + "shipping_documentation_file_id": 799719586, + "uncategorized_file_id": 567271523 + } + } +} From f4f89c4e274cf05f9ced2485ecd728e69530654a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 21 Jul 2022 17:36:06 +0200 Subject: [PATCH 027/110] [api] Add DisputeFileUpload resource --- README.md | 3 + index.d.ts | 37 +++++++++++ resources/dispute-file-upload.js | 27 ++++++++ resources/index.js | 1 + test/dispute-file-upload.test.js | 41 ++++++++++++ test/fixtures/dispute-file-upload/index.js | 4 ++ .../dispute-file-upload/req/create.json | 8 +++ .../fixtures/dispute-file-upload/req/index.js | 3 + .../dispute-file-upload/res/create.json | 15 +++++ .../fixtures/dispute-file-upload/res/get.json | 62 +++++++++++++++++++ .../fixtures/dispute-file-upload/res/index.js | 3 + 11 files changed, 204 insertions(+) create mode 100644 resources/dispute-file-upload.js create mode 100644 test/dispute-file-upload.test.js create mode 100644 test/fixtures/dispute-file-upload/index.js create mode 100644 test/fixtures/dispute-file-upload/req/create.json create mode 100644 test/fixtures/dispute-file-upload/req/index.js create mode 100644 test/fixtures/dispute-file-upload/res/create.json create mode 100644 test/fixtures/dispute-file-upload/res/get.json create mode 100644 test/fixtures/dispute-file-upload/res/index.js diff --git a/README.md b/README.md index 07867edb..15248318 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,9 @@ default. - disputeEvidence - `get(disputeId)` - `update(disputeId, params)` +- disputeFileUpload + - `create(disputeId, params)` + - `delete(disputeId, id)` - draftOrder - `complete(id[, params])` - `count()` diff --git a/index.d.ts b/index.d.ts index 8c134293..21e3576a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -293,6 +293,13 @@ declare class Shopify { params: Shopify.IUpdateDisputeEvidence ) => Promise; }; + disputeFileUpload: { + create: ( + disputeId: number, + params: Shopify.ICreateDisputeFileUpload + ) => Promise; + delete: (id: number) => Promise; + }; draftOrder: { complete: (id: number, params?: any) => Promise; count: () => Promise; @@ -1576,6 +1583,36 @@ declare namespace Shopify { refund_refusal_explanation: string; } + type DisputeFileUploadDisputeEvidenceType = + | 'cancellation_policy_file' + | 'customer_communication_file' + | 'customer_signature_file' + | 'refund_policy_file' + | 'service_documentation_file' + | 'shipping_documentation_file' + | 'uncategorized_file'; + + interface IDisputeFileUpload { + created_at: string; + dispute_evidence_id: number; + dispute_evidence_type: DisputeFileUploadDisputeEvidenceType; + file_size: number; + file_type: string; + filename: string; + id: number; + original_filename: string; + shop_id: number; + updated_at: string; + url: string; + } + + interface ICreateDisputeFileUpload { + document_type: DisputeFileUploadDisputeEvidenceType; + filename: string; + mimetype: string; + data: string; + } + interface IDraftOrderNoteAttribute { name: string; value: string; diff --git a/resources/dispute-file-upload.js b/resources/dispute-file-upload.js new file mode 100644 index 00000000..15d9a93c --- /dev/null +++ b/resources/dispute-file-upload.js @@ -0,0 +1,27 @@ +'use strict'; + +const assign = require('lodash/assign'); +const pick = require('lodash/pick'); + +const shopifyPaymentsChild = require('../mixins/shopify-payments-child'); +const baseChild = require('../mixins/base-child'); + +/** + * Creates a DisputeFileUpload instance. + * + * @param {Shopify} shopify Reference to the Shopify instance + * @constructor + * @public + */ +function DisputeFileUpload(shopify) { + this.shopify = shopify; + + this.parentName = 'disputes'; + this.name = 'dispute_file_uploads'; + this.key = 'dispute_file_upload'; +} + +assign(DisputeFileUpload.prototype, pick(baseChild, ['create', 'delete'])); +DisputeFileUpload.prototype.buildUrl = shopifyPaymentsChild.buildUrl; + +module.exports = DisputeFileUpload; diff --git a/resources/index.js b/resources/index.js index f06544dc..6711c42f 100644 --- a/resources/index.js +++ b/resources/index.js @@ -27,6 +27,7 @@ const map = { discountCodeCreationJob: 'discount-code-creation-job', dispute: 'dispute', disputeEvidence: 'dispute-evidence', + disputeFileUpload: 'dispute-file-upload', draftOrder: 'draft-order', event: 'event', fulfillment: 'fulfillment', diff --git a/test/dispute-file-upload.test.js b/test/dispute-file-upload.test.js new file mode 100644 index 00000000..682eb40a --- /dev/null +++ b/test/dispute-file-upload.test.js @@ -0,0 +1,41 @@ +describe('Shopify#disputeFileUpload', () => { + 'use strict'; + + const expect = require('chai').expect; + + const fixtures = require('./fixtures/dispute-file-upload'); + const common = require('./common'); + + const shopify = common.shopify; + const scope = common.scope; + + afterEach(() => expect(scope.pendingMocks()).to.deep.equal([])); + + it('uploads a file to a dispute', () => { + const input = fixtures.req.create; + const output = fixtures.res.create; + + scope + .post( + '/admin/shopify_payments/disputes/598735659/dispute_file_uploads.json', + input + ) + .reply(200, output); + + return shopify.disputeFileUpload + .create(598735659, input.dispute_file_upload) + .then((data) => expect(data).to.deep.equal(output.dispute_file_upload)); + }); + + it('deletes a dispute evidence file', () => { + const pathname = + '/admin/shopify_payments' + + '/disputes/598735659/dispute_file_uploads/799719586.json'; + + scope.delete(pathname).reply(200); + + return shopify.disputeFileUpload + .delete(598735659, 799719586) + .then((data) => expect(data).to.deep.equal({})); + }); +}); diff --git a/test/fixtures/dispute-file-upload/index.js b/test/fixtures/dispute-file-upload/index.js new file mode 100644 index 00000000..dd1de7d4 --- /dev/null +++ b/test/fixtures/dispute-file-upload/index.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.req = require('./req'); +exports.res = require('./res'); diff --git a/test/fixtures/dispute-file-upload/req/create.json b/test/fixtures/dispute-file-upload/req/create.json new file mode 100644 index 00000000..ff674538 --- /dev/null +++ b/test/fixtures/dispute-file-upload/req/create.json @@ -0,0 +1,8 @@ +{ + "dispute_file_upload": { + "document_type": "uncategorized_file", + "filename": "test.pdf", + "mimetype": "application/pdf", + "data": "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ZW5kc3RyZWFtCmVuZG9iagoyMSAwIG9iagoxNTk1MgplbmRvYmoKMjIgMCBvYmoKKExvcmVtIElwc3VtKQplbmRvYmoKMjMgMCBvYmoKKE1hYyBPUyBYIDEwLjEwIFF1YXJ0eiBQREZDb250ZXh0KQplbmRvYmoKMjQgMCBvYmoKKFNob3BpZnkgSW5jKQplbmRvYmoKMjUgMCBvYmoKKCkKZW5kb2JqCjI2IDAgb2JqCihXb3JkKQplbmRvYmoKMjcgMCBvYmoKKEQ6MjAxNDExMjUyMTM4NDlaMDAnMDAnKQplbmRvYmoKMjggMCBvYmoKKCkKZW5kb2JqCjI5IDAgb2JqClsgKCkgXQplbmRvYmoKMSAwIG9iago8PCAvVGl0bGUgMjIgMCBSIC9BdXRob3IgMjQgMCBSIC9TdWJqZWN0IDI1IDAgUiAvUHJvZHVjZXIgMjMgMCBSIC9DcmVhdG9yCjI2IDAgUiAvQ3JlYXRpb25EYXRlIDI3IDAgUiAvTW9kRGF0ZSAyNyAwIFIgL0tleXdvcmRzIDI4IDAgUiAvQUFQTDpLZXl3b3JkcwoyOSAwIFIgPj4KZW5kb2JqCnhyZWYKMCAzMAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMzM1NzYgMDAwMDAgbiAKMDAwMDAwNjg4NSAwMDAwMCBuIAowMDAwMDA5ODcwIDAwMDAwIG4gCjAwMDAwMDAwMjIgMDAwMDAgbiAKMDAwMDAwNjg2NSAwMDAwMCBuIAowMDAwMDA2OTg5IDAwMDAwIG4gCjAwMDAwMDk4MzQgMDAwMDAgbiAKMDAwMDAxNjU4NyAwMDAwMCBuIAowMDAwMDAwMDAwIDAwMDAwIG4gCjAwMDAwMTAwMDMgMDAwMDAgbiAKMDAwMDAwNzA5OCAwMDAwMCBuIAowMDAwMDA5ODEzIDAwMDAwIG4gCjAwMDAwMDk5NTMgMDAwMDAgbiAKMDAwMDAxMDQ5NSAwMDAwMCBuIAowMDAwMDEwMTY2IDAwMDAwIG4gCjAwMDAwMTA0NzUgMDAwMDAgbiAKMDAwMDAxMDczMyAwMDAwMCBuIAowMDAwMDE2NTY2IDAwMDAwIG4gCjAwMDAwMTcwMTQgMDAwMDAgbiAKMDAwMDAxNzI3NCAwMDAwMCBuIAowMDAwMDMzMzE3IDAwMDAwIG4gCjAwMDAwMzMzMzkgMDAwMDAgbiAKMDAwMDAzMzM2OSAwMDAwMCBuIAowMDAwMDMzNDIwIDAwMDAwIG4gCjAwMDAwMzM0NTAgMDAwMDAgbiAKMDAwMDAzMzQ2OSAwMDAwMCBuIAowMDAwMDMzNDkyIDAwMDAwIG4gCjAwMDAwMzM1MzQgMDAwMDAgbiAKMDAwMDAzMzU1MyAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDMwIC9Sb290IDEzIDAgUiAvSW5mbyAxIDAgUiAvSUQgWyA8YWFkYjdhZjU5ZjIxNWE0Y2U4Y2QzZWIwYzFiNmNjNTc+CjxhYWRiN2FmNTlmMjE1YTRjZThjZDNlYjBjMWI2Y2M1Nz4gXSA+PgpzdGFydHhyZWYKMzM3NTEKJSVFT0YK" + } +} diff --git a/test/fixtures/dispute-file-upload/req/index.js b/test/fixtures/dispute-file-upload/req/index.js new file mode 100644 index 00000000..c1005703 --- /dev/null +++ b/test/fixtures/dispute-file-upload/req/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.create = require('./create'); diff --git a/test/fixtures/dispute-file-upload/res/create.json b/test/fixtures/dispute-file-upload/res/create.json new file mode 100644 index 00000000..2dbd5bce --- /dev/null +++ b/test/fixtures/dispute-file-upload/res/create.json @@ -0,0 +1,15 @@ +{ + "dispute_file_upload": { + "id": 799719588, + "shop_id": 220006451, + "file_size": 34509, + "file_type": "application/pdf", + "original_filename": "test.pdf", + "filename": "fe287b11-cd9e-4281-bed6-7039adf27e71.pdf", + "created_at": "2022-06-07T12:06:05-04:00", + "updated_at": "2022-06-07T12:06:05-04:00", + "dispute_evidence_id": 819974671, + "dispute_evidence_type": "uncategorized_file", + "url": "https://storage.googleapis.com/shopify-gcs-test/s/files/1/0002/2000/6451/payments_file_uploads/fe287b11-cd9e-4281-bed6-7039adf27e71.pdf?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=shopify-core-gcs-test%40shopify-tiers.iam.gserviceaccount.com%2F20200912%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=2022-06-07T16:06:06Z&X-Goog-Expires=604800&X-Goog-SignedHeaders=host&response-content-disposition=&X-Goog-Signature=87811646776a232917997f6a4595871d9123629da45d35b2b915e7210eececff55a58147e79ac636861617cc21b5a2f6e5ea6547d9a20e9503e64caca84c9043ecccbf27d20142118017c60d2418edb7896c53eb466335fca9153d8d73c15645962eace819a3588301d55bcbc5b4ecf2225561de914b104e2069daa400e741a59a306d094f7b147c0ae2215f8203e4ce20a907c983a2e4be074c584725bd1c9e399f633499531160dac25b34054527795ef5ea48a4ac1d392585d4bc0976d62cab57e18d33547c9110599f39368983e4c882757900bac1eddcb41b87d4d26269bfe01b0161ef0a81088a0e4d6596380f4d3d37e4e72a8c5fefc6bc07551efb59" + } +} diff --git a/test/fixtures/dispute-file-upload/res/get.json b/test/fixtures/dispute-file-upload/res/get.json new file mode 100644 index 00000000..7c693ff0 --- /dev/null +++ b/test/fixtures/dispute-file-upload/res/get.json @@ -0,0 +1,62 @@ +{ + "dispute_evidence": { + "id": 819974671, + "payments_dispute_id": 598735659, + "access_activity_log": null, + "billing_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "cancellation_policy_disclosure": null, + "cancellation_rebuttal": null, + "customer_email_address": "example@email.com", + "customer_first_name": "Kermit", + "customer_last_name": "the Frog", + "product_description": "Product name: Draft\nTitle: 151cm\nPrice: $10.00\nQuantity: 1\nProduct Description: good board", + "refund_policy_disclosure": null, + "refund_refusal_explanation": null, + "shipping_address": { + "id": 867402159, + "address1": "123 Amoebobacterieae St", + "address2": "", + "city": "Ottawa", + "province": "Ontario", + "province_code": "ON", + "country": "Canada", + "country_code": "CA", + "zip": "K2P0V6" + }, + "uncategorized_text": "Sample uncategorized text", + "created_at": "2022-06-07T14:30:15-04:00", + "updated_at": "2022-06-07T14:30:33-04:00", + "submitted_by_merchant_on": null, + "fulfillments": [ + { + "shipping_carrier": "UPS", + "shipping_tracking_number": "1234", + "shipping_date": "2017-01-01" + }, + { + "shipping_carrier": "FedEx", + "shipping_tracking_number": "4321", + "shipping_date": "2017-01-02" + } + ], + "dispute_evidence_files": { + "cancellation_policy_file_id": null, + "customer_communication_file_id": 539650252, + "customer_signature_file_id": 799719586, + "refund_policy_file_id": null, + "service_documentation_file_id": null, + "shipping_documentation_file_id": 799719586, + "uncategorized_file_id": 567271523 + } + } +} diff --git a/test/fixtures/dispute-file-upload/res/index.js b/test/fixtures/dispute-file-upload/res/index.js new file mode 100644 index 00000000..c1005703 --- /dev/null +++ b/test/fixtures/dispute-file-upload/res/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.create = require('./create'); From 9f9a678e2b8c7e5a1daf84eb1a2ff903fa07b243 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 21 Jul 2022 20:54:01 +0200 Subject: [PATCH 028/110] [ts] Fix misleading comment --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 21e3576a..23dccfd3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for shopify-api-node 2.10.0 +// Type definitions for shopify-api-node // Project: shopify-api-node // Definitions by: Rich Buggy From bf2c998b31d34d52808a0447d9b674278ffb5225 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 21 Jul 2022 20:54:55 +0200 Subject: [PATCH 029/110] [dist] 3.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9067a72a..271478e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.10.2", + "version": "3.11.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From c77cc208be84f324ecaf0d5b43177864a317a125 Mon Sep 17 00:00:00 2001 From: MBogdan18 <43849364+MBogdan18@users.noreply.github.com> Date: Fri, 2 Sep 2022 21:41:42 +0300 Subject: [PATCH 030/110] [ts] Add missing webhook topics (#550) --- index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.d.ts b/index.d.ts index 23dccfd3..8d4b1c6d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3195,6 +3195,8 @@ declare namespace Shopify { | 'subscription_billing_attempts/challenged' | 'subscription_billing_attempts/failure' | 'subscription_billing_attempts/success' + | 'subscription_contracts/create' + | 'subscription_contracts/update' | 'tender_transactions/create' | 'themes/create' | 'themes/delete' From e9952867bbb1945d9d8932c0b4833478c47efe04 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 2 Sep 2022 20:43:53 +0200 Subject: [PATCH 031/110] [dist] 3.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 271478e9..6ad2a7c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.11.0", + "version": "3.11.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 3dfcd8bf0705f7fb8df1171718b0bef714596c2e Mon Sep 17 00:00:00 2001 From: Jason Gao Date: Fri, 23 Sep 2022 13:46:35 -0400 Subject: [PATCH 032/110] [fix] Respect the maxRetries option (#554) --- index.js | 77 +++++++++++++++++++++++--------------------- test/shopify.test.js | 16 +++++++++ 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/index.js b/index.js index 6fcef5f5..bca1b021 100644 --- a/index.js +++ b/index.js @@ -19,10 +19,11 @@ const retryableErrorCodes = new Set([ 'ENETUNREACH', 'EAI_AGAIN' ]); +const retryableErrorCodesArray = Array.from(retryableErrorCodes); -const retryableStatusCodes = new Set([ +const retryableStatusCodesArray = [ 408, 413, 429, 500, 502, 503, 504, 521, 522, 524 -]); +]; /** * Creates a Shopify instance. @@ -159,7 +160,10 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { limit: this.options.maxRetries, // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, - calculateDelay + calculateDelay, + methods: [method], + statusCodes: retryableStatusCodesArray, + errorCodes: retryableErrorCodesArray } : 0, method, @@ -275,7 +279,10 @@ Shopify.prototype.graphql = function graphql(data, variables) { limit: this.options.maxRetries, // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, - calculateDelay + calculateDelay, + methods: ['POST'], + statusCodes: retryableStatusCodesArray, + errorCodes: retryableErrorCodesArray } : 0, hooks: { @@ -304,18 +311,6 @@ Shopify.prototype.graphql = function graphql(data, variables) { resources.registerAll(Shopify); -/** - * Got `calculateDelay` hook function passed to decide how long to wait before - * retrying. - * - * @param {Object} retryObject Got's input for the retry logic - * @return {Number} The delay - * @private - */ -function calculateDelay(retryObject) { - return maybeRetryMS(retryObject.error) || retryObject.computedValue; -} - /** * Decorates an `Error` object with details from GraphQL errors in the response * body. @@ -351,31 +346,21 @@ function delay(ms) { * Given an error from Got, see if Shopify told us how long to wait before * retrying. * - * @param {Object} error Error object from Got call - * @return {(Number|null)} The duration in ms, or `null` + * @param {Object} retryObject Got's input for the retry logic + * @return {Number} The duration in ms * @private **/ -function maybeRetryMS(error) { - // For simplicity, retry network connectivity issues after a hardcoded 1s. - if (retryableErrorCodes.has(error.code)) { - return 1000; - } - +function calculateDelay(retryObject) { + const { error, computedValue } = retryObject; const response = error.response; - if (response.headers && response.headers['retry-after']) { - return response.headers['retry-after'] * 1000 || null; - } - - if (retryableStatusCodes.has(response.statusCode)) { - // Arbitrary 2 seconds, in case we get a 429 without a `Retry-After` - // response header, or 4xx/5xx series error that matches the Got retry - // defaults. - return 2 * 1000; - } - // Detect GraphQL request throttling. - if (response.body && typeof response.body === 'object') { + if ( + response && + response.statusCode === 200 && + response.body && + typeof response.body === 'object' + ) { const body = response.body; if ( @@ -393,7 +378,25 @@ function maybeRetryMS(error) { } } - return null; + // Stop retrying if the attempt limit has been reached or the request is not + // retryable. + if (computedValue === 0) { + return 0; + } + + // For simplicity, retry network connectivity issues after a hardcoded 1s. + if (retryableErrorCodes.has(error.code)) { + return 1000; + } + + if (response.headers && response.headers['retry-after']) { + return response.headers['retry-after'] * 1000 || computedValue; + } + + // Arbitrary 2 seconds, in case we get a 429 without a `Retry-After` + // response header, or 4xx/5xx series error that matches the Got retry + // defaults. + return 2 * 1000; } /** diff --git a/test/shopify.test.js b/test/shopify.test.js index 22cdb053..c06e2512 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -573,6 +573,22 @@ describe('Shopify', () => { }); }); + it('honors the maxRetries option', () => { + let attempts = 0; + scope + .get('/admin/shop.json') + .times(4) + .reply(429, () => { + attempts++; + return 'too many requests'; + }); + + return shopifyWithRetries.shop.get().catch((result) => { + expect(result.response.statusCode).equal(429); + expect(attempts).equal(4); + }); + }).timeout(8000); + it("doesn't retry 404 errors", () => { scope.get('/admin/products/10.json').reply(404, { error: 'not found' From 00c9f86f9da3f1b7b5fe388ac54daca8f553f7f5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Sep 2022 20:31:07 +0200 Subject: [PATCH 033/110] [minor] Fix nits --- index.js | 40 ++++++++++++++++++---------------------- test/shopify.test.js | 11 ++++++----- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/index.js b/index.js index bca1b021..f767546a 100644 --- a/index.js +++ b/index.js @@ -157,13 +157,13 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { retry: this.options.maxRetries > 0 ? { + calculateDelay, + errorCodes: retryableErrorCodesArray, limit: this.options.maxRetries, // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, - calculateDelay, methods: [method], - statusCodes: retryableStatusCodesArray, - errorCodes: retryableErrorCodesArray + statusCodes: retryableStatusCodesArray } : 0, method, @@ -276,13 +276,13 @@ Shopify.prototype.graphql = function graphql(data, variables) { retry: this.options.maxRetries > 0 ? { + calculateDelay, + errorCodes: retryableErrorCodesArray, limit: this.options.maxRetries, // Don't clamp Shopify `Retry-After` header values too low. maxRetryAfter: Infinity, - calculateDelay, methods: ['POST'], - statusCodes: retryableStatusCodesArray, - errorCodes: retryableErrorCodesArray + statusCodes: retryableStatusCodesArray } : 0, hooks: { @@ -359,23 +359,19 @@ function calculateDelay(retryObject) { response && response.statusCode === 200 && response.body && - typeof response.body === 'object' + typeof response.body === 'object' && + Array.isArray(response.body.errors) && + response.body.errors[0].extensions && + response.body.errors[0].extensions.code == 'THROTTLED' ) { - const body = response.body; - - if ( - Array.isArray(body.errors) && - body.errors[0].extensions && - body.errors[0].extensions.code == 'THROTTLED' - ) { - const costData = body.extensions.cost; - return ( - ((costData.requestedQueryCost - - costData.throttleStatus.currentlyAvailable) / - costData.throttleStatus.restoreRate) * - 1000 - ); - } + const costData = response.body.extensions.cost; + + return ( + ((costData.requestedQueryCost - + costData.throttleStatus.currentlyAvailable) / + costData.throttleStatus.restoreRate) * + 1000 + ); } // Stop retrying if the attempt limit has been reached or the request is not diff --git a/test/shopify.test.js b/test/shopify.test.js index c06e2512..3fc729ae 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -575,6 +575,7 @@ describe('Shopify', () => { it('honors the maxRetries option', () => { let attempts = 0; + scope .get('/admin/shop.json') .times(4) @@ -589,7 +590,7 @@ describe('Shopify', () => { }); }).timeout(8000); - it("doesn't retry 404 errors", () => { + it('does not retry 404 errors', () => { scope.get('/admin/products/10.json').reply(404, { error: 'not found' }); @@ -600,7 +601,7 @@ describe('Shopify', () => { }); }); - it("doesn't retry 422 errors that return an error string", () => { + it('does not retry 422 errors that return an error string', () => { scope.put('/admin/products/10.json').reply(422, { error: 'the product was invalid' }); @@ -613,7 +614,7 @@ describe('Shopify', () => { }); }); - it("doesn't retry 422 errors that return an errors array", () => { + it('does not retry 422 errors that return an errors array', () => { scope.put('/admin/products/10.json').reply(422, { errors: ['the product was invalid'] }); @@ -626,7 +627,7 @@ describe('Shopify', () => { }); }); - it("doesn't retry 422 errors that return an errors object", () => { + it('does not retry 422 errors that return an errors object', () => { scope.put('/admin/products/10.json').reply(422, { errors: { title: 'is required' @@ -1002,7 +1003,7 @@ describe('Shopify', () => { }); }); - it("doesn't retry errors from broken GraphQL queries", () => { + it('does not retry errors from broken GraphQL queries', () => { scope.post('/admin/api/graphql.json').reply(200, { errors: [ { From 9fc71926d6bee2b7284352b39e60dd3248a0cbd5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 23 Sep 2022 20:33:40 +0200 Subject: [PATCH 034/110] [dist] 3.11.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ad2a7c0..68ba1b37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.11.1", + "version": "3.11.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 0b3484e68b9af3736610c72f5af1c5b784137685 Mon Sep 17 00:00:00 2001 From: Kyle Summers <817780+summersk@users.noreply.github.com> Date: Sat, 1 Oct 2022 05:23:51 +1000 Subject: [PATCH 035/110] [ts] Add missing valuse to fulfillment event status enum (#555) --- index.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 8d4b1c6d..e313a648 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1781,11 +1781,16 @@ declare namespace Shopify { } type FulfillmentEventStatus = + | 'attempted_delivery' | 'confirmed' | 'delivered' | 'failure' | 'in_transit' - | 'out_for_delivery'; + | 'label_printed' + | 'label_purchased' + | 'out_for_delivery' + | 'picked_up' + | 'ready_for_pickup'; interface IFulfillmentEvent { address1: string | null; From fc41cf4ab92ef1a8cdcc1da71ded7425b562c9e6 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 30 Sep 2022 21:27:21 +0200 Subject: [PATCH 036/110] [dist] 3.11.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68ba1b37..6cd1e8b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.11.2", + "version": "3.11.3", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From dcbcd229e4ed4ff6ff82edd71474ed96d22fd725 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 19 Dec 2022 18:12:03 +0100 Subject: [PATCH 037/110] [doc] Fix CI badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15248318..0191a0b4 100644 --- a/README.md +++ b/README.md @@ -755,7 +755,7 @@ Used in our live products: [MoonMail][moonmail] & [MONEI][monei] [npm-shopify-api-node-badge]: https://img.shields.io/npm/v/shopify-api-node.svg [npm-shopify-api-node]: https://www.npmjs.com/package/shopify-api-node [ci-shopify-api-node-badge]: - https://img.shields.io/github/workflow/status/MONEI/Shopify-api-node/CI/master?label=CI + https://img.shields.io/github/actions/workflow/status/MONEI/Shopify-api-node/ci.yml?branch=master&label=CI [ci-shopify-api-node]: https://github.com/MONEI/Shopify-api-node/actions?query=workflow%3ACI+branch%3Amaster [coverage-shopify-api-node-badge]: From 6c2a0868c2185aba6b1d5ad6765554f94e4e15a1 Mon Sep 17 00:00:00 2001 From: Julius Plaras Date: Fri, 23 Dec 2022 07:19:20 -0500 Subject: [PATCH 038/110] [ts] Add initial_value to IGiftCard interface (#561) --- index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.d.ts b/index.d.ts index e313a648..445e5aba 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1960,6 +1960,7 @@ declare namespace Shopify { updated_at: string; disabled_at: string; expires_on: string; + initial_value: string; } interface IGiftCardAdjustment { From b8de68b7f6663ea0dcd605a2a371d0ddce542efb Mon Sep 17 00:00:00 2001 From: tkalliom Date: Tue, 27 Dec 2022 13:07:33 +0200 Subject: [PATCH 039/110] [api] Add cancelV2 action to Fulfillment resource --- README.md | 1 + index.d.ts | 1 + resources/fulfillment.js | 14 ++++++++++++++ test/fulfillment.test.js | 14 +++++++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0191a0b4..3ca1f24e 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,7 @@ default. - `list([params])` - fulfillment - `cancel(orderId, id)` + - `cancelV2(id)` - `complete(orderId, id)` - `count(orderId[, params)` - `create(orderId, params)` diff --git a/index.d.ts b/index.d.ts index 445e5aba..4b7a4c7b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -319,6 +319,7 @@ declare class Shopify { }; fulfillment: { cancel: (orderId: number, id: number) => Promise; + cancelV2: (id: number) => Promise; complete: (orderId: number, id: number) => Promise; count: (orderId: number, params?: any) => Promise; create: (orderId: number, params: any) => Promise; diff --git a/resources/fulfillment.js b/resources/fulfillment.js index ff1768d5..fa94b437 100644 --- a/resources/fulfillment.js +++ b/resources/fulfillment.js @@ -68,6 +68,20 @@ Fulfillment.prototype.cancel = function cancel(orderId, id) { .then((body) => body[this.key]); }; +/** + * Cancels a fulfillment. + * + * @param {Number} id Fulfillment ID + * @return {Promise} Promise that resolves with the result + * @public + */ +Fulfillment.prototype.cancelV2 = function cancelV2(id) { + const url = base.buildUrl.call(this, `${id}/cancel`); + return this.shopify + .request(url, 'POST', undefined, {}) + .then((body) => body[this.key]); +}; + /** * Creates a fulfillment for one or many fulfillment orders. The fulfillment * orders are associated with the same order and are assigned to the same diff --git a/test/fulfillment.test.js b/test/fulfillment.test.js index cc57347f..fcbea076 100644 --- a/test/fulfillment.test.js +++ b/test/fulfillment.test.js @@ -142,7 +142,7 @@ describe('Shopify#fulfillment', () => { .then((data) => expect(data).to.deep.equal(output.fulfillment)); }); - it('cancels a pending fulfillment', () => { + it('cancels a fulfillment for a specific order ID', () => { const output = fixtures.res.cancel; scope @@ -154,6 +154,18 @@ describe('Shopify#fulfillment', () => { .then((data) => expect(data).to.deep.equal(output.fulfillment)); }); + it('cancels a fulfillment', () => { + const output = fixtures.res.cancel; + + scope + .post('/admin/fulfillments/255858046/cancel.json', {}) + .reply(200, output); + + return shopify.fulfillment + .cancelV2(255858046) + .then((data) => expect(data).to.deep.equal(output.fulfillment)); + }); + it('injects the api version to the request path if provided', () => { scope .get(`/admin/api/${apiVersion}/orders/450789469/fulfillments/count.json`) From 01f0e7bcbe82426664b220bfd9e068cd2b4d09cf Mon Sep 17 00:00:00 2001 From: tkalliom Date: Tue, 27 Dec 2022 13:08:06 +0200 Subject: [PATCH 040/110] [api] Add fulfillments action to FulfillmentOrder resource --- README.md | 1 + index.d.ts | 3 + resources/fulfillment-order.js | 12 +++ .../fulfillment-order/res/fulfillments.json | 73 +++++++++++++++++++ test/fixtures/fulfillment-order/res/index.js | 1 + test/fulfillment-order.test.js | 12 +++ 6 files changed, 102 insertions(+) create mode 100644 test/fixtures/fulfillment-order/res/fulfillments.json diff --git a/README.md b/README.md index 3ca1f24e..fcb8fdee 100644 --- a/README.md +++ b/README.md @@ -478,6 +478,7 @@ default. - `locationsForMove(id)` - `move(id, locationId)` - `setFulfillmentOrdersDeadline(params)` + - `fulfillments(id)` - fulfillmentRequest - `accept(fulfillmentOrderId[, message])` - `create(fulfillmentOrderId, params)` diff --git a/index.d.ts b/index.d.ts index 4b7a4c7b..9a296283 100644 --- a/index.d.ts +++ b/index.d.ts @@ -385,6 +385,9 @@ declare class Shopify { setFulfillmentOrdersDeadline: ( params: Shopify.ISetFulfillmentOrdersDeadline ) => Promise; + fulfillments: ( + id: number + ) => Promise>; }; fulfillmentRequest: { accept: ( diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 419d8581..460fe10d 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -115,4 +115,16 @@ FulfillmentOrder.prototype.setFulfillmentOrdersDeadline = return this.shopify.request(url, 'POST', undefined, params); }; +/** + * Lists fulfillments for a fulfillment order. + * + * @param {Number} id Fulfillment order ID + * @return {Promise} Promise that resolves with the result + * @public + */ +FulfillmentOrder.prototype.fulfillments = function fulfillments(id) { + const url = this.buildUrl(`${id}/fulfillments`); + return this.shopify.request(url, 'GET', 'fulfillments'); +}; + module.exports = FulfillmentOrder; diff --git a/test/fixtures/fulfillment-order/res/fulfillments.json b/test/fixtures/fulfillment-order/res/fulfillments.json new file mode 100644 index 00000000..0bef6d39 --- /dev/null +++ b/test/fixtures/fulfillment-order/res/fulfillments.json @@ -0,0 +1,73 @@ +{ + "fulfillments": [ + { + "id": 1069019901, + "order_id": 450789469, + "status": "success", + "created_at": "2022-10-21T08:56:20-04:00", + "service": "manual", + "updated_at": "2022-10-21T08:56:20-04:00", + "tracking_company": "UPS", + "shipment_status": null, + "location_id": 24826418, + "line_items": [ + { + "id": 1071823190, + "variant_id": 43729076, + "title": "Draft", + "quantity": 1, + "sku": "draft-151", + "variant_title": "151cm", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 108828309, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "Draft - 151cm", + "variant_inventory_management": null, + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 0, + "price": "10.00", + "total_discount": "0.00", + "fulfillment_status": "fulfilled", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/1071823190", + "tax_lines": [] + } + ], + "tracking_number": "#☠1☢\n---\n4321\n", + "tracking_numbers": ["#☠1☢\n---\n4321\n"], + "tracking_url": "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=#☠1☢---4321", + "tracking_urls": [ + "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=#☠1☢---4321" + ], + "receipt": {}, + "name": "#1001.1", + "admin_graphql_api_id": "gid://shopify/Fulfillment/1069019901" + } + ] +} diff --git a/test/fixtures/fulfillment-order/res/index.js b/test/fixtures/fulfillment-order/res/index.js index 5f4be4f1..56f1bc0e 100644 --- a/test/fixtures/fulfillment-order/res/index.js +++ b/test/fixtures/fulfillment-order/res/index.js @@ -1,6 +1,7 @@ 'use strict'; exports.locationsForMove = require('./locations-for-move'); +exports.fulfillments = require('./fulfillments'); exports.cancel = require('./cancel'); exports.close = require('./close'); exports.list = require('./list'); diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index 0f36137f..b3e9626f 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -122,4 +122,16 @@ describe('Shopify#fulfillmentOrder', () => { expect(data).to.deep.equal({}); }); }); + + it('retrieves fulfillments associated with a fulfillment order', () => { + const output = fixtures.res.fulfillments; + + scope + .get('/admin/fulfillment_orders/1046000823/fulfillments.json') + .reply(200, output); + + return shopify.fulfillmentOrder + .fulfillments(1046000823) + .then((data) => expect(data).to.deep.equal(output.fulfillments)); + }); }); From c7a85e7f9170265894b3c032e94eff9e9268ab8f Mon Sep 17 00:00:00 2001 From: Kyle Summers <817780+summersk@users.noreply.github.com> Date: Wed, 28 Dec 2022 20:07:47 +1100 Subject: [PATCH 041/110] [fix] Ensure that response.body.errors is an array (#564) If `response.body.errors` is not an array, it means that the HTTP response status code is not 200 and in that case no special handling is needed. --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index f767546a..0cef5f36 100644 --- a/index.js +++ b/index.js @@ -293,7 +293,7 @@ Shopify.prototype.graphql = function graphql(data, variables) { this.updateGraphqlLimits(res.body.extensions.cost); } - if (res.body.errors) { + if (Array.isArray(res.body.errors)) { // Make Got consider this response errored and retry if needed. throw new Error(res.body.errors[0].message); } @@ -320,7 +320,7 @@ resources.registerAll(Shopify); * @private */ function decorateError(error) { - if (error.response && error.response.body.errors) { + if (error.response && Array.isArray(error.response.body.errors)) { const first = error.response.body.errors[0]; error.locations = first.locations; From 55865bddf0f0564c690819a15200f62f2c74ee74 Mon Sep 17 00:00:00 2001 From: Kyle Summers <817780+summersk@users.noreply.github.com> Date: Wed, 28 Dec 2022 20:08:49 +1100 Subject: [PATCH 042/110] [ts] Fix amount type (#565) --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 9a296283..8796a2ea 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2196,7 +2196,7 @@ declare namespace Shopify { | 'offsite'; interface IOrderDiscountCode { - amount: number; + amount: string; code: string; type: OrderDiscountCodeType; } From eeda19880b634d5080e3ab2cc5b77513276c8dbe Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sat, 3 Dec 2022 11:20:57 -0500 Subject: [PATCH 043/110] [api] Introduce the hooks option (#560) Add support for passing request lifecycle hooks down to `got`. Fixes: https://github.com/MONEI/Shopify-api-node/issues/556 --- README.md | 28 ++++++++ index.d.ts | 3 + index.js | 131 ++++++++++++++++++++-------------- test/shopify.test.js | 166 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index fcb8fdee..20425209 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ Creates a new `Shopify` instance. for requests to the REST API, and the throttled cost data for requests to the GraphQL API, and retry the request after that time has elapsed. Mutually exclusive with the `autoLimit` option. +- `hooks` - Optional - A list of `got` + [request hooks](https://github.com/sindresorhus/got/tree/v11#hooks) to attach + to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. Hooks + should be provided in the same format that Got expects them and will receive + the same arguments Got passes unchanged. #### Return value @@ -719,6 +724,29 @@ shopify .catch((err) => console.error(err)); ``` +## Hooks + +`shopify-api-node` supports being passed hooks which are called by `got` (the +underlying HTTP library) during the request lifecycle. + +For example, we can log every error that is encountered when using the +`maxRetries` option: + +```js +const shopify = new Shopify({ + shopName: 'your-shop-name', + accessToken: 'your-oauth-token', + maxRetries: 3, + // Pass the `beforeRetry` hook down to Got. + hooks: { + beforeRetry: [(options, error, retryCount) => console.error(error)] + } +}); +``` + +For more information on the available `got` hooks, see the +[`got` v11 hooks documentation](https://github.com/sindresorhus/got/tree/v11#hooks). + ## Become a master of the Shopify ecosystem by: - [Talking To Other Masters][talking-to-other-masters] diff --git a/index.d.ts b/index.d.ts index 8796a2ea..dbbd6eb4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,7 @@ // Type definitions for shopify-api-node // Project: shopify-api-node // Definitions by: Rich Buggy +import { Got } from 'got'; /*~ This is the module template file for class modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. @@ -787,6 +788,7 @@ declare namespace Shopify { presentmentPrices?: boolean; shopName: string; timeout?: number; + hooks?: Got.Hooks; } export interface IPrivateShopifyConfig { @@ -798,6 +800,7 @@ declare namespace Shopify { presentmentPrices?: boolean; shopName: string; timeout?: number; + hooks?: Got.Hooks; } export interface ICallLimits { diff --git a/index.js b/index.js index 0cef5f36..97df759f 100644 --- a/index.js +++ b/index.js @@ -154,33 +154,43 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { parseJson: this.options.parseJson, timeout: this.options.timeout, responseType: 'json', - retry: - this.options.maxRetries > 0 - ? { - calculateDelay, - errorCodes: retryableErrorCodesArray, - limit: this.options.maxRetries, - // Don't clamp Shopify `Retry-After` header values too low. - maxRetryAfter: Infinity, - methods: [method], - statusCodes: retryableStatusCodesArray - } - : 0, - method, - hooks: { - afterResponse: [ - (res) => { - this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); - return res; - } - ] - } + method + }; + + const afterResponse = (res) => { + this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); + return res; }; + if (this.options.hooks) { + options.hooks = { ...this.options.hooks }; + options.hooks.afterResponse = [afterResponse]; + + if (this.options.hooks.afterResponse) { + options.hooks.afterResponse.push(...this.options.hooks.afterResponse); + } + } else { + options.hooks = { afterResponse: [afterResponse] }; + } + if (data) { options.json = key ? { [key]: data } : data; } + if (this.options.maxRetries > 0) { + options.retry = { + calculateDelay, + errorCodes: retryableErrorCodesArray, + limit: this.options.maxRetries, + // Don't clamp Shopify `Retry-After` header values too low. + maxRetryAfter: Infinity, + methods: [method], + statusCodes: retryableStatusCodesArray + }; + } else { + options.retry = 0; + } + return got(uri, options).then((res) => { const body = res.body; @@ -272,40 +282,57 @@ Shopify.prototype.graphql = function graphql(data, variables) { timeout: this.options.timeout, responseType: 'json', method: 'POST', - body: json ? this.options.stringifyJson({ query: data, variables }) : data, - retry: - this.options.maxRetries > 0 - ? { - calculateDelay, - errorCodes: retryableErrorCodesArray, - limit: this.options.maxRetries, - // Don't clamp Shopify `Retry-After` header values too low. - maxRetryAfter: Infinity, - methods: ['POST'], - statusCodes: retryableStatusCodesArray - } - : 0, - hooks: { - afterResponse: [ - (res) => { - if (res.body) { - if (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; - } - ], - beforeError: [decorateError] + body: json ? this.options.stringifyJson({ query: data, variables }) : data + }; + + const afterResponse = (res) => { + if (res.body) { + if (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; }; + if (this.options.hooks) { + options.hooks = { ...this.options.hooks }; + options.hooks.afterResponse = [afterResponse]; + options.hooks.beforeError = [decorateError]; + + if (this.options.hooks.afterResponse) { + options.hooks.afterResponse.push(...this.options.hooks.afterResponse); + } + + if (this.options.hooks.beforeError) { + options.hooks.beforeError.push(...this.options.hooks.beforeError); + } + } else { + options.hooks = { + afterResponse: [afterResponse], + beforeError: [decorateError] + }; + } + + if (this.options.maxRetries > 0) { + options.retry = { + calculateDelay, + errorCodes: retryableErrorCodesArray, + limit: this.options.maxRetries, + // Don't clamp Shopify `Retry-After` header values too low. + maxRetryAfter: Infinity, + methods: ['POST'], + statusCodes: retryableStatusCodesArray + }; + } else { + options.retry = 0; + } + return got(uri, options).then(responseData); }; diff --git a/test/shopify.test.js b/test/shopify.test.js index 3fc729ae..f512584b 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -702,6 +702,87 @@ describe('Shopify', () => { expect(result.name).equal('My Cool Test Shop'); }); }).timeout(10000); + + it('calls hooks passed as options when accessing resources', () => { + function beforeRequest() { + beforeRequest.called = true; + } + + function afterResponse(x) { + afterResponse.called = true; + return x; + } + + const shopify = new Shopify({ + accessToken, + shopName, + hooks: { + beforeRequest: [beforeRequest], + afterResponse: [afterResponse] + } + }); + + addWorkingRESTRequestMock(scope); + + return shopify.shop.get().then(() => { + expect(beforeRequest.called).to.be.true; + expect(afterResponse.called).to.be.true; + }); + }); + + it('calls hooks passed as options when making raw requests', () => { + function beforeRequest() { + beforeRequest.called = true; + } + + function afterResponse(x) { + afterResponse.called = true; + return x; + } + + const shopify = new Shopify({ + accessToken, + shopName, + hooks: { + beforeRequest: [beforeRequest], + afterResponse: [afterResponse] + } + }); + + scope.get('/test').reply(200, {}); + + return shopify.request(url, 'GET').then(() => { + expect(beforeRequest.called).to.be.true; + expect(afterResponse.called).to.be.true; + }); + }); + + it('calls the beforeRetry hook for retried requests', () => { + function beforeRetry() { + beforeRetry.called = true; + } + + const shopify = new Shopify({ + accessToken, + shopName, + maxRetries: 3, + hooks: { + beforeRetry: [beforeRetry] + } + }); + + scope.get('/admin/shop.json').replyWithError({ + message: 'the network is broken', + code: 'ECONNRESET' + }); + + addWorkingRESTRequestMock(scope); + + return shopify.shop.get().then((result) => { + expect(beforeRetry.called).to.be.true; + expect(result.name).equal('My Cool Test Shop'); + }); + }); }); describe('Shopify#graphql', () => { @@ -1127,5 +1208,90 @@ describe('Shopify', () => { expect(result.shop.name).equal('My Cool Test Shop'); }); }).timeout(3000); + + it('calls hooks passed as options when making graphql requests', () => { + function beforeRequest() { + beforeRequest.called = true; + } + + function afterResponse(x) { + afterResponse.called = true; + return x; + } + + const shopify = new Shopify({ + accessToken, + shopName, + hooks: { + beforeRequest: [beforeRequest], + afterResponse: [afterResponse] + } + }); + + scope.post('/admin/api/graphql.json').reply(200, { + data: { foo: 'bar' } + }); + + return shopify.graphql('query').then(() => { + expect(beforeRequest.called).to.be.true; + expect(afterResponse.called).to.be.true; + }); + }); + + it('calls the beforeRetry hook for retried requests', () => { + function beforeRetry() { + beforeRetry.called = true; + } + + const shopify = new Shopify({ + accessToken, + shopName, + maxRetries: 3, + hooks: { + beforeRetry: [beforeRetry] + } + }); + + scope.post('/admin/api/graphql.json').replyWithError({ + message: 'the network is broken', + code: 'ECONNRESET' + }); + + addWorkingGraphQLRequestMock(scope); + + return shopify.graphql('query { shop { id name } }').then(() => { + expect(beforeRetry.called).to.be.true; + }); + }); + + it('calls the beforeError hook for errors', () => { + function beforeError(x) { + beforeError.called = true; + return x; + } + + const shopify = new Shopify({ + accessToken, + shopName, + hooks: { + beforeError: [beforeError] + } + }); + + const message = 'Something wrong happened'; + + scope.post('/admin/api/graphql.json').replyWithError(message); + + return shopify.graphql('query { shop { id name } }').then( + () => { + throw new Error('Test invalidation'); + }, + (err) => { + expect(beforeError.called).to.be.true; + expect(err).to.be.an.instanceof(got.RequestError); + expect(err.message).to.equal(message); + } + ); + }); }); }); From 19d3a2690f37e7b9d1f78e061af78586a6b8a6bc Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 1 Jan 2023 10:25:52 +0100 Subject: [PATCH 044/110] [fix] Ensure that error.response.body is truthy Ensure that `error.response.body` is truthy before reading the `errors` property. --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 97df759f..425f6f14 100644 --- a/index.js +++ b/index.js @@ -347,7 +347,11 @@ resources.registerAll(Shopify); * @private */ function decorateError(error) { - if (error.response && Array.isArray(error.response.body.errors)) { + if ( + error.response && + error.response.body && + Array.isArray(error.response.body.errors) + ) { const first = error.response.body.errors[0]; error.locations = first.locations; From cfafdf1ce006c5e9c85f447a0dc9f85ce2d4d42e Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 1 Jan 2023 10:34:40 +0100 Subject: [PATCH 045/110] [dist] 3.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cd1e8b7..75dff032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.11.3", + "version": "3.12.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "directories": { From 6f8c8a396cde90cadf7a236208ef43b11fcf9174 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 3 Jan 2023 09:57:50 -0500 Subject: [PATCH 046/110] [fix] Fix got types to properly reference the hooks type This fixes a type error introduced in eeda19880b634d50. Fixes: https://github.com/MONEI/Shopify-api-node/issues/566 --- index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index dbbd6eb4..59be13ba 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,7 @@ // Type definitions for shopify-api-node // Project: shopify-api-node // Definitions by: Rich Buggy -import { Got } from 'got'; +import { Hooks } from 'got'; /*~ This is the module template file for class modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. @@ -788,7 +788,7 @@ declare namespace Shopify { presentmentPrices?: boolean; shopName: string; timeout?: number; - hooks?: Got.Hooks; + hooks?: Hooks; } export interface IPrivateShopifyConfig { @@ -800,7 +800,7 @@ declare namespace Shopify { presentmentPrices?: boolean; shopName: string; timeout?: number; - hooks?: Got.Hooks; + hooks?: Hooks; } export interface ICallLimits { From a329b5ecf890be01f1d055c393991e91ded7a23f Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 3 Jan 2023 10:00:25 -0500 Subject: [PATCH 047/110] [test] Add a light test suite for the typescript types --- .github/workflows/ci.yml | 1 + index.test-d.ts | 43 ++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++- 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 index.test-d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa696304..ce7d815b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: - run: npm run lint if: matrix.node == 18 - run: npm test + - run: npm run test:types - uses: coverallsapp/github-action@1.1.3 if: matrix.node == 18 with: diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 00000000..2ed8f226 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,43 @@ +import { expectType } from 'tsd'; +import Shopify from '.'; + +// Can be constructed with public access token. +new Shopify({ + shopName: 'my-shopify-store.myshopify.com', + accessToken: '111' +}); + +// Can be constructed with public access token and version. +const client = new Shopify({ + shopName: 'my-shopify-store.myshopify.com', + accessToken: '111', + apiVersion: '2020-01' +}); + +// Can be constructed with Got hooks. +new Shopify({ + shopName: 'my-shopify-store.myshopify.com', + accessToken: '111', + hooks: { + beforeRequest: [ + (options) => { + options.headers['X-Test'] = 'test'; + } + ] + } +}); + +expectType(client.callLimits.remaining); +expectType(client.callLimits.current); +expectType(client.callLimits.max); + +const shop = await client.shop.get(); +expectType(shop.id); +expectType(shop.myshopify_domain); + +const product = await client.product.get(10); +expectType(product.id); +expectType(product.title); + +const result = await client.graphql(`query { shop { id myshopify_domain } }`); +expectType(result); diff --git a/package.json b/package.json index 75dff032..fcffa9a6 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ "lint-staged": "^13.0.0", "mocha": "^8.0.1", "nock": "^13.0.1", - "prettier": "^2.0.2" + "prettier": "^2.0.2", + "tsd": "^0.25.0" }, "scripts": { "test": "c8 --reporter=lcov --reporter=text mocha", + "test:types": "tsd", "watch": "mocha -w", "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,ts,yaml,yml}\"", "prepare": "husky install" From f2109be3c99374cd8e8681855ed9ff12cf58007b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 3 Jan 2023 18:32:43 +0100 Subject: [PATCH 048/110] [ts] move index.d.ts and index.test-d.ts to the types folder --- package.json | 3 ++- index.d.ts => types/index.d.ts | 0 index.test-d.ts => types/index.test-d.ts | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename index.d.ts => types/index.d.ts (100%) rename index.test-d.ts => types/index.test-d.ts (100%) diff --git a/package.json b/package.json index fcffa9a6..cc85bcef 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "3.12.0", "description": "Shopify API bindings for Node.js", "main": "index.js", + "types": "types/index.d.ts", "directories": { "test": "test" }, @@ -13,7 +14,7 @@ "resources", "mixins", "index.js", - "index.d.ts" + "types/index.d.ts" ], "dependencies": { "got": "^11.1.4", diff --git a/index.d.ts b/types/index.d.ts similarity index 100% rename from index.d.ts rename to types/index.d.ts diff --git a/index.test-d.ts b/types/index.test-d.ts similarity index 100% rename from index.test-d.ts rename to types/index.test-d.ts From fdbc785bb6c23b47ecccf705aa2f072d347ca2f2 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 3 Jan 2023 18:38:22 +0100 Subject: [PATCH 049/110] [dist] 3.12.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc85bcef..d8141af6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.0", + "version": "3.12.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 73007be958a7bbc5435631c18123b482b3b0c9c8 Mon Sep 17 00:00:00 2001 From: devlifetimebrands <103285695+devlifetimebrands@users.noreply.github.com> Date: Thu, 19 Jan 2023 01:57:30 -0500 Subject: [PATCH 050/110] [ts] Add missing properties to IFulfillmentOrder (#572) --- types/index.d.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 59be13ba..49b266c3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1893,6 +1893,22 @@ declare namespace Shopify { zip: string; } + interface IFulfillmentOrderFulfillmentHolds { + reason: string; + reason_notes: string; + } + + interface IFulfillmentOrderInternationalDuties { + incoterm: 'DAP' | 'DDP'; + } + + interface IFulfillmentOrderDeliveryMethod { + id: number; + method_type: 'local' | 'none' | 'pick_up' | 'retail' | 'shipping'; + min_delivery_date_time: string; + max_delivery_date_time: string; + } + interface IFulfillmentOrder { assigned_location: IFulfillmentOrderAssignedLocation; assigned_location_id: number; @@ -1906,6 +1922,9 @@ declare namespace Shopify { shop_id: number; status: FulfillmentOrderStatus; supported_actions: FulfillmentOrderSupportedAction[]; + fulfillment_holds: IFulfillmentOrderFulfillmentHolds[]; + international_duties: IFulfillmentOrderInternationalDuties; + delivery_method: IFulfillmentOrderDeliveryMethod; } interface ILocationForMoveLocation { From 584d4d0cf4e0e02280371c4ea4f0aa00cb024034 Mon Sep 17 00:00:00 2001 From: Julius Plaras Date: Sat, 21 Jan 2023 02:21:59 -0500 Subject: [PATCH 051/110] [ts] Fix delivery_method type (#574) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 49b266c3..7b8e5b53 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1924,7 +1924,7 @@ declare namespace Shopify { supported_actions: FulfillmentOrderSupportedAction[]; fulfillment_holds: IFulfillmentOrderFulfillmentHolds[]; international_duties: IFulfillmentOrderInternationalDuties; - delivery_method: IFulfillmentOrderDeliveryMethod; + delivery_method: IFulfillmentOrderDeliveryMethod | null; } interface ILocationForMoveLocation { From 358f95263c9cd1100008064a953a752308650fbe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 22 Jan 2023 21:11:54 +0100 Subject: [PATCH 052/110] [ts] Fix up IShop interface - Add the `checkout_api_supported`, `cookie_consent_level`, and `transactional_sms_disabled` properties. - Fix the type of the `county_taxes`, `customer_email`, and `google_apps_login_enabled` properties. Fixes: https://github.com/MONEI/Shopify-api-node/issues/573 --- types/index.d.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 7b8e5b53..04bafd02 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2874,23 +2874,25 @@ declare namespace Shopify { interface IShop { address1: string; address2: string; + checkout_api_supported: boolean; city: string; + cookie_consent_level: string; country: string; country_code: string; country_name: string; + county_taxes: true | null; created_at: string; - county_taxes: string; - customer_email: string | null; currency: string; + customer_email: string; domain: string; eligible_for_card_reader_giveaway: boolean; eligible_for_payments: boolean; - enabled_presentment_currencies: string[]; email: string; + enabled_presentment_currencies: string[]; finances: boolean; force_ssl?: boolean; google_apps_domain: string | null; - google_apps_login_enabled: any | null; + google_apps_login_enabled: true | null; has_discounts: boolean; has_gift_cards: boolean; has_storefront: boolean; @@ -2920,6 +2922,7 @@ declare namespace Shopify { tax_shipping: boolean | null; taxes_included: true | null; timezone: string; + transactional_sms_disabled: boolean; updated_at: string; weight_unit: string; zip: string; From 3669aacdfe789336241af1dd14ebef74eafb1ac1 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 22 Jan 2023 21:30:38 +0100 Subject: [PATCH 053/110] [dist] 3.12.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8141af6..ab21fb3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.1", + "version": "3.12.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 0385fc851ec201ec9bcb68be689f37e4d96224df Mon Sep 17 00:00:00 2001 From: frostycoles <38190084+frostycoles@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:55:25 -0500 Subject: [PATCH 054/110] [ts] Add missing statuses (#591) The `on_hold` and `scheduled` statuses were missing. --- types/index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 04bafd02..d7bfa37b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1860,7 +1860,9 @@ declare namespace Shopify { | 'closed' | 'in_progress' | 'incomplete' - | 'open'; + | 'open' + | 'on_hold' + | 'scheduled'; type FulfillmentOrderSupportedAction = | 'cancel_fulfillment_order' From 0b7dc2b0daf9cac5e2b68515a358cf3599a9028c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 20 Mar 2023 21:55:56 +0100 Subject: [PATCH 055/110] [dist] 3.12.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab21fb3f..f74bb574 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.2", + "version": "3.12.3", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 163fe80e4520f2a670973081dac68e106f4df707 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 22 Apr 2023 21:09:06 +0200 Subject: [PATCH 056/110] [ci] Test on node 20 --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce7d815b..913331d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: - 14 - 16 - 18 + - 20 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -22,10 +23,10 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: matrix.node == 18 + if: matrix.node == 20 - run: npm test - run: npm run test:types - uses: coverallsapp/github-action@1.1.3 - if: matrix.node == 18 + if: matrix.node == 20 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 744af50e8d5656afa6f4534e604301a7cd1dc866 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 1 May 2023 20:50:14 +0200 Subject: [PATCH 057/110] [ts] Add checksum to IAsset interface Fixes: https://github.com/MONEI/Shopify-api-node/issues/602 --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index d7bfa37b..4daff070 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1054,6 +1054,7 @@ declare namespace Shopify { interface IAsset { attachment?: string; + checksum: string; content_type: string; created_at: string; key: string; From 48928cf92349ebbd7bfe96f37282f0202c6ff01a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 1 May 2023 20:53:20 +0200 Subject: [PATCH 058/110] [dist] 3.12.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f74bb574..54e1a443 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.3", + "version": "3.12.4", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 479c41f22e01baf7daf062e1b1a034f637b1afc9 Mon Sep 17 00:00:00 2001 From: Jussi Mustonen Date: Tue, 16 May 2023 11:31:15 +0300 Subject: [PATCH 059/110] [ts] Add checkout_token to IOrder interface (#604) --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index 4daff070..f179a213 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2365,6 +2365,7 @@ declare namespace Shopify { cancelled_at: string | null; cart_token: string; client_details: IOrderClientDetails; + checkout_token: string | null; closed_at: string | null; confirmed: boolean; created_at: string; From ef7557009f0ab58cb8c38ce981af98766c26e172 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 16 May 2023 20:51:05 +0200 Subject: [PATCH 060/110] [dist] 3.12.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54e1a443..361f324d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.4", + "version": "3.12.5", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From b22de832069a0f29fbd4681e49bcf68828dbc4db Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jun 2023 20:38:48 +0200 Subject: [PATCH 061/110] [ci] Update coverallsapp/github-action action to v2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 913331d9..416f4045 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: if: matrix.node == 20 - run: npm test - run: npm run test:types - - uses: coverallsapp/github-action@1.1.3 + - uses: coverallsapp/github-action@v2 if: matrix.node == 20 with: github-token: ${{ secrets.GITHUB_TOKEN }} From a669e7b36a5a8a996d4d21738f653e062fec1ff5 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 16 Jun 2023 21:04:46 +0200 Subject: [PATCH 062/110] [pkg] Update tsd to version 0.27.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 361f324d..07d9a793 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "mocha": "^8.0.1", "nock": "^13.0.1", "prettier": "^2.0.2", - "tsd": "^0.25.0" + "tsd": "^0.27.0" }, "scripts": { "test": "c8 --reporter=lcov --reporter=text mocha", From 72728472094888a6916089a31d63ef84d5d174f0 Mon Sep 17 00:00:00 2001 From: Rich Gilbank Date: Mon, 10 Jul 2023 09:54:38 -0400 Subject: [PATCH 063/110] [doc] Fix broken link (#613) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 20425209..ae42b159 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,10 @@ Creates a new `Shopify` instance. GraphQL API, and retry the request after that time has elapsed. Mutually exclusive with the `autoLimit` option. - `hooks` - Optional - A list of `got` - [request hooks](https://github.com/sindresorhus/got/tree/v11#hooks) to attach - to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. Hooks - should be provided in the same format that Got expects them and will receive - the same arguments Got passes unchanged. + [request hooks](https://github.com/sindresorhus/got/tree/v11.8.6#hooks) to + attach to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. + Hooks should be provided in the same format that Got expects them and will + receive the same arguments Got passes unchanged. #### Return value From c36891da9b3e7e7daaf9b4bf1edae3ce4e315a53 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 14 Jul 2023 20:32:12 +0200 Subject: [PATCH 064/110] [pkg] Update prettier to version 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07d9a793..ca42f070 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "lint-staged": "^13.0.0", "mocha": "^8.0.1", "nock": "^13.0.1", - "prettier": "^2.0.2", + "prettier": "^3.0.0", "tsd": "^0.27.0" }, "scripts": { From daab990a50f56a0df80b49dc127ea6fa3a8a01be Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 14 Jul 2023 20:32:52 +0200 Subject: [PATCH 065/110] [pkg] Update eslint-plugin-prettier to version 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca42f070..6f8df9ec 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "chai": "^4.1.2", "eslint": "^8.0.0", "eslint-config-prettier": "^8.1.0", - "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", "lint-staged": "^13.0.0", From c9dc3d8933afd2a1562c4f65a0ea12b0c597f92d Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Wed, 26 Jul 2023 14:40:05 -0400 Subject: [PATCH 066/110] [ts] Add missing webhook topics (#621) Fixes: https://github.com/MONEI/Shopify-api-node/issues/616 --- types/index.d.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index f179a213..709aeba2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3171,6 +3171,7 @@ declare namespace Shopify { export type WebhookTopic = | 'app/uninstalled' + | 'app_subscriptions/update' | 'bulk_operations/finish' | 'carts/create' | 'carts/update' @@ -3183,20 +3184,61 @@ declare namespace Shopify { | 'collections/create' | 'collections/delete' | 'collections/update' + | 'companies/create' + | 'companies/delete' + | 'companies/update' + | 'company_contact_roles/assign' + | 'company_contact_roles/revoke' + | 'company_contacts/create' + | 'company_contacts/delete' + | 'company_contacts/update' + | 'company_locations/create' + | 'company_locations/delete' + | 'company_locations/update' + | 'customer.tags_added' + | 'customer.tags_removed' | 'customer_groups/create' | 'customer_groups/delete' | 'customer_groups/update' + | 'customer_payment_methods/create' + | 'customer_payment_methods/revoke' + | 'customer_payment_methods/update' | 'customers/create' | 'customers/delete' | 'customers/disable' | 'customers/enable' + | 'customers/merge' | 'customers/update' + | 'customers_email_marketing_consent/update' | 'customers_marketing_consent/update' + | 'disputes/create' + | 'disputes/update' + | 'domains/create' + | 'domains/destroy' + | 'domains/update' | 'draft_orders/create' | 'draft_orders/delete' | 'draft_orders/update' | 'fulfillment_events/create' | 'fulfillment_events/delete' + | 'fulfillment_orders/cancellation_request_accepted' + | 'fulfillment_orders/cancellation_request_rejected' + | 'fulfillment_orders/cancellation_request_submitted' + | 'fulfillment_orders/cancelled' + | 'fulfillment_orders/fulfillment_request_accepted' + | 'fulfillment_orders/fulfillment_request_rejected' + | 'fulfillment_orders/fulfillment_request_submitted' + | 'fulfillment_orders/fulfillment_service_failed_to_complete' + | 'fulfillment_orders/hold_released' + | 'fulfillment_orders/line_items_prepared_for_local_delivery' + | 'fulfillment_orders/line_items_prepared_for_pickup' + | 'fulfillment_orders/merged' + | 'fulfillment_orders/moved' + | 'fulfillment_orders/order_routing_complete' + | 'fulfillment_orders/placed_on_hold' + | 'fulfillment_orders/rescheduled' + | 'fulfillment_orders/scheduled_fulfillment_order_ready' + | 'fulfillment_orders/split' | 'fulfillments/create' | 'fulfillments/update' | 'inventory_items/create' @@ -3205,9 +3247,17 @@ declare namespace Shopify { | 'inventory_levels/connect' | 'inventory_levels/disconnect' | 'inventory_levels/update' + | 'locales/create' + | 'locales/destroy' + | 'locales/update' + | 'locations/activate' | 'locations/create' + | 'locations/deactivate' | 'locations/delete' | 'locations/update' + | 'markets/create' + | 'markets/delete' + | 'markets/update' | 'order_transactions/create' | 'orders/cancelled' | 'orders/create' @@ -3217,16 +3267,25 @@ declare namespace Shopify { | 'orders/paid' | 'orders/partially_fulfilled' | 'orders/updated' + | 'payment_schedules/due' | 'payment_terms/create' | 'payment_terms/delete' | 'payment_terms/update' + | 'product_feeds/full_sync' + | 'product_feeds/incremental_sync' | 'product_listings/add' | 'product_listings/remove' | 'product_listings/update' | 'products/create' | 'products/delete' | 'products/update' + | 'profiles/create' + | 'profiles/delete' + | 'profiles/update' | 'refunds/create' + | 'scheduled_product_listings/add' + | 'scheduled_product_listings/remove' + | 'scheduled_product_listings/update' | 'selling_plan_groups/create' | 'selling_plan_groups/delete' | 'selling_plan_groups/update' @@ -3234,6 +3293,9 @@ declare namespace Shopify { | 'subscription_billing_attempts/challenged' | 'subscription_billing_attempts/failure' | 'subscription_billing_attempts/success' + | 'subscription_billing_cycle_edits/create' + | 'subscription_billing_cycle_edits/delete' + | 'subscription_billing_cycle_edits/update' | 'subscription_contracts/create' | 'subscription_contracts/update' | 'tender_transactions/create' From 848b773bad91feac49de5c5336150c4db8c50826 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 26 Jul 2023 20:42:05 +0200 Subject: [PATCH 067/110] [dist] 3.12.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f8df9ec..f5fd5a60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.5", + "version": "3.12.6", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From ccaf04438e680b827543db82cafd015956995a91 Mon Sep 17 00:00:00 2001 From: Matt Cosentino Date: Sun, 6 Aug 2023 10:56:53 -0400 Subject: [PATCH 068/110] [doc] Fix usage example (#626) Fixes: https://github.com/MONEI/Shopify-api-node/issues/622 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae42b159..a3ca4172 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ shopify.metafield .create({ key: 'warehouse', value: 25, - value_type: 'integer', + type: 'integer', namespace: 'inventory', owner_resource: 'product', owner_id: 632910392 From e2a716bb4f822ef67849d724c8f224c21a6afd71 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 19 Aug 2023 10:32:32 +0200 Subject: [PATCH 069/110] [pkg] Update eslint-config-prettier to version 9.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5fd5a60..0acf0207 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "c8": "^7.3.0", "chai": "^4.1.2", "eslint": "^8.0.0", - "eslint-config-prettier": "^8.1.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", From feba071ac506ab9e5fa25e77d7a194647be0dd6b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 14 Sep 2023 21:43:50 +0200 Subject: [PATCH 070/110] [pkg] Update lint-staged to version 14.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0acf0207..97a0f515 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", - "lint-staged": "^13.0.0", + "lint-staged": "^14.0.1", "mocha": "^8.0.1", "nock": "^13.0.1", "prettier": "^3.0.0", From 6bd417f57b5357b7206a3542d36c298fcd62f965 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 14 Sep 2023 21:45:14 +0200 Subject: [PATCH 071/110] [ci] Update actions/checkout action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 416f4045..f057bf24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - 18 - 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} From edf16df44cd1427fd6b00736ee09aede4abe1bcf Mon Sep 17 00:00:00 2001 From: Reece F Date: Thu, 12 Oct 2023 16:04:51 +1000 Subject: [PATCH 072/110] [minor] Improve compatibility with bundlers (#634) --- resources/index.js | 152 ++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/resources/index.js b/resources/index.js index 6711c42f..83e43352 100644 --- a/resources/index.js +++ b/resources/index.js @@ -1,76 +1,76 @@ 'use strict'; const map = { - accessScope: 'access-scope', - apiPermission: 'api-permission', - applicationCharge: 'application-charge', - applicationCredit: 'application-credit', - article: 'article', - asset: 'asset', - balance: 'balance', - blog: 'blog', - cancellationRequest: 'cancellation-request', - carrierService: 'carrier-service', - checkout: 'checkout', - collect: 'collect', - collection: 'collection', - collectionListing: 'collection-listing', - comment: 'comment', - country: 'country', - currency: 'currency', - customCollection: 'custom-collection', - customer: 'customer', - customerAddress: 'customer-address', - customerSavedSearch: 'customer-saved-search', - deprecatedApiCall: 'deprecated-api-call', - discountCode: 'discount-code', - discountCodeCreationJob: 'discount-code-creation-job', - dispute: 'dispute', - disputeEvidence: 'dispute-evidence', - disputeFileUpload: 'dispute-file-upload', - draftOrder: 'draft-order', - event: 'event', - fulfillment: 'fulfillment', - fulfillmentEvent: 'fulfillment-event', - fulfillmentOrder: 'fulfillment-order', - fulfillmentRequest: 'fulfillment-request', - fulfillmentService: 'fulfillment-service', - giftCard: 'gift-card', - giftCardAdjustment: 'gift-card-adjustment', - inventoryItem: 'inventory-item', - inventoryLevel: 'inventory-level', - location: 'location', - marketingEvent: 'marketing-event', - metafield: 'metafield', - order: 'order', - orderRisk: 'order-risk', - page: 'page', - payment: 'payment', - payout: 'payout', - policy: 'policy', - priceRule: 'price-rule', - product: 'product', - productImage: 'product-image', - productListing: 'product-listing', - productResourceFeedback: 'product-resource-feedback', - productVariant: 'product-variant', - province: 'province', - recurringApplicationCharge: 'recurring-application-charge', - redirect: 'redirect', - refund: 'refund', - report: 'report', - resourceFeedback: 'resource-feedback', - scriptTag: 'script-tag', - shippingZone: 'shipping-zone', - shop: 'shop', - smartCollection: 'smart-collection', - storefrontAccessToken: 'storefront-access-token', - tenderTransaction: 'tender-transaction', - theme: 'theme', - transaction: 'transaction', - usageCharge: 'usage-charge', - user: 'user', - webhook: 'webhook' + accessScope: () => require('./access-scope'), + apiPermission: () => require('./api-permission'), + applicationCharge: () => require('./application-charge'), + applicationCredit: () => require('./application-credit'), + article: () => require('./article'), + asset: () => require('./asset'), + balance: () => require('./balance'), + blog: () => require('./blog'), + cancellationRequest: () => require('./cancellation-request'), + carrierService: () => require('./carrier-service'), + checkout: () => require('./checkout'), + collect: () => require('./collect'), + collection: () => require('./collection'), + collectionListing: () => require('./collection-listing'), + comment: () => require('./comment'), + country: () => require('./country'), + currency: () => require('./currency'), + customCollection: () => require('./custom-collection'), + customer: () => require('./customer'), + customerAddress: () => require('./customer-address'), + customerSavedSearch: () => require('./customer-saved-search'), + deprecatedApiCall: () => require('./deprecated-api-call'), + discountCode: () => require('./discount-code'), + discountCodeCreationJob: () => require('./discount-code-creation-job'), + dispute: () => require('./dispute'), + disputeEvidence: () => require('./dispute-evidence'), + disputeFileUpload: () => require('./dispute-file-upload'), + draftOrder: () => require('./draft-order'), + event: () => require('./event'), + fulfillment: () => require('./fulfillment'), + fulfillmentEvent: () => require('./fulfillment-event'), + fulfillmentOrder: () => require('./fulfillment-order'), + fulfillmentRequest: () => require('./fulfillment-request'), + fulfillmentService: () => require('./fulfillment-service'), + giftCard: () => require('./gift-card'), + giftCardAdjustment: () => require('./gift-card-adjustment'), + inventoryItem: () => require('./inventory-item'), + inventoryLevel: () => require('./inventory-level'), + location: () => require('./location'), + marketingEvent: () => require('./marketing-event'), + metafield: () => require('./metafield'), + order: () => require('./order'), + orderRisk: () => require('./order-risk'), + page: () => require('./page'), + payment: () => require('./payment'), + payout: () => require('./payout'), + policy: () => require('./policy'), + priceRule: () => require('./price-rule'), + product: () => require('./product'), + productImage: () => require('./product-image'), + productListing: () => require('./product-listing'), + productResourceFeedback: () => require('./product-resource-feedback'), + productVariant: () => require('./product-variant'), + province: () => require('./province'), + recurringApplicationCharge: () => require('./recurring-application-charge'), + redirect: () => require('./redirect'), + refund: () => require('./refund'), + report: () => require('./report'), + resourceFeedback: () => require('./resource-feedback'), + scriptTag: () => require('./script-tag'), + shippingZone: () => require('./shipping-zone'), + shop: () => require('./shop'), + smartCollection: () => require('./smart-collection'), + storefrontAccessToken: () => require('./storefront-access-token'), + tenderTransaction: () => require('./tender-transaction'), + theme: () => require('./theme'), + transaction: () => require('./transaction'), + usageCharge: () => require('./usage-charge'), + user: () => require('./user'), + webhook: () => require('./webhook') }; /** @@ -80,18 +80,18 @@ const map = { * @private */ function registerAll(Shopify) { - Object.keys(map).forEach((prop) => { - Object.defineProperty(Shopify.prototype, prop, { + Object.entries(map).forEach(([resourceName, importResource]) => { + Object.defineProperty(Shopify.prototype, resourceName, { configurable: true, get: function get() { - const resource = require(`./${map[prop]}`); + const resource = importResource(); - return Object.defineProperty(this, prop, { + return Object.defineProperty(this, resourceName, { value: new resource(this) - })[prop]; + })[resourceName]; }, set: function set(value) { - Object.defineProperty(this, prop, { value }); + Object.defineProperty(this, resourceName, { value }); } }); }); From 93d583843bde443512f8ec18f270fb780e0922ec Mon Sep 17 00:00:00 2001 From: Dennis Njoroge Date: Thu, 12 Oct 2023 09:11:10 +0300 Subject: [PATCH 073/110] [ts] Add contact_email and total_outstanding to IOrder interface (#635) Fixes: https://github.com/MONEI/Shopify-api-node/issues/633 --- types/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 709aeba2..6c1b5a5b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2368,6 +2368,7 @@ declare namespace Shopify { checkout_token: string | null; closed_at: string | null; confirmed: boolean; + contact_email: string | null; created_at: string; currency: string; current_subtotal_price: string; @@ -2428,6 +2429,7 @@ declare namespace Shopify { total_discounts_set: IMoneySet; total_line_items_price: string; total_line_items_price_set: IMoneySet; + total_outstanding: string; total_price: string; total_price_set: IMoneySet; total_shipping_price_set: IMoneySet; From c109341f0921a300e93d028d8f2dbfb1abe74bf3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 12 Oct 2023 08:12:46 +0200 Subject: [PATCH 074/110] [dist] 3.12.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97a0f515..523bc10e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.6", + "version": "3.12.7", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 56c91003b9a1d3182684adcd48d0b85956fa0987 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 Dec 2023 20:56:21 +0100 Subject: [PATCH 075/110] [ci] Update actions/setup-node action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f057bf24..b13e4f9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - 20 steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm install From 398a981ee3953a483f4d7fb3e3f903718c90e7e9 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 Dec 2023 20:58:12 +0100 Subject: [PATCH 076/110] [pkg] Update lint-staged to version 15.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 523bc10e..712fe366 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", - "lint-staged": "^14.0.1", + "lint-staged": "^15.2.0", "mocha": "^8.0.1", "nock": "^13.0.1", "prettier": "^3.0.0", From a75493b86e71ab2b783b073dc5b4f4a9d6e4e02d Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 7 Dec 2023 21:24:05 +0100 Subject: [PATCH 077/110] [codestyle] Ignore ternaries formatting --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index 6c1b5a5b..c91eddfd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3338,6 +3338,7 @@ declare namespace Shopify { topic: WebhookTopic; } + // prettier-ignore export type WebhookType = T extends 'app/uninstalled' ? IShop : T extends 'carts/create' From 4f02cc93ce11b5e6cf6b232c6f09e17b685e0836 Mon Sep 17 00:00:00 2001 From: Youkehai <42542308+Youkehai@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:18:26 +0800 Subject: [PATCH 078/110] [api] Introduce the agent option (#643) --- README.md | 4 ++++ index.js | 2 ++ types/index.d.ts | 4 +++- types/index.test-d.ts | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3ca4172..d1e45cc9 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ Creates a new `Shopify` instance. attach to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. Hooks should be provided in the same format that Got expects them and will receive the same arguments Got passes unchanged. +- `agent` - Optional - An object that is passed as the `agent` option to `got`. + This allows to use a proxy server. See + [Got documentation](https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#proxies) + for more details. #### Return value diff --git a/index.js b/index.js index 425f6f14..7e2204c2 100644 --- a/index.js +++ b/index.js @@ -154,6 +154,7 @@ Shopify.prototype.request = function request(uri, method, key, data, headers) { parseJson: this.options.parseJson, timeout: this.options.timeout, responseType: 'json', + agent: this.options.agent, method }; @@ -282,6 +283,7 @@ Shopify.prototype.graphql = function graphql(data, variables) { timeout: this.options.timeout, responseType: 'json', method: 'POST', + agent: this.options.agent, body: json ? this.options.stringifyJson({ query: data, variables }) : data }; diff --git a/types/index.d.ts b/types/index.d.ts index c91eddfd..92f81a18 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,7 +1,7 @@ // Type definitions for shopify-api-node // Project: shopify-api-node // Definitions by: Rich Buggy -import { Hooks } from 'got'; +import { Hooks, Agents } from 'got'; /*~ This is the module template file for class modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. @@ -789,6 +789,7 @@ declare namespace Shopify { shopName: string; timeout?: number; hooks?: Hooks; + agent?: Agents; } export interface IPrivateShopifyConfig { @@ -801,6 +802,7 @@ declare namespace Shopify { shopName: string; timeout?: number; hooks?: Hooks; + agent?: Agents; } export interface ICallLimits { diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 2ed8f226..cb49e076 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -27,6 +27,22 @@ new Shopify({ } }); +// Accepts the `agent` option. +new Shopify({ + shopName: 'my-shopify-store.myshopify.com', + accessToken: '111', + agent: { + // https: new HttpsProxyAgent({ + // keepAlive: true, + // keepAliveMsecs: 1000, + // maxSockets: 256, + // maxFreeSockets: 256, + // scheduling: 'lifo', + // proxy: 'https://localhost:8080' + // }) + } +}); + expectType(client.callLimits.remaining); expectType(client.callLimits.current); expectType(client.callLimits.max); From 92c7d7662cf9ab12e7f61aa62835f285468921f3 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jan 2024 16:30:44 +0100 Subject: [PATCH 079/110] [minor] Fix nits --- README.md | 32 ++++++++++++++++---------------- index.js | 16 ++++++++-------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index d1e45cc9..aa2da98f 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Creates a new `Shopify` instance. [OAuth 2.0][oauth] access token. This option is mutually exclusive with the `apiKey` and `password` options. If you are looking for a premade solution to obtain an access token, take a look at the [shopify-token][] module. +- `agent` - Optional - An object that is passed as the `agent` option to `got`. + This allows to use a proxy server. See + [Got documentation](https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#proxies) + for more details. - `apiVersion` - Optional - A string to specify the [Shopify API version][api-versioning] to use for requests. Defaults to the oldest supported stable version. @@ -53,6 +57,18 @@ Creates a new `Shopify` instance. specifies a limit of 2 requests per second with a burst of 35 requests. When set to `true` requests are limited as specified in the above example. Defaults to `false`. Mutually exclusive with the `maxRetries` option. +- `hooks` - Optional - A list of `got` + [request hooks](https://github.com/sindresorhus/got/tree/v11.8.6#hooks) to + attach to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. + Hooks should be provided in the same format that Got expects them and will + receive the same arguments Got passes unchanged. +- `maxRetries` - Optional - The number of times to attempt to make the request + to Shopify before giving up. Defaults to `0`, which means no automatic + retries. If set to a value greater than `0`, `shopify-api-node` will make up + to that many retries. `shopify-api-node` will respect the `Retry-After` header + for requests to the REST API, and the throttled cost data for requests to the + GraphQL API, and retry the request after that time has elapsed. Mutually + exclusive with the `autoLimit` option. - `parseJson` - Optional - The function used to parse JSON. The function is passed a single argument. This option allows the use of a custom JSON parser that might be needed to properly handle long integer IDs. Defaults to @@ -66,22 +82,6 @@ Creates a new `Shopify` instance. - `timeout` - Optional - The number of milliseconds before the request times out. If the request takes longer than `timeout`, it will be aborted. Defaults to `60000`, or 1 minute. -- `maxRetries` - Optional - The number of times to attempt to make the request - to Shopify before giving up. Defaults to `0`, which means no automatic - retries. If set to a value greater than `0`, `shopify-api-node` will make up - to that many retries. `shopify-api-node` will respect the `Retry-After` header - for requests to the REST API, and the throttled cost data for requests to the - GraphQL API, and retry the request after that time has elapsed. Mutually - exclusive with the `autoLimit` option. -- `hooks` - Optional - A list of `got` - [request hooks](https://github.com/sindresorhus/got/tree/v11.8.6#hooks) to - attach to all outgoing requests, like `beforeRetry`, `afterResponse`, etc. - Hooks should be provided in the same format that Got expects them and will - receive the same arguments Got passes unchanged. -- `agent` - Optional - An object that is passed as the `agent` option to `got`. - This allows to use a proxy server. See - [Got documentation](https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#proxies) - for more details. #### Return value diff --git a/index.js b/index.js index 7e2204c2..a4337eef 100644 --- a/index.js +++ b/index.js @@ -149,13 +149,13 @@ Shopify.prototype.updateLimits = function updateLimits(header) { */ Shopify.prototype.request = function request(uri, method, key, data, headers) { const options = { + agent: this.options.agent, headers: { ...headers, ...this.baseHeaders }, - stringifyJson: this.options.stringifyJson, + method, parseJson: this.options.parseJson, - timeout: this.options.timeout, responseType: 'json', - agent: this.options.agent, - method + stringifyJson: this.options.stringifyJson, + timeout: this.options.timeout }; const afterResponse = (res) => { @@ -275,16 +275,16 @@ Shopify.prototype.graphql = function graphql(data, variables) { const uri = { pathname, ...this.baseUrl }; const json = variables !== undefined && variables !== null; const options = { + agent: this.options.agent, + body: json ? this.options.stringifyJson({ query: data, variables }) : data, headers: { ...this.baseHeaders, 'Content-Type': json ? 'application/json' : 'application/graphql' }, + method: 'POST', parseJson: this.options.parseJson, - timeout: this.options.timeout, responseType: 'json', - method: 'POST', - agent: this.options.agent, - body: json ? this.options.stringifyJson({ query: data, variables }) : data + timeout: this.options.timeout }; const afterResponse = (res) => { From 6be11b2f5a03f670706068e47372455cb1c91c16 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 13 Jan 2024 16:33:55 +0100 Subject: [PATCH 080/110] [dist] 3.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 712fe366..0a432bd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.12.7", + "version": "3.13.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 66e2a83ce70f7c9b76106a4e45b1f55bff580748 Mon Sep 17 00:00:00 2001 From: patrickyau-visualsquares Date: Fri, 9 Feb 2024 04:12:51 +0800 Subject: [PATCH 081/110] [ts] Add email_marketing_consent to I{,Order}Customer (#646) --- types/index.d.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 92f81a18..9d5e7ad6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1387,8 +1387,15 @@ declare namespace Shopify { type CustomerState = 'declined' | 'disabled' | 'enabled' | 'invited'; + interface IEmailMarketingConsent { + state: string; + opt_in_level: string | null; + consent_updated_at: string; + } + interface ICustomer { - accepts_marketing: boolean; + accepts_marketing?: boolean; + email_marketing_consent?: IEmailMarketingConsent, addresses?: ICustomerAddress[]; created_at: string; currency: string; @@ -2186,7 +2193,8 @@ declare namespace Shopify { } interface IOrderCustomer { - accepts_marketing: boolean; + accepts_marketing?: boolean; + email_marketing_consent?: IEmailMarketingConsent, created_at: string; default_address: ICustomerAddress; email: string; From b1b40f2ec0568a914d6ad8de5b9d4763e719eaad Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Feb 2024 21:21:01 +0100 Subject: [PATCH 082/110] [lint] Fix lint error --- types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9d5e7ad6..f17f0252 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1395,7 +1395,7 @@ declare namespace Shopify { interface ICustomer { accepts_marketing?: boolean; - email_marketing_consent?: IEmailMarketingConsent, + email_marketing_consent?: IEmailMarketingConsent; addresses?: ICustomerAddress[]; created_at: string; currency: string; @@ -2194,7 +2194,7 @@ declare namespace Shopify { interface IOrderCustomer { accepts_marketing?: boolean; - email_marketing_consent?: IEmailMarketingConsent, + email_marketing_consent?: IEmailMarketingConsent; created_at: string; default_address: ICustomerAddress; email: string; From 6e09645e09a977821c53fe9fc4dd35a09fbb18e0 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 8 Feb 2024 21:40:50 +0100 Subject: [PATCH 083/110] [dist] 3.13.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a432bd6..d4d50876 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.13.0", + "version": "3.13.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 988162afd3a290bfeaca005bf3c2b203cf2d3711 Mon Sep 17 00:00:00 2001 From: Alexandre Saiz Verdaguer Date: Thu, 4 Apr 2024 07:37:05 +0200 Subject: [PATCH 084/110] Update README.md --- README.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index aa2da98f..74ca7392 100644 --- a/README.md +++ b/README.md @@ -766,9 +766,6 @@ For more information on the available `got` hooks, see the (add yours!) - [Sample Node Express app by Shopify][sample-node-express-app-by-shopify] -- [Wholesaler][wholesaler] -- [Wholesaler & Customer Pricing][wholesaler-customer-pricing] -- [Wholesaler PRO][wholesaler-pro] - [Youtube Traffic][youtube-traffic] - [Shipatron][shipatron] - [UPC Code Manager][upc-code-manager] @@ -776,9 +773,8 @@ For more information on the available `got` hooks, see the ## Supported by: -[microapps][microapps] +[MONEI][monei] -Used in our live products: [MoonMail][moonmail] & [MONEI][monei] ## License @@ -806,16 +802,8 @@ Used in our live products: [MoonMail][moonmail] & [MONEI][monei] [learning-from-others]: https://stackoverflow.com/questions/tagged/shopify [paginated-rest-results]: https://shopify.dev/api/usage/pagination-rest [polaris]: https://polaris.shopify.com/?ref=microapps -[microapps]: - http://microapps.com/?utm_source=shopify-api-node-module-repo-readme&utm_medium=click&utm_campaign=github -[moonmail]: - https://moonmail.io/?utm_source=shopify-api-node-module-repo-readme&utm_medium=click&utm_campaign=github [monei]: - https://monei.net/?utm_source=shopify-api-node-module-repo-readme&utm_medium=click&utm_campaign=github -[wholesaler]: https://apps.shopify.com/wholesaler?ref=microapps -[wholesaler-customer-pricing]: - https://apps.shopify.com/wholesaler-pro-1?ref=microapps -[wholesaler-pro]: https://apps.shopify.com/wholesaler-pro-2?ref=microapps + https://monei.com/?utm_source=shopify-api-node-module-repo-readme&utm_medium=click&utm_campaign=github [youtube-traffic]: https://apps.shopify.com/youtube-traffic?ref=microapps [shipatron]: https://shipatron.io [upc-code-manager]: https://apps.shopify.com/upc-code-manager-1 From 2f26a4193d87a98a5b06e39d7d06e11c4e88c794 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 6 Apr 2024 20:25:48 +0200 Subject: [PATCH 085/110] [lint] Fix lint error --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 74ca7392..cefc3e20 100644 --- a/README.md +++ b/README.md @@ -775,7 +775,6 @@ For more information on the available `got` hooks, see the [MONEI][monei] - ## License [MIT](LICENSE) From 78ca9bc93fe3b01c80aa941d34c8e0cfba78562b Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 13 May 2024 21:01:16 +0200 Subject: [PATCH 086/110] [pkg] Update eslint to version 9.2.0 --- .eslintrc.yaml | 16 ---------------- .lintstagedrc.json | 2 +- eslint.config.js | 24 ++++++++++++++++++++++++ package.json | 5 +++-- 4 files changed, 28 insertions(+), 19 deletions(-) delete mode 100644 .eslintrc.yaml create mode 100644 eslint.config.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index e20bce79..00000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,16 +0,0 @@ -env: - es6: true - mocha: true - node: true -extends: - - eslint:recommended - - plugin:prettier/recommended -parserOptions: - ecmaVersion: latest -rules: - no-var: error - prefer-const: error - quotes: - - error - - single - - avoidEscape: true diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 3c3f5b6b..9fef63c9 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,4 +1,4 @@ { "*.js": "eslint --fix", - "*.{json,md,ts,yml,yaml}": "prettier --write" + "*.{js,json,md,ts,yml,yaml}": "prettier --write" } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..282fab54 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +'use strict'; + +const pluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); +const globals = require('globals'); +const js = require('@eslint/js'); + +module.exports = [ + js.configs.recommended, + { + ignores: ['coverage/', 'node_modules/'], + languageOptions: { + ecmaVersion: 'latest', + globals: { + ...globals.mocha, + ...globals.node + } + }, + rules: { + 'no-var': 'error', + 'prefer-const': 'error' + } + }, + pluginPrettierRecommended +]; diff --git a/package.json b/package.json index d4d50876..70aecb38 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,10 @@ "devDependencies": { "c8": "^7.3.0", "chai": "^4.1.2", - "eslint": "^8.0.0", + "eslint": "^9.2.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "globals": "^15.2.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", "lint-staged": "^15.2.0", @@ -40,7 +41,7 @@ "test": "c8 --reporter=lcov --reporter=text mocha", "test:types": "tsd", "watch": "mocha -w", - "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,ts,yaml,yml}\"", + "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,ts,yaml,yml}\"", "prepare": "husky install" }, "repository": "MONEI/Shopify-api-node", From fd6d034440fd08fd9a97765c37152ecc5a6e7d5a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Mon, 13 May 2024 21:37:24 +0200 Subject: [PATCH 087/110] [ci] Test on node 22 --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b13e4f9c..d8c35590 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: - 16 - 18 - 20 + - 22 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -23,10 +24,10 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm run lint - if: matrix.node == 20 + if: matrix.node == 22 - run: npm test - run: npm run test:types - uses: coverallsapp/github-action@v2 - if: matrix.node == 20 + if: matrix.node == 22 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 137627e6375796a09f656f3b5cc319c00439a593 Mon Sep 17 00:00:00 2001 From: Ryan Balsdon Date: Sun, 30 Jun 2024 08:57:53 -0400 Subject: [PATCH 088/110] [ts] Update definitions for I{Cre,Upd}ateArticle (#653) --- types/index.d.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index f17f0252..bc5a896c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -996,32 +996,32 @@ declare namespace Shopify { } interface ICreateArticle { - author: string; - body_html: string; + author?: string; + body_html?: string; handle?: string; - image?: IBase64Image; + image?: ICreateArticleImage; metafields?: ICreateObjectMetafield[]; published?: boolean; published_at?: string; summary_html?: string | null; tags?: string; template_suffix?: string | null; - title: string; + title?: string; user_id?: number; } interface IUpdateArticle { - author: string; - body_html: string; + author?: string; + body_html?: string; handle?: string; - image?: IBase64Image; + image?: ICreateArticleImage; metafields?: ICreateObjectMetafield[]; published?: boolean; published_at?: string; summary_html?: string | null; tags?: string; template_suffix?: string | null; - title: string; + title?: string; user_id?: number; } @@ -1034,8 +1034,10 @@ declare namespace Shopify { alt: string | null; } - interface IBase64Image { - attachment: string; + interface ICreateArticleImage { + attachment?: string; + src?: string; + alt?: string; } interface IObjectMetafield { From 6b7aad4d77892e426e7f33ee213fb60dc1d34243 Mon Sep 17 00:00:00 2001 From: Nate Waddoups Date: Sun, 30 Jun 2024 06:05:26 -0700 Subject: [PATCH 089/110] [ts] Fix multipass_identifier type (#654) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index bc5a896c..e56f86e8 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1408,7 +1408,7 @@ declare namespace Shopify { last_name: string; metafield?: IObjectMetafield; // From https://help.shopify.com/api/reference/customer but not visible in test API call phone: string; - multipass_identifier: null; + multipass_identifier: string | null; last_order_id: number | null; last_order_name: string | null; note: string | null; From 19916ac257e106431bf1ec5b5951f68e2665a44c Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 30 Jun 2024 15:07:04 +0200 Subject: [PATCH 090/110] [dist] 3.13.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70aecb38..97eff423 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.13.1", + "version": "3.13.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 6e30c44d82fd4c08d77f9be5a767653bebefb7bc Mon Sep 17 00:00:00 2001 From: peter-visualsquares Date: Wed, 10 Jul 2024 00:41:06 +0800 Subject: [PATCH 091/110] [ts] Add current_quantity to IOrderLineItem interface (#656) --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index e56f86e8..260d2034 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2311,6 +2311,7 @@ declare namespace Shopify { } interface IOrderLineItem { + current_quantity?: number; discount_allocations: ILineItemDiscountAllocation[]; fulfillable_quantity: number; fulfillment_service: string; From 47b6bd13d0f0ec2a106c2e3a62e457e3a5e5b060 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 9 Jul 2024 19:03:09 +0200 Subject: [PATCH 092/110] [ts] Add discount_allocations to IFulfillmentLineItem interface Fixes: https://github.com/MONEI/Shopify-api-node/issues/655 --- types/index.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 260d2034..6b28b049 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1488,6 +1488,12 @@ declare namespace Shopify { value_type: ValueType; } + interface IDiscountAllocation { + amount: string; + amount_set: IMoneySet; + discount_application_index: number; + } + interface IDiscountCode { created_at: string; id: number; @@ -1779,6 +1785,7 @@ declare namespace Shopify { total_discount: string; fulfillment_status: IFulfillmentStatus; tax_lines: IFulfillmentLineItemTaxLine[]; + discount_allocations: IDiscountAllocation[]; } interface IFulfillment { From 159c2e7664b858ced9ee68bad2b03781b9dda377 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 9 Jul 2024 19:06:01 +0200 Subject: [PATCH 093/110] [dist] 3.13.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97eff423..2cd9a991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.13.2", + "version": "3.13.3", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 3dbf82aaa0434e7722a07c64d7796ca74f1a10bc Mon Sep 17 00:00:00 2001 From: Aaron Campbell <51107902+sleepdotexe@users.noreply.github.com> Date: Fri, 19 Jul 2024 06:07:51 +1000 Subject: [PATCH 094/110] [ts] Add discount webhook topics (#659) Fixes: https://github.com/MONEI/Shopify-api-node/issues/658 --- types/index.d.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 6b28b049..44c1d9e9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3233,6 +3233,11 @@ declare namespace Shopify { | 'customers/update' | 'customers_email_marketing_consent/update' | 'customers_marketing_consent/update' + | 'discounts/create' + | 'discounts/delete' + | 'discounts/redeemcode_added' + | 'discounts/redeemcode_removed' + | 'discounts/update' | 'disputes/create' | 'disputes/update' | 'domains/create' @@ -3399,6 +3404,16 @@ declare namespace Shopify { ? ICustomerSavedSearch : T extends 'customer_groups/delete' ? IDeletedItem + : T extends 'discounts/create' + ? IDiscountUpsertWebhook + : T extends 'discounts/delete' + ? IDiscountDeleteWebhook + : T extends 'discounts/redeemcode_added' + ? IDiscountRedeemCodeWebhook + : T extends 'discounts/redeemcode_removed' + ? IDiscountRedeemCodeWebhook + : T extends 'discounts/update' + ? IDiscountUpsertWebhook : T extends 'draft_orders/create' ? IDraftOrder : T extends 'draft_orders/update' @@ -3528,4 +3543,28 @@ declare namespace Shopify { min_delivery_date?: string; max_delivery_date?: string; } + + type DiscountStatus = 'ACTIVE' | 'EXPIRED' | 'SCHEDULED'; + + interface IDiscountUpsertWebhook { + admin_graphql_api_id: string; + title: string; + status: DiscountStatus; + created_at: string; + updated_at: string; + } + + interface IDiscountDeleteWebhook { + admin_graphql_api_id: string; + deleted_at: string; + } + + interface IDiscountRedeemCodeWebhook { + admin_graphql_api_id: string; + redeem_code: { + id: string; + code: string; + }; + updated_at: string; + } } From 6080d511a99fb541015a0a21f0b39769cef166fe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 18 Jul 2024 22:08:20 +0200 Subject: [PATCH 095/110] [dist] 3.13.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cd9a991..9a671545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.13.3", + "version": "3.13.4", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 7803fa18988847f160f9f2a23e7164787adba623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aldo=20Chac=C3=B3n?= <44204180+aldochaconc@users.noreply.github.com> Date: Tue, 20 Aug 2024 05:55:26 -0400 Subject: [PATCH 096/110] [api] Add hold action to FulfillmentOrder resource (#660) --- README.md | 3 +- resources/fulfillment-order.js | 17 ++++++ test/fixtures/fulfillment-order/req/hold.json | 7 +++ test/fixtures/fulfillment-order/req/index.js | 1 + test/fixtures/fulfillment-order/res/hold.json | 61 +++++++++++++++++++ test/fixtures/fulfillment-order/res/index.js | 1 + test/fulfillment-order.test.js | 15 +++++ types/index.d.ts | 21 +++++++ 8 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/fulfillment-order/req/hold.json create mode 100644 test/fixtures/fulfillment-order/res/hold.json diff --git a/README.md b/README.md index cefc3e20..ffca94c4 100644 --- a/README.md +++ b/README.md @@ -482,12 +482,13 @@ default. - fulfillmentOrder - `cancel(id, params)` - `close(id[, message])` + - `fulfillments(id)` - `get(id)` + - `hold(id, params)` - `list([params])` - `locationsForMove(id)` - `move(id, locationId)` - `setFulfillmentOrdersDeadline(params)` - - `fulfillments(id)` - fulfillmentRequest - `accept(fulfillmentOrderId[, message])` - `create(fulfillmentOrderId, params)` diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 460fe10d..04a5a8ff 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -127,4 +127,21 @@ FulfillmentOrder.prototype.fulfillments = function fulfillments(id) { return this.shopify.request(url, 'GET', 'fulfillments'); }; +/** + * Halts all fulfillment work on a fulfillment order with + * status OPEN and changes the status of the fulfillment order to ON_HOLD. + * + * @param {Number} id Fulfillment Order ID + * @param {Object} params An object containing the reason for the fulfillment + hold and additional optional information + * @return {Promise} Promise that resolves with the result + * @public + */ +FulfillmentOrder.prototype.hold = function hold(id, params) { + const url = this.buildUrl(`${id}/hold`); + return this.shopify + .request(url, 'POST', undefined, { fulfillment_hold: params }) + .then((body) => body[this.key]); +}; + module.exports = FulfillmentOrder; diff --git a/test/fixtures/fulfillment-order/req/hold.json b/test/fixtures/fulfillment-order/req/hold.json new file mode 100644 index 00000000..d7ee489d --- /dev/null +++ b/test/fixtures/fulfillment-order/req/hold.json @@ -0,0 +1,7 @@ +{ + "fulfillment_hold": { + "reason": "inventory_out_of_stock", + "reason_notes": "Not enough inventory to complete this work.", + "fulfillment_order_line_items": [{ "id": 1058737493, "quantity": 1 }] + } +} diff --git a/test/fixtures/fulfillment-order/req/index.js b/test/fixtures/fulfillment-order/req/index.js index 54a1d09a..6ed476d7 100644 --- a/test/fixtures/fulfillment-order/req/index.js +++ b/test/fixtures/fulfillment-order/req/index.js @@ -2,5 +2,6 @@ exports.cancel = require('./cancel'); exports.close = require('./close'); +exports.hold = require('./hold'); exports.move = require('./move'); exports.setFulfillmentOrdersDeadline = require('./set-fulfillment-orders-deadline'); diff --git a/test/fixtures/fulfillment-order/res/hold.json b/test/fixtures/fulfillment-order/res/hold.json new file mode 100644 index 00000000..31844af5 --- /dev/null +++ b/test/fixtures/fulfillment-order/res/hold.json @@ -0,0 +1,61 @@ +{ + "fulfillment_order": { + "id": 1046000789, + "shop_id": 548380009, + "order_id": 450789469, + "assigned_location_id": 24826418, + "request_status": "unsubmitted", + "status": "on_hold", + "fulfill_at": null, + "supported_actions": ["release_hold"], + "destination": { + "id": 1046000789, + "address1": "Chestnut Street 92", + "address2": "", + "city": "Louisville", + "company": null, + "country": "United States", + "email": "bob.norman@mail.example.com", + "first_name": "Bob", + "last_name": "Norman", + "phone": "+1(502)-459-2181", + "province": "Kentucky", + "zip": "40202" + }, + "line_items": [ + { + "id": 1058737493, + "shop_id": 548380009, + "fulfillment_order_id": 1046000789, + "quantity": 1, + "line_item_id": 518995019, + "inventory_item_id": 49148385, + "fulfillable_quantity": 1, + "variant_id": 49148385 + } + ], + "international_duties": null, + "fulfillment_holds": [ + { + "reason": "inventory_out_of_stock", + "reason_notes": "Not enough inventory to complete this work." + } + ], + "fulfill_by": null, + "created_at": "2024-07-24T06:26:32-04:00", + "updated_at": "2024-07-24T06:26:33-04:00", + "delivery_method": null, + "assigned_location": { + "address1": null, + "address2": null, + "city": null, + "country_code": "DE", + "location_id": 24826418, + "name": "Apple Api Shipwire", + "phone": null, + "province": null, + "zip": null + }, + "merchant_requests": [] + } +} diff --git a/test/fixtures/fulfillment-order/res/index.js b/test/fixtures/fulfillment-order/res/index.js index 56f1bc0e..21cdfab6 100644 --- a/test/fixtures/fulfillment-order/res/index.js +++ b/test/fixtures/fulfillment-order/res/index.js @@ -7,3 +7,4 @@ exports.close = require('./close'); exports.list = require('./list'); exports.move = require('./move'); exports.get = require('./get'); +exports.hold = require('./hold'); diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index b3e9626f..dfef4e40 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -134,4 +134,19 @@ describe('Shopify#fulfillmentOrder', () => { .fulfillments(1046000823) .then((data) => expect(data).to.deep.equal(output.fulfillments)); }); + + it('applies a fulfillment hold on an open fulfillment order', () => { + const input = fixtures.req.hold; + const output = fixtures.res.hold; + + scope + .post('/admin/fulfillment_orders/1046000789/hold.json', input) + .reply(200, output); + + return shopify.fulfillmentOrder + .hold(1046000789, input.fulfillment_hold) + .then((data) => { + expect(data).to.deep.equal(output.fulfillment_order); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 44c1d9e9..e1677ed2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -389,6 +389,10 @@ declare class Shopify { fulfillments: ( id: number ) => Promise>; + hold: ( + id: number, + params: Shopify.IFulfillmentHold + ) => Promise; }; fulfillmentRequest: { accept: ( @@ -3567,4 +3571,21 @@ declare namespace Shopify { }; updated_at: string; } + + interface IFulfillmentHoldFulfillmentOrderLineItem { + id: number; + quantity: number; + } + + interface IFulfillmentHold { + reason: + | 'awaiting_payment' + | 'high_risk_of_fraud' + | 'incorrect_address' + | 'inventory_out_of_stock' + | 'other'; + reason_notes?: string; + notify_merchant?: boolean; + fulfillment_order_line_items?: IFulfillmentHoldFulfillmentOrderLineItem[]; + } } From dadedd27234e2cd58d8402d470450d7eb3262f1a Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Aug 2024 12:26:29 +0200 Subject: [PATCH 097/110] [api] Add releaseHold action to FulfillmentOrder resource --- README.md | 1 + resources/fulfillment-order.js | 15 +++++ test/fixtures/fulfillment-order/res/index.js | 9 +-- .../fulfillment-order/res/release-hold.json | 56 +++++++++++++++++++ test/fulfillment-order.test.js | 12 ++++ types/index.d.ts | 15 ++--- 6 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/fulfillment-order/res/release-hold.json diff --git a/README.md b/README.md index ffca94c4..9ed827fb 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,7 @@ default. - `list([params])` - `locationsForMove(id)` - `move(id, locationId)` + - `releaseHold(id)` - `setFulfillmentOrdersDeadline(params)` - fulfillmentRequest - `accept(fulfillmentOrderId[, message])` diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 04a5a8ff..62cab6b4 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -144,4 +144,19 @@ FulfillmentOrder.prototype.hold = function hold(id, params) { .then((body) => body[this.key]); }; +/** + * Release the fulfillment hold on a fulfillment order and changes the status of + * the fulfillment order to `OPEN` or `SCHEDULED`. + * + * @param {Number} id Fulfillment Order ID + * @return {Promise} Promise that resolves with the result + * @public + */ +FulfillmentOrder.prototype.releaseHold = function releaseHold(id) { + const url = this.buildUrl(`${id}/release_hold`); + return this.shopify + .request(url, 'POST', undefined, {}) + .then((body) => body[this.key]); +}; + module.exports = FulfillmentOrder; diff --git a/test/fixtures/fulfillment-order/res/index.js b/test/fixtures/fulfillment-order/res/index.js index 21cdfab6..621092de 100644 --- a/test/fixtures/fulfillment-order/res/index.js +++ b/test/fixtures/fulfillment-order/res/index.js @@ -1,10 +1,11 @@ 'use strict'; -exports.locationsForMove = require('./locations-for-move'); -exports.fulfillments = require('./fulfillments'); exports.cancel = require('./cancel'); exports.close = require('./close'); -exports.list = require('./list'); -exports.move = require('./move'); +exports.fulfillments = require('./fulfillments'); exports.get = require('./get'); exports.hold = require('./hold'); +exports.list = require('./list'); +exports.locationsForMove = require('./locations-for-move'); +exports.move = require('./move'); +exports.releaseHold = require('./release-hold'); diff --git a/test/fixtures/fulfillment-order/res/release-hold.json b/test/fixtures/fulfillment-order/res/release-hold.json new file mode 100644 index 00000000..a8915b5b --- /dev/null +++ b/test/fixtures/fulfillment-order/res/release-hold.json @@ -0,0 +1,56 @@ +{ + "fulfillment_order": { + "id": 1046000790, + "shop_id": 548380009, + "order_id": 450789469, + "assigned_location_id": 24826418, + "request_status": "submitted", + "status": "open", + "fulfill_at": null, + "supported_actions": ["cancel_fulfillment_order"], + "destination": { + "id": 1046000790, + "address1": "Chestnut Street 92", + "address2": "", + "city": "Louisville", + "company": null, + "country": "United States", + "email": "bob.norman@mail.example.com", + "first_name": "Bob", + "last_name": "Norman", + "phone": "+1(502)-459-2181", + "province": "Kentucky", + "zip": "40202" + }, + "origin": { + "address1": null, + "address2": null, + "city": null, + "country_code": "DE", + "location_id": 24826418, + "name": "Apple Api Shipwire", + "phone": null, + "province": null, + "zip": null + }, + "line_items": [ + { + "id": 1058737494, + "shop_id": 548380009, + "fulfillment_order_id": 1046000790, + "quantity": 1, + "line_item_id": 518995019, + "inventory_item_id": 49148385, + "fulfillable_quantity": 1, + "variant_id": 49148385 + } + ], + "outgoing_requests": [], + "international_duties": null, + "fulfillment_holds": [], + "fulfill_by": null, + "created_at": "2024-07-24T06:26:33-04:00", + "updated_at": "2024-07-24T06:26:34-04:00", + "delivery_method": null + } +} diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index dfef4e40..f53d7d2c 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -149,4 +149,16 @@ describe('Shopify#fulfillmentOrder', () => { expect(data).to.deep.equal(output.fulfillment_order); }); }); + + it('releases the fulfillment hold on a fulfillment order', () => { + const output = fixtures.res.releaseHold; + + scope + .post('/admin/fulfillment_orders/1046000790/release_hold.json', {}) + .reply(200, output); + + return shopify.fulfillmentOrder.releaseHold(1046000790).then((data) => { + expect(data).to.deep.equal(output.fulfillment_order); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index e1677ed2..8078b229 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -376,23 +376,24 @@ declare class Shopify { params: Shopify.IFulfillmentOrder ) => Promise; close: (id: number, message?: string) => Promise; + fulfillments: ( + id: number + ) => Promise>; get: (id: number) => Promise; + hold: ( + id: number, + params: Shopify.IFulfillmentHold + ) => Promise; list: (params?: any) => Promise; locationsForMove: (id: number) => Promise; move: ( id: number, locationId: number ) => Promise; + releaseHold: (id: number) => Promise; setFulfillmentOrdersDeadline: ( params: Shopify.ISetFulfillmentOrdersDeadline ) => Promise; - fulfillments: ( - id: number - ) => Promise>; - hold: ( - id: number, - params: Shopify.IFulfillmentHold - ) => Promise; }; fulfillmentRequest: { accept: ( From 101cc52c7c450fb1d32c707eb4f92c1ef6ff64fe Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Aug 2024 12:44:58 +0200 Subject: [PATCH 098/110] [minor] Fix nits --- resources/fulfillment-order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 62cab6b4..878609ef 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -128,8 +128,8 @@ FulfillmentOrder.prototype.fulfillments = function fulfillments(id) { }; /** - * Halts all fulfillment work on a fulfillment order with - * status OPEN and changes the status of the fulfillment order to ON_HOLD. + * Halts all fulfillment work on a fulfillment order with status `OPEN` and + * changes the status of the fulfillment order to `ON_HOLD`. * * @param {Number} id Fulfillment Order ID * @param {Object} params An object containing the reason for the fulfillment From 990190802acdc8cf81ac48c5b0c7a1c2d6d682eb Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Aug 2024 13:59:54 +0200 Subject: [PATCH 099/110] [api] Add reschedule action to FulfillmentOrder resource --- README.md | 1 + resources/fulfillment-order.js | 18 ++++++ test/fixtures/fulfillment-order/req/index.js | 1 + .../fulfillment-order/req/reschedule.json | 1 + test/fixtures/fulfillment-order/res/index.js | 1 + .../fulfillment-order/res/reschedule.json | 56 +++++++++++++++++++ test/fulfillment-order.test.js | 15 +++++ types/index.d.ts | 4 ++ 8 files changed, 97 insertions(+) create mode 100644 test/fixtures/fulfillment-order/req/reschedule.json create mode 100644 test/fixtures/fulfillment-order/res/reschedule.json diff --git a/README.md b/README.md index 9ed827fb..aff2eec3 100644 --- a/README.md +++ b/README.md @@ -489,6 +489,7 @@ default. - `locationsForMove(id)` - `move(id, locationId)` - `releaseHold(id)` + - `reschedule(id, deadline)` - `setFulfillmentOrdersDeadline(params)` - fulfillmentRequest - `accept(fulfillmentOrderId[, message])` diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index 878609ef..cd187a3e 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -159,4 +159,22 @@ FulfillmentOrder.prototype.releaseHold = function releaseHold(id) { .then((body) => body[this.key]); }; +/** + * Reschedules a scheduled fulfillment order. Updates the value of the + * `fulfill_at field` on a scheduled fulfillment order. The fulfillment order + * will be marked as ready for fulfillment at this date and time. + * + * @param {Number} id Fulfillment Order ID + * @param {String} deadline The new fulfillment deadline of the fulfillment + * order + * @return {Promise} Promise that resolves with the result + * @public + */ +FulfillmentOrder.prototype.reschedule = function reschedule(id, deadline) { + const url = this.buildUrl(`${id}/reschedule`); + return this.shopify.request(url, 'POST', this.key, { + new_fulfill_at: deadline + }); +}; + module.exports = FulfillmentOrder; diff --git a/test/fixtures/fulfillment-order/req/index.js b/test/fixtures/fulfillment-order/req/index.js index 6ed476d7..50acf82c 100644 --- a/test/fixtures/fulfillment-order/req/index.js +++ b/test/fixtures/fulfillment-order/req/index.js @@ -4,4 +4,5 @@ exports.cancel = require('./cancel'); exports.close = require('./close'); exports.hold = require('./hold'); exports.move = require('./move'); +exports.reschedule = require('./reschedule'); exports.setFulfillmentOrdersDeadline = require('./set-fulfillment-orders-deadline'); diff --git a/test/fixtures/fulfillment-order/req/reschedule.json b/test/fixtures/fulfillment-order/req/reschedule.json new file mode 100644 index 00000000..cced9fcc --- /dev/null +++ b/test/fixtures/fulfillment-order/req/reschedule.json @@ -0,0 +1 @@ +{ "fulfillment_order": { "new_fulfill_at": "2025-08-24 10:26 UTC" } } diff --git a/test/fixtures/fulfillment-order/res/index.js b/test/fixtures/fulfillment-order/res/index.js index 621092de..b2cc035e 100644 --- a/test/fixtures/fulfillment-order/res/index.js +++ b/test/fixtures/fulfillment-order/res/index.js @@ -9,3 +9,4 @@ exports.list = require('./list'); exports.locationsForMove = require('./locations-for-move'); exports.move = require('./move'); exports.releaseHold = require('./release-hold'); +exports.reschedule = require('./reschedule'); diff --git a/test/fixtures/fulfillment-order/res/reschedule.json b/test/fixtures/fulfillment-order/res/reschedule.json new file mode 100644 index 00000000..b67c40a3 --- /dev/null +++ b/test/fixtures/fulfillment-order/res/reschedule.json @@ -0,0 +1,56 @@ +{ + "fulfillment_order": { + "id": 1046000788, + "shop_id": 548380009, + "order_id": 450789469, + "assigned_location_id": 24826418, + "request_status": "unsubmitted", + "status": "scheduled", + "fulfill_at": "2025-08-24T06:26:00-04:00", + "supported_actions": ["mark_as_open"], + "destination": { + "id": 1046000788, + "address1": "Chestnut Street 92", + "address2": "", + "city": "Louisville", + "company": null, + "country": "United States", + "email": "bob.norman@mail.example.com", + "first_name": "Bob", + "last_name": "Norman", + "phone": "+1(502)-459-2181", + "province": "Kentucky", + "zip": "40202" + }, + "line_items": [ + { + "id": 1058737492, + "shop_id": 548380009, + "fulfillment_order_id": 1046000788, + "quantity": 1, + "line_item_id": 518995019, + "inventory_item_id": 49148385, + "fulfillable_quantity": 1, + "variant_id": 49148385 + } + ], + "international_duties": null, + "fulfillment_holds": [], + "fulfill_by": null, + "created_at": "2024-07-24T06:26:30-04:00", + "updated_at": "2024-07-24T06:26:32-04:00", + "delivery_method": null, + "assigned_location": { + "address1": null, + "address2": null, + "city": null, + "country_code": "DE", + "location_id": 24826418, + "name": "Apple Api Shipwire", + "phone": null, + "province": null, + "zip": null + }, + "merchant_requests": [] + } +} diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index f53d7d2c..d38d37db 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -161,4 +161,19 @@ describe('Shopify#fulfillmentOrder', () => { expect(data).to.deep.equal(output.fulfillment_order); }); }); + + it('reschedules the fulfill_at time of a scheduled fulfillment order', () => { + const input = fixtures.req.reschedule; + const output = fixtures.res.reschedule; + + scope + .post('/admin/fulfillment_orders/1046000788/reschedule.json', input) + .reply(200, output); + + return shopify.fulfillmentOrder + .reschedule(1046000788, '2025-08-24 10:26 UTC') + .then((data) => { + expect(data).to.deep.equal(output.fulfillment_order); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 8078b229..0d3f77d4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -391,6 +391,10 @@ declare class Shopify { locationId: number ) => Promise; releaseHold: (id: number) => Promise; + reschedule: ( + id: number, + deadline: string + ) => Promise; setFulfillmentOrdersDeadline: ( params: Shopify.ISetFulfillmentOrdersDeadline ) => Promise; From 715d536c9224105164849b1320b85fbedd9ba0a8 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Aug 2024 14:11:23 +0200 Subject: [PATCH 100/110] [api] Remove the params argument from FulfillmentOrder#cancel() --- README.md | 2 +- resources/fulfillment-order.js | 7 +- .../fulfillment-order/req/cancel.json | 50 -------------- test/fixtures/fulfillment-order/req/index.js | 1 - .../fulfillment-order/res/cancel.json | 65 ++++++++++--------- test/fulfillment-order.test.js | 5 +- types/index.d.ts | 5 +- 7 files changed, 41 insertions(+), 94 deletions(-) delete mode 100644 test/fixtures/fulfillment-order/req/cancel.json diff --git a/README.md b/README.md index aff2eec3..fffa84af 100644 --- a/README.md +++ b/README.md @@ -480,7 +480,7 @@ default. - `list(orderId, fulfillmentId[, params])` - `update(orderId, fulfillmentId, id, params)` - fulfillmentOrder - - `cancel(id, params)` + - `cancel(id)` - `close(id[, message])` - `fulfillments(id)` - `get(id)` diff --git a/resources/fulfillment-order.js b/resources/fulfillment-order.js index cd187a3e..730f620e 100644 --- a/resources/fulfillment-order.js +++ b/resources/fulfillment-order.js @@ -41,13 +41,14 @@ FulfillmentOrder.prototype.list = function list(params) { * Marks a fulfillment order as cancelled. * * @param {Number} id Fulfillment order ID - * @param {Object} params Fulfillment order properties * @return {Promise} Promise that resolves with the result * @public */ -FulfillmentOrder.prototype.cancel = function cancel(id, params) { +FulfillmentOrder.prototype.cancel = function cancel(id) { const url = this.buildUrl(`${id}/cancel`); - return this.shopify.request(url, 'POST', this.key, params); + return this.shopify + .request(url, 'POST', undefined, {}) + .then((body) => body[this.key]); }; /** diff --git a/test/fixtures/fulfillment-order/req/cancel.json b/test/fixtures/fulfillment-order/req/cancel.json deleted file mode 100644 index 25931b7a..00000000 --- a/test/fixtures/fulfillment-order/req/cancel.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "fulfillment_order": { - "id": 1025578640, - "shop_id": 690933842, - "order_id": 450789469, - "assigned_location_id": 48752903, - "fulfillment_service_handle": "mars-fulfillment", - "request_status": "submitted", - "status": "open", - "supported_actions": ["cancel_fulfillment_order"], - "destination": { - "id": 1025578634, - "address1": "Chestnut Street 92", - "address2": "", - "city": "Louisville", - "company": null, - "country": "United States", - "email": "bob.norman@hostmail.com", - "first_name": "Bob", - "last_name": "Norman", - "phone": "555-625-1199", - "province": "Kentucky", - "zip": "40202" - }, - "line_items": [ - { - "id": 1025578653, - "shop_id": 690933842, - "fulfillment_order_id": 1025578640, - "quantity": 1, - "line_item_id": 518995019, - "inventory_item_id": 49148385, - "fulfillable_quantity": 1, - "variant_id": 49148385 - } - ], - "assigned_location": { - "address1": null, - "address2": null, - "city": null, - "country_code": "DE", - "location_id": 48752903, - "name": "Apple Api Shipwire", - "phone": null, - "province": null, - "zip": null - }, - "merchant_requests": [] - } -} diff --git a/test/fixtures/fulfillment-order/req/index.js b/test/fixtures/fulfillment-order/req/index.js index 50acf82c..6dc7beab 100644 --- a/test/fixtures/fulfillment-order/req/index.js +++ b/test/fixtures/fulfillment-order/req/index.js @@ -1,6 +1,5 @@ 'use strict'; -exports.cancel = require('./cancel'); exports.close = require('./close'); exports.hold = require('./hold'); exports.move = require('./move'); diff --git a/test/fixtures/fulfillment-order/res/cancel.json b/test/fixtures/fulfillment-order/res/cancel.json index bf0a2aa4..2175ede0 100644 --- a/test/fixtures/fulfillment-order/res/cancel.json +++ b/test/fixtures/fulfillment-order/res/cancel.json @@ -1,45 +1,40 @@ { "fulfillment_order": { - "id": 1025578640, - "shop_id": 690933842, + "id": 1046000791, + "shop_id": 548380009, "order_id": 450789469, - "assigned_location_id": 48752903, - "fulfillment_service_handle": "mars-fulfillment", + "assigned_location_id": 24826418, "request_status": "submitted", "status": "closed", + "fulfill_at": null, "supported_actions": [], "destination": { - "id": 1025578634, + "id": 1046000791, "address1": "Chestnut Street 92", "address2": "", "city": "Louisville", "company": null, "country": "United States", - "email": "bob.norman@hostmail.com", + "email": "bob.norman@mail.example.com", "first_name": "Bob", "last_name": "Norman", - "phone": "555-625-1199", + "phone": "+1(502)-459-2181", "province": "Kentucky", "zip": "40202" }, - "line_items": [ - { - "id": 1025578653, - "shop_id": 690933842, - "fulfillment_order_id": 1025578640, - "quantity": 1, - "line_item_id": 518995019, - "inventory_item_id": 49148385, - "fulfillable_quantity": 1, - "variant_id": 49148385 - } - ], + "line_items": [], + "international_duties": null, + "fulfillment_holds": [], + "fulfill_by": null, + "created_at": "2024-07-24T06:26:35-04:00", + "updated_at": "2024-07-24T06:26:36-04:00", + "delivery_method": null, "assigned_location": { "address1": null, "address2": null, "city": null, "country_code": "DE", - "location_id": 48752903, + "location_id": 24826418, "name": "Apple Api Shipwire", "phone": null, "province": null, @@ -48,33 +43,33 @@ "merchant_requests": [] }, "replacement_fulfillment_order": { - "id": 1025578641, - "shop_id": 690933842, + "id": 1046000792, + "shop_id": 548380009, "order_id": 450789469, - "assigned_location_id": 48752903, - "fulfillment_service_handle": "mars-fulfillment", + "assigned_location_id": 24826418, "request_status": "unsubmitted", "status": "open", - "supported_actions": ["request_fulfillment", "create_fulfillment"], + "fulfill_at": null, + "supported_actions": ["request_fulfillment", "hold"], "destination": { - "id": 1025578635, + "id": 1046000792, "address1": "Chestnut Street 92", "address2": "", "city": "Louisville", "company": null, "country": "United States", - "email": "bob.norman@hostmail.com", + "email": "bob.norman@mail.example.com", "first_name": "Bob", "last_name": "Norman", - "phone": "555-625-1199", + "phone": "+1(502)-459-2181", "province": "Kentucky", "zip": "40202" }, "line_items": [ { - "id": 1025578654, - "shop_id": 690933842, - "fulfillment_order_id": 1025578641, + "id": 1058737495, + "shop_id": 548380009, + "fulfillment_order_id": 1046000792, "quantity": 1, "line_item_id": 518995019, "inventory_item_id": 49148385, @@ -82,12 +77,18 @@ "variant_id": 49148385 } ], + "international_duties": null, + "fulfillment_holds": [], + "fulfill_by": null, + "created_at": "2024-07-24T06:26:36-04:00", + "updated_at": "2024-07-24T06:26:36-04:00", + "delivery_method": null, "assigned_location": { "address1": null, "address2": null, "city": null, "country_code": "DE", - "location_id": 48752903, + "location_id": 24826418, "name": "Apple Api Shipwire", "phone": null, "province": null, diff --git a/test/fulfillment-order.test.js b/test/fulfillment-order.test.js index d38d37db..f376f807 100644 --- a/test/fulfillment-order.test.js +++ b/test/fulfillment-order.test.js @@ -40,15 +40,14 @@ describe('Shopify#fulfillmentOrder', () => { }); it('cancels a fulfillment order', () => { - const input = fixtures.req.cancel; const output = fixtures.res.cancel; scope - .post('/admin/fulfillment_orders/1025578640/cancel.json', input) + .post('/admin/fulfillment_orders/1046000791/cancel.json', {}) .reply(200, output); return shopify.fulfillmentOrder - .cancel(1025578640, input.fulfillment_order) + .cancel(1046000791) .then((data) => expect(data).to.deep.equal(output.fulfillment_order)); }); diff --git a/types/index.d.ts b/types/index.d.ts index 0d3f77d4..f1f54dff 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -371,10 +371,7 @@ declare class Shopify { ) => Promise; }; fulfillmentOrder: { - cancel: ( - id: number, - params: Shopify.IFulfillmentOrder - ) => Promise; + cancel: (id: number) => Promise; close: (id: number, message?: string) => Promise; fulfillments: ( id: number From be6706d08fe004e6484f10912db1c698ba4b3a89 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Tue, 20 Aug 2024 14:43:20 +0200 Subject: [PATCH 101/110] [dist] 3.14.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a671545..83d6f3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.13.4", + "version": "3.14.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From b253236d838058af25e461cdd5955f5087bd6552 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Dec 2024 09:59:23 +0100 Subject: [PATCH 102/110] [ts] Add confirmation_number to IOrder interface Fixes: https://github.com/MONEI/Shopify-api-node/issues/664 --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index f1f54dff..24976d21 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2393,6 +2393,7 @@ declare namespace Shopify { client_details: IOrderClientDetails; checkout_token: string | null; closed_at: string | null; + confirmation_number: string; confirmed: boolean; contact_email: string | null; created_at: string; From 48e23de7b3d2428c6640410e68a242ca099f12ae Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 8 Dec 2024 10:06:09 +0100 Subject: [PATCH 103/110] [dist] 3.14.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83d6f3c2..49e8c4f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.14.0", + "version": "3.14.1", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 6cf1b538e27a46f98c7993ca8a7355e9981c6995 Mon Sep 17 00:00:00 2001 From: Kyle Tate Date: Wed, 12 Feb 2025 15:32:39 -0500 Subject: [PATCH 104/110] [fix] Always set the Content-Type header to application/json Fixes: https://github.com/MONEI/Shopify-api-node/issues/669 --- index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index a4337eef..14616cfd 100644 --- a/index.js +++ b/index.js @@ -273,13 +273,12 @@ Shopify.prototype.graphql = function graphql(data, variables) { pathname += '/graphql.json'; const uri = { pathname, ...this.baseUrl }; - const json = variables !== undefined && variables !== null; const options = { agent: this.options.agent, - body: json ? this.options.stringifyJson({ query: data, variables }) : data, + body: this.options.stringifyJson({ query: data, variables }), headers: { ...this.baseHeaders, - 'Content-Type': json ? 'application/json' : 'application/graphql' + 'Content-Type': 'application/json' }, method: 'POST', parseJson: this.options.parseJson, From 857bfe14cbd0a1da9bf1998ddbd06a1694e27c63 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Wed, 12 Feb 2025 21:33:18 +0100 Subject: [PATCH 105/110] [dist] 3.14.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49e8c4f9..31e55030 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.14.1", + "version": "3.14.2", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts", From 509912e54316740c97aef4c415e1587f275d0301 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 5 Apr 2025 11:32:46 +0200 Subject: [PATCH 106/110] [pkg] Update eslint-config-prettier to version 10.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31e55030..8e983d8f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "c8": "^7.3.0", "chai": "^4.1.2", "eslint": "^9.2.0", - "eslint-config-prettier": "^9.0.0", + "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.0.0", "globals": "^15.2.0", "husky": "^8.0.1", From e3c0e0ba4796090738af5aab48eec1212921f90f Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 5 Apr 2025 11:33:48 +0200 Subject: [PATCH 107/110] [pkg] Update globals to version 16.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e983d8f..a1368500 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "eslint": "^9.2.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.0.0", - "globals": "^15.2.0", + "globals": "^16.0.0", "husky": "^8.0.1", "json-bigint": "^1.0.0", "lint-staged": "^15.2.0", From 066f84ac871c725e3f5eadd4138eb96776b7568d Mon Sep 17 00:00:00 2001 From: Kyle Tate Date: Sun, 6 Apr 2025 08:53:59 -0400 Subject: [PATCH 108/110] [minor] Add error-throwing hook last (#673) By running user-provided hooks first, users can nullify `response.body.errors` and avoid the subsequent error. --- 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 }); From 65f667f1076a9eac7be9a7a204dfdbfa62a48cec Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 6 Apr 2025 15:25:23 +0200 Subject: [PATCH 109/110] [test] Simplify test --- test/shopify.test.js | 131 ++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/test/shopify.test.js b/test/shopify.test.js index a3cb2a47..5a0a9bed 100644 --- a/test/shopify.test.js +++ b/test/shopify.test.js @@ -917,81 +917,6 @@ 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 }); @@ -1313,6 +1238,62 @@ describe('Shopify', () => { }); }); + it('runs the afterResponse error hook after the user hooks', () => { + function afterResponse(response) { + afterResponse.errors = response.body.errors; + // + // Prevents errors from being thrown. + // + response.body.errors = undefined; + return response; + } + + const data = { + customers: { + edges: [ + { + node: { + id: 'gid://shopify/Customer/1234567890', + email: null + } + } + ] + } + }; + + const errors = [ + { + 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.' + } + } + ]; + + scope.post('/admin/api/graphql.json').reply(200, { data, errors }); + + const shopify = new Shopify({ + shopName, + accessToken, + hooks: { + afterResponse: [afterResponse] + } + }); + + return shopify.graphql('query').then((result) => { + expect(afterResponse.errors).to.deep.equal(errors); + expect(result).to.deep.equal(data); + }); + }); + it('calls the beforeRetry hook for retried requests', () => { function beforeRetry() { beforeRetry.called = true; From c6159df4c8b9f184a73006188d251741e697ef78 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Fri, 11 Apr 2025 13:21:22 +0200 Subject: [PATCH 110/110] [dist] 3.15.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1368500..bbd540e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-api-node", - "version": "3.14.2", + "version": "3.15.0", "description": "Shopify API bindings for Node.js", "main": "index.js", "types": "types/index.d.ts",