1 | |
2 |
|
3 | |
4 | * Add Products to Collection |
5 | * Manually add products (numeric IDs or GIDs) to a collection. The products are added as manual selections on the collection's own source, which is created if the collection has none yet. |
6 | */ |
7 | export async function main( |
8 | auth: RT.Shopify, |
9 | collection_id: string, |
10 | product_ids: string[] |
11 | ) { |
12 | const id = collection_id.startsWith("gid://") |
13 | ? collection_id |
14 | : `gid://shopify/Collection/${collection_id}` |
15 | const selections = product_ids.map((p) => ({ |
16 | productId: p.startsWith("gid://") ? p : `gid://shopify/Product/${p}`, |
17 | })) |
18 | const url = `https://${auth.store_name}.myshopify.com/admin/api/2026-10/graphql.json` |
19 | const headers = { |
20 | "X-Shopify-Access-Token": auth.token, |
21 | "Content-Type": "application/json", |
22 | Accept: "application/json", |
23 | } |
24 |
|
25 | |
26 | const lookup = await fetch(url, { |
27 | method: "POST", |
28 | headers, |
29 | body: JSON.stringify({ |
30 | query: `query ($id: ID!) { collection(id: $id) { title sources { id ... on CollectionConditionsSource { shareable } } } }`, |
31 | variables: { id }, |
32 | }), |
33 | }) |
34 | if (!lookup.ok) { |
35 | throw new Error(`${lookup.status} ${await lookup.text()}`) |
36 | } |
37 | const found = await lookup.json() |
38 | if (found.errors) { |
39 | throw new Error(JSON.stringify(found.errors)) |
40 | } |
41 | const collection = found.data.collection |
42 | if (!collection) { |
43 | throw new Error(`Collection ${id} not found`) |
44 | } |
45 | const source = collection.sources.find( |
46 | (s: { shareable?: boolean }) => s.shareable === false |
47 | ) |
48 |
|
49 | |
50 | const response = await fetch(url, { |
51 | method: "POST", |
52 | headers, |
53 | body: JSON.stringify({ |
54 | query: `mutation ($collection: CollectionUpdateInput!) { collectionUpdate(collection: $collection) { collection { id legacyResourceId title productsCount { count } sources { id title } } userErrors { field message } } }`, |
55 | variables: { |
56 | collection: source |
57 | ? { |
58 | id, |
59 | sourcesToUpdate: [ |
60 | { |
61 | condition: { |
62 | id: source.id, |
63 | inclusion: { selectionsToAdd: selections }, |
64 | }, |
65 | }, |
66 | ], |
67 | } |
68 | : { |
69 | id, |
70 | sourcesToCreate: [ |
71 | { |
72 | source: { |
73 | title: collection.title, |
74 | inclusion: { selections }, |
75 | }, |
76 | }, |
77 | ], |
78 | }, |
79 | }, |
80 | }), |
81 | }) |
82 |
|
83 | if (!response.ok) { |
84 | throw new Error(`${response.status} ${await response.text()}`) |
85 | } |
86 |
|
87 | const { data, errors } = await response.json() |
88 | if (errors) { |
89 | throw new Error(JSON.stringify(errors)) |
90 | } |
91 | if (data.collectionUpdate.userErrors.length) { |
92 | throw new Error(JSON.stringify(data.collectionUpdate.userErrors)) |
93 | } |
94 |
|
95 | return data.collectionUpdate |
96 | } |
97 |
|