← back to Majilite Onboard
publish_shopify: reconcile-in-place (DWMJ->DWCC SKU rename + add Per-Yard retail variant on 153 live products), collision-safe create for 7 missing (TK-10403)
abda2a6c3de953bce276be457f7e546cf237f67c · 2026-08-10 16:02:54 -0700 · Steve Abrams
Files touched
M scripts/publish_shopify.js
Diff
commit abda2a6c3de953bce276be457f7e546cf237f67c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 16:02:54 2026 -0700
publish_shopify: reconcile-in-place (DWMJ->DWCC SKU rename + add Per-Yard retail variant on 153 live products), collision-safe create for 7 missing (TK-10403)
---
scripts/publish_shopify.js | 76 +++++++++++++++++++++++++++++-----------------
1 file changed, 48 insertions(+), 28 deletions(-)
diff --git a/scripts/publish_shopify.js b/scripts/publish_shopify.js
index 015a861..e3fa13e 100644
--- a/scripts/publish_shopify.js
+++ b/scripts/publish_shopify.js
@@ -99,42 +99,62 @@ function bodyHtml(p) {
+ `<li>Durability: ${s.durability}</li><li>Flammability: ${s.flammability_inherent}</li>`
+ `<li>Cleanability: ${s.cleanability_code} · Crocking ${s.crocking}</li>`
+ `<li>PFAS-, PVC- and plasticizer-free · no off-gassing</li></ul>`
- + `<p><em>Sample swatch. Sold by the yard (54" wide) — contact us for yardage pricing.</em></p>`;
+ + `<p><em>Available by the yard (54" wide) at $${p.pricing.retail}/yd, or order a $4.25 sample swatch.</em></p>`;
}
async function publishOne(p) {
+ const num = p.dw_sku.replace('DWCC-', '');
+ const oldKey = `DWMJ-${num}`; // key under which it was first published (153 of these)
+ const newKey = p.dw_sku; // DWCC-600xxx
const sampleSku = `${p.dw_sku}-Sample`;
- if (published[p.dw_sku] && published[p.dw_sku].productId) return { skip: 'recorded' };
- const exists = await existingVariant(sampleSku);
- if (exists) { published[p.dw_sku] = { productId: exists.product.id, handle: exists.product.handle, status: exists.product.status, note: 'pre-existing' }; savePub(); return { skip: 'exists' }; }
- if (DRY) return { dry: true, title: p.title, sku: sampleSku };
+ const yardSku = p.dw_sku;
- const handle = `majilite-${slugify(p.pattern + '-' + p.color)}`;
- const tags = Array.from(new Set([...p.tags, p.dw_sku, 'display_variant', 'Sample'])).join(', ');
+ if (published[newKey] && published[newKey].reconciled) return { skip: 'done' };
+ const prior = published[oldKey]; // existing live DWMJ product, if any
+ const mode = (prior && prior.productId) ? 'update' : 'create';
+ if (DRY) return { dry: true, mode, title: p.title };
- const input = {
+ const baseHandle = `majilite-${slugify(p.pattern + '-' + p.color)}`;
+ const tags = Array.from(new Set([...p.tags, p.dw_sku, 'display_variant', 'Sample', 'By The Yard']));
+ const buildInput = (handle) => ({
+ ...(mode === 'update' ? { id: prior.productId } : {}),
handle, title: p.title, vendor: p.vendor, productType: p.product_type, status: STATUS,
- descriptionHtml: bodyHtml(p), tags: tags.split(', '),
- productOptions: [{ name: 'Type', values: [{ name: 'Sample' }] }],
- variants: [{ optionValues: [{ optionName: 'Type', name: 'Sample' }], price: '4.25', sku: sampleSku, inventoryPolicy: 'CONTINUE', taxable: true }],
+ descriptionHtml: bodyHtml(p), tags,
+ productOptions: [{ name: 'Type', values: [{ name: 'Sample' }, { name: 'Per Yard' }] }],
+ variants: [
+ { optionValues: [{ optionName: 'Type', name: 'Sample' }], price: '4.25', sku: sampleSku, inventoryPolicy: 'CONTINUE', taxable: true },
+ { optionValues: [{ optionName: 'Type', name: 'Per Yard' }], price: String(p.pricing.retail), sku: yardSku, inventoryPolicy: 'CONTINUE', taxable: true },
+ ],
metafields: metafields(p),
+ });
+ const run = async (handle) => {
+ const d = await gql(`mutation($input:ProductSetInput!){ productSet(synchronous:true, input:$input){ product{ id handle status } userErrors{ field message } } }`, { input: buildInput(handle) });
+ return d.productSet;
};
- const d = await gql(`mutation($input:ProductSetInput!){ productSet(synchronous:true, input:$input){ product{ id handle status } userErrors{ field message } } }`, { input });
- const ue = d.productSet.userErrors; if (ue && ue.length) throw new Error('productSet: ' + JSON.stringify(ue));
- const prod = d.productSet.product;
-
- // attach swatch image
- let media = 'none';
- try {
- const resourceUrl = await uploadImage(path.join(ROOT, 'public', p.image), `${p.dw_sku}.png`);
- const m = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ status } mediaUserErrors{ field message } } }`,
- { id: prod.id, media: [{ originalSource: resourceUrl, alt: `${p.title} — ${p.vendor} ${p.brand}`, mediaContentType: 'IMAGE' }] });
- const me = m.productCreateMedia.mediaUserErrors; media = (me && me.length) ? 'err:' + JSON.stringify(me) : 'ok';
- } catch (e) { media = 'img-fail:' + e.message.slice(0, 80); }
-
- published[p.dw_sku] = { productId: prod.id, handle: prod.handle, status: prod.status, sample_sku: sampleSku, image: media, at: new Date().toISOString() };
+
+ let ps = await run(baseHandle);
+ if (ps.userErrors && ps.userErrors.length) {
+ const collision = JSON.stringify(ps.userErrors).toLowerCase().includes('handle');
+ if (mode === 'create' && collision) ps = await run(`${baseHandle}-${num}`); // collision-safe create
+ if (ps.userErrors && ps.userErrors.length) throw new Error('productSet: ' + JSON.stringify(ps.userErrors));
+ }
+ const prod = ps.product;
+
+ // image: creates get a fresh swatch; updates keep the swatch they already have
+ let media = 'kept';
+ if (mode === 'create') {
+ media = 'none';
+ try {
+ const resourceUrl = await uploadImage(path.join(ROOT, 'public', p.image), `${p.dw_sku}.png`);
+ const m = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ status } mediaUserErrors{ field message } } }`,
+ { id: prod.id, media: [{ originalSource: resourceUrl, alt: `${p.title} — ${p.vendor} ${p.brand}`, mediaContentType: 'IMAGE' }] });
+ const me = m.productCreateMedia.mediaUserErrors; media = (me && me.length) ? 'err:' + JSON.stringify(me) : 'ok';
+ } catch (e) { media = 'img-fail:' + e.message.slice(0, 80); }
+ }
+
+ published[newKey] = { productId: prod.id, handle: prod.handle, status: prod.status, sample_sku: sampleSku, yard_sku: yardSku, retail: p.pricing.retail, mode, from: (mode === 'update' ? oldKey : 'new'), image: media, reconciled: true, at: new Date().toISOString() };
savePub();
- return { created: prod.id, handle: prod.handle, image: media };
+ return { [mode === 'update' ? 'updated' : 'created']: prod.id, handle: prod.handle, image: media, mode };
}
(async () => {
@@ -144,8 +164,8 @@ async function publishOne(p) {
try {
const r = await publishOne(p);
if (r.skip) { skipped++; process.stdout.write(`· ${p.dw_sku} skip(${r.skip})\n`); }
- else if (r.dry) { process.stdout.write(`plan ${p.dw_sku} ${p.title} → ${r.sku}\n`); }
- else { created++; process.stdout.write(`✓ ${p.dw_sku} ${p.title} → ${r.handle} [img:${r.image}] (${created})\n`); }
+ else if (r.dry) { process.stdout.write(`plan ${p.dw_sku} [${r.mode}] ${p.title}\n`); }
+ else { created++; const verb = r.updated ? '↑upd' : '✓new'; process.stdout.write(`${verb} ${p.dw_sku} ${p.title} → ${r.handle} [img:${r.image}] (${created})\n`); }
} catch (e) { failed++; process.stdout.write(`✗ ${p.dw_sku} ${p.title} — ${e.message.slice(0, 160)}\n`); }
await sleep(350);
}
← 2f00dac Majilite Metallic Specialties I: rename series DWMJ->DWCC (k
·
back to Majilite Onboard
·
auto-data-snapshot: 2026-08-10T16:09:17 (1 data files) — dat a6acf50 →