302 lines
9.9 KiB
JavaScript
Executable File
302 lines
9.9 KiB
JavaScript
Executable File
const axios = require('axios');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const readline = require('readline');
|
|
const path = require('path');
|
|
|
|
// ─── CONFIG ─────────────────────────────────────────────────────────────────────
|
|
const SHOP = 'veloxautomotive.myshopify.com';
|
|
const ACCESS_TOKEN = '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';
|
|
const API_VERSION = '2023-10';
|
|
const TURN14_ACCESS_TOKEN = '4d82c2d2a2ddb59a6da68afb722f50dd43f4640b';
|
|
|
|
// ─── HELPERS ────────────────────────────────────────────────────────────────────
|
|
function timestamp() {
|
|
const now = new Date();
|
|
const yyyy = now.getFullYear();
|
|
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(now.getDate()).padStart(2, '0');
|
|
const hh = String(now.getHours()).padStart(2, '0');
|
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
return `${yyyy}${MM}${dd}_${hh}${mm}${ss}`;
|
|
}
|
|
|
|
// ─── GLOBALS ─────────────────────────────────────────────────────────────────────
|
|
const desiredInventoryByHandle = {};
|
|
const exportDir = path.join(__dirname, 'exports');
|
|
|
|
// ─── 1. START BULK EXPORT ────────────────────────────────────────────────────────
|
|
async function startBulkExport() {
|
|
console.log('[1] Starting bulk export…');
|
|
const mutation = `
|
|
mutation {
|
|
bulkOperationRunQuery(
|
|
query: """
|
|
{
|
|
products {
|
|
edges {
|
|
node {
|
|
id
|
|
handle
|
|
variants {
|
|
edges {
|
|
node {
|
|
id
|
|
inventoryItem { id }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
) {
|
|
bulkOperation { id status }
|
|
userErrors { field message }
|
|
}
|
|
}
|
|
`;
|
|
|
|
const resp = await axios.post(
|
|
`https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
|
|
{ query: mutation },
|
|
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
|
|
);
|
|
|
|
const err = resp.data.data.bulkOperationRunQuery.userErrors;
|
|
if (err.length) {
|
|
console.error('[1] ❌ Errors starting export:', err);
|
|
throw new Error('Error starting export');
|
|
}
|
|
|
|
console.log('[1] ✔️ Export kicked off:', resp.data.data.bulkOperationRunQuery.bulkOperation);
|
|
}
|
|
|
|
// ─── 2. POLL UNTIL READY ─────────────────────────────────────────────────────────
|
|
async function pollUntilReady() {
|
|
console.log('[2] Polling for bulk-export completion…');
|
|
const query = `{
|
|
currentBulkOperation {
|
|
status
|
|
url
|
|
errorCode
|
|
}
|
|
}`;
|
|
|
|
while (true) {
|
|
const resp = await axios.post(
|
|
`https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
|
|
{ query },
|
|
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
|
|
);
|
|
const op = resp.data.data.currentBulkOperation;
|
|
console.log(`[2] Status: ${op.status}`);
|
|
|
|
if (op.status === 'COMPLETED') {
|
|
console.log('[2] ✔️ Completed. URL:', op.url);
|
|
return op.url;
|
|
}
|
|
if (op.status === 'FAILED') {
|
|
console.error('[2] ❌ Failed with code:', op.errorCode);
|
|
throw new Error('Bulk operation failed');
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 5000));
|
|
}
|
|
}
|
|
|
|
// ─── 3. DOWNLOAD FILE ────────────────────────────────────────────────────────────
|
|
async function downloadToFile(fileUrl) {
|
|
const fname = path.join(exportDir, `${timestamp()}_bulk_export.ndjson`);
|
|
console.log('[3] Downloading export to', fname);
|
|
|
|
const fileStream = fs.createWriteStream(fname);
|
|
await new Promise((resolve, reject) => {
|
|
https.get(fileUrl, res => {
|
|
res.pipe(fileStream);
|
|
fileStream.on('finish', () => {
|
|
console.log('[3] ✔️ Download complete');
|
|
fileStream.close(resolve);
|
|
});
|
|
fileStream.on('error', err => {
|
|
console.error('[3] ❌ Download error:', err);
|
|
reject(err);
|
|
});
|
|
});
|
|
});
|
|
|
|
return fname;
|
|
}
|
|
|
|
// ─── 4. PARSE HANDLE MAP ─────────────────────────────────────────────────────────
|
|
async function buildHandleMap(ndjsonPath) {
|
|
console.log('[4] Parsing NDJSON and building handle map…');
|
|
const handleMap = {};
|
|
let totalLines = 0;
|
|
|
|
const rl = readline.createInterface({
|
|
input: fs.createReadStream(ndjsonPath),
|
|
crlfDelay: Infinity
|
|
});
|
|
|
|
for await (const line of rl) {
|
|
totalLines++;
|
|
try {
|
|
const rec = JSON.parse(line);
|
|
const h = rec.handle;
|
|
if (desiredInventoryByHandle.hasOwnProperty(h)) {
|
|
handleMap[h] = rec.variants.edges.map(v => ({
|
|
variantId: v.node.id,
|
|
inventoryItemId: v.node.inventoryItem.id
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
console.error('[4] ⚠️ JSON parse error on line', totalLines, e);
|
|
}
|
|
}
|
|
|
|
console.log(`[4] ✔️ Parsed ${totalLines} lines. Matched handles:`, Object.keys(handleMap));
|
|
|
|
const mapPath = path.join(exportDir, `${timestamp()}_handle_map.json`);
|
|
fs.writeFileSync(mapPath, JSON.stringify(handleMap, null, 2));
|
|
console.log('[4] ✔️ Saved handle→inventory map to', mapPath);
|
|
|
|
return handleMap;
|
|
}
|
|
|
|
// ─── 5. BUILD BULK MUTATION ───────────────────────────────────────────────────────
|
|
function makeBulkUpdateMutation(handleMap) {
|
|
console.log('[5] Building bulk update mutation…');
|
|
const ops = [];
|
|
|
|
for (const [handle, items] of Object.entries(handleMap)) {
|
|
const qty = desiredInventoryByHandle[handle];
|
|
items.forEach(({ inventoryItemId }) => {
|
|
ops.push(`
|
|
inventoryAdjustQuantity(
|
|
input: {inventoryItemId: "${inventoryItemId}", availableDelta: ${qty}}
|
|
) {
|
|
inventoryLevel { id available }
|
|
userErrors { field message }
|
|
}
|
|
`);
|
|
});
|
|
}
|
|
|
|
const full = `
|
|
mutation {
|
|
bulkOperationRunMutation(
|
|
mutation: """
|
|
mutation {
|
|
${ops.join('\n')}
|
|
}
|
|
"""
|
|
) {
|
|
bulkOperation { id status }
|
|
userErrors { field message }
|
|
}
|
|
}
|
|
`;
|
|
console.log('[5] ✔️ Mutation built with', ops.length, 'operations');
|
|
return full;
|
|
}
|
|
|
|
// ─── FETCH LOCATIONS (OPTIONAL DEBUGGING) ────────────────────────────────────────
|
|
async function fetchAllLocations(pageSize = 250) {
|
|
const endpoint = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
|
|
let hasNextPage = true;
|
|
let cursor = null;
|
|
const allLocations = [];
|
|
|
|
while (hasNextPage) {
|
|
const query = `
|
|
query ($first: Int!, $after: String) {
|
|
locations(first: $first, after: $after) {
|
|
pageInfo {
|
|
hasNextPage
|
|
}
|
|
edges {
|
|
cursor
|
|
node {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const variables = { first: pageSize, after: cursor };
|
|
|
|
const resp = await axios.post(
|
|
endpoint,
|
|
{ query, variables },
|
|
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
|
|
);
|
|
|
|
const locs = resp.data.data.locations;
|
|
locs.edges.forEach(edge => {
|
|
allLocations.push(edge.node);
|
|
});
|
|
|
|
hasNextPage = locs.pageInfo.hasNextPage;
|
|
cursor = hasNextPage ? locs.edges[locs.edges.length - 1].cursor : null;
|
|
}
|
|
|
|
console.log(`✅ Retrieved ${allLocations.length} locations`);
|
|
return allLocations;
|
|
}
|
|
|
|
// ─── MAIN ─────────────────────────────────────────────────────────────────────────
|
|
async function main() {
|
|
try {
|
|
// Fetch inventory from Turn14
|
|
const turn14Res = await fetch(`https://turn14.data4autos.com/v1/inventory/allupdates`, {
|
|
headers: {
|
|
Authorization: `Bearer ${TURN14_ACCESS_TOKEN}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
const turn14Data = await turn14Res.json();
|
|
|
|
for (const item of turn14Data) {
|
|
desiredInventoryByHandle[item.id] = item.totalQuantity;
|
|
}
|
|
|
|
console.log('✅ Inventory fetched from Turn14:', Object.keys(desiredInventoryByHandle).length, 'items');
|
|
console.log(desiredInventoryByHandle)
|
|
if (!fs.existsSync(exportDir)) {
|
|
console.log('Creating exports directory at', exportDir);
|
|
fs.mkdirSync(exportDir);
|
|
}
|
|
|
|
await fetchAllLocations(20);
|
|
await startBulkExport();
|
|
const url = await pollUntilReady();
|
|
const ndjsonPath = await downloadToFile(url);
|
|
const handleMap = await buildHandleMap(ndjsonPath);
|
|
|
|
const mutation = makeBulkUpdateMutation(handleMap);
|
|
console.log('[5] Kicking off inventory update…');
|
|
|
|
const resp = await axios.post(
|
|
`https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
|
|
{ query: mutation },
|
|
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
|
|
);
|
|
|
|
const errs = resp.data.data.bulkOperationRunMutation.userErrors;
|
|
if (errs.length) {
|
|
console.error('[5] ❌ Inventory bulk update errors:', errs);
|
|
} else {
|
|
console.log('[5] ✔️ Inventory bulk update kickstarted successfully');
|
|
}
|
|
} catch (err) {
|
|
console.error('🚨 Uncaught error:', err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|