← back to Designer Wallcoverings
mailers/upload-vendor-logos.js
103 lines
#!/usr/bin/env node
// One-shot: upload vendor logos to Shopify Files via stagedUploadsCreate -> PUT -> fileCreate -> poll READY.
// Uses SHOPIFY_DRAFT_TOKEN (has write_files). Live store designer-laboratory-sandbox.
const fs = require('fs');
const path = require('path');
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const TOKEN = process.env.SHOPIFY_DRAFT_TOKEN;
if (!TOKEN) { console.error('Missing SHOPIFY_DRAFT_TOKEN'); process.exit(1); }
const DIR = path.join(__dirname, '..', 'vendor-logos');
// Files that need fresh upload (the other 3 already exist in Files and are reused).
// china-seas.png is actually a WebP container -> declare true mimeType.
const FILES = [
{ file: 'china-seas.png', mime: 'image/webp' },
{ file: 'quadrille.png', mime: 'image/png' },
{ file: 'cole-son.png', mime: 'image/png' },
];
async function gql(query, variables) {
const res = await fetch(`https://${DOMAIN}/admin/api/${API}/graphql.json`, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const j = await res.json();
if (j.errors) throw new Error('GraphQL errors: ' + JSON.stringify(j.errors));
return j.data;
}
async function stagedUpload(filename, mime, sizeBytes) {
const q = `mutation stagedUploadsCreate($input:[StagedUploadInput!]!){
stagedUploadsCreate(input:$input){
stagedTargets{ url resourceUrl parameters{ name value } }
userErrors{ field message }
}
}`;
const d = await gql(q, { input: [{
resource: 'FILE', filename, mimeType: mime, httpMethod: 'POST', fileSize: String(sizeBytes),
}]});
const ue = d.stagedUploadsCreate.userErrors;
if (ue && ue.length) throw new Error('stagedUploadsCreate: ' + JSON.stringify(ue));
return d.stagedUploadsCreate.stagedTargets[0];
}
async function putBytes(target, buf, mime) {
// Google Cloud Storage staged target uses multipart POST with the provided params.
const fd = new FormData();
for (const p of target.parameters) fd.append(p.name, p.value);
fd.append('file', new Blob([buf], { type: mime }));
const res = await fetch(target.url, { method: 'POST', body: fd });
if (!(res.status >= 200 && res.status < 300)) {
const t = await res.text();
throw new Error(`Upload PUT/POST failed ${res.status}: ${t.slice(0,300)}`);
}
}
async function fileCreate(resourceUrl, filename, alt) {
const q = `mutation fileCreate($files:[FileCreateInput!]!){
fileCreate(files:$files){
files{ id fileStatus alt ... on MediaImage { image { url } } }
userErrors{ field message }
}
}`;
const d = await gql(q, { files: [{
originalSource: resourceUrl, contentType: 'IMAGE', alt, filename,
}]});
const ue = d.fileCreate.userErrors;
if (ue && ue.length) throw new Error('fileCreate: ' + JSON.stringify(ue));
return d.fileCreate.files[0];
}
async function poll(id) {
const q = `query($id:ID!){ node(id:$id){ ... on MediaImage { id fileStatus image { url } } } }`;
for (let i = 0; i < 30; i++) {
const d = await gql(q, { id });
const n = d.node;
if (n && n.fileStatus === 'READY' && n.image && n.image.url) return n.image.url;
if (n && n.fileStatus === 'FAILED') throw new Error('file FAILED: ' + id);
await new Promise(r => setTimeout(r, 2000));
}
throw new Error('timeout waiting READY: ' + id);
}
(async () => {
const out = {};
for (const { file, mime } of FILES) {
const fp = path.join(DIR, file);
const buf = fs.readFileSync(fp);
const alt = file.replace(/\.[a-z]+$/i, '').replace(/-/g, ' ') + ' logo';
process.stdout.write(`Uploading ${file} (${mime}, ${buf.length}B)... `);
const target = await stagedUpload(file, mime, buf.length);
await putBytes(target, buf, mime);
const created = await fileCreate(target.resourceUrl, file, alt);
const url = await poll(created.id);
out[file] = url;
console.log('READY ->', url);
}
console.log('\nRESULT_JSON ' + JSON.stringify(out));
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });