[object Object]

← back to Majilite Jewelry Cases

Fix Shopify push: REST base64 attach + extract numeric id from GID (root cause of silent fail); canary verified live

7b71af91c556d373cb34f516876be09af12f1b96 · 2026-08-10 15:58:27 -0700 · Steve Abrams

Files touched

Diff

commit 7b71af91c556d373cb34f516876be09af12f1b96
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 15:58:27 2026 -0700

    Fix Shopify push: REST base64 attach + extract numeric id from GID (root cause of silent fail); canary verified live
---
 scripts/push-shopify.mjs | 38 ++++++++++++++++++++------------------
 1 file changed, 20 insertions(+), 18 deletions(-)

diff --git a/scripts/push-shopify.mjs b/scripts/push-shopify.mjs
index 916598a..a44be74 100644
--- a/scripts/push-shopify.mjs
+++ b/scripts/push-shopify.mjs
@@ -5,6 +5,7 @@
 import fs from 'node:fs';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { execSync } from 'node:child_process';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const ROOT = path.resolve(__dirname, '..');
@@ -60,27 +61,28 @@ const gql = async (query, variables) => {
   });
   return r.json();
 };
-async function stageAndAttach(r) {
-  const buf = fs.readFileSync(r.png);
-  const staged = await gql(`mutation($input:[StagedUploadInput!]!){stagedUploadsCreate(input:$input){stagedTargets{url resourceUrl parameters{name value}} userErrors{message}}}`,
-    { input: [{ filename: `${r.sku}-jewelry-case.png`, mimeType: 'image/png', resource: 'IMAGE', httpMethod: 'POST' }] });
-  const t = staged?.data?.stagedUploadsCreate?.stagedTargets?.[0];
-  if (!t) throw new Error('stage failed: ' + JSON.stringify(staged).slice(0, 200));
-  const form = new FormData();
-  for (const p of t.parameters) form.append(p.name, p.value);
-  form.append('file', new Blob([buf], { type: 'image/png' }), `${r.sku}.png`);
-  const up = await fetch(t.url, { method: 'POST', body: form });
-  if (!up.ok && up.status !== 201) throw new Error('upload ' + up.status);
-  const attach = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){productCreateMedia(productId:$id,media:$media){mediaUserErrors{message} media{status}}}`,
-    { id: `gid://shopify/Product/${r.shopify_id}`, media: [{ originalSource: t.resourceUrl, mediaContentType: 'IMAGE', alt: `Jewelry display case in ${r.sku}` }] });
-  const errs = attach?.data?.productCreateMedia?.mediaUserErrors;
-  if (errs && errs.length) throw new Error(JSON.stringify(errs));
-  return true;
+// REST image attach with base64 — reliable, no staged-upload handshake.
+// Returns the created image id (proof of a real attach), throws with the real body on failure.
+async function attachViaRest(r) {
+  const numericId = String(r.shopify_id).split('/').pop(); // shopify_id may be a GID
+  // downscale to a compact JPG to keep the base64 payload small + reliable
+  const tmp = `/tmp/jc-${numericId}.jpg`;
+  let src = r.png;
+  try { execSync(`magick "${r.png}" -resize 1000x1000 -quality 82 "${tmp}"`); src = tmp; } catch {}
+  const b64 = fs.readFileSync(src).toString('base64');
+  const res = await fetch(`https://${SHOP}/admin/api/${API}/products/${numericId}/images.json`, {
+    method: 'POST',
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ image: { attachment: b64, filename: `${r.sku}-jewelry-case.png`, alt: `Jewelry display case in ${r.sku}` } }),
+  });
+  const j = await res.json();
+  if (!res.ok || !j.image?.id) throw new Error(`REST ${res.status}: ${JSON.stringify(j).slice(0, 220)}`);
+  return j.image.id;
 }
 let ok = 0, bad = 0;
 for (const r of targets) {
-  try { await stageAndAttach(r); ok++; console.log(`  + ${r.handle}`); }
+  try { const imgId = await attachViaRest(r); ok++; console.log(`  + ${r.handle}  (image ${imgId})`); }
   catch (e) { bad++; console.log(`  ! ${r.handle}: ${e.message}`); }
-  await new Promise(z => setTimeout(z, 500));
+  await new Promise(z => setTimeout(z, 600));
 }
 console.log(`\nLIVE PUSH done. attached=${ok} failed=${bad}`);

← d3b0a1c auto-data-snapshot: 2026-08-10T15:38:06 (2 data files) — pac  ·  back to Majilite Jewelry Cases  ·  chore: lint (drop dead gql/bySky), refactor, hoverproof clea c2a07c2 →