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

343 lines
11 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 FormData from 'form-data';
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';
const exportDir = path.join(__dirname, 'exports');
const JSONL_FILENAME = 'bulk_inventory.jsonl';
const desiredInventoryByHandle = {};
// ─── HELPERS ────────────────────────────────────────────────────────
function timestamp() {
const now = new Date();
return now.toISOString().replace(/[-:]/g, '').replace('T', '_').split('.')[0];
}
// ─── STAGE FILE UPLOAD ──────────────────────────────────────────────
async function getStagedUploadPath() {
const mutation = `
mutation {
stagedUploadsCreate(input:[{
resource: BULK_MUTATION_VARIABLES,
filename: "${JSONL_FILENAME}",
mimeType: "text/jsonl",
httpMethod: POST
}]){
stagedTargets {
url
resourceUrl
parameters { name value }
}
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 result = resp.data.data.stagedUploadsCreate;
if (result.userErrors.length) {
throw new Error('Staged upload error: ' + JSON.stringify(result.userErrors));
}
const target = result.stagedTargets[0];
const keyParam = target.parameters.find(p => p.name === 'key');
if (!keyParam) throw new Error('Missing "key" parameter in staged upload target');
// console.log("target",target)
return {
uploadUrl: target.url,
parameters: target.parameters,
stagedUploadPath: keyParam.value,
};
}
async function uploadJSONLFile(uploadUrl, params, filePath) {
const form = new FormData();
params.forEach(({ name, value }) => form.append(name, value));
form.append('file', fs.createReadStream(filePath));
await axios.post(uploadUrl, form, {
headers: form.getHeaders()
});
console.log('✅ Uploaded JSONL file to Shopify');
}
async function runBulkMutation(stagedUploadPath) {
console.log("Resource URL",stagedUploadPath)
const mutation = `
mutation bulkInventoryAdjust($input: InventoryAdjustQuantityInput!) {
inventoryAdjustQuantity(input: $input) {
inventoryLevel { id available }
userErrors { field message }
}
}
`;
const wrappedMutation = `
mutation {
bulkOperationRunMutation(
mutation: """${mutation}""",
stagedUploadPath: "${stagedUploadPath}"
) {
bulkOperation { id status }
userErrors { field message }
}
}
`;
const resp = await axios.post(
`https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
{ query: wrappedMutation },
{ headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN } }
);
console.log('📦 Mutation Response:', JSON.stringify(resp.data, null, 2));
const result = resp.data.data?.bulkOperationRunMutation;
if (!result) throw new Error('bulkOperationRunMutation failed.');
if (result.userErrors.length) {
console.error('❌ Mutation Errors:', result.userErrors);
} else {
console.log('✅ Bulk inventory update started with ID:', result.bulkOperation.id);
}
}
// ─── EXPORT / NDJSON LOGIC ─────────────────────────────────────────
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) throw new Error(JSON.stringify(err));
console.log('[1] ✔️ Export started:', resp.data.data.bulkOperationRunQuery.bulkOperation.id);
}
async function pollUntilReady() {
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 export. File URL:', op.url);
return op.url;
}
if (op.status === 'FAILED') {
throw new Error('Bulk export failed: ' + op.errorCode);
}
await new Promise(r => setTimeout(r, 5000));
}
}
async function downloadToFile(fileUrl) {
const fname = path.join(exportDir, `${timestamp()}_bulk_export.ndjson`);
const fileStream = fs.createWriteStream(fname);
await new Promise((resolve, reject) => {
https.get(fileUrl, res => {
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close(resolve);
});
fileStream.on('error', reject);
});
});
console.log(`[3] ✔️ Downloaded export to ${fname}`);
return fname;
}
async function buildHandleMap(ndjsonPath) {
console.log('[4] Building handle→inventory map...');
const handleMap = {};
const productIdToHandle = {};
desiredInventoryByHandle["84485"] = 2000;
const rl = readline.createInterface({
input: fs.createReadStream(ndjsonPath),
crlfDelay: Infinity
});
for await (const line of rl) {
try {
const rec = JSON.parse(line);
if (rec.handle && rec.id) {
productIdToHandle[rec.id] = rec.handle;
}
if (rec.__parentId && rec.inventoryItem?.id) {
const handle = productIdToHandle[rec.__parentId];
if (handle && desiredInventoryByHandle[handle] != null) {
if (!handleMap[handle]) handleMap[handle] = [];
handleMap[handle].push({
variantId: rec.id,
inventoryItemId: rec.inventoryItem.id
});
}
}
} catch (e) {
console.error('⚠️ JSON parse error:', e.message);
}
}
console.log(`[4] ✔️ Matched ${Object.keys(handleMap).length} handles`);
const mapPath = path.join(exportDir, `${timestamp()}_handle_map.json`);
fs.writeFileSync(mapPath, JSON.stringify(handleMap, null, 2));
return handleMap;
}
// function writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, outputPath) {
// 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 }) => {
// stream.write(JSON.stringify({ input: { inventoryItemId, availableDelta: qty } }) + '\n');
// lineCount++;
// });
// }
// stream.end();
// console.log(`✅ JSONL: wrote ${lineCount} inventory adjustments to ${outputPath}`);
// }
function writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, outputPath, locationId) {
const stream = fs.createWriteStream(outputPath);
let lineCount = 0;
const inventoryUpdates = [];
for (const [handle, items] of Object.entries(handleMap)) {
const qty = desiredInventoryByHandle[handle];
if (qty == null) continue;
items.forEach(({ inventoryItemId }) => {
inventoryUpdates.push({
inventoryItemId,
availableDelta: qty,
locationId,
});
lineCount++;
});
}
// Write all entries to the JSONL stream
inventoryUpdates.forEach((entry) => {
stream.write(JSON.stringify({ input: entry }) + '\n');
});
stream.end();
console.log(`✅ JSONL: wrote ${lineCount} inventory adjustments to ${outputPath}`);
}
// ─── MAIN FLOW ─────────────────────────────────────────────────────
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(`✅ Loaded ${Object.keys(desiredInventoryByHandle).length} inventory items from Turn14`);
if (!fs.existsSync(exportDir)) fs.mkdirSync(exportDir);
await startBulkExport();
const url = await pollUntilReady();
const ndjsonPath = await downloadToFile(url);
const handleMap = await buildHandleMap(ndjsonPath);
const jsonlPath = path.join(exportDir, JSONL_FILENAME);
console.log(handleMap)
// writeBulkInventoryJSONL(handleMap, desiredInventoryByHandle, jsonlPath);
// const { uploadUrl, stagedUploadPath, parameters } = await getStagedUploadPath();
// console.log()
// await uploadJSONLFile(uploadUrl, parameters, jsonlPath);
// await runBulkMutation(stagedUploadPath);
} catch (err) {
console.error('🚨 Error:', err);
process.exit(1);
}