@klaudworks/shopify-mcp 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +531 -0
- package/dist/index.js +107 -0
- package/dist/lib/formatters.js +72 -0
- package/dist/lib/shopifyAuth.js +96 -0
- package/dist/lib/toolUtils.js +36 -0
- package/dist/tools/completeDraftOrder.js +76 -0
- package/dist/tools/createCustomer.js +142 -0
- package/dist/tools/createDraftOrder.js +160 -0
- package/dist/tools/createFulfillment.js +100 -0
- package/dist/tools/createProduct.js +123 -0
- package/dist/tools/createRefund.js +115 -0
- package/dist/tools/deleteCustomer.js +47 -0
- package/dist/tools/deleteMetafields.js +54 -0
- package/dist/tools/deleteProduct.js +43 -0
- package/dist/tools/deleteProductVariants.js +82 -0
- package/dist/tools/getCollectionById.js +123 -0
- package/dist/tools/getCollections.js +88 -0
- package/dist/tools/getCustomerById.js +122 -0
- package/dist/tools/getCustomerOrders.js +131 -0
- package/dist/tools/getCustomers.js +125 -0
- package/dist/tools/getFulfillmentOrders.js +106 -0
- package/dist/tools/getInventoryItems.js +86 -0
- package/dist/tools/getInventoryLevels.js +82 -0
- package/dist/tools/getLocations.js +85 -0
- package/dist/tools/getMarkets.js +91 -0
- package/dist/tools/getMetafieldDefinitions.js +112 -0
- package/dist/tools/getMetafields.js +68 -0
- package/dist/tools/getOrderById.js +212 -0
- package/dist/tools/getOrderRefundDetails.js +131 -0
- package/dist/tools/getOrderTransactions.js +85 -0
- package/dist/tools/getOrders.js +148 -0
- package/dist/tools/getPriceLists.js +92 -0
- package/dist/tools/getProductById.js +171 -0
- package/dist/tools/getProductVariantsDetailed.js +139 -0
- package/dist/tools/getProducts.js +155 -0
- package/dist/tools/getShopInfo.js +74 -0
- package/dist/tools/manageCustomerAddress.js +149 -0
- package/dist/tools/manageProductOptions.js +293 -0
- package/dist/tools/manageProductVariants.js +203 -0
- package/dist/tools/manageTags.js +79 -0
- package/dist/tools/mergeCustomers.js +74 -0
- package/dist/tools/orderCancel.js +77 -0
- package/dist/tools/orderCloseOpen.js +74 -0
- package/dist/tools/orderMarkAsPaid.js +51 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/setInventoryQuantities.js +74 -0
- package/dist/tools/setMetafields.js +61 -0
- package/dist/tools/updateCustomer.js +119 -0
- package/dist/tools/updateOrder.js +131 -0
- package/dist/tools/updateProduct.js +132 -0
- package/package.json +66 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { handleToolError } from "../lib/toolUtils.js";
|
|
4
|
+
// Input schema for getProducts
|
|
5
|
+
const GetProductsInputSchema = z.object({
|
|
6
|
+
searchTitle: z.string().optional().describe("Search by title (convenience filter, wraps in title:*...*). Use 'query' for advanced filtering."),
|
|
7
|
+
limit: z.number().default(10),
|
|
8
|
+
after: z.string().optional().describe("Cursor for forward pagination"),
|
|
9
|
+
before: z.string().optional().describe("Cursor for backward pagination"),
|
|
10
|
+
sortKey: z.enum([
|
|
11
|
+
"CREATED_AT", "ID", "INVENTORY_TOTAL", "PRODUCT_TYPE",
|
|
12
|
+
"PUBLISHED_AT", "RELEVANCE", "TITLE", "UPDATED_AT", "VENDOR"
|
|
13
|
+
]).optional().describe("Sort key for products"),
|
|
14
|
+
reverse: z.boolean().optional().describe("Reverse the sort order"),
|
|
15
|
+
query: z.string().optional().describe("Raw query string for advanced filtering (e.g. 'status:active vendor:Nike tag:sale')")
|
|
16
|
+
});
|
|
17
|
+
// Will be initialized in index.ts
|
|
18
|
+
let shopifyClient;
|
|
19
|
+
const getProducts = {
|
|
20
|
+
name: "get-products",
|
|
21
|
+
description: "Get all products or search by title",
|
|
22
|
+
schema: GetProductsInputSchema,
|
|
23
|
+
// Add initialize method to set up the GraphQL client
|
|
24
|
+
initialize(client) {
|
|
25
|
+
shopifyClient = client;
|
|
26
|
+
},
|
|
27
|
+
execute: async (input) => {
|
|
28
|
+
try {
|
|
29
|
+
const { searchTitle, limit, after, before, sortKey, reverse, query: rawQuery } = input;
|
|
30
|
+
// Build query string from convenience filters and raw query
|
|
31
|
+
const queryParts = [];
|
|
32
|
+
if (searchTitle) {
|
|
33
|
+
queryParts.push(`title:*${searchTitle}*`);
|
|
34
|
+
}
|
|
35
|
+
if (rawQuery) {
|
|
36
|
+
queryParts.push(rawQuery);
|
|
37
|
+
}
|
|
38
|
+
const queryFilter = queryParts.join(" ") || undefined;
|
|
39
|
+
const query = gql `
|
|
40
|
+
#graphql
|
|
41
|
+
|
|
42
|
+
query GetProducts($first: Int!, $query: String, $after: String, $before: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
|
|
43
|
+
products(first: $first, query: $query, after: $after, before: $before, sortKey: $sortKey, reverse: $reverse) {
|
|
44
|
+
edges {
|
|
45
|
+
node {
|
|
46
|
+
id
|
|
47
|
+
title
|
|
48
|
+
description
|
|
49
|
+
handle
|
|
50
|
+
status
|
|
51
|
+
createdAt
|
|
52
|
+
updatedAt
|
|
53
|
+
totalInventory
|
|
54
|
+
priceRangeV2 {
|
|
55
|
+
minVariantPrice {
|
|
56
|
+
amount
|
|
57
|
+
currencyCode
|
|
58
|
+
}
|
|
59
|
+
maxVariantPrice {
|
|
60
|
+
amount
|
|
61
|
+
currencyCode
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
media(first: 1) {
|
|
65
|
+
edges {
|
|
66
|
+
node {
|
|
67
|
+
... on MediaImage {
|
|
68
|
+
id
|
|
69
|
+
image {
|
|
70
|
+
url
|
|
71
|
+
altText
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
variants(first: 5) {
|
|
78
|
+
edges {
|
|
79
|
+
node {
|
|
80
|
+
id
|
|
81
|
+
title
|
|
82
|
+
price
|
|
83
|
+
inventoryQuantity
|
|
84
|
+
sku
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
pageInfo {
|
|
91
|
+
hasNextPage
|
|
92
|
+
hasPreviousPage
|
|
93
|
+
startCursor
|
|
94
|
+
endCursor
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
const variables = {
|
|
100
|
+
first: limit,
|
|
101
|
+
query: queryFilter,
|
|
102
|
+
...(after && { after }),
|
|
103
|
+
...(before && { before }),
|
|
104
|
+
...(sortKey && { sortKey }),
|
|
105
|
+
...(reverse !== undefined && { reverse })
|
|
106
|
+
};
|
|
107
|
+
const data = (await shopifyClient.request(query, variables));
|
|
108
|
+
// Extract and format product data
|
|
109
|
+
const products = data.products.edges.map((edge) => {
|
|
110
|
+
const product = edge.node;
|
|
111
|
+
// Format variants
|
|
112
|
+
const variants = product.variants.edges.map((variantEdge) => ({
|
|
113
|
+
id: variantEdge.node.id,
|
|
114
|
+
title: variantEdge.node.title,
|
|
115
|
+
price: variantEdge.node.price,
|
|
116
|
+
inventoryQuantity: variantEdge.node.inventoryQuantity,
|
|
117
|
+
sku: variantEdge.node.sku
|
|
118
|
+
}));
|
|
119
|
+
// Get first image if it exists
|
|
120
|
+
const firstMedia = product.media.edges.find((e) => e.node.image);
|
|
121
|
+
const imageUrl = firstMedia?.node.image?.url || null;
|
|
122
|
+
return {
|
|
123
|
+
id: product.id,
|
|
124
|
+
title: product.title,
|
|
125
|
+
description: product.description,
|
|
126
|
+
handle: product.handle,
|
|
127
|
+
status: product.status,
|
|
128
|
+
createdAt: product.createdAt,
|
|
129
|
+
updatedAt: product.updatedAt,
|
|
130
|
+
totalInventory: product.totalInventory,
|
|
131
|
+
priceRange: {
|
|
132
|
+
minPrice: {
|
|
133
|
+
amount: product.priceRangeV2.minVariantPrice.amount,
|
|
134
|
+
currencyCode: product.priceRangeV2.minVariantPrice.currencyCode
|
|
135
|
+
},
|
|
136
|
+
maxPrice: {
|
|
137
|
+
amount: product.priceRangeV2.maxVariantPrice.amount,
|
|
138
|
+
currencyCode: product.priceRangeV2.maxVariantPrice.currencyCode
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
imageUrl,
|
|
142
|
+
variants
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
products,
|
|
147
|
+
pageInfo: data.products.pageInfo
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
handleToolError("fetch products", error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
export { getProducts };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { handleToolError } from "../lib/toolUtils.js";
|
|
4
|
+
const GetShopInfoInputSchema = z.object({});
|
|
5
|
+
let shopifyClient;
|
|
6
|
+
const getShopInfo = {
|
|
7
|
+
name: "get-shop-info",
|
|
8
|
+
description: "Get shop configuration including name, plan, currencies, features, payment settings, tax config, and contact info",
|
|
9
|
+
schema: GetShopInfoInputSchema,
|
|
10
|
+
initialize(client) {
|
|
11
|
+
shopifyClient = client;
|
|
12
|
+
},
|
|
13
|
+
execute: async (_input) => {
|
|
14
|
+
try {
|
|
15
|
+
const query = gql `
|
|
16
|
+
#graphql
|
|
17
|
+
|
|
18
|
+
query GetShopInfo {
|
|
19
|
+
shop {
|
|
20
|
+
id
|
|
21
|
+
name
|
|
22
|
+
email
|
|
23
|
+
contactEmail
|
|
24
|
+
myshopifyDomain
|
|
25
|
+
primaryDomain {
|
|
26
|
+
url
|
|
27
|
+
host
|
|
28
|
+
}
|
|
29
|
+
plan {
|
|
30
|
+
publicDisplayName
|
|
31
|
+
partnerDevelopment
|
|
32
|
+
shopifyPlus
|
|
33
|
+
}
|
|
34
|
+
currencyCode
|
|
35
|
+
enabledPresentmentCurrencies
|
|
36
|
+
ianaTimezone
|
|
37
|
+
timezoneAbbreviation
|
|
38
|
+
taxShipping
|
|
39
|
+
taxesIncluded
|
|
40
|
+
setupRequired
|
|
41
|
+
features {
|
|
42
|
+
giftCards
|
|
43
|
+
reports
|
|
44
|
+
storefront
|
|
45
|
+
harmonizedSystemCode
|
|
46
|
+
avalaraAvatax
|
|
47
|
+
sellsSubscriptions
|
|
48
|
+
}
|
|
49
|
+
paymentSettings {
|
|
50
|
+
supportedDigitalWallets
|
|
51
|
+
}
|
|
52
|
+
shopAddress {
|
|
53
|
+
address1
|
|
54
|
+
address2
|
|
55
|
+
city
|
|
56
|
+
province
|
|
57
|
+
provinceCode
|
|
58
|
+
country
|
|
59
|
+
countryCodeV2
|
|
60
|
+
zip
|
|
61
|
+
phone
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const data = await shopifyClient.request(query);
|
|
67
|
+
return { shop: data.shop };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
handleToolError("fetch shop info", error);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
export { getShopInfo };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { checkUserErrors, handleToolError } from "../lib/toolUtils.js";
|
|
4
|
+
import { shippingAddressSchema } from "../lib/formatters.js";
|
|
5
|
+
const ManageCustomerAddressInputSchema = z.object({
|
|
6
|
+
customerId: z.string().describe("Customer GID, e.g. gid://shopify/Customer/123"),
|
|
7
|
+
action: z.enum(["create", "update", "delete"]).describe("Action to perform"),
|
|
8
|
+
addressId: z.string().optional().describe("Address GID (required for update and delete)"),
|
|
9
|
+
address: shippingAddressSchema.optional().describe("Address fields (required for create and update)"),
|
|
10
|
+
setAsDefault: z.boolean().optional().describe("Set this address as the customer's default"),
|
|
11
|
+
});
|
|
12
|
+
let shopifyClient;
|
|
13
|
+
const manageCustomerAddress = {
|
|
14
|
+
name: "manage-customer-address",
|
|
15
|
+
description: "Create, update, or delete a customer's mailing address. Can optionally set as default.",
|
|
16
|
+
schema: ManageCustomerAddressInputSchema,
|
|
17
|
+
initialize(client) {
|
|
18
|
+
shopifyClient = client;
|
|
19
|
+
},
|
|
20
|
+
execute: async (input) => {
|
|
21
|
+
try {
|
|
22
|
+
if (input.action === "create") {
|
|
23
|
+
if (!input.address) {
|
|
24
|
+
throw new Error("Address fields are required for create action");
|
|
25
|
+
}
|
|
26
|
+
const query = gql `
|
|
27
|
+
#graphql
|
|
28
|
+
|
|
29
|
+
mutation customerAddressCreate(
|
|
30
|
+
$customerId: ID!
|
|
31
|
+
$address: MailingAddressInput!
|
|
32
|
+
$setAsDefault: Boolean
|
|
33
|
+
) {
|
|
34
|
+
customerAddressCreate(
|
|
35
|
+
customerId: $customerId
|
|
36
|
+
address: $address
|
|
37
|
+
setAsDefault: $setAsDefault
|
|
38
|
+
) {
|
|
39
|
+
address {
|
|
40
|
+
id
|
|
41
|
+
address1
|
|
42
|
+
address2
|
|
43
|
+
city
|
|
44
|
+
company
|
|
45
|
+
countryCodeV2
|
|
46
|
+
firstName
|
|
47
|
+
lastName
|
|
48
|
+
phone
|
|
49
|
+
provinceCode
|
|
50
|
+
zip
|
|
51
|
+
}
|
|
52
|
+
userErrors {
|
|
53
|
+
field
|
|
54
|
+
message
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
const data = (await shopifyClient.request(query, {
|
|
60
|
+
customerId: input.customerId,
|
|
61
|
+
address: input.address,
|
|
62
|
+
setAsDefault: input.setAsDefault,
|
|
63
|
+
}));
|
|
64
|
+
checkUserErrors(data.customerAddressCreate.userErrors, "create address");
|
|
65
|
+
return { address: data.customerAddressCreate.address };
|
|
66
|
+
}
|
|
67
|
+
else if (input.action === "update") {
|
|
68
|
+
if (!input.addressId) {
|
|
69
|
+
throw new Error("addressId is required for update action");
|
|
70
|
+
}
|
|
71
|
+
if (!input.address) {
|
|
72
|
+
throw new Error("Address fields are required for update action");
|
|
73
|
+
}
|
|
74
|
+
const query = gql `
|
|
75
|
+
#graphql
|
|
76
|
+
|
|
77
|
+
mutation customerAddressUpdate(
|
|
78
|
+
$customerId: ID!
|
|
79
|
+
$addressId: ID!
|
|
80
|
+
$address: MailingAddressInput!
|
|
81
|
+
$setAsDefault: Boolean
|
|
82
|
+
) {
|
|
83
|
+
customerAddressUpdate(
|
|
84
|
+
customerId: $customerId
|
|
85
|
+
addressId: $addressId
|
|
86
|
+
address: $address
|
|
87
|
+
setAsDefault: $setAsDefault
|
|
88
|
+
) {
|
|
89
|
+
address {
|
|
90
|
+
id
|
|
91
|
+
address1
|
|
92
|
+
address2
|
|
93
|
+
city
|
|
94
|
+
company
|
|
95
|
+
countryCodeV2
|
|
96
|
+
firstName
|
|
97
|
+
lastName
|
|
98
|
+
phone
|
|
99
|
+
provinceCode
|
|
100
|
+
zip
|
|
101
|
+
}
|
|
102
|
+
userErrors {
|
|
103
|
+
field
|
|
104
|
+
message
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
const data = (await shopifyClient.request(query, {
|
|
110
|
+
customerId: input.customerId,
|
|
111
|
+
addressId: input.addressId,
|
|
112
|
+
address: input.address,
|
|
113
|
+
setAsDefault: input.setAsDefault,
|
|
114
|
+
}));
|
|
115
|
+
checkUserErrors(data.customerAddressUpdate.userErrors, "update address");
|
|
116
|
+
return { address: data.customerAddressUpdate.address };
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// delete
|
|
120
|
+
if (!input.addressId) {
|
|
121
|
+
throw new Error("addressId is required for delete action");
|
|
122
|
+
}
|
|
123
|
+
const query = gql `
|
|
124
|
+
#graphql
|
|
125
|
+
|
|
126
|
+
mutation customerAddressDelete($customerId: ID!, $addressId: ID!) {
|
|
127
|
+
customerAddressDelete(customerId: $customerId, addressId: $addressId) {
|
|
128
|
+
deletedAddressId
|
|
129
|
+
userErrors {
|
|
130
|
+
field
|
|
131
|
+
message
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
const data = (await shopifyClient.request(query, {
|
|
137
|
+
customerId: input.customerId,
|
|
138
|
+
addressId: input.addressId,
|
|
139
|
+
}));
|
|
140
|
+
checkUserErrors(data.customerAddressDelete.userErrors, "delete address");
|
|
141
|
+
return { deletedAddressId: data.customerAddressDelete.deletedAddressId };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
handleToolError(`${input.action} customer address`, error);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
export { manageCustomerAddress };
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { checkUserErrors, handleToolError } from "../lib/toolUtils.js";
|
|
4
|
+
// Input schema for manageProductOptions
|
|
5
|
+
const ManageProductOptionsInputSchema = z.object({
|
|
6
|
+
productId: z.string().min(1).describe("Shopify product GID"),
|
|
7
|
+
action: z.enum(["create", "update", "delete"]),
|
|
8
|
+
variantStrategy: z
|
|
9
|
+
.enum(["LEAVE_AS_IS", "CREATE"])
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Strategy for variant creation when adding options. LEAVE_AS_IS (default) keeps existing variants, CREATE generates new variant combinations."),
|
|
12
|
+
// For create
|
|
13
|
+
options: z
|
|
14
|
+
.array(z.object({
|
|
15
|
+
name: z.string().describe("Option name, e.g. 'Size' or 'Color'"),
|
|
16
|
+
position: z.number().optional().describe("Position of the option (1-based)"),
|
|
17
|
+
values: z
|
|
18
|
+
.array(z.string())
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Option values, e.g. ['Small', 'Medium', 'Large']"),
|
|
21
|
+
}))
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Options to create (action=create)"),
|
|
24
|
+
// For update
|
|
25
|
+
optionId: z.string().optional().describe("Option GID to update (action=update)"),
|
|
26
|
+
name: z.string().optional().describe("New name for the option (action=update)"),
|
|
27
|
+
position: z.number().optional().describe("New position (action=update)"),
|
|
28
|
+
valuesToAdd: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Values to add (action=update)"),
|
|
32
|
+
valuesToDelete: z
|
|
33
|
+
.array(z.string())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Value GIDs to delete (action=update)"),
|
|
36
|
+
// For delete
|
|
37
|
+
optionIds: z
|
|
38
|
+
.array(z.string())
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Option GIDs to delete (action=delete)"),
|
|
41
|
+
});
|
|
42
|
+
// Will be initialized in index.ts
|
|
43
|
+
let shopifyClient;
|
|
44
|
+
const manageProductOptions = {
|
|
45
|
+
name: "manage-product-options",
|
|
46
|
+
description: "Create, update, or delete product options (e.g. Size, Color). Use action='create' to add options, 'update' to rename or add/remove values, 'delete' to remove options.",
|
|
47
|
+
schema: ManageProductOptionsInputSchema,
|
|
48
|
+
initialize(client) {
|
|
49
|
+
shopifyClient = client;
|
|
50
|
+
},
|
|
51
|
+
execute: async (input) => {
|
|
52
|
+
try {
|
|
53
|
+
const { productId, action } = input;
|
|
54
|
+
if (action === "create") {
|
|
55
|
+
if (!input.options?.length) {
|
|
56
|
+
throw new Error("options array is required for action=create");
|
|
57
|
+
}
|
|
58
|
+
const query = gql `
|
|
59
|
+
#graphql
|
|
60
|
+
|
|
61
|
+
mutation productOptionsCreate(
|
|
62
|
+
$productId: ID!
|
|
63
|
+
$options: [OptionCreateInput!]!
|
|
64
|
+
$variantStrategy: ProductOptionCreateVariantStrategy
|
|
65
|
+
) {
|
|
66
|
+
productOptionsCreate(
|
|
67
|
+
productId: $productId
|
|
68
|
+
options: $options
|
|
69
|
+
variantStrategy: $variantStrategy
|
|
70
|
+
) {
|
|
71
|
+
product {
|
|
72
|
+
...ProductOptionsFields
|
|
73
|
+
}
|
|
74
|
+
userErrors {
|
|
75
|
+
field
|
|
76
|
+
message
|
|
77
|
+
code
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fragment ProductOptionsFields on Product {
|
|
83
|
+
id
|
|
84
|
+
title
|
|
85
|
+
options {
|
|
86
|
+
id
|
|
87
|
+
name
|
|
88
|
+
position
|
|
89
|
+
optionValues {
|
|
90
|
+
id
|
|
91
|
+
name
|
|
92
|
+
hasVariants
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
variants(first: 20) {
|
|
96
|
+
edges {
|
|
97
|
+
node {
|
|
98
|
+
id
|
|
99
|
+
title
|
|
100
|
+
price
|
|
101
|
+
selectedOptions {
|
|
102
|
+
name
|
|
103
|
+
value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const options = input.options.map((o) => ({
|
|
111
|
+
name: o.name,
|
|
112
|
+
...(o.position !== undefined && { position: o.position }),
|
|
113
|
+
...(o.values && {
|
|
114
|
+
values: o.values.map((v) => ({ name: v })),
|
|
115
|
+
}),
|
|
116
|
+
}));
|
|
117
|
+
const data = (await shopifyClient.request(query, {
|
|
118
|
+
productId,
|
|
119
|
+
options,
|
|
120
|
+
variantStrategy: input.variantStrategy || "LEAVE_AS_IS",
|
|
121
|
+
}));
|
|
122
|
+
checkUserErrors(data.productOptionsCreate.userErrors, "create options");
|
|
123
|
+
return formatProductResponse(data.productOptionsCreate.product);
|
|
124
|
+
}
|
|
125
|
+
if (action === "update") {
|
|
126
|
+
if (!input.optionId) {
|
|
127
|
+
throw new Error("optionId is required for action=update");
|
|
128
|
+
}
|
|
129
|
+
const query = gql `
|
|
130
|
+
#graphql
|
|
131
|
+
|
|
132
|
+
mutation productOptionUpdate(
|
|
133
|
+
$productId: ID!
|
|
134
|
+
$option: OptionUpdateInput!
|
|
135
|
+
$optionValuesToAdd: [OptionValueCreateInput!]
|
|
136
|
+
$optionValuesToDelete: [ID!]
|
|
137
|
+
) {
|
|
138
|
+
productOptionUpdate(
|
|
139
|
+
productId: $productId
|
|
140
|
+
option: $option
|
|
141
|
+
optionValuesToAdd: $optionValuesToAdd
|
|
142
|
+
optionValuesToDelete: $optionValuesToDelete
|
|
143
|
+
) {
|
|
144
|
+
product {
|
|
145
|
+
...ProductOptionsFields
|
|
146
|
+
}
|
|
147
|
+
userErrors {
|
|
148
|
+
field
|
|
149
|
+
message
|
|
150
|
+
code
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fragment ProductOptionsFields on Product {
|
|
156
|
+
id
|
|
157
|
+
title
|
|
158
|
+
options {
|
|
159
|
+
id
|
|
160
|
+
name
|
|
161
|
+
position
|
|
162
|
+
optionValues {
|
|
163
|
+
id
|
|
164
|
+
name
|
|
165
|
+
hasVariants
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
variants(first: 20) {
|
|
169
|
+
edges {
|
|
170
|
+
node {
|
|
171
|
+
id
|
|
172
|
+
title
|
|
173
|
+
price
|
|
174
|
+
selectedOptions {
|
|
175
|
+
name
|
|
176
|
+
value
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
const option = { id: input.optionId };
|
|
184
|
+
if (input.name)
|
|
185
|
+
option.name = input.name;
|
|
186
|
+
if (input.position !== undefined)
|
|
187
|
+
option.position = input.position;
|
|
188
|
+
const variables = { productId, option };
|
|
189
|
+
if (input.valuesToAdd?.length) {
|
|
190
|
+
variables.optionValuesToAdd = input.valuesToAdd.map((v) => ({
|
|
191
|
+
name: v,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
if (input.valuesToDelete?.length) {
|
|
195
|
+
variables.optionValuesToDelete = input.valuesToDelete;
|
|
196
|
+
}
|
|
197
|
+
const data = (await shopifyClient.request(query, variables));
|
|
198
|
+
checkUserErrors(data.productOptionUpdate.userErrors, "update option");
|
|
199
|
+
return formatProductResponse(data.productOptionUpdate.product);
|
|
200
|
+
}
|
|
201
|
+
if (action === "delete") {
|
|
202
|
+
if (!input.optionIds?.length) {
|
|
203
|
+
throw new Error("optionIds array is required for action=delete");
|
|
204
|
+
}
|
|
205
|
+
const query = gql `
|
|
206
|
+
#graphql
|
|
207
|
+
|
|
208
|
+
mutation productOptionsDelete(
|
|
209
|
+
$productId: ID!
|
|
210
|
+
$options: [ID!]!
|
|
211
|
+
) {
|
|
212
|
+
productOptionsDelete(
|
|
213
|
+
productId: $productId
|
|
214
|
+
options: $options
|
|
215
|
+
) {
|
|
216
|
+
product {
|
|
217
|
+
...ProductOptionsFields
|
|
218
|
+
}
|
|
219
|
+
userErrors {
|
|
220
|
+
field
|
|
221
|
+
message
|
|
222
|
+
code
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fragment ProductOptionsFields on Product {
|
|
228
|
+
id
|
|
229
|
+
title
|
|
230
|
+
options {
|
|
231
|
+
id
|
|
232
|
+
name
|
|
233
|
+
position
|
|
234
|
+
optionValues {
|
|
235
|
+
id
|
|
236
|
+
name
|
|
237
|
+
hasVariants
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
variants(first: 20) {
|
|
241
|
+
edges {
|
|
242
|
+
node {
|
|
243
|
+
id
|
|
244
|
+
title
|
|
245
|
+
price
|
|
246
|
+
selectedOptions {
|
|
247
|
+
name
|
|
248
|
+
value
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
const data = (await shopifyClient.request(query, {
|
|
256
|
+
productId,
|
|
257
|
+
options: input.optionIds,
|
|
258
|
+
}));
|
|
259
|
+
checkUserErrors(data.productOptionsDelete.userErrors, "delete options");
|
|
260
|
+
return formatProductResponse(data.productOptionsDelete.product);
|
|
261
|
+
}
|
|
262
|
+
throw new Error(`Unknown action: ${action}`);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
handleToolError("manage product options", error);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
function formatProductResponse(product) {
|
|
270
|
+
return {
|
|
271
|
+
product: {
|
|
272
|
+
id: product.id,
|
|
273
|
+
title: product.title,
|
|
274
|
+
options: product.options.map((o) => ({
|
|
275
|
+
id: o.id,
|
|
276
|
+
name: o.name,
|
|
277
|
+
position: o.position,
|
|
278
|
+
values: o.optionValues.map((v) => ({
|
|
279
|
+
id: v.id,
|
|
280
|
+
name: v.name,
|
|
281
|
+
hasVariants: v.hasVariants,
|
|
282
|
+
})),
|
|
283
|
+
})),
|
|
284
|
+
variants: product.variants.edges.map((e) => ({
|
|
285
|
+
id: e.node.id,
|
|
286
|
+
title: e.node.title,
|
|
287
|
+
price: e.node.price,
|
|
288
|
+
options: e.node.selectedOptions,
|
|
289
|
+
})),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export { manageProductOptions };
|