← 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); });