#
tokens: 38149/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .DS_Store
├── .gitignore
├── Dockerfile
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── __tests__
│   │   └── ShopifyClient.test.ts
│   ├── .DS_Store
│   ├── index.ts
│   └── ShopifyClient
│       ├── ShopifyClient.ts
│       └── ShopifyClientPort.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | .env
2 | node_modules
3 | build
4 | dist
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Shopify MCP Server
  2 | 
  3 | MCP Server for Shopify API, enabling interaction with store data through GraphQL API. This server provides tools for managing products, customers, orders, and more.
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/bemvhpy885"><img width="380" height="200" src="https://glama.ai/mcp/servers/bemvhpy885/badge" alt="Shopify Server MCP server" /></a>
  6 | 
  7 | ## Features
  8 | 
  9 | * **Product Management**: Search and retrieve product information
 10 | * **Customer Management**: Load customer data and manage customer tags
 11 | * **Order Management**: Advanced order querying and filtering
 12 | * **GraphQL Integration**: Direct integration with Shopify's GraphQL Admin API
 13 | * **Comprehensive Error Handling**: Clear error messages for API and authentication issues
 14 | 
 15 | ## Tools
 16 | 
 17 | 1. `get-products`
 18 |    * Get all products or search by title
 19 |    * Inputs:
 20 |      * `searchTitle` (optional string): Filter products by title
 21 |      * `limit` (number): Maximum number of products to return
 22 |    * Returns: Formatted product details including title, description, handle, and variants
 23 | 
 24 | 2. `get-products-by-collection`
 25 |    * Get products from a specific collection
 26 |    * Inputs:
 27 |      * `collectionId` (string): ID of the collection to get products from
 28 |      * `limit` (optional number, default: 10): Maximum number of products to return
 29 |    * Returns: Formatted product details from the specified collection
 30 | 
 31 | 3. `get-products-by-ids`
 32 |    * Get products by their IDs
 33 |    * Inputs:
 34 |      * `productIds` (array of strings): Array of product IDs to retrieve
 35 |    * Returns: Formatted product details for the specified products
 36 | 
 37 | 4. `update-product-price`
 38 |    * Update product prices for its ID
 39 |    * Inputs:
 40 |      * `productId` (string): ID of the product to update
 41 |      * `price` (string): New price for the product
 42 |    * Returns: Response of the update
 43 | 
 44 | 5. `get-variants-by-ids`
 45 |    * Get product variants by their IDs
 46 |    * Inputs:
 47 |      * `variantIds` (array of strings): Array of variant IDs to retrieve
 48 |    * Returns: Detailed variant information including product details
 49 | 
 50 | 6. `get-customers`
 51 |    * Get shopify customers with pagination support
 52 |    * Inputs:
 53 |      * `limit` (optional number): Maximum number of customers to return
 54 |      * `next` (optional string): Next page cursor
 55 |    * Returns: Customer data in JSON format
 56 | 
 57 | 7. `tag-customer`
 58 |    * Add tags to a customer
 59 |    * Inputs:
 60 |      * `customerId` (string): Customer ID to tag
 61 |      * `tags` (array of strings): Tags to add to the customer
 62 |    * Returns: Success or failure message
 63 | 
 64 | 8. `get-orders`
 65 |    * Get orders with advanced filtering and sorting
 66 |    * Inputs:
 67 |      * `first` (optional number): Limit of orders to return
 68 |      * `after` (optional string): Next page cursor
 69 |      * `query` (optional string): Filter orders using query syntax
 70 |      * `sortKey` (optional enum): Field to sort by ('PROCESSED_AT', 'TOTAL_PRICE', 'ID', 'CREATED_AT', 'UPDATED_AT', 'ORDER_NUMBER')
 71 |      * `reverse` (optional boolean): Reverse sort order
 72 |    * Returns: Formatted order details
 73 | 
 74 | 9. `get-order`
 75 |    * Get a single order by ID
 76 |    * Inputs:
 77 |      * `orderId` (string): ID of the order to retrieve
 78 |    * Returns: Detailed order information
 79 | 
 80 | 10. `create-discount`
 81 |    * Create a basic discount code
 82 |    * Inputs:
 83 |      * `title` (string): Title of the discount
 84 |      * `code` (string): Discount code that customers will enter
 85 |      * `valueType` (enum): Type of discount ('percentage' or 'fixed_amount')
 86 |      * `value` (number): Discount value (percentage as decimal or fixed amount)
 87 |      * `startsAt` (string): Start date in ISO format
 88 |      * `endsAt` (optional string): Optional end date in ISO format
 89 |      * `appliesOncePerCustomer` (boolean): Whether discount can be used only once per customer
 90 |    * Returns: Created discount details
 91 | 
 92 | 11. `create-draft-order`
 93 |     * Create a draft order
 94 |     * Inputs:
 95 |       * `lineItems` (array): Array of items with variantId and quantity
 96 |       * `email` (string): Customer email
 97 |       * `shippingAddress` (object): Shipping address details
 98 |       * `note` (optional string): Optional note for the order
 99 |     * Returns: Created draft order details
