2026-04-13 05:23:25 +00:00

396 lines
14 KiB
JavaScript
Executable File

import axios from 'axios';
import https from 'https';
import fs from 'fs';
import readline from 'readline';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ─── 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(path.dirname(new URL(import.meta.url).pathname), '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
// });
// desiredInventoryByHandle["84485"] = 2000;
// for await (const line of rl) {
// totalLines++;
// try {
// const rec = JSON.parse(line);
// const h = rec.handle;
// if (desiredInventoryByHandle.hasOwnProperty(h)) {
// const invtdataline = JSON.parse(line + 1);
// handleMap[h] = {
// variantId: invtdataline.id,
// inventoryItemId: invtdataline.inventoryItem.id
// };
// }
// } catch (e) {
// console.error('[4] ⚠️ JSON parse error on line', totalLines, e);
// }
// }
// console.log(handleMap)
// 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;
// }
async function buildHandleMap(ndjsonPath) {
console.log('[4] Parsing NDJSON and building handle map…');
desiredInventoryByHandle["84485"] = 2000;
const handleMap = {};
const productIdToHandle = {};
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);
// It's a product line
if (rec.handle && rec.id) {
productIdToHandle[rec.id] = rec.handle;
}
// It's a variant line
if (rec.__parentId && rec.inventoryItem && rec.inventoryItem.id) {
const handle = productIdToHandle[rec.__parentId];
if (handle && desiredInventoryByHandle.hasOwnProperty(handle)) {
if (!handleMap[handle]) handleMap[handle] = [];
handleMap[handle].push({
variantId: rec.id,
inventoryItemId: rec.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).length}`);
// Save map to disk if needed
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;
}
function writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, outputPath) {
console.log('[5] Creating JSONL file for bulk inventory update…');
const stream = fs.createWriteStream(outputPath);
let lineCount = 0;
for (const [handle, items] of Object.entries(handleMap)) {
const qty = desiredInventoryByHandle[handle];
if (qty == null) continue;
items.forEach(({ inventoryItemId }) => {
const line = JSON.stringify({
input: {
inventoryItemId,
availableDelta: qty
}
});
stream.write(line + '\n');
lineCount++;
});
}
stream.end();
console.log(`[5] ✔️ Wrote ${lineCount} inventory adjustments to ${outputPath}`);
}
// ─── 5. BUILD BULK MUTATION ───────────────────────────────────────────────────────
function makeBulkUpdateMutation(handleMap) {
const filePath = path.join(__dirname, 'exports', 'bulk_inventory.jsonl');
// Create the JSONL file instead of building a GraphQL mutation string
writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, filePath);
console.log('[6] 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('[6] ✔️ Mutation built with', ops.length, 'operations');
console.log(full)
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 ─────────────────────────────────────────────────────────────────────────
try {
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 } }
);
console.log('🚧 Full response:', JSON.stringify(resp.data, null, 2));
console.log(resp.data.data)
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);
}