1 | |
2 |
|
3 | |
4 | * Create Product |
5 | * Create a product, optionally with options (e.g. Size: S, M, L) and one variant per option combination. Each variant lists its option values in the same order as options. Without options, Shopify creates a single default variant. Image URLs are fetched by Shopify and attached as media. |
6 | */ |
7 | export async function main( |
8 | auth: RT.Shopify, |
9 | title: string, |
10 | description_html: string | undefined, |
11 | vendor: string | undefined, |
12 | product_type: string | undefined, |
13 | tags: string[] | undefined, |
14 | status: "DRAFT" | "ACTIVE" | "UNLISTED" | "ARCHIVED" | undefined, |
15 | options: { name: string; values: string[] }[] | undefined, |
16 | variants: |
17 | | { |
18 | option_values: string[] |
19 | price?: string |
20 | compare_at_price?: string |
21 | sku?: string |
22 | }[] |
23 | | undefined, |
24 | image_urls: string[] | undefined |
25 | ) { |
26 | const response = await fetch( |
27 | `https://${auth.store_name}.myshopify.com/admin/api/2026-10/graphql.json`, |
28 | { |
29 | method: "POST", |
30 | headers: { |
31 | "X-Shopify-Access-Token": auth.token, |
32 | "Content-Type": "application/json", |
33 | Accept: "application/json", |
34 | }, |
35 | body: JSON.stringify({ |
36 | query: `mutation ($input: ProductSetInput!) { productSet(input: $input, synchronous: true) { product { id legacyResourceId title handle status vendor productType tags createdAt options { id name optionValues { id name } } variants(first: 100) { nodes { id legacyResourceId title sku price compareAtPrice selectedOptions { name value } inventoryItem { id } } } } userErrors { field message code } } }`, |
37 | variables: { |
38 | input: { |
39 | title, |
40 | descriptionHtml: description_html || undefined, |
41 | vendor: vendor || undefined, |
42 | productType: product_type || undefined, |
43 | tags: tags?.length ? tags : undefined, |
44 | status: status || undefined, |
45 | productOptions: options?.length |
46 | ? options.map((o) => ({ |
47 | name: o.name, |
48 | values: o.values.map((name) => ({ name })), |
49 | })) |
50 | : undefined, |
51 | variants: variants?.length |
52 | ? variants.map((v) => ({ |
53 | optionValues: v.option_values.map((name, i) => ({ |
54 | optionName: options?.[i]?.name, |
55 | name, |
56 | })), |
57 | price: v.price || undefined, |
58 | compareAtPrice: v.compare_at_price || undefined, |
59 | sku: v.sku || undefined, |
60 | })) |
61 | : undefined, |
62 | files: image_urls?.length |
63 | ? image_urls.map((originalSource) => ({ |
64 | originalSource, |
65 | contentType: "IMAGE", |
66 | })) |
67 | : undefined, |
68 | }, |
69 | }, |
70 | }), |
71 | } |
72 | ) |
73 |
|
74 | if (!response.ok) { |
75 | throw new Error(`${response.status} ${await response.text()}`) |
76 | } |
77 |
|
78 | const { data, errors } = await response.json() |
79 | if (errors) { |
80 | throw new Error(JSON.stringify(errors)) |
81 | } |
82 | if (data.productSet.userErrors.length) { |
83 | throw new Error(JSON.stringify(data.productSet.userErrors)) |
84 | } |
85 |
|
86 | return data.productSet |
87 | } |
88 |
|