← back to Dw Marketing Reels
colorwheel/upload-12-images.js
75 lines
#!/usr/bin/env node
// Attach the 12 sourced images as featured media on their live Shopify products.
// Flow per product: stagedUploadsCreate → multipart POST to the staged target →
// productCreateMedia(originalSource=resourceUrl). APPROVED gated write (2026-07-16).
import https from 'node:https';
import { readFile } from 'node:fs/promises';
import { extname } from 'node:path';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const DIR = '/Users/macstudio3/Projects/dw-marketing-reels/sourced-images/imageless-14';
const ITEMS = [
['EUR-71130', 'gid://shopify/Product/7515289845811', 'W7814-01__w7814-01_n6lger3vo8gdn9ly.webp'],
['EUR-71059', 'gid://shopify/Product/7515288109107', 'W7682-01__w7682-01_ff8svfx2jzq6e1ez.webp'],
['EUR-70743', 'gid://shopify/Product/7515278704691', 'W7024-03__w7024-03.webp'],
['EUR-70486', 'gid://shopify/Product/7514199457843', 'W6541-02__4591.webp'],
['EUR-70279', 'gid://shopify/Product/7514192871475', 'W5223-03__w5223-03.webp'],
['EUR-70883', 'gid://shopify/Product/7515283554355', 'W7334-1__w7334-01.webp'],
['EUR-70328', 'gid://shopify/Product/7514194444339', 'EUR-70328__5314.webp'],
['EUR-70402', 'gid://shopify/Product/7514196901939', 'EUR-70402__5053.webp'],
['EUR-70351', 'gid://shopify/Product/7514195066931', 'EUR-70351__5223.webp'],
['EUR-80361', 'gid://shopify/Product/7513505431603', 'NCW4352-05_bonnelles.jpg'],
['EUR-80261', 'gid://shopify/Product/7513501499443', 'NCW4270-03__coromandel-offwhite.webp'],
['EUR-80263', 'gid://shopify/Product/7513501564979', 'NCW4270-05__coromandel-offwhite.webp'],
];
const mime = f => { const e = extname(f).toLowerCase(); return e === '.jpg' || e === '.jpeg' ? 'image/jpeg' : e === '.png' ? 'image/png' : 'image/webp'; };
function gql(query, variables) {
const body = JSON.stringify({ query, variables });
return new Promise((resolve, reject) => {
const req = https.request({ hostname: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) } },
r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error(d.slice(0, 200))); } }); });
req.on('error', reject); req.write(body); req.end();
});
}
function uploadToTarget(target, fileBuf, filename, mimeType) {
return new Promise((resolve, reject) => {
const boundary = '----dw' + Date.now();
const parts = [];
for (const p of target.parameters) parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="${p.name}"\r\n\r\n${p.value}\r\n`);
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mimeType}\r\n\r\n`);
const body = Buffer.concat([Buffer.from(parts.join(''), 'utf8'), fileBuf, Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8')]);
const u = new URL(target.url);
const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST',
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length } },
res => { let d = ''; res.on('data', c => d += c); res.on('end', () => (res.statusCode >= 200 && res.statusCode < 300) ? resolve() : reject(new Error('upload ' + res.statusCode + ' ' + d.slice(0, 160)))); });
req.on('error', reject); req.write(body); req.end();
});
}
(async () => {
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
let ok = 0, fail = 0;
for (const [sku, pid, file] of ITEMS) {
try {
const buf = await readFile(`${DIR}/${file}`);
const mt = mime(file);
const sr = await gql(`mutation($input:[StagedUploadInput!]!){ stagedUploadsCreate(input:$input){ stagedTargets{ url resourceUrl parameters{ name value } } userErrors{ message } } }`,
{ input: [{ filename: file, mimeType: mt, resource: 'IMAGE', httpMethod: 'POST', fileSize: String(buf.length) }] });
const tgt = sr.data?.stagedUploadsCreate?.stagedTargets?.[0];
const se = sr.data?.stagedUploadsCreate?.userErrors;
if (!tgt) throw new Error('staged: ' + JSON.stringify(se || sr.errors));
await uploadToTarget(tgt, buf, file, mt);
const cr = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ status } mediaUserErrors{ message } } }`,
{ id: pid, media: [{ originalSource: tgt.resourceUrl, mediaContentType: 'IMAGE' }] });
const me = cr.data?.productCreateMedia?.mediaUserErrors;
if (me && me.length) throw new Error('attach: ' + JSON.stringify(me));
console.log(`OK ${sku} ${cr.data.productCreateMedia.media[0].status} ${file}`);
ok++;
} catch (e) { console.log(`ERR ${sku}: ${e.message}`); fail++; }
}
console.log(`\nUploaded ${ok}/${ITEMS.length}, ${fail} failed.`);
})();