← back to Dw Gemini Skip TK11321
lib/live-product-fields.js
62 lines
'use strict';
// Explicit catalog namespaces only. Never infer a width from a title, price,
// description, or an unrelated shipping/package dimension.
const WIDTH_NAMESPACES = ['global', 'custom', 'dwc', 'specs', 'carnegie'];
function field(nodes, namespace, key) {
return (nodes || []).find(m => m.namespace === namespace && m.key === key &&
typeof m.value === 'string' && m.value.trim())?.value.trim() || '';
}
function widthFromLive(product) {
const nodes = product.metafields?.nodes || [];
for (const namespace of WIDTH_NAMESPACES) {
if (namespace === 'carnegie' && String(product.vendor).toLowerCase() !== 'carnegie') continue;
const value = field(nodes, namespace, 'width');
if (value) return { value, source: `${namespace}.width` };
}
return { value: '', source: null };
}
function canonicalProduct(product, dwSku, vendor) {
const nodes = product.metafields?.nodes || [];
const get = (namespace, key) => field(nodes, namespace, key);
const images = (product.images?.nodes || []).map(image => image.url);
const specs = {
width: widthFromLive(product).value,
length: get('global', 'length'), repeat: get('global', 'repeat'),
material: get('global', 'material') || get('custom', 'material') || get('specs', 'material'),
unitOfMeasure: get('global', 'unit_of_measure'),
};
return { title: product.title, dwSku: dwSku || '', vendor, tags: product.tags || [],
descriptionHtml: product.descriptionHtml || '', specs, vendorSpecs: specs,
images, vendorImages: images,
variants: (product.variants?.nodes || []).map(v => ({ sku: v.sku, title: v.title })),
};
}
async function completeLiveProduct(product, query) {
const result = { ...product };
for (const connection of ['variants', 'metafields']) {
let page = result[connection];
const nodes = [...(page?.nodes || [])];
const cursors = new Set();
while (page?.pageInfo?.hasNextPage) {
const cursor = page.pageInfo.endCursor;
if (!cursor || cursors.has(cursor) || cursors.size >= 100)
throw new Error(`Cannot safely paginate ${connection}`);
cursors.add(cursor);
const fields = connection === 'variants' ? 'price sku title' : 'namespace key value';
const response = await query(`query($id:ID!,$cursor:String!){product(id:$id){id
${connection}(first:100,after:$cursor){nodes{${fields}} pageInfo{hasNextPage endCursor}}
}}`, { id: product.id, cursor });
const next = response.json?.data?.product;
if (response.status !== 200 || response.json?.errors?.length || next?.id !== product.id ||
!Array.isArray(next[connection]?.nodes) || typeof next[connection]?.pageInfo?.hasNextPage !== 'boolean')
throw new Error(`Incomplete Shopify ${connection} response`);
page = next[connection];
nodes.push(...page.nodes);
}
result[connection] = { ...page, nodes };
}
return result;
}
module.exports = { WIDTH_NAMESPACES, widthFromLive, canonicalProduct, completeLiveProduct };