← 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
M scripts/push-shopify.mjs
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 →