100 | 
101 | 12. `complete-draft-order`
102 |     * Complete a draft order
103 |     * Inputs:
104 |       * `draftOrderId` (string): ID of the draft order to complete
105 |       * `variantId` (string): ID of the variant in the draft order
106 |     * Returns: Completed order details
107 | 
108 | 13. `get-collections`
109 |     * Get all collections
110 |     * Inputs:
111 |       * `limit` (optional number, default: 10): Maximum number of collections to return
112 |       * `name` (optional string): Filter collections by name
113 |     * Returns: Collection details
114 | 
115 | 14. `get-shop`
116 |     * Get shop details
117 |     * Inputs: None
118 |     * Returns: Basic shop information
119 | 
120 | 15. `get-shop-details`
121 |     * Get extended shop details including shipping countries
122 |     * Inputs: None
123 |     * Returns: Extended shop information including shipping countries
124 | 
125 | 16. `manage-webhook`
126 |     * Subscribe, find, or unsubscribe webhooks
127 |     * Inputs:
128 |       * `action` (enum): Action to perform ('subscribe', 'find', 'unsubscribe')
129 |       * `callbackUrl` (string): Webhook callback URL
130 |       * `topic` (enum): Webhook topic to subscribe to
131 |       * `webhookId` (optional string): Webhook ID (required for unsubscribe)
132 |     * Returns: Webhook details or success message
133 | 
134 | ## Setup
135 | 
136 | ### Shopify Access Token
137 | 
138 | To use this MCP server, you'll need to create a custom app in your Shopify store:
139 | 
140 | 1. From your Shopify admin, go to **Settings** > **Apps and sales channels**
141 | 2. Click **Develop apps** (you may need to enable developer preview first)
142 | 3. Click **Create an app**
143 | 4. Set a name for your app (e.g., "Shopify MCP Server")
144 | 5. Click **Configure Admin API scopes**
145 | 6. Select the following scopes:
146 |    * `read_products`, `write_products`
147 |    * `read_customers`, `write_customers`
148 |    * `read_orders`, `write_orders`
149 | 7. Click **Save**
150 | 8. Click **Install app**
151 | 9. Click **Install** to give the app access to your store data
152 | 10. After installation, you'll see your **Admin API access token**
153 | 11. Copy this token - you'll need it for configuration
154 | 
155 | Note: Store your access token securely. It provides access to your store data and should never be shared or committed to version control.
156 | More details on how to create a Shopify app can be found [here](https://help.shopify.com/en/manual/apps/app-types/custom-apps).
157 | 
158 | ### Usage with Claude Desktop
159 | 
160 | Add to your `claude_desktop_config.json`:
161 | 
162 | ```json
163 | {
164 |   "mcpServers": {
165 |     "shopify": {
166 |       "command": "npx",
167 |       "args": ["-y", "shopify-mcp-server"],
168 |       "env": {
169 |         "SHOPIFY_ACCESS_TOKEN": "<YOUR_ACCESS_TOKEN>",
170 |         "MYSHOPIFY_DOMAIN": "<YOUR_SHOP>.myshopify.com"
171 |       }
172 |     }
173 |   }
174 | }
175 | ```
176 | 
177 | ## Development
178 | 
179 | 1. Clone the repository
180 | 2. Install dependencies:
181 | ```bash
182 | npm install
183 | ```
184 | 3. Create a `.env` file:
185 | ```
186 | SHOPIFY_ACCESS_TOKEN=your_access_token
187 | MYSHOPIFY_DOMAIN=your-store.myshopify.com
188 | ```
189 | 4. Build the project:
190 | ```bash
191 | npm run build
192 | ```
193 | 5. Run tests:
194 | ```bash
195 | npm test
196 | ```
197 | 
198 | ## Dependencies
199 | 
200 | - @modelcontextprotocol/sdk - MCP protocol implementation
201 | - graphql-request - GraphQL client for Shopify API
202 | - zod - Runtime type validation
203 | 
204 | ## Contributing
205 | 
206 | Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) first.
207 | 
208 | ## License
209 | 
210 | MIT
211 | 
212 | ## Community
213 | 
214 | - [MCP GitHub Discussions](https://github.com/modelcontextprotocol/servers/discussions)
215 | - [Report Issues](https://github.com/your-username/shopify-mcp-server/issues)
216 | 
217 | ---
218 | 
219 | Built with ❤️ using the [Model Context Protocol](https://modelcontextprotocol.io) 
220 | 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default {
 2 |   preset: 'ts-jest',
 3 |   testEnvironment: 'node',
 4 |   extensionsToTreatAsEsm: ['.ts'],
 5 |   moduleNameMapper: {
 6 |     '^(\\.{1,2}/.*)\\.js$': '$1',
 7 |   },
 8 |   transform: {
 9 |     '^.+\\.tsx?$': ['ts-jest', {
10 |       useESM: true,
11 |     }],
12 |   },
13 | }; 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22.12-alpine AS builder
 2 | 
 3 | # Must be entire project because `prepare` script is run during `npm install` and requires all files.
 4 | COPY src /app
 5 | COPY tsconfig.json /tsconfig.json
 6 | COPY package.json /package.json
 7 | COPY package-lock.json /package-lock.json
 8 | 
 9 | WORKDIR /app
10 | 
11 | ENV NODE_ENV=production
12 | 
13 | RUN npm ci --ignore-scripts --omit-dev
14 | 
15 | ENTRYPOINT ["node", "dist/index.js"]
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "module": "Nodenext",
 5 |       "moduleResolution": "Nodenext",
 6 |       "outDir": "./dist",
 7 |       "rootDir": "./src",
 8 |       "strict": true,
 9 |       "esModuleInterop": true,
10 |       "skipLibCheck": true,
11 |       "forceConsistentCasingInFileNames": true
12 |     },
13 |     "include": ["src/**/*"],
14 |     "exclude": ["node_modules",  "src/__tests__"]
15 |   }
16 |   
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/deployments
 2 | 
 3 | build:
 4 |   dockerBuildPath: ../../
 5 | 
 6 | startCommand:
 7 |   configSchema:
 8 |     # JSON Schema defining the configuration options for the MCP.
 9 |     type: object
10 |     required:
11 |       - shopifyAccessToken
12 |       - shopifyDomain
13 |     properties:
14 |       shopifyAccessToken:
15 |         type: string
16 |         description: The personal access token for accessing the Shopify API.
17 |       shopifyDomain:
18 |         type: string
19 |         description: The domain of the Shopify store.
20 |   commandFunction:
21 |     # A function that produces the CLI command to start the MCP on stdio.
22 |     |-
23 |     (config) => ({ command: 'node', args: ['dist/index.js'], env: { SHOPIFY_ACCESS_TOKEN: config.shopifyAccessToken, MYSHOPIFY_DOMAIN: config.shopifyDomain } })
24 |   type: stdio
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "shopify-mcp-server",
 3 |   "version": "1.0.1",
 4 |   "main": "index.js",
 5 |   "scripts": {
 6 |     "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
 7 |     "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\""
 8 |   },
 9 |   "keywords": [],
10 |   "author": "Amir Bengherbi",
11 |   "license": "MIT",
12 |   "description": "MCP Server for Shopify API, enabling interaction with store data through GraphQL API.",
13 |   "dependencies": {
14 |     "@modelcontextprotocol/sdk": "^1.4.1",
15 |     "graphql-request": "^7.1.2",
16 |     "zod": "^3.24.1"
17 |   },
18 |   "devDependencies": {
19 |     "@types/jest": "^29.5.14",
20 |     "@types/node": "^22.10.10",
21 |     "dotenv": "^16.4.7",
22 |     "jest": "^29.7.0",
23 |     "ts-jest": "^29.2.5",
24 |     "typescript": "^5.7.3"
25 |   },
26 |   "type": "module",
27 |   "files": [
28 |     "dist"
29 |   ],
30 |   "bin": {
31 |     "shopify-mcp-server": "./dist/index.js"
32 |   }
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/__tests__/ShopifyClient.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { config } from "dotenv";
  2 | import { ShopifyClient } from "../ShopifyClient/ShopifyClient.js";
  3 | import {
  4 |   CreateBasicDiscountCodeInput,
  5 |   CreateDraftOrderPayload,
  6 |   ShopifyWebhookTopic,
  7 | } from "../ShopifyClient/ShopifyClientPort.js";
  8 | 
  9 | // Load environment variables from .env file
 10 | config();
 11 | 
 12 | const SHOPIFY_ACCESS_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
 13 | const MYSHOPIFY_DOMAIN = process.env.MYSHOPIFY_DOMAIN;
 14 | 
 15 | if (!SHOPIFY_ACCESS_TOKEN || !MYSHOPIFY_DOMAIN) {
 16 |   throw new Error(
 17 |     "SHOPIFY_ACCESS_TOKEN and MYSHOPIFY_DOMAIN must be set in .env file"
 18 |   );
 19 | }
 20 | 
 21 | describe("ShopifyClient", () => {
 22 |   let client: ShopifyClient;
 23 | 
 24 |   beforeEach(() => {
 25 |     client = new ShopifyClient();
 26 |   });
 27 | 
 28 |   describe("Products", () => {
 29 |     it("should load products", async () => {
 30 |       const products = await client.loadProducts(
 31 |         SHOPIFY_ACCESS_TOKEN,
 32 |         MYSHOPIFY_DOMAIN,
 33 |         "*",
 34 |         100
 35 |       );
 36 |       expect(products).toBeDefined();
 37 |       expect(products.products).toBeDefined();
 38 |       expect(products.currencyCode).toBeDefined();
 39 |     });
 40 | 
 41 |     it("should load products by collection id", async () => {
 42 |       // load collections to get a valid collection id
 43 |       const collections = await client.loadCollections(
 44 |         SHOPIFY_ACCESS_TOKEN,
 45 |         MYSHOPIFY_DOMAIN,
 46 |         { limit: 1 }
 47 |       );
 48 |       const collectionId = collections.collections[0]?.id.toString();
 49 |       expect(collectionId).toBeDefined();
 50 | 
 51 |       const products = await client.loadProductsByCollectionId(
 52 |         SHOPIFY_ACCESS_TOKEN,
 53 |         MYSHOPIFY_DOMAIN,
 54 |         collectionId,
 55 |         10
 56 |       );
 57 |       expect(products).toBeDefined();
 58 |       expect(products.products).toBeDefined();
 59 |       expect(products.currencyCode).toBeDefined();
 60 |     });
 61 | 
 62 |     it("should load products by ids", async () => {
 63 |       // load products to get a valid product id
 64 |       const allProducts = await client.loadProducts(
 65 |         SHOPIFY_ACCESS_TOKEN,
 66 |         MYSHOPIFY_DOMAIN,
 67 |         "*",
 68 |         100
 69 |       );
 70 |       const productIds = allProducts.products.map((product) =>
 71 |         product.id.toString()
 72 |       );
 73 |       const products = await client.loadProductsByIds(
 74 |         SHOPIFY_ACCESS_TOKEN,
 75 |         MYSHOPIFY_DOMAIN,
 76 |         productIds
 77 |       );
 78 |       expect(products).toBeDefined();
 79 |       expect(products.products).toBeDefined();
 80 |       expect(products.currencyCode).toBeDefined();
 81 |     });
 82 | 
 83 |     it("should load variants by ids", async () => {
 84 |       // load products to get a valid product id
 85 |       const allProducts = await client.loadProducts(
 86 |         SHOPIFY_ACCESS_TOKEN,
 87 |         MYSHOPIFY_DOMAIN,
 88 |         "*",
 89 |         100
 90 |       );
 91 | 
 92 |       const variantIds = allProducts.products.flatMap((product) =>
 93 |         product.variants.edges.map((variant) => variant.node.id.toString())
 94 |       );
 95 | 
 96 |       const variants = await client.loadVariantsByIds(
 97 |         SHOPIFY_ACCESS_TOKEN,
 98 |         MYSHOPIFY_DOMAIN,
 99 |         variantIds
100 |       );
101 |       expect(variants).toBeDefined();
102 |       expect(variants.variants).toBeDefined();
103 |       expect(variants.currencyCode).toBeDefined();
104 |     });
105 |   });
106 | 
107 |   describe("Customers", () => {
108 |     it("should load customers", async () => {
109 |       const customers = await client.loadCustomers(
110 |         SHOPIFY_ACCESS_TOKEN,
111 |         MYSHOPIFY_DOMAIN,
112 |         100
113 |       );
114 |       expect(customers).toBeDefined();
115 |       expect(customers.customers).toBeDefined();
116 |     });
117 | 
118 |     it("should tag customer", async () => {
119 |       // load customers to get a valid customer id
120 |       const customers = await client.loadCustomers(
121 |         SHOPIFY_ACCESS_TOKEN,
122 |         MYSHOPIFY_DOMAIN,
123 |         100
124 |       );
125 |       const customerId = customers.customers[0]?.id?.toString();
126 |       expect(customerId).toBeDefined();
127 |       if (!customerId) {
128 |         throw new Error("No customer id found");
129 |       }
130 | 
131 |       const tagged = await client.tagCustomer(
132 |         SHOPIFY_ACCESS_TOKEN,
133 |         MYSHOPIFY_DOMAIN,
134 |         ["test"],
135 |         customerId
136 |       );
137 |       expect(tagged).toBe(true);
138 |     });
139 |   });
140 | 
141 |   describe("Orders", () => {
142 |     it("should load orders", async () => {
143 |       const orders = await client.loadOrders(
144 |         SHOPIFY_ACCESS_TOKEN,
145 |         MYSHOPIFY_DOMAIN,
146 |         {
147 |           first: 100,
148 |         }
149 |       );
150 |       expect(orders).toBeDefined();
151 |       expect(orders.orders).toBeDefined();
152 |       expect(orders.pageInfo).toBeDefined();
153 |     });
154 | 
155 |     it("should load single order", async () => {
156 |       // load orders to get a valid order id
157 |       const orders = await client.loadOrders(
158 |         SHOPIFY_ACCESS_TOKEN,
159 |         MYSHOPIFY_DOMAIN,
160 |         {
161 |           first: 100,
162 |         }
163 |       );
164 |       const orderId = orders.orders[0]?.id?.toString();
165 |       expect(orderId).toBeDefined();
166 |       if (!orderId) {
167 |         throw new Error("No order id found");
168 |       }
169 | 
170 |       const order = await client.loadOrder(
171 |         SHOPIFY_ACCESS_TOKEN,
172 |         MYSHOPIFY_DOMAIN,
173 |         { orderId: client.getIdFromGid(orderId) }
174 |       );
175 |       expect(order).toBeDefined();
176 |       expect(order.id).toBeDefined();
177 |     });
178 |   });
179 | 
180 |   describe("Discounts", () => {
181 |     it("should create and delete basic discount code", async () => {
182 |       const discountInput: CreateBasicDiscountCodeInput = {
183 |         title: "Test Discount",
184 |         code: "TEST123",
185 |         startsAt: new Date().toISOString(),
186 |         valueType: "percentage",
187 |         value: 0.1,
188 |         includeCollectionIds: [],
189 |         excludeCollectionIds: [],
190 |         appliesOncePerCustomer: true,
191 |         combinesWith: {
192 |           productDiscounts: true,
193 |           orderDiscounts: true,
194 |           shippingDiscounts: true,
195 |         },
196 |       };
197 | 
198 |       const discount = await client.createBasicDiscountCode(
199 |         SHOPIFY_ACCESS_TOKEN,
200 |         MYSHOPIFY_DOMAIN,
201 |         discountInput
202 |       );
203 |       expect(discount).toBeDefined();
204 |       expect(discount.id).toBeDefined();
205 |       expect(discount.code).toBe(discountInput.code);
206 | 
207 |       await client.deleteBasicDiscountCode(
208 |         SHOPIFY_ACCESS_TOKEN,
209 |         MYSHOPIFY_DOMAIN,
210 |         discount.id
211 |       );
212 |     });
213 |   });
214 | 
215 |   describe("Draft Orders", () => {
216 |     it("should create and complete draft order", async () => {
217 |       // load products to get a valid variant id
218 |       const allProducts = await client.loadProducts(
219 |         SHOPIFY_ACCESS_TOKEN,
220 |         MYSHOPIFY_DOMAIN,
221 |         null,
222 |         100
223 |       );
224 |       const variantIds = allProducts.products.flatMap((product) =>
225 |         product.variants.edges.map((variant) => variant.node.id.toString())
226 |       );
227 |       const variantId = variantIds[0];
228 |       expect(variantId).toBeDefined();
229 |       if (!variantId) {
230 |         throw new Error("No variant id found");
231 |       }
232 |       const draftOrderData: CreateDraftOrderPayload = {
233 |         lineItems: [
234 |           {
235 |             variantId,
236 |             quantity: 1,
237 |           },
238 |         ],
239 |         email: "[email protected]",
240 |         shippingAddress: {
241 |           address1: "123 Test St",
242 |           city: "Test City",
243 |           province: "Test Province",
244 |           country: "Test Country",
245 |           zip: "12345",
246 |           firstName: "Test",
247 |           lastName: "User",
248 |           countryCode: "US",
249 |         },
250 |         billingAddress: {
251 |           address1: "123 Test St",
252 |           city: "Test City",
253 |           province: "Test Province",
254 |           country: "Test Country",
255 |           zip: "12345",
256 |           firstName: "Test",
257 |           lastName: "User",
258 |           countryCode: "US",
259 |         },
260 |         tags: "test",
261 |         note: "Test draft order",
262 |       };
263 | 
264 |       const draftOrder = await client.createDraftOrder(
265 |         SHOPIFY_ACCESS_TOKEN,
266 |         MYSHOPIFY_DOMAIN,
267 |         draftOrderData
268 |       );
269 |       expect(draftOrder).toBeDefined();
270 |       expect(draftOrder.draftOrderId).toBeDefined();
271 | 
272 |       const completedOrder = await client.completeDraftOrder(
273 |         SHOPIFY_ACCESS_TOKEN,
274 |         MYSHOPIFY_DOMAIN,
275 |         draftOrder.draftOrderId,
276 |         draftOrderData.lineItems[0].variantId
277 |       );
278 |       expect(completedOrder).toBeDefined();
279 |       expect(completedOrder.orderId).toBeDefined();
280 |     });
281 |   });
282 | 
283 |   describe("Collections", () => {
284 |     it("should load collections", async () => {
285 |       const collections = await client.loadCollections(
286 |         SHOPIFY_ACCESS_TOKEN,
287 |         MYSHOPIFY_DOMAIN,
288 |         { limit: 10 }
289 |       );
290 |       expect(collections).toBeDefined();
291 |       expect(collections.collections).toBeDefined();
292 |     });
293 |   });
294 | 
295 |   describe("Shop", () => {
296 |     it("should load shop", async () => {
297 |       const shop = await client.loadShop(
298 |         SHOPIFY_ACCESS_TOKEN,
299 |         MYSHOPIFY_DOMAIN
300 |       );
301 |       expect(shop).toBeDefined();
302 |       expect(shop.shop).toBeDefined();
303 |     });
304 | 
305 |     it("should load shop details", async () => {
306 |       const shopDetails = await client.loadShopDetail(
307 |         SHOPIFY_ACCESS_TOKEN,
308 |         MYSHOPIFY_DOMAIN
309 |       );
310 |       expect(shopDetails).toBeDefined();
311 |       expect(shopDetails.data).toBeDefined();
312 |     });
313 |   });
314 | 
315 |   describe("Webhooks", () => {
316 |     it("should manage webhooks", async () => {
317 |       const callbackUrl = "https://example.com/webhook";
318 |       const topic = ShopifyWebhookTopic.ORDERS_UPDATED;
319 | 
320 |       const webhook = await client.subscribeWebhook(
321 |         SHOPIFY_ACCESS_TOKEN,
322 |         MYSHOPIFY_DOMAIN,
323 |         callbackUrl,
324 |         topic
325 |       );
326 |       expect(webhook).toBeDefined();
327 |       expect(webhook.id).toBeDefined();
328 | 
329 |       const foundWebhook = await client.findWebhookByTopicAndCallbackUrl(
330 |         SHOPIFY_ACCESS_TOKEN,
331 |         MYSHOPIFY_DOMAIN,
332 |         callbackUrl,
333 |         topic
334 |       );
335 |       expect(foundWebhook).toBeDefined();
336 |       expect(foundWebhook?.id).toBe(webhook.id);
337 | 
338 |       if (!foundWebhook?.id) {
339 |         throw new Error("No webhook id found");
340 |       }
341 |       const webhookId = foundWebhook.id;
342 |       await client.unsubscribeWebhook(
343 |         SHOPIFY_ACCESS_TOKEN,
344 |         MYSHOPIFY_DOMAIN,
345 |         webhookId
346 |       );
347 | 
348 |       const deletedWebhook = await client.findWebhookByTopicAndCallbackUrl(
349 |         SHOPIFY_ACCESS_TOKEN,
350 |         MYSHOPIFY_DOMAIN,
351 |         callbackUrl,
352 |         topic
353 |       );
354 |       expect(deletedWebhook).toBeNull();
355 |     });
356 |   });
357 | 
358 |   describe("Utility Methods", () => {
359 |     it("should get ID from GID", () => {
360 |       const gid = "gid://shopify/Product/123456789";
361 |       const id = client.getIdFromGid(gid);
362 |       expect(id).toBe("123456789");
363 |     });
364 |   });
365 | });
366 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { z } from "zod";
  6 | import { ShopifyClient } from "./ShopifyClient/ShopifyClient.js";
  7 | import {
  8 |   CustomError,
  9 |   ProductNode,
 10 |   ShopifyOrderGraphql,
 11 |   CreateBasicDiscountCodeInput,
 12 |   CreateDraftOrderPayload,
 13 |   ShopifyWebhookTopic,
 14 | } from "./ShopifyClient/ShopifyClientPort.js";
 15 | 
 16 | const server = new McpServer({
 17 |   name: "shopify-tools",
 18 |   version: "1.0.1",
 19 | });
 20 | 
 21 | const SHOPIFY_ACCESS_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
 22 | if (!SHOPIFY_ACCESS_TOKEN) {
 23 |   console.error("Error: SHOPIFY_ACCESS_TOKEN environment variable is required");
 24 |   process.exit(1);
 25 | }
 26 | 
 27 | const MYSHOPIFY_DOMAIN = process.env.MYSHOPIFY_DOMAIN;
 28 | if (!MYSHOPIFY_DOMAIN) {
 29 |   console.error("Error: MYSHOPIFY_DOMAIN environment variable is required");
 30 |   process.exit(1);
 31 | }
 32 | 
 33 | function formatProduct(product: ProductNode): string {
 34 |   return `
 35 |   Product: ${product.title} 
 36 |   id: ${product.id}
 37 |   description: ${product.description} 
 38 |   handle: ${product.handle}
 39 |   variants: ${product.variants.edges
 40 |     .map(
 41 |       (variant) => `variant.title: ${variant.node.title}
 42 |     variant.id: ${variant.node.id}
 43 |     variant.price: ${variant.node.price}
 44 |     variant.sku: ${variant.node.sku}
 45 |     variant.inventoryPolicy: ${variant.node.inventoryPolicy}
 46 |     `
 47 |     )
 48 |     .join(", ")}
 49 |   `;
 50 | }
 51 | 
 52 | function formatOrder(order: ShopifyOrderGraphql): string {
 53 |   return `
 54 |   Order: ${order.name} (${order.id})
 55 |   Created At: ${order.createdAt}
 56 |   Status: ${order.displayFinancialStatus || "N/A"}
 57 |   Email: ${order.email || "N/A"}
 58 |   Phone: ${order.phone || "N/A"}
 59 |   
 60 |   Total Price: ${order.totalPriceSet.shopMoney.amount} ${
 61 |     order.totalPriceSet.shopMoney.currencyCode
 62 |   }
 63 |   
 64 |   Customer: ${
 65 |     order.customer
 66 |       ? `
 67 |     ID: ${order.customer.id}
 68 |     Email: ${order.customer.email}`
 69 |       : "No customer information"
 70 |   }
 71 | 
 72 |   Shipping Address: ${
 73 |     order.shippingAddress
 74 |       ? `
 75 |     Province: ${order.shippingAddress.provinceCode || "N/A"}
 76 |     Country: ${order.shippingAddress.countryCode}`
 77 |       : "No shipping address"
 78 |   }
 79 | 
 80 |   Line Items: ${
 81 |     order.lineItems.nodes.length > 0
 82 |       ? order.lineItems.nodes
 83 |           .map(
 84 |             (item) => `
 85 |     Title: ${item.title}
 86 |     Quantity: ${item.quantity}
 87 |     Price: ${item.originalTotalSet.shopMoney.amount} ${
 88 |               item.originalTotalSet.shopMoney.currencyCode
 89 |             }
 90 |     Variant: ${
 91 |       item.variant
 92 |         ? `
 93 |       Title: ${item.variant.title}
 94 |       SKU: ${item.variant.sku || "N/A"}
 95 |       Price: ${item.variant.price}`
 96 |         : "No variant information"
 97 |     }`
 98 |           )
 99 |           .join("\n")
100 |       : "No items"
101 |   }
102 |   `;
103 | }
104 | 
105 | // Products Tools
106 | server.tool(
107 |   "get-products",
108 |   "Get all products or search by title",
109 |   {
110 |     searchTitle: z
111 |       .string()
112 |       .optional()
113 |       .describe("Search title, if missing, will return all products"),
114 |     limit: z.number().describe("Maximum number of products to return"),
115 |   },
116 |   async ({ searchTitle, limit }) => {
117 |     const client = new ShopifyClient();
118 |     try {
119 |       const products = await client.loadProducts(
120 |         SHOPIFY_ACCESS_TOKEN,
121 |         MYSHOPIFY_DOMAIN,
122 |         searchTitle ?? null,
123 |         limit
124 |       );
125 |       const formattedProducts = products.products.map(formatProduct);
126 |       return {
127 |         content: [{ type: "text", text: formattedProducts.join("\n") }],
128 |       };
129 |     } catch (error) {
130 |       return handleError("Failed to retrieve products data", error);
131 |     }
132 |   }
133 | );
134 | 
135 | server.tool(
136 |   "get-products-by-collection",
137 |   "Get products from a specific collection",
138 |   {
139 |     collectionId: z
140 |       .string()
141 |       .describe("ID of the collection to get products from"),
142 |     limit: z
143 |       .number()
144 |       .optional()
145 |       .default(10)
146 |       .describe("Maximum number of products to return"),
147 |   },
148 |   async ({ collectionId, limit }) => {
149 |     const client = new ShopifyClient();
150 |     try {
151 |       const products = await client.loadProductsByCollectionId(
152 |         SHOPIFY_ACCESS_TOKEN,
153 |         MYSHOPIFY_DOMAIN,
154 |         collectionId,
155 |         limit
156 |       );
157 |       const formattedProducts = products.products.map(formatProduct);
158 |       return {
159 |         content: [{ type: "text", text: formattedProducts.join("\n") }],
160 |       };
161 |     } catch (error) {
162 |       return handleError("Failed to retrieve products from collection", error);
163 |     }
164 |   }
165 | );
166 | 
167 | server.tool(
168 |   "get-products-by-ids",
169 |   "Get products by their IDs",
170 |   {
171 |     productIds: z
172 |       .array(z.string())
173 |       .describe("Array of product IDs to retrieve"),
174 |   },
175 |   async ({ productIds }) => {
176 |     const client = new ShopifyClient();
177 |     try {
178 |       const products = await client.loadProductsByIds(
179 |         SHOPIFY_ACCESS_TOKEN,
180 |         MYSHOPIFY_DOMAIN,
181 |         productIds
182 |       );
183 |       const formattedProducts = products.products.map(formatProduct);
184 |       return {
185 |         content: [{ type: "text", text: formattedProducts.join("\n") }],
186 |       };
187 |     } catch (error) {
188 |       return handleError("Failed to retrieve products by IDs", error);
189 |     }
190 |   }
191 | );
192 | 
193 | server.tool(
194 |   "update-product-price",
195 |   "Update the price of a product by its ID for all variants",
196 |   {
197 |     productId: z.string()
198 |       .describe("ID of the product to update"),
199 |     price: z.string()
200 |     .describe("Price of the product to update to"),
201 |   },
202 |   async ({ productId, price }) => {
203 |     const client = new ShopifyClient();
204 |     try {
205 |       const response = await client.updateProductPrice(
206 |         SHOPIFY_ACCESS_TOKEN,
207 |         MYSHOPIFY_DOMAIN,
208 |         productId,
209 |         price
210 |       );
211 |       return {
212 |         content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
213 |       };
214 |     } catch (error) {
215 |       return handleError("Failed to update product price", error);
216 |     }
217 |   }
218 | );
219 | 
220 | server.tool(
221 |   "get-variants-by-ids",
222 |   "Get product variants by their IDs",
223 |   {
224 |     variantIds: z
225 |       .array(z.string())
226 |       .describe("Array of variant IDs to retrieve"),
227 |   },
228 |   async ({ variantIds }) => {
229 |     const client = new ShopifyClient();
230 |     try {
231 |       const variants = await client.loadVariantsByIds(
232 |         SHOPIFY_ACCESS_TOKEN,
233 |         MYSHOPIFY_DOMAIN,
234 |         variantIds
235 |       );
236 |       return {
237 |         content: [{ type: "text", text: JSON.stringify(variants, null, 2) }],
238 |       };
239 |     } catch (error) {
240 |       return handleError("Failed to retrieve variants", error);
241 |     }
242 |   }
243 | );
244 | 
245 | // Customer Tools
246 | server.tool(
247 |   "get-customers",
248 |   "Get shopify customers with pagination support",
249 |   {
250 |     limit: z.number().optional().describe("Limit of customers to return"),
251 |     next: z.string().optional().describe("Next page cursor"),
252 |   },
253 |   async ({ limit, next }) => {
254 |     const client = new ShopifyClient();
255 |     try {
256 |       const response = await client.loadCustomers(
257 |         SHOPIFY_ACCESS_TOKEN,
258 |         MYSHOPIFY_DOMAIN,
259 |         limit,
260 |         next
261 |       );
262 |       return {
263 |         content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
264 |       };
265 |     } catch (error) {
266 |       return handleError("Failed to retrieve customers data", error);
267 |     }
268 |   }
269 | );
270 | 
271 | server.tool(
272 |   "tag-customer",
273 |   "Add tags to a customer",
274 |   {
275 |     customerId: z.string().describe("Customer ID to tag"),
276 |     tags: z.array(z.string()).describe("Tags to add to the customer"),
277 |   },
278 |   async ({ customerId, tags }) => {
279 |     const client = new ShopifyClient();
280 |     try {
281 |       const success = await client.tagCustomer(
282 |         SHOPIFY_ACCESS_TOKEN,
283 |         MYSHOPIFY_DOMAIN,
284 |         tags,
285 |         customerId
286 |       );
287 |       return {
288 |         content: [
289 |           {
290 |             type: "text",
291 |             text: success
292 |               ? "Successfully tagged customer"
293 |               : "Failed to tag customer",
294 |           },
295 |         ],
296 |       };
297 |     } catch (error) {
298 |       return handleError("Failed to tag customer", error);
299 |     }
300 |   }
301 | );
302 | 
303 | // Order Tools
304 | server.tool(
305 |   "get-orders",
306 |   "Get shopify orders with advanced filtering and sorting",
307 |   {
308 |     first: z.number().optional().describe("Limit of orders to return"),
309 |     after: z.string().optional().describe("Next page cursor"),
310 |     query: z.string().optional().describe("Filter orders using query syntax"),
311 |     sortKey: z
312 |       .enum([
313 |         "PROCESSED_AT",
314 |         "TOTAL_PRICE",
315 |         "ID",
316 |         "CREATED_AT",
317 |         "UPDATED_AT",
318 |         "ORDER_NUMBER",
319 |       ])
320 |       .optional()
321 |       .describe("Field to sort by"),
322 |     reverse: z.boolean().optional().describe("Reverse sort order"),
323 |   },
324 |   async ({ first, after, query, sortKey, reverse }) => {
325 |     const client = new ShopifyClient();
326 |     try {
327 |       const response = await client.loadOrders(
328 |         SHOPIFY_ACCESS_TOKEN,
329 |         MYSHOPIFY_DOMAIN,
330 |         {
331 |           first,
332 |           after,
333 |           query,
334 |           sortKey,
335 |           reverse,
336 |         }
337 |       );
338 |       const formattedOrders = response.orders.map(formatOrder);
339 |       return {
340 |         content: [{ type: "text", text: formattedOrders.join("\n---\n") }],
341 |       };
342 |     } catch (error) {
343 |       return handleError("Failed to retrieve orders data", error);
344 |     }
345 |   }
346 | );
347 | 
348 | server.tool(
349 |   "get-order",
350 |   "Get a single order by ID",
351 |   {
352 |     orderId: z.string().describe("ID of the order to retrieve"),
353 |   },
354 |   async ({ orderId }) => {
355 |     const client = new ShopifyClient();
356 |     try {
357 |       const order = await client.loadOrder(
358 |         SHOPIFY_ACCESS_TOKEN,
359 |         MYSHOPIFY_DOMAIN,
360 |         { orderId }
361 |       );
362 |       return {
363 |         content: [{ type: "text", text: JSON.stringify(order, null, 2) }],
364 |       };
365 |     } catch (error) {
366 |       return handleError("Failed to retrieve order", error);
367 |     }
368 |   }
369 | );
370 | 
371 | // Discount Tools
372 | server.tool(
373 |   "create-discount",
374 |   "Create a basic discount code",
375 |   {
376 |     title: z.string().describe("Title of the discount"),
377 |     code: z.string().describe("Discount code that customers will enter"),
378 |     valueType: z
379 |       .enum(["percentage", "fixed_amount"])
380 |       .describe("Type of discount"),
381 |     value: z
382 |       .number()
383 |       .describe("Discount value (percentage as decimal or fixed amount)"),
384 |     startsAt: z.string().describe("Start date in ISO format"),
385 |     endsAt: z.string().optional().describe("Optional end date in ISO format"),
386 |     appliesOncePerCustomer: z
387 |       .boolean()
388 |       .describe("Whether discount can be used only once per customer"),
389 |   },
390 |   async ({
391 |     title,
392 |     code,
393 |     valueType,
394 |     value,
395 |     startsAt,
396 |     endsAt,
397 |     appliesOncePerCustomer,
398 |   }) => {
399 |     const client = new ShopifyClient();
400 |     try {
401 |       const discountInput: CreateBasicDiscountCodeInput = {
402 |         title,
403 |         code,
404 |         valueType,
405 |         value,
406 |         startsAt,
407 |         endsAt,
408 |         includeCollectionIds: [],
409 |         excludeCollectionIds: [],
410 |         appliesOncePerCustomer,
411 |         combinesWith: {
412 |           productDiscounts: true,
413 |           orderDiscounts: true,
414 |           shippingDiscounts: true,
415 |         },
416 |       };
417 |       const discount = await client.createBasicDiscountCode(
418 |         SHOPIFY_ACCESS_TOKEN,
419 |         MYSHOPIFY_DOMAIN,
420 |         discountInput
421 |       );
422 |       return {
423 |         content: [{ type: "text", text: JSON.stringify(discount, null, 2) }],
424 |       };
425 |     } catch (error) {
426 |       return handleError("Failed to create discount", error);
427 |     }
428 |   }
429 | );
430 | 
431 | // Draft Order Tools
432 | server.tool(
433 |   "create-draft-order",
434 |   "Create a draft order",
435 |   {
436 |     lineItems: z
437 |       .array(
438 |         z.object({
439 |           variantId: z.string(),
440 |           quantity: z.number(),
441 |         })
442 |       )
443 |       .describe("Line items to add to the order"),
444 |     email: z.string().email().describe("Customer email"),
445 |     shippingAddress: z
446 |       .object({
447 |         address1: z.string(),
448 |         city: z.string(),
449 |         province: z.string(),
450 |         country: z.string(),
451 |         zip: z.string(),
452 |         firstName: z.string(),
453 |         lastName: z.string(),
454 |         countryCode: z.string(),
455 |       })
456 |       .describe("Shipping address details"),
457 |     note: z.string().optional().describe("Optional note for the order"),
458 |   },
459 |   async ({ lineItems, email, shippingAddress, note }) => {
460 |     const client = new ShopifyClient();
461 |     try {
462 |       const draftOrderData: CreateDraftOrderPayload = {
463 |         lineItems,
464 |         email,
465 |         shippingAddress,
466 |         billingAddress: shippingAddress, // Using same address for billing
467 |         tags: "draft",
468 |         note: note || "",
469 |       };
470 |       const draftOrder = await client.createDraftOrder(
471 |         SHOPIFY_ACCESS_TOKEN,
472 |         MYSHOPIFY_DOMAIN,
473 |         draftOrderData
474 |       );
475 |       return {
476 |         content: [{ type: "text", text: JSON.stringify(draftOrder, null, 2) }],
477 |       };
478 |     } catch (error) {
479 |       return handleError("Failed to create draft order", error);
480 |     }
481 |   }
482 | );
483 | 
484 | server.tool(
485 |   "complete-draft-order",
486 |   "Complete a draft order",
487 |   {
488 |     draftOrderId: z.string().describe("ID of the draft order to complete"),
489 |     variantId: z.string().describe("ID of the variant in the draft order"),
490 |   },
491 |   async ({ draftOrderId, variantId }) => {
492 |     const client = new ShopifyClient();
493 |     try {
494 |       const completedOrder = await client.completeDraftOrder(
495 |         SHOPIFY_ACCESS_TOKEN,
496 |         MYSHOPIFY_DOMAIN,
497 |         draftOrderId,
498 |         variantId
499 |       );
500 |       return {
501 |         content: [
502 |           { type: "text", text: JSON.stringify(completedOrder, null, 2) },
503 |         ],
504 |       };
505 |     } catch (error) {
506 |       return handleError("Failed to complete draft order", error);
507 |     }
508 |   }
509 | );
510 | 
511 | // Collection Tools
512 | server.tool(
513 |   "get-collections",
514 |   "Get all collections",
515 |   {
516 |     limit: z
517 |       .number()
518 |       .optional()
519 |       .default(10)
520 |       .describe("Maximum number of collections to return"),
521 |     name: z.string().optional().describe("Filter collections by name"),
522 |   },
523 |   async ({ limit, name }) => {
524 |     const client = new ShopifyClient();
525 |     try {
526 |       const collections = await client.loadCollections(
527 |         SHOPIFY_ACCESS_TOKEN,
528 |         MYSHOPIFY_DOMAIN,
529 |         { limit, name }
530 |       );
531 |       return {
532 |         content: [{ type: "text", text: JSON.stringify(collections, null, 2) }],
533 |       };
534 |     } catch (error) {
535 |       return handleError("Failed to retrieve collections", error);
536 |     }
537 |   }
538 | );
539 | 
540 | // Shop Tools
541 | server.tool("get-shop", "Get shop details", {}, async () => {
542 |   const client = new ShopifyClient();
543 |   try {
544 |     const shop = await client.loadShop(SHOPIFY_ACCESS_TOKEN, MYSHOPIFY_DOMAIN);
545 |     return {
546 |       content: [{ type: "text", text: JSON.stringify(shop, null, 2) }],
547 |     };
548 |   } catch (error) {
549 |     return handleError("Failed to retrieve shop details", error);
550 |   }
551 | });
552 | 
553 | server.tool(
554 |   "get-shop-details",
555 |   "Get extended shop details including shipping countries",
556 |   {},
557 |   async () => {
558 |     const client = new ShopifyClient();
559 |     try {
560 |       const shopDetails = await client.loadShopDetail(
561 |         SHOPIFY_ACCESS_TOKEN,
562 |         MYSHOPIFY_DOMAIN
563 |       );
564 |       return {
565 |         content: [{ type: "text", text: JSON.stringify(shopDetails, null, 2) }],
566 |       };
567 |     } catch (error) {
568 |       return handleError("Failed to retrieve extended shop details", error);
569 |     }
570 |   }
571 | );
572 | 
573 | // Webhook Tools
574 | server.tool(
575 |   "manage-webhook",
576 |   "Subscribe, find, or unsubscribe webhooks",
577 |   {
578 |     action: z
579 |       .enum(["subscribe", "find", "unsubscribe"])
580 |       .describe("Action to perform with webhook"),
581 |     callbackUrl: z.string().url().describe("Webhook callback URL"),
582 |     topic: z
583 |       .nativeEnum(ShopifyWebhookTopic)
584 |       .describe("Webhook topic to subscribe to"),
585 |     webhookId: z
586 |       .string()
587 |       .optional()
588 |       .describe("Webhook ID (required for unsubscribe)"),
589 |   },
590 |   async ({ action, callbackUrl, topic, webhookId }) => {
591 |     const client = new ShopifyClient();
592 |     try {
593 |       switch (action) {
594 |         case "subscribe": {
595 |           const webhook = await client.subscribeWebhook(
596 |             SHOPIFY_ACCESS_TOKEN,
597 |             MYSHOPIFY_DOMAIN,
598 |             callbackUrl,
599 |             topic
600 |           );
601 |           return {
602 |             content: [{ type: "text", text: JSON.stringify(webhook, null, 2) }],
603 |           };
604 |         }
605 |         case "find": {
606 |           const webhook = await client.findWebhookByTopicAndCallbackUrl(
607 |             SHOPIFY_ACCESS_TOKEN,
608 |             MYSHOPIFY_DOMAIN,
609 |             callbackUrl,
610 |             topic
611 |           );
612 |           return {
613 |             content: [{ type: "text", text: JSON.stringify(webhook, null, 2) }],
614 |           };
615 |         }
616 |         case "unsubscribe": {
617 |           if (!webhookId) {
618 |             throw new Error("webhookId is required for unsubscribe action");
619 |           }
620 |           await client.unsubscribeWebhook(
621 |             SHOPIFY_ACCESS_TOKEN,
622 |             MYSHOPIFY_DOMAIN,
623 |             webhookId
624 |           );
625 |           return {
626 |             content: [
627 |               { type: "text", text: "Webhook unsubscribed successfully" },
628 |             ],
629 |           };
630 |         }
631 |       }
632 |     } catch (error) {
633 |       return handleError("Failed to manage webhook", error);
634 |     }
635 |   }
636 | );
637 | 
638 | // Utility function to handle errors
639 | function handleError(
640 |   defaultMessage: string,
641 |   error: unknown
642 | ): {
643 |   content: { type: "text"; text: string }[];
644 |   isError: boolean;
645 | } {
646 |   let errorMessage = defaultMessage;
647 |   if (error instanceof CustomError) {
648 |     errorMessage = `${defaultMessage}: ${error.message}`;
649 |   }
650 |   return {
651 |     content: [{ type: "text", text: errorMessage }],
652 |     isError: true,
653 |   };
654 | }
655 | 
656 | async function main() {
657 |   const transport = new StdioServerTransport();
658 |   await server.connect(transport);
659 |   console.error("Shopify MCP Server running on stdio");
660 | }
661 | 
662 | main().catch((error) => {
663 |   console.error("Fatal error in main():", error);
664 |   process.exit(1);
665 | });
666 | 
```

--------------------------------------------------------------------------------
/src/ShopifyClient/ShopifyClientPort.ts:
--------------------------------------------------------------------------------

```typescript
   1 | export type Nullable<T> = T | null;
   2 | export type ISODate = string;
   3 | export type Maybe<T> = T | null | undefined;
   4 | 
   5 | export type CreateDiscountCodeResponse = {
   6 |   id: string;
   7 |   priceRuleId: string;
   8 |   code: string;
   9 |   usageCount: number;
  10 | };
  11 | 
  12 | export enum ShopifyWebhookTopicGraphql {
  13 |   ORDERS_UPDATED = "ORDERS_UPDATED",
  14 | }
  15 | 
  16 | export enum ShopifyWebhookTopic {
  17 |   ORDERS_UPDATED = "orders/updated",
  18 | }
  19 | 
  20 | export type ShopifyWebhook = {
  21 |   id: string;
  22 |   callbackUrl: string;
  23 |   topic: ShopifyWebhookTopic;
  24 | };
  25 | 
  26 | export type ShopifyPriceRule = {
  27 |   id: number;
  28 |   value_type: string;
  29 |   value: string;
  30 |   customer_selection: string;
  31 |   target_type: string;
  32 |   target_selection: string;
  33 |   allocation_method: string;
  34 |   allocation_limit: number | null;
  35 |   once_per_customer: boolean;
  36 |   usage_limit: number | null;
  37 |   starts_at: string;
  38 |   ends_at: string | null;
  39 |   created_at: string;
  40 |   updated_at: string;
  41 |   entitled_product_ids: number[];
  42 |   entitled_variant_ids: number[];
  43 |   entitled_collection_ids: number[];
  44 |   entitled_country_ids: number[];
  45 |   prerequisite_product_ids: number[];
  46 |   prerequisite_variant_ids: number[];
  47 |   prerequisite_collection_ids: number[];
  48 |   prerequisite_saved_search_ids: number[];
  49 |   prerequisite_customer_ids: number[];
  50 |   prerequisite_subtotal_range: {
  51 |     greater_than_or_equal_to: string;
  52 |   } | null;
  53 |   prerequisite_quantity_range: {
  54 |     greater_than_or_equal_to: number;
  55 |   } | null;
  56 |   prerequisite_shipping_price_range: {
  57 |     less_than_or_equal_to: string;
  58 |   } | null;
  59 |   prerequisite_to_entitlement_quantity_ratio: {
  60 |     prerequisite_quantity: number;
  61 |     entitled_quantity: number;
  62 |   } | null;
  63 |   title: string;
  64 |   admin_graphql_api_id: string;
  65 | };
  66 | 
  67 | export type ShopifyCreatePriceRuleResponse = {
  68 |   price_rule: ShopifyPriceRule;
  69 | };
  70 | 
  71 | export type ShopifyDiscountCode = {
  72 |   id: number;
  73 |   price_rule_id: number;
  74 |   code: string;
  75 |   usage_count: number;
  76 |   created_at: string;
  77 |   updated_at: string;
  78 | };
  79 | 
  80 | export type ShopifyCreateDiscountCodeResponse = {
  81 |   discount_code: ShopifyDiscountCode;
  82 | };
  83 | 
  84 | export type CreatePriceRuleInput = {
  85 |   title: string;
  86 |   targetType: "LINE_ITEM" | "SHIPPING_LINE";
  87 |   allocationMethod: "ACROSS" | "EACH";
  88 |   valueType: "fixed_amount" | "percentage";
  89 |   value: string;
  90 |   entitledCollectionIds: string[];
  91 |   usageLimit?: number;
  92 |   startsAt: ISODate;
  93 |   endsAt?: ISODate;
  94 | };
  95 | 
  96 | export type CreateBasicDiscountCodeInput = {
  97 |   title: string;
  98 |   code: string;
  99 |   startsAt: ISODate;
 100 |   endsAt?: ISODate;
 101 |   valueType: string;
 102 |   value: number;
 103 |   usageLimit?: number;
 104 |   includeCollectionIds: string[];
 105 |   excludeCollectionIds: string[];
 106 |   appliesOncePerCustomer: boolean;
 107 |   combinesWith: {
 108 |     productDiscounts: boolean;
 109 |     orderDiscounts: boolean;
 110 |     shippingDiscounts: boolean;
 111 |   };
 112 | };
 113 | 
 114 | export type CreateBasicDiscountCodeResponse = {
 115 |   id: string;
 116 |   code: string;
 117 | };
 118 | 
 119 | export type BasicDiscountCodeResponse = {
 120 |   data: {
 121 |     discountCodeBasicCreate: {
 122 |       codeDiscountNode: {
 123 |         id: string;
 124 |         codeDiscount: {
 125 |           title: string;
 126 |           codes: {
 127 |             nodes: Array<{
 128 |               code: string;
 129 |             }>;
 130 |           };
 131 |           startsAt: string;
 132 |           endsAt: string;
 133 |           customerSelection: {
 134 |             allCustomers: boolean;
 135 |           };
 136 |           customerGets: {
 137 |             appliesOnOneTimePurchase: boolean;
 138 |             appliesOnSubscription: boolean;
 139 |             value: {
 140 |               percentage?: number;
 141 |               amount?: {
 142 |                 amount: number;
 143 |                 currencyCode: string;
 144 |               };
 145 |             };
 146 |             items: {
 147 |               allItems: boolean;
 148 |             };
 149 |           };
 150 |           appliesOncePerCustomer: boolean;
 151 |           recurringCycleLimit: number;
 152 |         };
 153 |       };
 154 |       userErrors: Array<{
 155 |         field: string[];
 156 |         code: string;
 157 |         message: string;
 158 |       }>;
 159 |     };
 160 |   };
 161 | };
 162 | 
 163 | export type CreatePriceRuleResponse = {
 164 |   id: string;
 165 | };
 166 | 
 167 | export type UpdateProductPriceResponse ={
 168 |   success: boolean;
 169 |   errors?: Array<{field: string; message: string}>;
 170 |   product?: {
 171 |     id: string;
 172 |     variants: {
 173 |       edges: Array<{
 174 |         node: {
 175 |           price: string;
 176 |         };
 177 |       }>;
 178 |     };
 179 |   };
 180 | }
 181 | 
 182 | type DiscountCode = {
 183 |   code: string | null;
 184 |   amount: string | null;
 185 |   type: string | null;
 186 | };
 187 | 
 188 | export type ShopifyCustomer = {
 189 |   id?: number;
 190 |   email?: string;
 191 |   first_name?: string;
 192 |   last_name?: string;
 193 |   phone?: string;
 194 |   orders_count?: number;
 195 |   email_marketing_consent?: {
 196 |     state?: "subscribed" | "not_subscribed" | null;
 197 |     opt_in_level?: "single_opt_in" | "confirmed_opt_in" | "unknown" | null;
 198 |     consent_updated_at?: string;
 199 |   };
 200 | 
 201 |   sms_marketing_consent?: {
 202 |     state?: string;
 203 |     opt_in_level?: string | null;
 204 |     consent_updated_at?: string;
 205 |     consent_collected_from?: string;
 206 |   };
 207 |   tags?: string;
 208 |   currency?: string;
 209 |   default_address?: {
 210 |     first_name?: string | null;
 211 |     last_name?: string | null;
 212 |     company?: string | null;
 213 |     address1?: string | null;
 214 |     address2?: string | null;
 215 |     city?: string | null;
 216 |     province?: string | null;
 217 |     country?: string | null;
 218 |     zip?: string | null;
 219 |     phone?: string | null;
 220 |     name?: string | null;
 221 |     province_code?: string | null;
 222 |     country_code?: string | null;
 223 |     country_name?: string | null;
 224 |   };
 225 | };
 226 | 
 227 | export type LoadCustomersResponse = {
 228 |   customers: Array<ShopifyCustomer>;
 229 |   next?: string | undefined;
 230 | };
 231 | 
 232 | export type ShopifyOrder = {
 233 |   id: string;
 234 |   createdAt: string;
 235 |   currencyCode: string;
 236 |   discountApplications: {
 237 |     nodes: Array<{
 238 |       code: string | null;
 239 |       value: {
 240 |         amount: string | null;
 241 |         percentage: number | null;
 242 |       };
 243 |       __typename: string;
 244 |     }>;
 245 |   };
 246 |   displayFinancialStatus: string | null;
 247 |   name: string;
 248 |   totalPriceSet: {
 249 |     shopMoney: { amount: string; currencyCode: string };
 250 |     presentmentMoney: { amount: string; currencyCode: string };
 251 |   };
 252 |   totalShippingPriceSet: {
 253 |     shopMoney: { amount: string; currencyCode: string };
 254 |     presentmentMoney: { amount: string; currencyCode: string };
 255 |   };
 256 |   customer?: {
 257 |     id: string;
 258 |     email: string;
 259 |     firstName: string;
 260 |     lastName: string;
 261 |     phone: string;
 262 |   };
 263 | };
 264 | 
 265 | export type ShopifyOrdersResponse = {
 266 |   data: {
 267 |     orders: {
 268 |       edges: Array<{
 269 |         node: ShopifyOrder;
 270 |       }>;
 271 |       pageInfo: {
 272 |         hasNextPage: boolean;
 273 |         endCursor: string;
 274 |       };
 275 |     };
 276 |   };
 277 | };
 278 | 
 279 | export function isShopifyOrder(
 280 |   shopifyOrder: any
 281 | ): shopifyOrder is ShopifyOrder {
 282 |   return (
 283 |     shopifyOrder &&
 284 |     "id" in shopifyOrder &&
 285 |     "createdAt" in shopifyOrder &&
 286 |     "currencyCode" in shopifyOrder &&
 287 |     "discountApplications" in shopifyOrder &&
 288 |     "displayFinancialStatus" in shopifyOrder &&
 289 |     "name" in shopifyOrder &&
 290 |     "totalPriceSet" in shopifyOrder &&
 291 |     "totalShippingPriceSet" in shopifyOrder
 292 |   );
 293 | }
 294 | 
 295 | // Shopify webhook payload is the same type as the order
 296 | // We expose the same type for having an easier to read and consistent API across all webshop clients
 297 | export type ShopifyOrderWebhookPayload = ShopifyOrder;
 298 | 
 299 | export function isShopifyOrderWebhookPayload(
 300 |   webhookPayload: any
 301 | ): webhookPayload is ShopifyOrderWebhookPayload {
 302 |   return isShopifyOrder(webhookPayload);
 303 | }
 304 | 
 305 | export type ShopifyCollectionsQueryParams = {
 306 |   sinceId?: string; // Retrieve all orders after the specified ID
 307 |   name?: string;
 308 |   limit: number;
 309 | };
 310 | 
 311 | export type ShopifyCollection = {
 312 |   id: number;
 313 |   handle: string;
 314 |   title: string;
 315 |   updated_at: string;
 316 |   body_html: Nullable<string>;
 317 |   published_at: string;
 318 |   sort_order: string;
 319 |   template_suffix?: Nullable<string>;
 320 |   published_scope: string;
 321 |   image?: {
 322 |     src: string;
 323 |     alt: string;
 324 |   };
 325 | };
 326 | 
 327 | export type ShopifySmartCollectionsResponse = {
 328 |   smart_collections: ShopifyCollection[];
 329 | };
 330 | 
 331 | export type ShopifyCustomCollectionsResponse = {
 332 |   custom_collections: ShopifyCollection[];
 333 | };
 334 | 
 335 | export type LoadCollectionsResponse = {
 336 |   collections: ShopifyCollection[];
 337 |   next?: string;
 338 | };
 339 | 
 340 | export type ShopifyShop = {
 341 |   id: string;
 342 |   name: string;
 343 |   domain: string;
 344 |   myshopify_domain: string;
 345 |   currency: string;
 346 |   enabled_presentment_currencies: string[];
 347 |   address1: string;
 348 |   created_at: string;
 349 |   updated_at: string;
 350 | };
 351 | 
 352 | export type LoadStorefrontsResponse = {
 353 |   shop: ShopifyShop;
 354 | };
 355 | 
 356 | export type ShopifyQueryParams = {
 357 |   query?: string; // Custom query string for advanced filtering
 358 |   sortKey?:
 359 |     | "PROCESSED_AT"
 360 |     | "TOTAL_PRICE"
 361 |     | "ID"
 362 |     | "CREATED_AT"
 363 |     | "UPDATED_AT"
 364 |     | "ORDER_NUMBER";
 365 |   reverse?: boolean;
 366 |   before?: string;
 367 |   after?: string;
 368 |   // Keeping these for backwards compatibility, but they should be used in query string
 369 |   sinceId?: string;
 370 |   updatedAtMin?: string;
 371 |   createdAtMin?: string;
 372 |   financialStatus?:
 373 |     | "AUTHORIZED"
 374 |     | "PENDING"
 375 |     | "PAID"
 376 |     | "PARTIALLY_PAID"
 377 |     | "REFUNDED"
 378 |     | "VOIDED"
 379 |     | "PARTIALLY_REFUNDED"
 380 |     | "ANY"
 381 |     | "UNPAID";
 382 |   ids?: string[];
 383 |   status?: "OPEN" | "CLOSED" | "CANCELLED" | "ANY";
 384 |   limit?: number;
 385 | };
 386 | 
 387 | export type ShippingZone = {
 388 |   id: string;
 389 |   name: string;
 390 |   countries: Array<{
 391 |     id: string;
 392 |     name: string;
 393 |     code: string;
 394 |   }>;
 395 | };
 396 | 
 397 | export type ShopifyLoadOrderQueryParams = {
 398 |   orderId: string;
 399 |   fields?: string[];
 400 | };
 401 | 
 402 | export type ProductImage = {
 403 |   src: string;
 404 |   height: number;
 405 |   width: number;
 406 | };
 407 | 
 408 | export type ProductOption = {
 409 |   id: string;
 410 |   name: string;
 411 |   values: string[];
 412 | };
 413 | 
 414 | export type SelectedProductOption = {
 415 |   name: string;
 416 |   value: string;
 417 | };
 418 | 
 419 | export type ProductVariant = {
 420 |   id: string;
 421 |   title: string;
 422 |   price: string;
 423 |   sku: string;
 424 |   availableForSale: boolean;
 425 |   image: Nullable<ProductImage>;
 426 |   inventoryPolicy: "CONTINUE" | "DENY";
 427 |   selectedOptions: SelectedProductOption[];
 428 | };
 429 | 
 430 | export type ShopResponse = {
 431 |   data: {
 432 |     shop: {
 433 |       shipsToCountries: string[];
 434 |     };
 435 |   };
 436 | };
 437 | 
 438 | export type MarketResponse = {
 439 |   data: {
 440 |     market: {
 441 |       name: string;
 442 |       enabled: string;
 443 |       regions: {
 444 |         nodes: {
 445 |           name: string;
 446 |           code: string;
 447 |         };
 448 |       };
 449 |     };
 450 |   };
 451 | };
 452 | 
 453 | export type GetPriceRuleInput = { query?: string };
 454 | 
 455 | export type GetPriceRuleResponse = {
 456 |   priceRules: {
 457 |     nodes: [
 458 |       {
 459 |         id: string;
 460 |         title: string;
 461 |         status: string;
 462 |       }
 463 |     ];
 464 |   };
 465 | };
 466 | 
 467 | export type ProductVariantWithProductDetails = ProductVariant & {
 468 |   product: {
 469 |     id: string;
 470 |     title: string;
 471 |     description: string;
 472 |     images: {
 473 |       edges: {
 474 |         node: ProductImage;
 475 |       }[];
 476 |     };
 477 |   };
 478 | };
 479 | 
 480 | export type ProductNode = {
 481 |   id: string;
 482 |   handle: string;
 483 |   title: string;
 484 |   description: string;
 485 |   publishedAt: string;
 486 |   updatedAt: string;
 487 |   options: ProductOption[];
 488 |   images: {
 489 |     edges: {
 490 |       node: ProductImage;
 491 |     }[];
 492 |   };
 493 |   variants: {
 494 |     edges: {
 495 |       node: ProductVariant;
 496 |     }[];
 497 |   };
 498 | };
 499 | 
 500 | export type LoadProductsResponse = {
 501 |   currencyCode: string;
 502 |   products: ProductNode[];
 503 |   next?: string;
 504 | };
 505 | 
 506 | export type LoadProductsByIdsResponse = {
 507 |   currencyCode: string;
 508 |   products: ProductNode[];
 509 | };
 510 | 
 511 | export type LoadVariantsByIdResponse = {
 512 |   currencyCode: string;
 513 |   variants: ProductVariantWithProductDetails[];
 514 | };
 515 | 
 516 | export type CreateDraftOrderPayload = {
 517 |   lineItems: Array<{
 518 |     variantId: string;
 519 |     quantity: number;
 520 |     appliedDiscount?: {
 521 |       title: string;
 522 |       value: number;
 523 |       valueType: "FIXED_AMOUNT" | "PERCENTAGE";
 524 |     };
 525 |   }>;
 526 |   shippingAddress: {
 527 |     address1: string;
 528 |     address2?: string;
 529 |     countryCode: string;
 530 |     firstName: string;
 531 |     lastName: string;
 532 |     zip: string;
 533 |     city: string;
 534 |     country: string;
 535 |     province?: string;
 536 |     provinceCode?: string;
 537 |     phone?: string;
 538 |   };
 539 |   billingAddress: {
 540 |     address1: string;
 541 |     address2?: string;
 542 |     countryCode: string;
 543 |     firstName: string;
 544 |     lastName: string;
 545 |     zip: string;
 546 |     city: string;
 547 |     country: string;
 548 |     province?: string;
 549 |     provinceCode?: string;
 550 |     phone?: string;
 551 |   };
 552 |   email: string;
 553 |   tags: string;
 554 |   note: string;
 555 | };
 556 | 
 557 | export type DraftOrderResponse = {
 558 |   draftOrderId: string;
 559 |   draftOrderName: string;
 560 | };
 561 | 
 562 | export type CompleteDraftOrderResponse = {
 563 |   draftOrderId: string;
 564 |   draftOrderName: string;
 565 |   orderId: string;
 566 | };
 567 | 
 568 | function serializeError(err: any): any {
 569 |   if (Array.isArray(err)) {
 570 |     return err.map((item) => serializeError(item));
 571 |   } else if (typeof err === "object" && err !== null) {
 572 |     const result: Record<string, any> = {};
 573 |     Object.getOwnPropertyNames(err).forEach((key) => {
 574 |       result[key] = serializeError(err[key]);
 575 |     });
 576 |     return result;
 577 |   }
 578 |   return err;
 579 | }
 580 | 
 581 | type InnerError =
 582 |   | Error
 583 |   | Error[]
 584 |   | string
 585 |   | string[]
 586 |   | Record<string, any>
 587 |   | undefined;
 588 | 
 589 | export interface CustomErrorPayload {
 590 |   customCode?: string;
 591 |   message?: string;
 592 | 
 593 |   innerError?: InnerError;
 594 | 
 595 |   /**
 596 |    * Used to add custom data that will be logged
 597 |    */
 598 |   contextData?: any;
 599 | }
 600 | 
 601 | export class CustomError extends Error {
 602 |   public code: string;
 603 | 
 604 |   public innerError: InnerError;
 605 | 
 606 |   public contextData: any;
 607 | 
 608 |   constructor(message: string, code: string, payload: CustomErrorPayload = {}) {
 609 |     super(message);
 610 |     this.code = payload.customCode ? `${code}.${payload.customCode}` : code;
 611 |     if (payload.message) this.message = message;
 612 |     this.innerError = payload.innerError;
 613 |     this.contextData = payload.contextData;
 614 |     this.name = this.constructor.name;
 615 |   }
 616 | 
 617 |   toJSON(): unknown {
 618 |     return {
 619 |       message: this.message,
 620 |       innerError: serializeError(this.innerError),
 621 |       name: this.name,
 622 |       code: this.code,
 623 |       contextData: this.contextData,
 624 |     };
 625 |   }
 626 | 
 627 |   static is<E extends typeof CustomError & { code: string }>(
 628 |     error: any,
 629 |     ErrorClass: E
 630 |   ): error is InstanceType<E> {
 631 |     return "code" in error && error.code === ErrorClass.code;
 632 |   }
 633 | }
 634 | 
 635 | export class ShopifyClientErrorBase extends CustomError {
 636 |   // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
 637 |   static make(message: string, code: string) {
 638 |     return class extends ShopifyClientErrorBase {
 639 |       static code = code;
 640 | 
 641 |       constructor(payload?: CustomErrorPayload) {
 642 |         super(message, code, payload);
 643 |       }
 644 |     };
 645 |   }
 646 | }
 647 | 
 648 | export class ShopifyCastObjError extends ShopifyClientErrorBase.make(
 649 |   "Error occurred on Shopify cast object",
 650 |   "SHOPIFY_CLIENT.SHOPIFY_CAST_ERROR"
 651 | ) {}
 652 | 
 653 | export class ShopifyAuthorizationError extends ShopifyClientErrorBase.make(
 654 |   "Shopify authorization error",
 655 |   "SHOPIFY_CLIENT.AUTHORIZATION_ERROR"
 656 | ) {}
 657 | 
 658 | export class ShopifyRequestError extends ShopifyClientErrorBase.make(
 659 |   "Shopify request error",
 660 |   "SHOPIFY_CLIENT.REQUEST_ERROR"
 661 | ) {}
 662 | 
 663 | export class ShopifyInputError extends ShopifyClientErrorBase.make(
 664 |   "Shopify input error",
 665 |   "SHOPIFY_CLIENT.INPUT_ERROR"
 666 | ) {}
 667 | 
 668 | export class ShopifyRateLimitingError extends ShopifyClientErrorBase.make(
 669 |   "Shopify rate limiting error",
 670 |   "SHOPIFY_CLIENT.RATE_LIMITING_ERROR"
 671 | ) {}
 672 | 
 673 | export class ShopifyServerInfrastructureError extends ShopifyClientErrorBase.make(
 674 |   "Shopify server or infrastructure error",
 675 |   "SHOPIFY_CLIENT.SERVER_INFRASTRUCTURE_ERROR"
 676 | ) {}
 677 | 
 678 | export class ShopifyPaymentError extends ShopifyClientErrorBase.make(
 679 |   "Shopify payment error",
 680 |   "SHOPIFY_CLIENT.PAYMENT_ERROR"
 681 | ) {}
 682 | export class GeneralShopifyClientError extends ShopifyClientErrorBase.make(
 683 |   "Error occurred on Shopify API client",
 684 |   "SHOPIFY_CLIENT.SHOPIFY_CLIENT_ERROR"
 685 | ) {}
 686 | export class ShopifyWebShopNotFoundError extends ShopifyClientErrorBase.make(
 687 |   "The Shopify webshop not found",
 688 |   "SHOPIFY_CLIENT.WEBSHOP_CONNECTION_NOT_FOUND"
 689 | ) {}
 690 | 
 691 | export class ShopifyProductVariantNotFoundError extends ShopifyClientErrorBase.make(
 692 |   "The Shopify product variant not found",
 693 |   "SHOPIFY_CLIENT.PRODUCT_VARIANT_NOT_FOUND"
 694 | ) {}
 695 | 
 696 | export class ShopifyProductVariantNotAvailableForSaleError extends ShopifyClientErrorBase.make(
 697 |   "The Shopify product variant is not available for sale",
 698 |   "SHOPIFY_CLIENT.PRODUCT_VARIANT_NOT_AVAILABLE_FOR_SALE"
 699 | ) {}
 700 | 
 701 | export class InvalidShopifyCurrencyError extends ShopifyClientErrorBase.make(
 702 |   "The Shopify currency is invalid",
 703 |   "SHOPIFY_CLIENT.INVALID_CURRENCY"
 704 | ) {}
 705 | 
 706 | export class ShopifyWebhookNotFoundError extends ShopifyClientErrorBase.make(
 707 |   "The Shopify webhook not found",
 708 |   "SHOPIFY_CLIENT.WEBHOOK_NOT_FOUND"
 709 | ) {}
 710 | 
 711 | export class ShopifyWebhookAlreadyExistsError extends ShopifyClientErrorBase.make(
 712 |   "The Shopify webhook already exists",
 713 |   "SHOPIFY_CLIENT.WEBHOOK_ALREADY_EXISTS"
 714 | ) {}
 715 | 
 716 | export function getHttpShopifyError(
 717 |   error: any,
 718 |   statusCode: number,
 719 |   contextData?: Record<string, any>
 720 | ): ShopifyClientErrorBase {
 721 |   switch (statusCode) {
 722 |     case 401:
 723 |     case 403:
 724 |     case 423:
 725 |     case 430:
 726 |       return new ShopifyAuthorizationError({ innerError: error, contextData });
 727 | 
 728 |     case 400:
 729 |     case 405:
 730 |     case 406:
 731 |     case 414:
 732 |     case 415:
 733 |     case 783:
 734 |       return new ShopifyRequestError({ innerError: error, contextData });
 735 | 
 736 |     case 404:
 737 |     case 409:
 738 |     case 422:
 739 |       return new ShopifyInputError({ innerError: error, contextData });
 740 | 
 741 |     case 429:
 742 |       return new ShopifyRateLimitingError({ innerError: error, contextData });
 743 | 
 744 |     case 500:
 745 |     case 501:
 746 |     case 502:
 747 |     case 503:
 748 |     case 504:
 749 |     case 530:
 750 |     case 540:
 751 |       return new ShopifyServerInfrastructureError({
 752 |         innerError: error,
 753 |         contextData,
 754 |       });
 755 | 
 756 |     case 402:
 757 |       return new ShopifyPaymentError({ innerError: error, contextData });
 758 | 
 759 |     default:
 760 |       return new GeneralShopifyClientError({
 761 |         innerError: error,
 762 |         contextData,
 763 |       });
 764 |   }
 765 | }
 766 | 
 767 | export function getGraphqlShopifyUserError(
 768 |   errors: any[],
 769 |   contextData?: Record<string, any>
 770 | ): ShopifyClientErrorBase {
 771 |   const hasErrorWithMessage = (messages: string[]): boolean =>
 772 |     errors.some((error) => messages.includes(error.message));
 773 | 
 774 |   if (hasErrorWithMessage(["Product variant not found."])) {
 775 |     return new ShopifyProductVariantNotFoundError({
 776 |       innerError: errors,
 777 |       contextData,
 778 |     });
 779 |   }
 780 | 
 781 |   if (hasErrorWithMessage(["Webhook subscription does not exist"])) {
 782 |     return new ShopifyWebhookNotFoundError({
 783 |       innerError: errors,
 784 |       contextData,
 785 |     });
 786 |   }
 787 | 
 788 |   if (hasErrorWithMessage(["Address for this topic has already been taken"])) {
 789 |     return new ShopifyWebhookAlreadyExistsError({
 790 |       innerError: errors,
 791 |       contextData,
 792 |     });
 793 |   }
 794 | 
 795 |   return new GeneralShopifyClientError({
 796 |     innerError: errors,
 797 |     contextData,
 798 |   });
 799 | }
 800 | 
 801 | export function getGraphqlShopifyError(
 802 |   errors: any[],
 803 |   statusCode: number,
 804 |   contextData?: Record<string, any>
 805 | ): ShopifyClientErrorBase {
 806 |   const hasErrorWithCode = (codes: string[]): boolean =>
 807 |     errors.some((error) => codes.includes(error.extensions?.code));
 808 | 
 809 |   switch (statusCode) {
 810 |     case 403:
 811 |     case 423:
 812 |       return new ShopifyAuthorizationError({
 813 |         innerError: errors,
 814 |         contextData,
 815 |       });
 816 | 
 817 |     case 400:
 818 |       return new ShopifyRequestError({
 819 |         innerError: errors,
 820 |         contextData,
 821 |       });
 822 | 
 823 |     case 404:
 824 |       return new ShopifyInputError({
 825 |         innerError: errors,
 826 |         contextData,
 827 |       });
 828 | 
 829 |     case 500:
 830 |     case 501:
 831 |     case 502:
 832 |     case 503:
 833 |     case 504:
 834 |     case 530:
 835 |     case 540:
 836 |       return new ShopifyServerInfrastructureError({
 837 |         innerError: errors,
 838 |         contextData,
 839 |       });
 840 | 
 841 |     case 402:
 842 |       return new ShopifyPaymentError({
 843 |         innerError: errors,
 844 |         contextData,
 845 |       });
 846 | 
 847 |     default:
 848 |       if (hasErrorWithCode(["UNAUTHORIZED", "ACCESS_DENIED", "FORBIDDEN"])) {
 849 |         return new ShopifyAuthorizationError({
 850 |           innerError: errors,
 851 |           contextData,
 852 |         });
 853 |       }
 854 | 
 855 |       if (hasErrorWithCode(["UNPROCESSABLE"])) {
 856 |         return new ShopifyInputError({
 857 |           innerError: errors,
 858 |           contextData,
 859 |         });
 860 |       }
 861 | 
 862 |       if (hasErrorWithCode(["THROTTLED"])) {
 863 |         return new ShopifyRateLimitingError({
 864 |           innerError: errors,
 865 |           contextData,
 866 |         });
 867 |       }
 868 | 
 869 |       if (hasErrorWithCode(["INTERNAL_SERVER_ERROR"])) {
 870 |         return new ShopifyServerInfrastructureError({
 871 |           innerError: errors,
 872 |           contextData,
 873 |         });
 874 |       }
 875 | 
 876 |       return new GeneralShopifyClientError({
 877 |         innerError: errors,
 878 |         contextData,
 879 |       });
 880 |   }
 881 | }
 882 | 
 883 | export type ShopifyOrderGraphql = {
 884 |   id: string;
 885 |   name: string;
 886 |   createdAt: string;
 887 |   displayFinancialStatus: string;
 888 |   email: string;
 889 |   phone: string | null;
 890 |   totalPriceSet: {
 891 |     shopMoney: { amount: string; currencyCode: string };
 892 |     presentmentMoney: { amount: string; currencyCode: string };
 893 |   };
 894 |   customer: {
 895 |     id: string;
 896 |     email: string;
 897 |   } | null;
 898 |   shippingAddress: {
 899 |     provinceCode: string | null;
 900 |     countryCode: string;
 901 |   } | null;
 902 |   lineItems: {
 903 |     nodes: Array<{
 904 |       id: string;
 905 |       title: string;
 906 |       quantity: number;
 907 |       originalTotalSet: {
 908 |         shopMoney: { amount: string; currencyCode: string };
 909 |       };
 910 |       variant: {
 911 |         id: string;
 912 |         title: string;
 913 |         sku: string | null;
 914 |         price: string;
 915 |       } | null;
 916 |     }>;
 917 |   };
 918 | };
 919 | 
 920 | export type ShopifyOrdersGraphqlQueryParams = {
 921 |   first?: number;
 922 |   after?: string;
 923 |   query?: string;
 924 |   sortKey?:
 925 |     | "PROCESSED_AT"
 926 |     | "TOTAL_PRICE"
 927 |     | "ID"
 928 |     | "CREATED_AT"
 929 |     | "UPDATED_AT"
 930 |     | "ORDER_NUMBER";
 931 |   reverse?: boolean;
 932 | };
 933 | 
 934 | export type ShopifyOrdersGraphqlResponse = {
 935 |   orders: ShopifyOrderGraphql[];
 936 |   pageInfo: {
 937 |     hasNextPage: boolean;
 938 |     endCursor: string | null;
 939 |   };
 940 | };
 941 | 
 942 | export interface ShopifyClientPort {
 943 |   createPriceRule(
 944 |     accessToken: string,
 945 |     shop: string,
 946 |     priceRuleInput: CreatePriceRuleInput
 947 |   ): Promise<CreatePriceRuleResponse>;
 948 | 
 949 |   createDiscountCode(
 950 |     accessToken: string,
 951 |     shop: string,
 952 |     code: string,
 953 |     priceRuleId: string
 954 |   ): Promise<CreateDiscountCodeResponse>;
 955 | 
 956 |   deletePriceRule(
 957 |     accessToken: string,
 958 |     shop: string,
 959 |     priceRuleId: string
 960 |   ): Promise<void>;
 961 | 
 962 |   deleteDiscountCode(
 963 |     accessToken: string,
 964 |     shop: string,
 965 |     priceRuleId: string,
 966 |     discountCodeId: string
 967 |   ): Promise<void>;
 968 | 
 969 |   createBasicDiscountCode(
 970 |     accessToken: string,
 971 |     shop: string,
 972 |     discountInput: CreateBasicDiscountCodeInput
 973 |   ): Promise<CreateBasicDiscountCodeResponse>;
 974 | 
 975 |   deleteBasicDiscountCode(
 976 |     accessToken: string,
 977 |     shop: string,
 978 |     discountCodeId: string
 979 |   ): Promise<void>;
 980 | 
 981 |   loadOrders(
 982 |     accessToken: string,
 983 |     shop: string,
 984 |     queryParams: ShopifyOrdersGraphqlQueryParams
 985 |   ): Promise<ShopifyOrdersGraphqlResponse>;
 986 | 
 987 |   loadOrder(
 988 |     accessToken: string,
 989 |     myshopifyDomain: string,
 990 |     queryParams: ShopifyLoadOrderQueryParams
 991 |   ): Promise<ShopifyOrder>;
 992 | 
 993 |   subscribeWebhook(
 994 |     accessToken: string,
 995 |     myshopifyDomain: string,
 996 |     callbackUrl: string,
 997 |     topic: ShopifyWebhookTopic
 998 |   ): Promise<ShopifyWebhook>;
 999 | 
1000 |   unsubscribeWebhook(
1001 |     accessToken: string,
1002 |     myshopifyDomain: string,
1003 |     webhookId: string
1004 |   ): Promise<void>;
1005 | 
1006 |   findWebhookByTopicAndCallbackUrl(
1007 |     accessToken: string,
1008 |     myshopifyDomain: string,
1009 |     callbackUrl: string,
1010 |     topic: ShopifyWebhookTopic
1011 |   ): Promise<ShopifyWebhook | null>;
1012 | 
1013 |   loadCollections(
1014 |     accessToken: string,
1015 |     myshopifyDomain: string,
1016 |     queryParams: ShopifyQueryParams,
1017 |     next?: string
1018 |   ): Promise<LoadCollectionsResponse>;
1019 | 
1020 |   loadShop(
1021 |     accessToken: string,
1022 |     myshopifyDomain: string
1023 |   ): Promise<LoadStorefrontsResponse>;
1024 | 
1025 |   loadCustomers(
1026 |     accessToken: string,
1027 |     myshopifyDomain: string,
1028 |     limit?: number,
1029 |     next?: string
1030 |   ): Promise<LoadCustomersResponse>;
1031 | 
1032 |   tagCustomer(
1033 |     accessToken: string,
1034 |     myshopifyDomain: string,
1035 |     tags: string[],
1036 |     customerId: string
1037 |   ): Promise<boolean>;
1038 | 
1039 |   loadProducts(
1040 |     accessToken: string,
1041 |     myshopifyDomain: string,
1042 |     searchTitle: string | null,
1043 |     limit?: number,
1044 |     afterCursor?: string
1045 |   ): Promise<LoadProductsResponse>;
1046 | 
1047 |   loadProductsByCollectionId(
1048 |     accessToken: string,
1049 |     myshopifyDomain: string,
1050 |     collectionId: string,
1051 |     limit?: number,
1052 |     afterCursor?: string
1053 |   ): Promise<LoadProductsResponse>;
1054 | 
1055 |   loadProductsByIds(
1056 |     accessToken: string,
1057 |     shop: string,
1058 |     productIds: string[]
1059 |   ): Promise<LoadProductsByIdsResponse>;
1060 | 
1061 |   updateProductPrice(
1062 |     accessToken: string,
1063 |     shop: string,
1064 |     productId: string,
1065 |     price: string
1066 |   ): Promise<UpdateProductPriceResponse>;
1067 | 
1068 |   loadVariantsByIds(
1069 |     accessToken: string,
1070 |     shop: string,
1071 |     variantIds: string[]
1072 |   ): Promise<LoadVariantsByIdResponse>;
1073 | 
1074 |   createDraftOrder(
1075 |     accessToken: string,
1076 |     shop: string,
1077 |     draftOrderData: CreateDraftOrderPayload,
1078 |     idempotencyKey: string
1079 |   ): Promise<DraftOrderResponse>;
1080 | 
1081 |   completeDraftOrder(
1082 |     accessToken: string,
1083 |     shop: string,
1084 |     draftOrderId: string,
1085 |     variantId: string
1086 |   ): Promise<CompleteDraftOrderResponse>;
1087 | 
1088 |   getIdFromGid(gid: string): string;
1089 | 
1090 |   loadShopDetail(accessToken: string, shop: string): Promise<ShopResponse>;
1091 | }
1092 | 
```

--------------------------------------------------------------------------------
/src/ShopifyClient/ShopifyClient.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import {
   2 |   CompleteDraftOrderResponse,
   3 |   CreateBasicDiscountCodeInput,
   4 |   CreateBasicDiscountCodeResponse,
   5 |   BasicDiscountCodeResponse,
   6 |   CreateDiscountCodeResponse,
   7 |   CreateDraftOrderPayload,
   8 |   CreatePriceRuleInput,
   9 |   CreatePriceRuleResponse,
  10 |   DraftOrderResponse,
  11 |   GeneralShopifyClientError,
  12 |   GetPriceRuleInput,
  13 |   GetPriceRuleResponse,
  14 |   LoadCollectionsResponse,
  15 |   LoadCustomersResponse,
  16 |   LoadProductsResponse,
  17 |   LoadStorefrontsResponse,
  18 |   LoadVariantsByIdResponse,
  19 |   ProductNode,
  20 |   ProductVariantWithProductDetails,
  21 |   ShopResponse,
  22 |   ShopifyAuthorizationError,
  23 |   ShopifyClientErrorBase,
  24 |   ShopifyCollection,
  25 |   ShopifyCollectionsQueryParams,
  26 |   ShopifyCustomCollectionsResponse,
  27 |   ShopifyInputError,
  28 |   ShopifyLoadOrderQueryParams,
  29 |   ShopifyOrder,
  30 |   ShopifyPaymentError,
  31 |   ShopifyProductVariantNotAvailableForSaleError,
  32 |   ShopifyProductVariantNotFoundError,
  33 |   ShopifyRequestError,
  34 |   ShopifySmartCollectionsResponse,
  35 |   ShopifyWebhook,
  36 |   getGraphqlShopifyError,
  37 |   getGraphqlShopifyUserError,
  38 |   getHttpShopifyError,
  39 |   ShopifyWebhookTopic,
  40 |   ShopifyWebhookTopicGraphql,
  41 |   ShopifyClientPort,
  42 |   UpdateProductPriceResponse,
  43 |   CustomError,
  44 |   Maybe,
  45 |   ShopifyOrdersGraphqlQueryParams,
  46 |   ShopifyOrdersGraphqlResponse,
  47 |   ShopifyOrderGraphql,
  48 | } from "./ShopifyClientPort.js";
  49 | import { gql } from "graphql-request";
  50 | 
  51 | const productImagesFragment = gql`
  52 |   src
  53 |   height
  54 |   width
  55 | `;
  56 | 
  57 | const productVariantsFragment = gql`
  58 |   id
  59 |   title
  60 |   price
  61 |   sku
  62 |   image {
  63 |     ${productImagesFragment}
  64 |   }
  65 |   availableForSale
  66 |   inventoryPolicy
  67 |   selectedOptions {
  68 |     name
  69 |     value
  70 |   }
  71 | `;
  72 | 
  73 | const productFragment = gql`
  74 |   id
  75 |   handle
  76 |   title
  77 |   description
  78 |   publishedAt
  79 |   updatedAt
  80 |   options {
  81 |     id
  82 |     name
  83 |     values
  84 |   }
  85 |   images(first: 20) {
  86 |     edges {
  87 |       node {
  88 |         ${productImagesFragment}
  89 |       }
  90 |     }
  91 |   }
  92 |   variants(first: 250) {
  93 |     edges {
  94 |       node {
  95 |         ${productVariantsFragment}
  96 |       }
  97 |     }
  98 |   }
  99 | `;
 100 | 
 101 | export class ShopifyClient implements ShopifyClientPort {
 102 |   private readonly logger = console;
 103 | 
 104 |   private SHOPIFY_API_VERSION = "2024-04";
 105 | 
 106 |   static getShopifyOrdersNextPage(link: Maybe<string>): string | undefined {
 107 |     if (!link) return;
 108 |     if (!link.includes("next")) return;
 109 | 
 110 |     if (link.includes("next") && link.includes("previous")) {
 111 |       return link
 112 |         .split('rel="previous"')[1]
 113 |         .split("page_info=")[1]
 114 |         .split('>; rel="next"')[0];
 115 |     }
 116 | 
 117 |     return link.split("page_info=")[1].split('>; rel="next"')[0];
 118 |   }
 119 | 
 120 |   async shopifyHTTPRequest<T>({
 121 |     method,
 122 |     url,
 123 |     accessToken,
 124 |     params,
 125 |     data,
 126 |   }: {
 127 |     method: "GET" | "POST" | "DELETE" | "PUT";
 128 |     url: string;
 129 |     accessToken: string;
 130 |     params?: Record<string, any>;
 131 |     data?: Record<string, any>;
 132 |   }): Promise<{ data: T; headers: Headers }> {
 133 |     try {
 134 |       // Add query parameters to URL if they exist
 135 |       if (params) {
 136 |         const queryParams = new URLSearchParams();
 137 |         Object.entries(params).forEach(([key, value]) => {
 138 |           if (value !== undefined) {
 139 |             queryParams.append(key, String(value));
 140 |           }
 141 |         });
 142 |         url = `${url}${url.includes("?") ? "&" : "?"}${queryParams.toString()}`;
 143 |       }
 144 | 
 145 |       const response = await fetch(url, {
 146 |         method,
 147 |         headers: {
 148 |           "X-Shopify-Access-Token": accessToken,
 149 |           ...(data ? { "Content-Type": "application/json" } : {}),
 150 |         },
 151 |         ...(data ? { body: JSON.stringify(data) } : {}),
 152 |       });
 153 | 
 154 |       if (!response.ok) {
 155 |         const responseData = await response
 156 |           .json()
 157 |           .catch(() => response.statusText);
 158 |         const responseError =
 159 |           responseData.error ??
 160 |           responseData.errors ??
 161 |           responseData ??
 162 |           response.status;
 163 |         throw getHttpShopifyError(responseError, response.status, {
 164 |           url,
 165 |           params,
 166 |           method,
 167 |           data: responseData,
 168 |         });
 169 |       }
 170 | 
 171 |       const responseData = await response.json();
 172 |       return {
 173 |         data: responseData,
 174 |         headers: response.headers,
 175 |       };
 176 |     } catch (error: any) {
 177 |       let shopifyError: ShopifyClientErrorBase;
 178 |       if (error instanceof ShopifyClientErrorBase) {
 179 |         shopifyError = error;
 180 |       } else {
 181 |         shopifyError = new GeneralShopifyClientError({
 182 |           innerError: error,
 183 |           contextData: {
 184 |             url,
 185 |             params,
 186 |             method,
 187 |           },
 188 |         });
 189 |       }
 190 | 
 191 |       if (
 192 |         shopifyError instanceof ShopifyRequestError ||
 193 |         shopifyError instanceof GeneralShopifyClientError
 194 |       ) {
 195 |         this.logger.error(shopifyError);
 196 |       } else if (
 197 |         shopifyError instanceof ShopifyInputError ||
 198 |         shopifyError instanceof ShopifyAuthorizationError ||
 199 |         shopifyError instanceof ShopifyPaymentError
 200 |       ) {
 201 |         this.logger.debug(shopifyError);
 202 |       } else {
 203 |         this.logger.warn(shopifyError);
 204 |       }
 205 | 
 206 |       throw shopifyError;
 207 |     }
 208 |   }
 209 | 
 210 |   async shopifyGraphqlRequest<T>({
 211 |     url,
 212 |     accessToken,
 213 |     query,
 214 |     variables,
 215 |   }: {
 216 |     url: string;
 217 |     accessToken: string;
 218 |     query: string;
 219 |     variables?: Record<string, any>;
 220 |   }): Promise<{ data: T; headers: Headers }> {
 221 |     try {
 222 |       const response = await fetch(url, {
 223 |         method: "POST",
 224 |         headers: {
 225 |           "X-Shopify-Access-Token": accessToken,
 226 |           "Content-Type": "application/json",
 227 |         },
 228 |         body: JSON.stringify({ query, variables }),
 229 |       });
 230 | 
 231 |       const responseData = await response.json();
 232 | 
 233 |       if (!response.ok || responseData?.errors) {
 234 |         const error = new Error("Shopify GraphQL Error");
 235 |         throw Object.assign(error, {
 236 |           response: { data: responseData, status: response.status },
 237 |         });
 238 |       }
 239 | 
 240 |       return {
 241 |         data: responseData,
 242 |         headers: response.headers,
 243 |       };
 244 |     } catch (error: any) {
 245 |       let shopifyError: ShopifyClientErrorBase;
 246 |       if (error.response) {
 247 |         const responseError =
 248 |           error.response.data.error ??
 249 |           error.response.data.errors ??
 250 |           error.response.data ??
 251 |           error.response.status;
 252 |         shopifyError = getGraphqlShopifyError(
 253 |           responseError,
 254 |           error.response.status,
 255 |           {
 256 |             url,
 257 |             query,
 258 |             variables,
 259 |             data: error.response.data,
 260 |           }
 261 |         );
 262 |       } else {
 263 |         shopifyError = new GeneralShopifyClientError({
 264 |           innerError: error,
 265 |           contextData: {
 266 |             url,
 267 |             query,
 268 |             variables,
 269 |           },
 270 |         });
 271 |       }
 272 | 
 273 |       if (
 274 |         shopifyError instanceof ShopifyRequestError ||
 275 |         shopifyError instanceof GeneralShopifyClientError
 276 |       ) {
 277 |         this.logger.error(shopifyError);
 278 |       } else if (
 279 |         shopifyError instanceof ShopifyInputError ||
 280 |         shopifyError instanceof ShopifyAuthorizationError ||
 281 |         shopifyError instanceof ShopifyPaymentError
 282 |       ) {
 283 |         this.logger.debug(shopifyError);
 284 |       } else {
 285 |         this.logger.warn(shopifyError);
 286 |       }
 287 | 
 288 |       throw shopifyError;
 289 |     }
 290 |   }
 291 | 
 292 |   private async getMyShopifyDomain(
 293 |     accessToken: string,
 294 |     shop: string
 295 |   ): Promise<string> {
 296 |     // POST requests are getting converted into GET on custom domain, so we need to retrieve the myshopify domain from the shop object
 297 |     const loadedShop = await this.loadShop(accessToken, shop);
 298 |     return loadedShop.shop.myshopify_domain;
 299 |   }
 300 | 
 301 |   async checkSubscriptionEligibility(
 302 |     accessToken: string,
 303 |     myshopifyDomain: string
 304 |   ): Promise<boolean> {
 305 |     const graphqlQuery = gql`
 306 |       query CheckSubscriptionEligibility {
 307 |         shop {
 308 |           features {
 309 |             eligibleForSubscriptions
 310 |             sellsSubscriptions
 311 |           }
 312 |         }
 313 |       }
 314 |     `;
 315 | 
 316 |     const res = await this.shopifyGraphqlRequest<{
 317 |       data: {
 318 |         shop: {
 319 |           features: {
 320 |             eligibleForSubscriptions: boolean;
 321 |             sellsSubscriptions: boolean;
 322 |           };
 323 |         };
 324 |       };
 325 |     }>({
 326 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 327 |       accessToken,
 328 |       query: graphqlQuery,
 329 |     });
 330 | 
 331 |     return (
 332 |       res.data.data.shop.features.eligibleForSubscriptions &&
 333 |       res.data.data.shop.features.sellsSubscriptions
 334 |     );
 335 |   }
 336 | 
 337 |   async createBasicDiscountCode(
 338 |     accessToken: string,
 339 |     shop: string,
 340 |     discountInput: CreateBasicDiscountCodeInput
 341 |   ): Promise<CreateBasicDiscountCodeResponse> {
 342 |     if (discountInput.valueType === "percentage") {
 343 |       if (discountInput.value < 0 || discountInput.value > 1) {
 344 |         throw new CustomError(
 345 |           "Invalid input: percentage value must be between 0 and 1",
 346 |           "InvalidInputError",
 347 |           {
 348 |             contextData: {
 349 |               discountInput,
 350 |               shop,
 351 |             },
 352 |           }
 353 |         );
 354 |       }
 355 |     }
 356 | 
 357 |     if (discountInput.valueType === "fixed_amount") {
 358 |       if (discountInput.value <= 0) {
 359 |         throw new CustomError(
 360 |           "Invalid input: fixed_amount value must be greater than 0",
 361 |           "InvalidInputError",
 362 |           {
 363 |             contextData: {
 364 |               discountInput,
 365 |               shop,
 366 |             },
 367 |           }
 368 |         );
 369 |       }
 370 |     }
 371 | 
 372 |     const myShopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 373 | 
 374 |     const isEligibleForSubscription = await this.checkSubscriptionEligibility(
 375 |       accessToken,
 376 |       myShopifyDomain
 377 |     );
 378 | 
 379 |     const graphqlQuery =
 380 |       this.graphqlQueryPreparationForCreateBasicDiscountCode();
 381 | 
 382 |     const variables = this.prepareBasicDiscountCodeVariable(
 383 |       discountInput,
 384 |       isEligibleForSubscription
 385 |     );
 386 | 
 387 |     const res = await this.shopifyGraphqlRequest<BasicDiscountCodeResponse>({
 388 |       url: `https://${myShopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 389 |       accessToken,
 390 |       query: graphqlQuery,
 391 |       variables,
 392 |     });
 393 | 
 394 |     const id = res.data.data.discountCodeBasicCreate.codeDiscountNode.id;
 395 |     const codeDiscount =
 396 |       res.data.data.discountCodeBasicCreate.codeDiscountNode.codeDiscount.codes
 397 |         .nodes[0];
 398 |     const userErrors = res.data.data.discountCodeBasicCreate.userErrors;
 399 | 
 400 |     if (userErrors.length > 0) {
 401 |       throw getGraphqlShopifyUserError(userErrors, {
 402 |         shop,
 403 |         discountInput,
 404 |       });
 405 |     }
 406 | 
 407 |     return {
 408 |       id,
 409 |       code: codeDiscount.code,
 410 |     };
 411 |   }
 412 | 
 413 |   private graphqlQueryPreparationForCreateBasicDiscountCode(): string {
 414 |     return gql`
 415 |       mutation discountCodeBasicCreate(
 416 |         $basicCodeDiscount: DiscountCodeBasicInput!
 417 |       ) {
 418 |         discountCodeBasicCreate(basicCodeDiscount: $basicCodeDiscount) {
 419 |           codeDiscountNode {
 420 |             id
 421 |             codeDiscount {
 422 |               ... on DiscountCodeBasic {
 423 |                 title
 424 |                 codes(first: 10) {
 425 |                   nodes {
 426 |                     code
 427 |                   }
 428 |                 }
 429 |                 startsAt
 430 |                 endsAt
 431 |                 customerSelection {
 432 |                   ... on DiscountCustomerAll {
 433 |                     allCustomers
 434 |                   }
 435 |                 }
 436 |                 customerGets {
 437 |                   appliesOnOneTimePurchase
 438 |                   appliesOnSubscription
 439 |                   value {
 440 |                     ... on DiscountPercentage {
 441 |                       percentage
 442 |                     }
 443 |                     ... on DiscountAmount {
 444 |                       amount {
 445 |                         amount
 446 |                         currencyCode
 447 |                       }
 448 |                       appliesOnEachItem
 449 |                     }
 450 |                   }
 451 |                   items {
 452 |                     ... on AllDiscountItems {
 453 |                       allItems
 454 |                     }
 455 |                   }
 456 |                 }
 457 |                 appliesOncePerCustomer
 458 |               }
 459 |             }
 460 |           }
 461 |           userErrors {
 462 |             field
 463 |             code
 464 |             message
 465 |           }
 466 |         }
 467 |       }
 468 |     `;
 469 |   }
 470 | 
 471 |   private prepareBasicDiscountCodeVariable(
 472 |     discountInput: CreateBasicDiscountCodeInput,
 473 |     isEligibleForSubscription: boolean
 474 |   ): any {
 475 |     return {
 476 |       basicCodeDiscount: {
 477 |         title: discountInput.title,
 478 |         code: discountInput.code,
 479 |         startsAt: discountInput.startsAt,
 480 |         endsAt: discountInput.endsAt,
 481 |         customerSelection: {
 482 |           all: true,
 483 |         },
 484 |         customerGets: {
 485 |           appliesOnOneTimePurchase: isEligibleForSubscription
 486 |             ? true
 487 |             : undefined,
 488 |           appliesOnSubscription: isEligibleForSubscription ? true : undefined,
 489 |           value: {
 490 |             percentage:
 491 |               discountInput.valueType === "percentage"
 492 |                 ? discountInput.value
 493 |                 : undefined,
 494 |             discountAmount:
 495 |               discountInput.valueType === "fixed_amount"
 496 |                 ? {
 497 |                     amount: discountInput.value,
 498 |                     appliesOnEachItem: false,
 499 |                   }
 500 |                 : undefined,
 501 |           },
 502 |           items: {
 503 |             all:
 504 |               discountInput.excludeCollectionIds.length === 0 &&
 505 |               discountInput.includeCollectionIds.length === 0,
 506 |             collections:
 507 |               discountInput.includeCollectionIds.length ||
 508 |               discountInput.excludeCollectionIds.length
 509 |                 ? {
 510 |                     add: discountInput.includeCollectionIds.map(
 511 |                       (id) => `gid://shopify/Collection/${id}`
 512 |                     ),
 513 |                     remove: discountInput.excludeCollectionIds.map(
 514 |                       (id) => `gid://shopify/Collection/${id}`
 515 |                     ),
 516 |                   }
 517 |                 : undefined,
 518 |           },
 519 |         },
 520 |         appliesOncePerCustomer: discountInput.appliesOncePerCustomer,
 521 |         recurringCycleLimit: isEligibleForSubscription
 522 |           ? discountInput.valueType === "fixed_amount"
 523 |             ? 1
 524 |             : null
 525 |           : undefined,
 526 |         usageLimit: discountInput.usageLimit,
 527 |         combinesWith: {
 528 |           productDiscounts: discountInput.combinesWith.productDiscounts,
 529 |           orderDiscounts: discountInput.combinesWith.orderDiscounts,
 530 |           shippingDiscounts: discountInput.combinesWith.shippingDiscounts,
 531 |         },
 532 |       },
 533 |     };
 534 |   }
 535 | 
 536 |   async createPriceRule(
 537 |     accessToken: string,
 538 |     shop: string,
 539 |     priceRuleInput: CreatePriceRuleInput
 540 |   ): Promise<CreatePriceRuleResponse> {
 541 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 542 | 
 543 |     const graphqlQuery = gql`
 544 |       mutation priceRuleCreate($priceRule: PriceRuleInput!) {
 545 |         priceRuleCreate(priceRule: $priceRule) {
 546 |           priceRule {
 547 |             id
 548 |           }
 549 |           priceRuleDiscountCode {
 550 |             id
 551 |             code
 552 |           }
 553 |           priceRuleUserErrors {
 554 |             field
 555 |             message
 556 |           }
 557 |           userErrors {
 558 |             field
 559 |             message
 560 |           }
 561 |         }
 562 |       }
 563 |     `;
 564 | 
 565 |     const res = await this.shopifyGraphqlRequest<{
 566 |       data: {
 567 |         priceRuleCreate: {
 568 |           priceRule: {
 569 |             id: string;
 570 |           };
 571 |           priceRuleUserErrors: Array<{
 572 |             field: string[];
 573 |             message: string;
 574 |           }>;
 575 |           userErrors: Array<{
 576 |             field: string[];
 577 |             message: string;
 578 |           }>;
 579 |         };
 580 |       };
 581 |     }>({
 582 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 583 |       accessToken,
 584 |       query: graphqlQuery,
 585 |       variables: {
 586 |         priceRule: {
 587 |           title: priceRuleInput.title,
 588 |           allocationMethod: priceRuleInput.allocationMethod,
 589 |           target: priceRuleInput.targetType,
 590 |           value:
 591 |             priceRuleInput.valueType === "fixed_amount"
 592 |               ? { fixedAmountValue: priceRuleInput.value }
 593 |               : { percentageValue: parseFloat(priceRuleInput.value) },
 594 |           validityPeriod: {
 595 |             start: priceRuleInput.startsAt,
 596 |             end: priceRuleInput.endsAt,
 597 |           },
 598 |           usageLimit: priceRuleInput.usageLimit,
 599 |           customerSelection: {
 600 |             forAllCustomers: true,
 601 |           },
 602 |           itemEntitlements: {
 603 |             collectionIds: priceRuleInput.entitledCollectionIds.map(
 604 |               (id) => `gid://shopify/Collection/${id}`
 605 |             ),
 606 |             targetAllLineItems:
 607 |               priceRuleInput.entitledCollectionIds.length === 0,
 608 |           },
 609 |           combinesWith: {
 610 |             productDiscounts: true,
 611 |             orderDiscounts: false,
 612 |             shippingDiscounts: true,
 613 |           },
 614 |         },
 615 |       },
 616 |     });
 617 | 
 618 |     const priceRule = res.data.data.priceRuleCreate.priceRule;
 619 |     const userErrors = res.data.data.priceRuleCreate.userErrors;
 620 | 
 621 |     if (userErrors.length > 0) {
 622 |       throw getGraphqlShopifyUserError(userErrors, {
 623 |         shop,
 624 |         priceRuleInput,
 625 |       });
 626 |     }
 627 | 
 628 |     return {
 629 |       id: priceRule.id,
 630 |     };
 631 |   }
 632 | 
 633 |   async createDiscountCode(
 634 |     accessToken: string,
 635 |     shop: string,
 636 |     code: string,
 637 |     priceRuleId: string
 638 |   ): Promise<CreateDiscountCodeResponse> {
 639 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 640 | 
 641 |     const graphqlQuery = gql`
 642 |       mutation priceRuleDiscountCodeCreate($priceRuleId: ID!, $code: String!) {
 643 |         priceRuleDiscountCodeCreate(priceRuleId: $priceRuleId, code: $code) {
 644 |           priceRuleUserErrors {
 645 |             field
 646 |             message
 647 |             code
 648 |           }
 649 |           priceRule {
 650 |             id
 651 |             title
 652 |           }
 653 |           priceRuleDiscountCode {
 654 |             id
 655 |             code
 656 |             usageCount
 657 |           }
 658 |         }
 659 |       }
 660 |     `;
 661 | 
 662 |     const res = await this.shopifyGraphqlRequest<{
 663 |       data: {
 664 |         priceRuleDiscountCodeCreate: {
 665 |           priceRuleUserErrors: Array<{
 666 |             field: string[];
 667 |             message: string;
 668 |             code: string;
 669 |           }>;
 670 |           priceRule: {
 671 |             id: string;
 672 |             title: string;
 673 |           };
 674 |           priceRuleDiscountCode: {
 675 |             id: string;
 676 |             code: string;
 677 |             usageCount: number;
 678 |           };
 679 |         };
 680 |       };
 681 |     }>({
 682 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 683 |       accessToken,
 684 |       query: graphqlQuery,
 685 |       variables: {
 686 |         priceRuleId,
 687 |         code,
 688 |       },
 689 |     });
 690 | 
 691 |     const discountCode =
 692 |       res.data.data.priceRuleDiscountCodeCreate.priceRuleDiscountCode;
 693 |     const userErrors =
 694 |       res.data.data.priceRuleDiscountCodeCreate.priceRuleUserErrors;
 695 | 
 696 |     if (userErrors.length > 0) {
 697 |       throw getGraphqlShopifyUserError(userErrors, {
 698 |         shop,
 699 |         code,
 700 |         priceRuleId,
 701 |       });
 702 |     }
 703 | 
 704 |     return {
 705 |       id: priceRuleId,
 706 |       priceRuleId: priceRuleId,
 707 |       code: discountCode.code,
 708 |       usageCount: discountCode.usageCount,
 709 |     };
 710 |   }
 711 | 
 712 |   async deleteBasicDiscountCode(
 713 |     accessToken: string,
 714 |     shop: string,
 715 |     discountCodeId: string
 716 |   ): Promise<void> {
 717 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 718 | 
 719 |     const graphqlQuery = gql`
 720 |       mutation discountCodeDelete($id: ID!) {
 721 |         discountCodeDelete(id: $id) {
 722 |           deletedCodeDiscountId
 723 |           userErrors {
 724 |             field
 725 |             code
 726 |             message
 727 |           }
 728 |         }
 729 |       }
 730 |     `;
 731 | 
 732 |     const res = await this.shopifyGraphqlRequest<{
 733 |       data: {
 734 |         discountCodeDelete: {
 735 |           deletedCodeDiscountId: string;
 736 |           userErrors: Array<{
 737 |             field: string[];
 738 |             code: string;
 739 |             message: string;
 740 |           }>;
 741 |         };
 742 |       };
 743 |     }>({
 744 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 745 |       accessToken,
 746 |       query: graphqlQuery,
 747 |       variables: {
 748 |         id: discountCodeId,
 749 |       },
 750 |     });
 751 | 
 752 |     const userErrors = res.data.data.discountCodeDelete.userErrors;
 753 | 
 754 |     if (userErrors.length > 0) {
 755 |       throw getGraphqlShopifyUserError(userErrors, {
 756 |         shop,
 757 |         discountCodeId,
 758 |       });
 759 |     }
 760 |   }
 761 | 
 762 |   async deletePriceRule(
 763 |     accessToken: string,
 764 |     shop: string,
 765 |     priceRuleId: string
 766 |   ): Promise<void> {
 767 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 768 | 
 769 |     await this.shopifyHTTPRequest({
 770 |       method: "DELETE",
 771 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/price_rules/${priceRuleId}.json`,
 772 |       accessToken,
 773 |     });
 774 |   }
 775 | 
 776 |   async deleteDiscountCode(
 777 |     accessToken: string,
 778 |     shop: string,
 779 |     priceRuleId: string,
 780 |     discountCodeId: string
 781 |   ): Promise<void> {
 782 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 783 | 
 784 |     await this.shopifyHTTPRequest({
 785 |       method: "DELETE",
 786 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/price_rules/${priceRuleId}/discount_codes/${discountCodeId}.json`,
 787 |       accessToken,
 788 |     });
 789 |   }
 790 | 
 791 |   async loadOrders(
 792 |     accessToken: string,
 793 |     shop: string,
 794 |     queryParams: ShopifyOrdersGraphqlQueryParams
 795 |   ): Promise<ShopifyOrdersGraphqlResponse> {
 796 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 797 | 
 798 |     const graphqlQuery = gql`
 799 |       query getOrdersDetailed(
 800 |         $first: Int
 801 |         $after: String
 802 |         $query: String
 803 |         $sortKey: OrderSortKeys
 804 |         $reverse: Boolean
 805 |       ) {
 806 |         orders(
 807 |           first: $first
 808 |           after: $after
 809 |           query: $query
 810 |           sortKey: $sortKey
 811 |           reverse: $reverse
 812 |         ) {
 813 |           nodes {
 814 |             id
 815 |             name
 816 |             createdAt
 817 |             displayFinancialStatus
 818 |             email
 819 |             phone
 820 |             totalPriceSet {
 821 |               shopMoney {
 822 |                 amount
 823 |                 currencyCode
 824 |               }
 825 |               presentmentMoney {
 826 |                 amount
 827 |                 currencyCode
 828 |               }
 829 |             }
 830 |             customer {
 831 |               id
 832 |               email
 833 |             }
 834 |             shippingAddress {
 835 |               provinceCode
 836 |               countryCode
 837 |             }
 838 |             lineItems(first: 50) {
 839 |               nodes {
 840 |                 id
 841 |                 title
 842 |                 quantity
 843 |                 originalTotalSet {
 844 |                   shopMoney {
 845 |                     amount
 846 |                     currencyCode
 847 |                   }
 848 |                 }
 849 |                 variant {
 850 |                   id
 851 |                   title
 852 |                   sku
 853 |                   price
 854 |                 }
 855 |               }
 856 |             }
 857 |           }
 858 |           pageInfo {
 859 |             hasNextPage
 860 |             endCursor
 861 |           }
 862 |         }
 863 |       }
 864 |     `;
 865 | 
 866 |     const variables = {
 867 |       first: queryParams.first || 50,
 868 |       after: queryParams.after,
 869 |       query: queryParams.query,
 870 |       sortKey: queryParams.sortKey,
 871 |       reverse: queryParams.reverse,
 872 |     };
 873 | 
 874 |     const res = await this.shopifyGraphqlRequest<{
 875 |       data: {
 876 |         orders: {
 877 |           nodes: ShopifyOrderGraphql[];
 878 |           pageInfo: {
 879 |             hasNextPage: boolean;
 880 |             endCursor: string | null;
 881 |           };
 882 |         };
 883 |       };
 884 |     }>({
 885 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
 886 |       accessToken,
 887 |       query: graphqlQuery,
 888 |       variables,
 889 |     });
 890 | 
 891 |     return {
 892 |       orders: res.data.data.orders.nodes,
 893 |       pageInfo: res.data.data.orders.pageInfo,
 894 |     };
 895 |   }
 896 | 
 897 |   async loadOrder(
 898 |     accessToken: string,
 899 |     shop: string,
 900 |     queryParams: ShopifyLoadOrderQueryParams
 901 |   ): Promise<ShopifyOrder> {
 902 |     const res = await this.shopifyHTTPRequest<{ order: ShopifyOrder }>({
 903 |       method: "GET",
 904 |       url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/orders/${queryParams.orderId}.json`,
 905 |       accessToken,
 906 |       params: {
 907 |         fields: this.getOrdersFields(queryParams.fields),
 908 |       },
 909 |     });
 910 | 
 911 |     return res.data.order;
 912 |   }
 913 | 
 914 |   async loadCollections(
 915 |     accessToken: string,
 916 |     shop: string,
 917 |     queryParams: ShopifyCollectionsQueryParams,
 918 |     next?: string
 919 |   ): Promise<LoadCollectionsResponse> {
 920 |     const nextList = next?.split(",");
 921 |     const customNext = nextList?.[0];
 922 |     const smartNext = nextList?.[1];
 923 |     let customCollections: ShopifyCollection[] = [];
 924 |     let customCollectionsNextPage;
 925 |     let smartCollections: ShopifyCollection[] = [];
 926 |     let smartCollectionsNextPage;
 927 | 
 928 |     if (customNext !== "undefined") {
 929 |       const customRes =
 930 |         await this.shopifyHTTPRequest<ShopifyCustomCollectionsResponse>({
 931 |           method: "GET",
 932 |           url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/custom_collections.json`,
 933 |           accessToken,
 934 |           params: {
 935 |             limit: queryParams.limit,
 936 |             page_info: customNext,
 937 |             title: customNext ? undefined : queryParams.name,
 938 |             since_id: customNext ? undefined : queryParams.sinceId,
 939 |           },
 940 |         });
 941 | 
 942 |       customCollections = customRes.data?.custom_collections || [];
 943 | 
 944 |       customCollectionsNextPage = ShopifyClient.getShopifyOrdersNextPage(
 945 |         customRes.headers?.get("link")
 946 |       );
 947 |     }
 948 |     if (smartNext !== "undefined") {
 949 |       const smartRes =
 950 |         await this.shopifyHTTPRequest<ShopifySmartCollectionsResponse>({
 951 |           method: "GET",
 952 |           url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/smart_collections.json`,
 953 |           accessToken,
 954 |           params: {
 955 |             limit: queryParams.limit,
 956 |             page_info: smartNext,
 957 |             title: smartNext ? undefined : queryParams.name,
 958 |             since_id: smartNext ? undefined : queryParams.sinceId,
 959 |           },
 960 |         });
 961 | 
 962 |       smartCollections = smartRes.data?.smart_collections || [];
 963 | 
 964 |       smartCollectionsNextPage = ShopifyClient.getShopifyOrdersNextPage(
 965 |         smartRes.headers?.get("link")
 966 |       );
 967 |     }
 968 |     const collections = [...customCollections, ...smartCollections];
 969 | 
 970 |     if (customCollectionsNextPage || smartCollectionsNextPage) {
 971 |       next = `${customCollectionsNextPage},${smartCollectionsNextPage}`;
 972 |     } else {
 973 |       next = undefined;
 974 |     }
 975 |     return { collections, next };
 976 |   }
 977 | 
 978 |   async loadShop(
 979 |     accessToken: string,
 980 |     shop: string
 981 |   ): Promise<LoadStorefrontsResponse> {
 982 |     const res = await this.shopifyHTTPRequest<LoadStorefrontsResponse>({
 983 |       method: "GET",
 984 |       url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/shop.json`,
 985 |       accessToken,
 986 |     });
 987 | 
 988 |     return res.data;
 989 |   }
 990 | 
 991 |   async loadShopDetail(
 992 |     accessToken: string,
 993 |     shop: string
 994 |   ): Promise<ShopResponse> {
 995 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
 996 | 
 997 |     const graphqlQuery = gql`
 998 |       {
 999 |         shop {
1000 |           shipsToCountries
1001 |         }
1002 |       }
1003 |     `;
1004 | 
1005 |     const res = await this.shopifyGraphqlRequest<ShopResponse>({
1006 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1007 |       accessToken,
1008 |       query: graphqlQuery,
1009 |     });
1010 | 
1011 |     return res.data;
1012 |   }
1013 | 
1014 |   async loadMarkets(accessToken: string, shop: string): Promise<ShopResponse> {
1015 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1016 | 
1017 |     const graphqlQuery = gql`
1018 |       {
1019 |         markets(first: 100) {
1020 |           nodes {
1021 |             name
1022 |             enabled
1023 |             regions {
1024 |               nodes {
1025 |                 name
1026 |                 ... on MarketRegionCountry {
1027 |                   code
1028 |                   __typename
1029 |                 }
1030 |               }
1031 |             }
1032 |           }
1033 |         }
1034 |       }
1035 |     `;
1036 | 
1037 |     const res = await this.shopifyGraphqlRequest<ShopResponse>({
1038 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1039 |       accessToken,
1040 |       query: graphqlQuery,
1041 |     });
1042 | 
1043 |     return res.data;
1044 |   }
1045 | 
1046 |   async loadProductsByCollectionId(
1047 |     accessToken: string,
1048 |     shop: string,
1049 |     collectionId: string,
1050 |     limit: number = 10,
1051 |     afterCursor?: string
1052 |   ): Promise<LoadProductsResponse> {
1053 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1054 | 
1055 |     const graphqlQuery = gql`
1056 |       {
1057 |         shop {
1058 |           currencyCode
1059 |         }
1060 |         collection(id: "gid://shopify/Collection/${collectionId}") {
1061 |           products(
1062 |             first: ${limit}${afterCursor ? `, after: "${afterCursor}"` : ""}
1063 |           ) {
1064 |             edges {
1065 |               node {
1066 |                 ${productFragment}
1067 |               }
1068 |             }
1069 |             pageInfo {
1070 |               hasNextPage
1071 |               endCursor
1072 |             }
1073 |           }
1074 |         }
1075 |       }
1076 |     `;
1077 | 
1078 |     const res = await this.shopifyGraphqlRequest<{
1079 |       data: {
1080 |         shop: {
1081 |           currencyCode: string;
1082 |         };
1083 |         collection: {
1084 |           products: {
1085 |             edges: Array<{
1086 |               node: ProductNode;
1087 |             }>;
1088 |             pageInfo: {
1089 |               hasNextPage: boolean;
1090 |               endCursor: string;
1091 |             };
1092 |           };
1093 |         };
1094 |       };
1095 |     }>({
1096 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1097 |       accessToken,
1098 |       query: graphqlQuery,
1099 |     });
1100 | 
1101 |     const data = res.data.data;
1102 |     const edges = data.collection.products.edges;
1103 |     const products = edges.map((edge) => edge.node);
1104 |     const pageInfo = data.collection.products.pageInfo;
1105 |     const next = pageInfo.hasNextPage ? pageInfo.endCursor : undefined;
1106 |     const currencyCode = data.shop.currencyCode;
1107 | 
1108 |     return { products, next, currencyCode };
1109 |   }
1110 | 
1111 |   async loadProducts(
1112 |     accessToken: string,
1113 |     myshopifyDomain: string,
1114 |     searchTitle: string | null,
1115 |     limit: number = 10,
1116 |     afterCursor?: string
1117 |   ): Promise<LoadProductsResponse> {
1118 |     const titleFilter = searchTitle ? `title:*${searchTitle}*` : "";
1119 |     const graphqlQuery = gql`
1120 |       {
1121 |         shop {
1122 |           currencyCode
1123 |         }
1124 |         products(first: ${limit}, query: "${titleFilter}"${
1125 |       afterCursor ? `, after: "${afterCursor}"` : ""
1126 |     }) {
1127 |           edges {
1128 |             node {
1129 |               ${productFragment}
1130 |             }
1131 |           }
1132 |           pageInfo {
1133 |             hasNextPage
1134 |             endCursor
1135 |           }
1136 |         }
1137 |       }
1138 |     `;
1139 | 
1140 |     const res = await this.shopifyGraphqlRequest<{
1141 |       data: {
1142 |         shop: {
1143 |           currencyCode: string;
1144 |         };
1145 |         products: {
1146 |           edges: Array<{
1147 |             node: ProductNode;
1148 |           }>;
1149 |           pageInfo: {
1150 |             hasNextPage: boolean;
1151 |             endCursor: string;
1152 |           };
1153 |         };
1154 |       };
1155 |     }>({
1156 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1157 |       accessToken,
1158 |       query: graphqlQuery,
1159 |     });
1160 | 
1161 |     const data = res.data.data;
1162 |     const edges = data.products.edges;
1163 |     const products = edges.map((edge) => edge.node);
1164 |     const pageInfo = data.products.pageInfo;
1165 |     const next = pageInfo.hasNextPage ? pageInfo.endCursor : undefined;
1166 |     const currencyCode = data.shop.currencyCode;
1167 | 
1168 |     return { products, next, currencyCode };
1169 |   }
1170 | 
1171 |   async loadVariantsByIds(
1172 |     accessToken: string,
1173 |     shop: string,
1174 |     variantIds: string[]
1175 |   ): Promise<LoadVariantsByIdResponse> {
1176 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1177 | 
1178 |     const graphqlQuery = gql`
1179 |       {
1180 |         shop {
1181 |           currencyCode
1182 |         }
1183 |         nodes(ids: ${JSON.stringify(variantIds)}) {
1184 |           __typename
1185 |           ... on ProductVariant {
1186 |             ${productVariantsFragment}
1187 |             product {
1188 |               id
1189 |               title
1190 |               description
1191 |               images(first: 20) {
1192 |                 edges {
1193 |                   node {
1194 |                     ${productImagesFragment}
1195 |                   }
1196 |                 }
1197 |               }
1198 |             }
1199 |           }
1200 |         }
1201 |       }
1202 |     `;
1203 | 
1204 |     const res = await this.shopifyGraphqlRequest<{
1205 |       data: {
1206 |         shop: {
1207 |           currencyCode: string;
1208 |         };
1209 |         nodes: Array<
1210 |           | ({
1211 |               __typename: string;
1212 |             } & ProductVariantWithProductDetails)
1213 |           | null
1214 |         >;
1215 |       };
1216 |     }>({
1217 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1218 |       accessToken,
1219 |       query: graphqlQuery,
1220 |     });
1221 | 
1222 |     const variants = res.data.data.nodes.filter(
1223 |       (
1224 |         node
1225 |       ): node is {
1226 |         __typename: string;
1227 |       } & ProductVariantWithProductDetails =>
1228 |         node?.__typename === "ProductVariant"
1229 |     );
1230 |     const currencyCode = res.data.data.shop.currencyCode;
1231 | 
1232 |     return { variants, currencyCode };
1233 |   }
1234 | 
1235 |   async createDraftOrder(
1236 |     accessToken: string,
1237 |     myshopifyDomain: string,
1238 |     draftOrderData: CreateDraftOrderPayload
1239 |   ): Promise<DraftOrderResponse> {
1240 |     const graphqlQuery = gql`
1241 |       mutation draftOrderCreate($input: DraftOrderInput!) {
1242 |         draftOrderCreate(input: $input) {
1243 |           draftOrder {
1244 |             id
1245 |             name
1246 |           }
1247 |           userErrors {
1248 |             field
1249 |             message
1250 |           }
1251 |         }
1252 |       }
1253 |     `;
1254 | 
1255 |     const res = await this.shopifyGraphqlRequest<{
1256 |       data: {
1257 |         draftOrderCreate: {
1258 |           draftOrder: {
1259 |             id: string;
1260 |             name: string;
1261 |           };
1262 |           userErrors: Array<{
1263 |             field: string[];
1264 |             message: string;
1265 |           }>;
1266 |         };
1267 |       };
1268 |     }>({
1269 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1270 |       accessToken,
1271 |       query: graphqlQuery,
1272 |       variables: {
1273 |         input: draftOrderData,
1274 |       },
1275 |     });
1276 | 
1277 |     const draftOrder = res.data.data.draftOrderCreate.draftOrder;
1278 |     const userErrors = res.data.data.draftOrderCreate.userErrors;
1279 | 
1280 |     if (userErrors.length > 0) {
1281 |       throw getGraphqlShopifyUserError(userErrors, {
1282 |         myshopifyDomain,
1283 |         draftOrderData,
1284 |       });
1285 |     }
1286 | 
1287 |     return {
1288 |       draftOrderId: draftOrder.id,
1289 |       draftOrderName: draftOrder.name,
1290 |     };
1291 |   }
1292 | 
1293 |   async completeDraftOrder(
1294 |     accessToken: string,
1295 |     shop: string,
1296 |     draftOrderId: string,
1297 |     variantId: string
1298 |   ): Promise<CompleteDraftOrderResponse> {
1299 |     // First, load the variant to check if it's available for sale
1300 |     const variantResult = await this.loadVariantsByIds(accessToken, shop, [
1301 |       variantId,
1302 |     ]);
1303 | 
1304 |     if (!variantResult.variants || variantResult.variants.length === 0) {
1305 |       throw new ShopifyProductVariantNotFoundError({
1306 |         contextData: {
1307 |           shop,
1308 |           variantId,
1309 |         },
1310 |       });
1311 |     }
1312 | 
1313 |     const variant = variantResult.variants[0];
1314 | 
1315 |     if (!variant.availableForSale) {
1316 |       throw new ShopifyProductVariantNotAvailableForSaleError({
1317 |         contextData: {
1318 |           shop,
1319 |           variantId,
1320 |         },
1321 |       });
1322 |     }
1323 | 
1324 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1325 | 
1326 |     const graphqlQuery = gql`
1327 |       mutation draftOrderComplete($id: ID!) {
1328 |         draftOrderComplete(id: $id) {
1329 |           draftOrder {
1330 |             id
1331 |             name
1332 |             order {
1333 |               id
1334 |             }
1335 |           }
1336 |           userErrors {
1337 |             field
1338 |             message
1339 |           }
1340 |         }
1341 |       }
1342 |     `;
1343 | 
1344 |     const res = await this.shopifyGraphqlRequest<{
1345 |       data: {
1346 |         draftOrderComplete: {
1347 |           draftOrder: {
1348 |             id: string;
1349 |             name: string;
1350 |             order: {
1351 |               id: string;
1352 |             };
1353 |           };
1354 |           userErrors: Array<{
1355 |             field: string[];
1356 |             message: string;
1357 |           }>;
1358 |         };
1359 |       };
1360 |     }>({
1361 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1362 |       accessToken,
1363 |       query: graphqlQuery,
1364 |       variables: {
1365 |         id: draftOrderId,
1366 |       },
1367 |     });
1368 | 
1369 |     const draftOrder = res.data.data.draftOrderComplete.draftOrder;
1370 |     const order = draftOrder.order;
1371 |     const userErrors = res.data.data.draftOrderComplete.userErrors;
1372 | 
1373 |     if (userErrors && userErrors.length > 0) {
1374 |       throw getGraphqlShopifyUserError(userErrors, {
1375 |         shop,
1376 |         draftOrderId,
1377 |         variantId,
1378 |       });
1379 |     }
1380 | 
1381 |     return {
1382 |       draftOrderId: draftOrder.id,
1383 |       orderId: order.id,
1384 |       draftOrderName: draftOrder.name,
1385 |     };
1386 |   }
1387 | 
1388 |   async loadProductsByIds(
1389 |     accessToken: string,
1390 |     shop: string,
1391 |     productIds: string[]
1392 |   ): Promise<LoadProductsResponse> {
1393 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1394 | 
1395 |     const graphqlQuery = gql`
1396 |       {
1397 |         shop {
1398 |           currencyCode
1399 |         }
1400 |         nodes(ids: ${JSON.stringify(productIds)}) {
1401 |           __typename
1402 |           ... on Product {
1403 |             ${productFragment}
1404 |           }
1405 |         }
1406 |       }
1407 |     `;
1408 | 
1409 |     const res = await this.shopifyGraphqlRequest<{
1410 |       data: {
1411 |         shop: {
1412 |           currencyCode: string;
1413 |         };
1414 |         nodes: Array<
1415 |           | ({
1416 |               __typename: string;
1417 |             } & ProductNode)
1418 |           | null
1419 |         >;
1420 |       };
1421 |     }>({
1422 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1423 |       accessToken,
1424 |       query: graphqlQuery,
1425 |     });
1426 | 
1427 |     const data = res.data.data;
1428 | 
1429 |     const products = data.nodes.filter(
1430 |       (
1431 |         node
1432 |       ): node is {
1433 |         __typename: string;
1434 |       } & ProductNode => node?.__typename === "Product"
1435 |     );
1436 |     const currencyCode = data.shop.currencyCode;
1437 | 
1438 |     return { products, currencyCode };
1439 |   }
1440 | 
1441 |   async updateProductPrice(
1442 |     accessToken: string,
1443 |     shop: string,
1444 |     productId: string,
1445 |     price: string
1446 |   ): Promise<UpdateProductPriceResponse> {
1447 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1448 |     
1449 |     const graphqlQuery = gql`
1450 |       mutation productUpdate($input: ProductInput!) {
1451 |         productUpdate(input: $input) {
1452 |           product {
1453 |             id
1454 |             priceRangeV2 {
1455 |               minVariantPrice {
1456 |                 amount
1457 |                 currencyCode
1458 |               }
1459 |               maxVariantPrice {
1460 |                 amount
1461 |                 currencyCode
1462 |               }
1463 |             }
1464 |             variants(first: 100) {
1465 |               edges {
1466 |                 node {
1467 |                   id
1468 |                   price
1469 |                 }
1470 |               }
1471 |             }
1472 |           }
1473 |           userErrors {
1474 |             field
1475 |             message
1476 |           }
1477 |         }
1478 |       }
1479 |     `;
1480 |   
1481 |     const variables = {
1482 |       input: {
1483 |         id: productId,
1484 |         variants: {
1485 |           price: price
1486 |         }
1487 |       }
1488 |     };
1489 |   
1490 |     const res = await this.shopifyGraphqlRequest<{
1491 |       data: {
1492 |         productUpdate: {
1493 |           product: {
1494 |             id: string;
1495 |             priceRangeV2: {
1496 |               minVariantPrice: {amount: string; currencyCode: string};
1497 |               maxVariantPrice: {amount: string; currencyCode: string};
1498 |             };
1499 |             variants: {
1500 |               edges: Array<{
1501 |                 node: {
1502 |                   id: string;
1503 |                   price: string;
1504 |                 };
1505 |               }>;
1506 |             };
1507 |           };
1508 |           userErrors: Array<{field: string; message: string}>;
1509 |         };
1510 |       };
1511 |     }>({
1512 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1513 |       accessToken,
1514 |       query: graphqlQuery,
1515 |       variables
1516 |     });
1517 |   
1518 |     const data = res.data.data;
1519 |     
1520 |     if (data.productUpdate.userErrors.length > 0) {
1521 |       return {
1522 |         success: false,
1523 |         errors: data.productUpdate.userErrors
1524 |       };
1525 |     }
1526 |   
1527 |     return {
1528 |       success: true,
1529 |       product: data.productUpdate.product
1530 |     };
1531 |   }
1532 | 
1533 |   async loadCustomers(
1534 |     accessToken: string,
1535 |     shop: string,
1536 |     limit?: number,
1537 |     next?: string
1538 |   ): Promise<LoadCustomersResponse> {
1539 |     const res = await this.shopifyHTTPRequest<{ customers: any[] }>({
1540 |       method: "GET",
1541 |       url: `https://${shop}/admin/api/${this.SHOPIFY_API_VERSION}/customers.json`,
1542 |       accessToken,
1543 |       params: {
1544 |         limit: limit ?? 250,
1545 |         page_info: next,
1546 |         fields: ["id", "email", "tags"].join(","),
1547 |       },
1548 |     });
1549 | 
1550 |     const customers = res.data.customers;
1551 |     const nextPageInfo = ShopifyClient.getShopifyOrdersNextPage(
1552 |       res.headers.get("link")
1553 |     );
1554 | 
1555 |     return { customers, next: nextPageInfo };
1556 |   }
1557 | 
1558 |   async tagCustomer(
1559 |     accessToken: string,
1560 |     shop: string,
1561 |     tags: string[],
1562 |     externalCustomerId: string
1563 |   ): Promise<boolean> {
1564 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1565 | 
1566 |     const graphqlQuery = gql`
1567 |       mutation tagsAdd($id: ID!, $tags: [String!]!) {
1568 |         tagsAdd(id: $id, tags: $tags) {
1569 |           userErrors {
1570 |             field
1571 |             message
1572 |           }
1573 |           node {
1574 |             id
1575 |           }
1576 |         }
1577 |       }
1578 |     `;
1579 | 
1580 |     const res = await this.shopifyGraphqlRequest<{
1581 |       data: {
1582 |         tagsAdd: {
1583 |           userErrors: Array<{
1584 |             field: string[];
1585 |             message: string;
1586 |           }>;
1587 |           node: {
1588 |             id: string;
1589 |           };
1590 |         };
1591 |       };
1592 |     }>({
1593 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1594 |       accessToken,
1595 |       query: graphqlQuery,
1596 |       variables: {
1597 |         id: `gid://shopify/Customer/${externalCustomerId}`,
1598 |         tags,
1599 |       },
1600 |     });
1601 | 
1602 |     const userErrors = res.data.data.tagsAdd.userErrors;
1603 |     if (userErrors.length > 0) {
1604 |       const errorMessages = userErrors.map((error) => error.message).join(", ");
1605 |       throw new Error(errorMessages);
1606 |     }
1607 | 
1608 |     return true;
1609 |   }
1610 | 
1611 |   async subscribeWebhook(
1612 |     accessToken: string,
1613 |     shop: string,
1614 |     callbackUrl: string,
1615 |     topic: ShopifyWebhookTopic
1616 |   ): Promise<ShopifyWebhook> {
1617 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1618 | 
1619 |     const graphqlQuery = gql`
1620 |       mutation webhookSubscriptionCreate(
1621 |         $topic: WebhookSubscriptionTopic!
1622 |         $webhookSubscription: WebhookSubscriptionInput!
1623 |       ) {
1624 |         webhookSubscriptionCreate(
1625 |           topic: $topic
1626 |           webhookSubscription: $webhookSubscription
1627 |         ) {
1628 |           webhookSubscription {
1629 |             id
1630 |             topic
1631 |             endpoint {
1632 |               __typename
1633 |               ... on WebhookHttpEndpoint {
1634 |                 callbackUrl
1635 |               }
1636 |             }
1637 |           }
1638 |           userErrors {
1639 |             field
1640 |             message
1641 |           }
1642 |         }
1643 |       }
1644 |     `;
1645 | 
1646 |     const res = await this.shopifyGraphqlRequest<{
1647 |       data: {
1648 |         webhookSubscriptionCreate: {
1649 |           webhookSubscription: {
1650 |             id: string;
1651 |             topic: ShopifyWebhookTopicGraphql;
1652 |             endpoint: {
1653 |               callbackUrl: string;
1654 |             };
1655 |           };
1656 |           userErrors: Array<{
1657 |             field: string[];
1658 |             message: string;
1659 |           }>;
1660 |         };
1661 |       };
1662 |     }>({
1663 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1664 |       accessToken,
1665 |       query: graphqlQuery,
1666 |       variables: {
1667 |         topic: this.mapTopicToGraphqlTopic(topic),
1668 |         webhookSubscription: {
1669 |           callbackUrl,
1670 |         },
1671 |       },
1672 |     });
1673 | 
1674 |     const webhookSubscription =
1675 |       res.data.data.webhookSubscriptionCreate.webhookSubscription;
1676 |     const userErrors = res.data.data.webhookSubscriptionCreate.userErrors;
1677 | 
1678 |     if (userErrors.length > 0) {
1679 |       throw getGraphqlShopifyUserError(userErrors, {
1680 |         shop,
1681 |         topic,
1682 |         callbackUrl: callbackUrl,
1683 |       });
1684 |     }
1685 | 
1686 |     return {
1687 |       id: webhookSubscription.id,
1688 |       topic: this.mapGraphqlTopicToTopic(webhookSubscription.topic),
1689 |       callbackUrl: webhookSubscription.endpoint.callbackUrl,
1690 |     };
1691 |   }
1692 | 
1693 |   async findWebhookByTopicAndCallbackUrl(
1694 |     accessToken: string,
1695 |     shop: string,
1696 |     callbackUrl: string,
1697 |     topic: ShopifyWebhookTopic
1698 |   ): Promise<ShopifyWebhook | null> {
1699 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1700 | 
1701 |     const graphqlQuery = gql`
1702 |       query webhookSubscriptions(
1703 |         $topics: [WebhookSubscriptionTopic!]
1704 |         $callbackUrl: URL!
1705 |       ) {
1706 |         webhookSubscriptions(
1707 |           first: 10
1708 |           topics: $topics
1709 |           callbackUrl: $callbackUrl
1710 |         ) {
1711 |           edges {
1712 |             node {
1713 |               id
1714 |               topic
1715 |               endpoint {
1716 |                 __typename
1717 |                 ... on WebhookHttpEndpoint {
1718 |                   callbackUrl
1719 |                 }
1720 |               }
1721 |             }
1722 |           }
1723 |         }
1724 |       }
1725 |     `;
1726 | 
1727 |     const res = await this.shopifyGraphqlRequest<{
1728 |       data: {
1729 |         webhookSubscriptions: {
1730 |           edges: {
1731 |             node: {
1732 |               id: string;
1733 |               topic: ShopifyWebhookTopicGraphql;
1734 |               endpoint: {
1735 |                 callbackUrl: string;
1736 |               };
1737 |             };
1738 |           }[];
1739 |         };
1740 |       };
1741 |     }>({
1742 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1743 |       accessToken,
1744 |       query: graphqlQuery,
1745 |       variables: {
1746 |         topics: [this.mapTopicToGraphqlTopic(topic)],
1747 |         callbackUrl,
1748 |       },
1749 |     });
1750 | 
1751 |     const webhookSubscriptions = res.data.data.webhookSubscriptions.edges;
1752 |     if (webhookSubscriptions.length === 0) {
1753 |       return null;
1754 |     }
1755 | 
1756 |     const webhookSubscription = webhookSubscriptions[0].node;
1757 |     return {
1758 |       id: webhookSubscription.id,
1759 |       topic: this.mapGraphqlTopicToTopic(webhookSubscription.topic),
1760 |       callbackUrl: webhookSubscription.endpoint.callbackUrl,
1761 |     };
1762 |   }
1763 | 
1764 |   async unsubscribeWebhook(
1765 |     accessToken: string,
1766 |     shop: string,
1767 |     webhookId: string
1768 |   ): Promise<void> {
1769 |     const myshopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1770 | 
1771 |     const graphqlQuery = gql`
1772 |       mutation webhookSubscriptionDelete($id: ID!) {
1773 |         webhookSubscriptionDelete(id: $id) {
1774 |           userErrors {
1775 |             field
1776 |             message
1777 |           }
1778 |           deletedWebhookSubscriptionId
1779 |         }
1780 |       }
1781 |     `;
1782 | 
1783 |     const res = await this.shopifyGraphqlRequest<{
1784 |       data: {
1785 |         webhookSubscriptionDelete: {
1786 |           deletedWebhookSubscriptionId: string;
1787 |           userErrors: Array<{
1788 |             field: string[];
1789 |             message: string;
1790 |           }>;
1791 |         };
1792 |       };
1793 |     }>({
1794 |       url: `https://${myshopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1795 |       accessToken,
1796 |       query: graphqlQuery,
1797 |       variables: {
1798 |         id: webhookId,
1799 |       },
1800 |     });
1801 | 
1802 |     const userErrors = res.data.data.webhookSubscriptionDelete.userErrors;
1803 | 
1804 |     if (userErrors.length > 0) {
1805 |       throw getGraphqlShopifyUserError(userErrors, {
1806 |         shop,
1807 |         webhookId,
1808 |       });
1809 |     }
1810 |   }
1811 | 
1812 |   private getOrdersFields(fields?: string[]): string {
1813 |     const defaultFields = [
1814 |       "id",
1815 |       "order_number",
1816 |       "total_price",
1817 |       "discount_codes",
1818 |       "currency",
1819 |       "financial_status",
1820 |       "total_shipping_price_set",
1821 |       "created_at",
1822 |       "customer",
1823 |       "email",
1824 |     ];
1825 | 
1826 |     if (!fields) return defaultFields.join(",");
1827 | 
1828 |     return [...defaultFields, ...fields].join(",");
1829 |   }
1830 | 
1831 |   private getIds(ids?: string[]): string | undefined {
1832 |     if (!ids) return;
1833 |     return ids.join(",");
1834 |   }
1835 | 
1836 |   public getIdFromGid(gid: string): string {
1837 |     const id = gid.split("/").pop();
1838 |     if (!id) {
1839 |       throw new Error("Invalid GID");
1840 |     }
1841 |     return id;
1842 |   }
1843 | 
1844 |   async getPriceRule(
1845 |     accessToken: string,
1846 |     shop: string,
1847 |     priceRuleInput: GetPriceRuleInput
1848 |   ): Promise<GetPriceRuleResponse> {
1849 |     const myShopifyDomain = await this.getMyShopifyDomain(accessToken, shop);
1850 | 
1851 |     const graphqlQuery = gql`
1852 |       query priceRules(first:250,$query: String) {
1853 |         priceRules(query: $query) {
1854 |           nodes {
1855 |             id
1856 |             title
1857 |             status
1858 |           }
1859 |         }
1860 |       }
1861 |     `;
1862 | 
1863 |     const res = await this.shopifyGraphqlRequest<{
1864 |       data: GetPriceRuleResponse;
1865 |     }>({
1866 |       url: `https://${myShopifyDomain}/admin/api/${this.SHOPIFY_API_VERSION}/graphql.json`,
1867 |       accessToken,
1868 |       query: graphqlQuery,
1869 |       variables: priceRuleInput,
1870 |     });
1871 | 
1872 |     return res.data.data;
1873 |   }
1874 | 
1875 |   private mapGraphqlTopicToTopic(
1876 |     topic: ShopifyWebhookTopicGraphql
1877 |   ): ShopifyWebhookTopic {
1878 |     switch (topic) {
1879 |       case ShopifyWebhookTopicGraphql.ORDERS_UPDATED:
1880 |         return ShopifyWebhookTopic.ORDERS_UPDATED;
1881 |     }
1882 |   }
1883 | 
1884 |   private mapTopicToGraphqlTopic(
1885 |     topic: ShopifyWebhookTopic
1886 |   ): ShopifyWebhookTopicGraphql {
1887 |     switch (topic) {
1888 |       case ShopifyWebhookTopic.ORDERS_UPDATED:
1889 |         return ShopifyWebhookTopicGraphql.ORDERS_UPDATED;
1890 |     }
1891 |   }
1892 | }
1893 | 
```