1 | |
2 |
|
3 | |
4 | * Create Product Variant |
5 | * Add a variant to an existing product. option_values maps each option name to its value (e.g. {"Size": "XL", "Color": "Red"}); a new value is added to the option automatically. If the product only has its "Default Title" variant, that variant is replaced. Set available with location_id to stock the variant at that location. |
6 | */ |
7 | export async function main( |
8 | auth: RT.Shopify, |
9 | product_id: string, |
10 | option_values: { [key: string]: string }, |
11 | price: string | undefined, |
12 | compare_at_price: string | undefined, |
13 | sku: string | undefined, |
14 | barcode: string | undefined, |
15 | weight: number | undefined, |
16 | weight_unit: "KILOGRAMS" | "GRAMS" | "POUNDS" | "OUNCES" | undefined, |
17 | track_inventory: boolean | undefined, |
18 | location_id: string | undefined, |
19 | available: number | undefined |
20 | ) { |
21 | const productId = product_id.startsWith("gid://") |
22 | ? product_id |
23 | : `gid://shopify/Product/${product_id}` |
24 | const locationId = location_id |
25 | ? location_id.startsWith("gid://") |
26 | ? location_id |
27 | : `gid://shopify/Location/${location_id}` |
28 | : undefined |
29 |
|
30 | const response = await fetch( |
31 | `https://${auth.store_name}.myshopify.com/admin/api/2026-10/graphql.json`, |
32 | { |
33 | method: "POST", |
34 | headers: { |
35 | "X-Shopify-Access-Token": auth.token, |
36 | "Content-Type": "application/json", |
37 | Accept: "application/json", |
38 | }, |
39 | body: JSON.stringify({ |
40 | query: `mutation ($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkCreate(productId: $productId, variants: $variants) { productVariants { id legacyResourceId title sku price compareAtPrice inventoryQuantity selectedOptions { name value } barcodes(first: 5) { nodes { value type } } inventoryItem { id tracked measurement { weight { value unit } } } } userErrors { field message code } } }`, |
41 | variables: { |
42 | productId, |
43 | variants: [ |
44 | { |
45 | optionValues: Object.entries(option_values).map( |
46 | ([optionName, name]) => ({ optionName, name }) |
47 | ), |
48 | price: price || undefined, |
49 | compareAtPrice: compare_at_price || undefined, |
50 | barcodes: barcode ? [{ value: barcode }] : undefined, |
51 | inventoryItem: { |
52 | sku: sku || undefined, |
53 | tracked: track_inventory || available != null || undefined, |
54 | measurement: |
55 | weight != null |
56 | ? { |
57 | weight: { |
58 | value: weight, |
59 | unit: weight_unit || "KILOGRAMS", |
60 | }, |
61 | } |
62 | : undefined, |
63 | }, |
64 | inventoryQuantities: |
65 | locationId && available != null |
66 | ? [{ locationId, availableQuantity: available }] |
67 | : undefined, |
68 | }, |
69 | ], |
70 | }, |
71 | }), |
72 | } |
73 | ) |
74 |
|
75 | if (!response.ok) { |
76 | throw new Error(`${response.status} ${await response.text()}`) |
77 | } |
78 |
|
79 | const { data, errors } = await response.json() |
80 | if (errors) { |
81 | throw new Error(JSON.stringify(errors)) |
82 | } |
83 | if (data.productVariantsBulkCreate.userErrors.length) { |
84 | throw new Error(JSON.stringify(data.productVariantsBulkCreate.userErrors)) |
85 | } |
86 |
|
87 | return data.productVariantsBulkCreate |
88 | } |
89 |
|