Update import scripts through products
This commit is contained in:
@@ -268,11 +268,18 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
`, [batchIds]);
|
`, [batchIds]);
|
||||||
|
|
||||||
if (discounts.length > 0) {
|
if (discounts.length > 0) {
|
||||||
const placeholders = discounts.map((_, idx) =>
|
const uniqueDiscounts = new Map();
|
||||||
`($${idx * 3 + 1}, $${idx * 3 + 2}, $${idx * 3 + 3})`
|
discounts.forEach(d => {
|
||||||
).join(",");
|
const key = `${d.order_id}-${d.pid}`;
|
||||||
const values = discounts.flatMap(d => [d.order_id, d.pid, d.discount]);
|
uniqueDiscounts.set(key, d);
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = Array.from(uniqueDiscounts.values()).flatMap(d => [d.order_id, d.pid, d.discount || 0]);
|
||||||
|
if (values.length > 0) {
|
||||||
|
const placeholders = Array.from({length: uniqueDiscounts.size}, (_, idx) => {
|
||||||
|
const base = idx * 3;
|
||||||
|
return `($${base + 1}, $${base + 2}, $${base + 3})`;
|
||||||
|
}).join(",");
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO temp_order_discounts (order_id, pid, discount)
|
INSERT INTO temp_order_discounts (order_id, pid, discount)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
@@ -281,6 +288,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
`, values);
|
`, values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get tax information in batches - Keep MySQL compatible for production
|
// Get tax information in batches - Keep MySQL compatible for production
|
||||||
for (let i = 0; i < orderIds.length; i += 5000) {
|
for (let i = 0; i < orderIds.length; i += 5000) {
|
||||||
@@ -404,7 +412,6 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
|
|
||||||
// Filter orders and track missing products
|
// Filter orders and track missing products
|
||||||
const validOrders = [];
|
const validOrders = [];
|
||||||
const values = [];
|
|
||||||
const processedOrderItems = new Set();
|
const processedOrderItems = new Set();
|
||||||
const processedOrders = new Set();
|
const processedOrders = new Set();
|
||||||
|
|
||||||
@@ -425,7 +432,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13}, $${base + 14}, $${base + 15})`;
|
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13}, $${base + 14}, $${base + 15})`;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
|
|
||||||
const values = validOrders.flatMap(o => [
|
const batchValues = validOrders.flatMap(o => [
|
||||||
o.order_number,
|
o.order_number,
|
||||||
o.pid,
|
o.pid,
|
||||||
o.SKU,
|
o.SKU,
|
||||||
@@ -471,7 +478,7 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
|
|||||||
COUNT(*) FILTER (WHERE xmax = 0) as inserted,
|
COUNT(*) FILTER (WHERE xmax = 0) as inserted,
|
||||||
COUNT(*) FILTER (WHERE xmax <> 0) as updated
|
COUNT(*) FILTER (WHERE xmax <> 0) as updated
|
||||||
FROM inserted_orders
|
FROM inserted_orders
|
||||||
`, values);
|
`, batchValues);
|
||||||
|
|
||||||
const { inserted, updated } = result.rows[0];
|
const { inserted, updated } = result.rows[0];
|
||||||
recordsAdded += inserted;
|
recordsAdded += inserted;
|
||||||
|
|||||||
@@ -35,17 +35,28 @@ async function withRetry(operation, errorMessage) {
|
|||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupTemporaryTables(connection) {
|
// Add helper function at the top of the file
|
||||||
await connection.query(`
|
function validateDate(mysqlDate) {
|
||||||
DROP TABLE IF EXISTS temp_products;
|
if (!mysqlDate || mysqlDate === '0000-00-00' || mysqlDate === '0000-00-00 00:00:00') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Check if the date is valid
|
||||||
|
const date = new Date(mysqlDate);
|
||||||
|
return isNaN(date.getTime()) ? null : mysqlDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupTemporaryTables(connection) {
|
||||||
|
// Drop the table if it exists
|
||||||
|
await connection.query('DROP TABLE IF EXISTS temp_products');
|
||||||
|
|
||||||
|
// Create the temporary table
|
||||||
|
await connection.query(`
|
||||||
CREATE TEMP TABLE temp_products (
|
CREATE TEMP TABLE temp_products (
|
||||||
pid BIGINT NOT NULL,
|
pid BIGINT NOT NULL,
|
||||||
title VARCHAR(255),
|
title VARCHAR(255),
|
||||||
description TEXT,
|
description TEXT,
|
||||||
SKU VARCHAR(50),
|
SKU VARCHAR(50),
|
||||||
stock_quantity INTEGER DEFAULT 0,
|
stock_quantity INTEGER DEFAULT 0,
|
||||||
pending_qty INTEGER DEFAULT 0,
|
|
||||||
preorder_count INTEGER DEFAULT 0,
|
preorder_count INTEGER DEFAULT 0,
|
||||||
notions_inv_count INTEGER DEFAULT 0,
|
notions_inv_count INTEGER DEFAULT 0,
|
||||||
price DECIMAL(10,3) NOT NULL DEFAULT 0,
|
price DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
@@ -58,7 +69,7 @@ async function setupTemporaryTables(connection) {
|
|||||||
line VARCHAR(100),
|
line VARCHAR(100),
|
||||||
subline VARCHAR(100),
|
subline VARCHAR(100),
|
||||||
artist VARCHAR(100),
|
artist VARCHAR(100),
|
||||||
category_ids TEXT,
|
categories TEXT,
|
||||||
created_at TIMESTAMP,
|
created_at TIMESTAMP,
|
||||||
first_received TIMESTAMP,
|
first_received TIMESTAMP,
|
||||||
landing_cost_price DECIMAL(10,3),
|
landing_cost_price DECIMAL(10,3),
|
||||||
@@ -66,9 +77,11 @@ async function setupTemporaryTables(connection) {
|
|||||||
harmonized_tariff_code VARCHAR(50),
|
harmonized_tariff_code VARCHAR(50),
|
||||||
updated_at TIMESTAMP,
|
updated_at TIMESTAMP,
|
||||||
visible BOOLEAN,
|
visible BOOLEAN,
|
||||||
|
managing_stock BOOLEAN DEFAULT true,
|
||||||
replenishable BOOLEAN,
|
replenishable BOOLEAN,
|
||||||
permalink VARCHAR(255),
|
permalink VARCHAR(255),
|
||||||
moq DECIMAL(10,3),
|
moq INTEGER DEFAULT 1,
|
||||||
|
uom INTEGER DEFAULT 1,
|
||||||
rating DECIMAL(10,2),
|
rating DECIMAL(10,2),
|
||||||
reviews INTEGER,
|
reviews INTEGER,
|
||||||
weight DECIMAL(10,3),
|
weight DECIMAL(10,3),
|
||||||
@@ -81,12 +94,17 @@ async function setupTemporaryTables(connection) {
|
|||||||
baskets INTEGER,
|
baskets INTEGER,
|
||||||
notifies INTEGER,
|
notifies INTEGER,
|
||||||
date_last_sold TIMESTAMP,
|
date_last_sold TIMESTAMP,
|
||||||
|
image VARCHAR(255),
|
||||||
|
image_175 VARCHAR(255),
|
||||||
|
image_full VARCHAR(255),
|
||||||
|
options TEXT,
|
||||||
|
tags TEXT,
|
||||||
needs_update BOOLEAN DEFAULT TRUE,
|
needs_update BOOLEAN DEFAULT TRUE,
|
||||||
PRIMARY KEY (pid)
|
PRIMARY KEY (pid)
|
||||||
);
|
)`);
|
||||||
|
|
||||||
CREATE INDEX idx_temp_products_needs_update ON temp_products (needs_update);
|
// Create the index
|
||||||
`);
|
await connection.query('CREATE INDEX idx_temp_products_needs_update ON temp_products (needs_update)');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupTemporaryTables(connection) {
|
async function cleanupTemporaryTables(connection) {
|
||||||
@@ -205,17 +223,18 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
|
|||||||
const batch = prodData.slice(i, i + BATCH_SIZE);
|
const batch = prodData.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
const placeholders = batch.map((_, idx) => {
|
const placeholders = batch.map((_, idx) => {
|
||||||
const base = idx * 41; // 41 columns
|
const base = idx * 47; // 47 columns
|
||||||
return `(${Array.from({ length: 41 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
|
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
|
|
||||||
const values = batch.flatMap(row => [
|
const values = batch.flatMap(row => {
|
||||||
|
const imageUrls = getImageUrls(row.pid);
|
||||||
|
return [
|
||||||
row.pid,
|
row.pid,
|
||||||
row.title,
|
row.title,
|
||||||
row.description,
|
row.description,
|
||||||
row.SKU,
|
row.itemnumber || '',
|
||||||
row.stock_quantity,
|
row.stock_quantity > 5000 ? 0 : Math.max(0, row.stock_quantity),
|
||||||
row.pending_qty,
|
|
||||||
row.preorder_count,
|
row.preorder_count,
|
||||||
row.notions_inv_count,
|
row.notions_inv_count,
|
||||||
row.price,
|
row.price,
|
||||||
@@ -229,16 +248,18 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
|
|||||||
row.subline,
|
row.subline,
|
||||||
row.artist,
|
row.artist,
|
||||||
row.category_ids,
|
row.category_ids,
|
||||||
row.date_created,
|
validateDate(row.date_created),
|
||||||
row.first_received,
|
validateDate(row.first_received),
|
||||||
row.landing_cost_price,
|
row.landing_cost_price,
|
||||||
row.barcode,
|
row.barcode,
|
||||||
row.harmonized_tariff_code,
|
row.harmonized_tariff_code,
|
||||||
row.updated_at,
|
validateDate(row.updated_at),
|
||||||
row.visible,
|
row.visible,
|
||||||
|
true,
|
||||||
row.replenishable,
|
row.replenishable,
|
||||||
row.permalink,
|
row.permalink,
|
||||||
row.moq,
|
Math.max(1, Math.round(row.moq || 1)),
|
||||||
|
1,
|
||||||
row.rating,
|
row.rating,
|
||||||
row.reviews,
|
row.reviews,
|
||||||
row.weight,
|
row.weight,
|
||||||
@@ -250,19 +271,25 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
|
|||||||
row.total_sold,
|
row.total_sold,
|
||||||
row.baskets,
|
row.baskets,
|
||||||
row.notifies,
|
row.notifies,
|
||||||
row.date_last_sold
|
validateDate(row.date_last_sold),
|
||||||
]);
|
imageUrls.image,
|
||||||
|
imageUrls.image_175,
|
||||||
|
imageUrls.image_full,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const [result] = await localConnection.query(`
|
const [result] = await localConnection.query(`
|
||||||
WITH inserted_products AS (
|
WITH inserted_products AS (
|
||||||
INSERT INTO products (
|
INSERT INTO products (
|
||||||
pid, title, description, SKU, stock_quantity, pending_qty, preorder_count,
|
pid, title, description, SKU, stock_quantity, preorder_count, notions_inv_count,
|
||||||
notions_inv_count, price, regular_price, cost_price, vendor, vendor_reference,
|
price, regular_price, cost_price, vendor, vendor_reference, notions_reference,
|
||||||
notions_reference, brand, line, subline, artist, category_ids, created_at,
|
brand, line, subline, artist, categories, created_at, first_received,
|
||||||
first_received, landing_cost_price, barcode, harmonized_tariff_code,
|
landing_cost_price, barcode, harmonized_tariff_code, updated_at, visible,
|
||||||
updated_at, visible, replenishable, permalink, moq, rating, reviews,
|
managing_stock, replenishable, permalink, moq, uom, rating, reviews,
|
||||||
weight, length, width, height, country_of_origin, location, total_sold,
|
weight, length, width, height, country_of_origin, location, total_sold,
|
||||||
baskets, notifies, date_last_sold
|
baskets, notifies, date_last_sold, image, image_175, image_full, options, tags
|
||||||
)
|
)
|
||||||
VALUES ${placeholders}
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (pid) DO NOTHING
|
ON CONFLICT (pid) DO NOTHING
|
||||||
@@ -285,7 +312,7 @@ async function importMissingProducts(prodConnection, localConnection, missingPid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function materializeCalculations(prodConnection, localConnection, incrementalUpdate = true, lastSyncTime = '1970-01-01') {
|
async function materializeCalculations(prodConnection, localConnection, incrementalUpdate = true, lastSyncTime = '1970-01-01', startTime = Date.now()) {
|
||||||
outputProgress({
|
outputProgress({
|
||||||
status: "running",
|
status: "running",
|
||||||
operation: "Products import",
|
operation: "Products import",
|
||||||
@@ -315,36 +342,8 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
) THEN 0
|
) THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END AS replenishable,
|
END AS replenishable,
|
||||||
COALESCE(si.available_local, 0) - COALESCE(
|
COALESCE(si.available_local, 0) as stock_quantity,
|
||||||
(SELECT SUM(oi.qty_ordered - oi.qty_placed)
|
0 as pending_qty,
|
||||||
FROM order_items oi
|
|
||||||
JOIN _order o ON oi.order_id = o.order_id
|
|
||||||
WHERE oi.prod_pid = p.pid
|
|
||||||
AND o.date_placed != '0000-00-00 00:00:00'
|
|
||||||
AND o.date_shipped = '0000-00-00 00:00:00'
|
|
||||||
AND oi.pick_finished = 0
|
|
||||||
AND oi.qty_back = 0
|
|
||||||
AND o.order_status != 15
|
|
||||||
AND o.order_status < 90
|
|
||||||
AND oi.qty_ordered >= oi.qty_placed
|
|
||||||
AND oi.qty_ordered > 0
|
|
||||||
), 0
|
|
||||||
) as stock_quantity,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT SUM(oi.qty_ordered - oi.qty_placed)
|
|
||||||
FROM order_items oi
|
|
||||||
JOIN _order o ON oi.order_id = o.order_id
|
|
||||||
WHERE oi.prod_pid = p.pid
|
|
||||||
AND o.date_placed != '0000-00-00 00:00:00'
|
|
||||||
AND o.date_shipped = '0000-00-00 00:00:00'
|
|
||||||
AND oi.pick_finished = 0
|
|
||||||
AND oi.qty_back = 0
|
|
||||||
AND o.order_status != 15
|
|
||||||
AND o.order_status < 90
|
|
||||||
AND oi.qty_ordered >= oi.qty_placed
|
|
||||||
AND oi.qty_ordered > 0
|
|
||||||
), 0
|
|
||||||
) as pending_qty,
|
|
||||||
COALESCE(ci.onpreorder, 0) as preorder_count,
|
COALESCE(ci.onpreorder, 0) as preorder_count,
|
||||||
COALESCE(pnb.inventory, 0) as notions_inv_count,
|
COALESCE(pnb.inventory, 0) as notions_inv_count,
|
||||||
COALESCE(pcp.price_each, 0) as price,
|
COALESCE(pcp.price_each, 0) as price,
|
||||||
@@ -423,18 +422,18 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
|
|
||||||
await withRetry(async () => {
|
await withRetry(async () => {
|
||||||
const placeholders = batch.map((_, idx) => {
|
const placeholders = batch.map((_, idx) => {
|
||||||
const offset = idx * 41; // 41 columns
|
const base = idx * 47; // 47 columns
|
||||||
return `(${Array.from({ length: 41 }, (_, i) => `$${offset + i + 1}`).join(', ')})`;
|
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
|
|
||||||
const values = batch.flatMap(row => [
|
const values = batch.flatMap(row => {
|
||||||
|
const imageUrls = getImageUrls(row.pid);
|
||||||
|
return [
|
||||||
row.pid,
|
row.pid,
|
||||||
row.title,
|
row.title,
|
||||||
row.description,
|
row.description,
|
||||||
row.SKU,
|
row.itemnumber || '',
|
||||||
// Set stock quantity to 0 if it's over 5000
|
|
||||||
row.stock_quantity > 5000 ? 0 : Math.max(0, row.stock_quantity),
|
row.stock_quantity > 5000 ? 0 : Math.max(0, row.stock_quantity),
|
||||||
row.pending_qty,
|
|
||||||
row.preorder_count,
|
row.preorder_count,
|
||||||
row.notions_inv_count,
|
row.notions_inv_count,
|
||||||
row.price,
|
row.price,
|
||||||
@@ -448,17 +447,19 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
row.subline,
|
row.subline,
|
||||||
row.artist,
|
row.artist,
|
||||||
row.category_ids,
|
row.category_ids,
|
||||||
row.date_created, // map to created_at
|
validateDate(row.date_created),
|
||||||
row.first_received,
|
validateDate(row.first_received),
|
||||||
row.landing_cost_price,
|
row.landing_cost_price,
|
||||||
row.barcode,
|
row.barcode,
|
||||||
row.harmonized_tariff_code,
|
row.harmonized_tariff_code,
|
||||||
row.updated_at,
|
validateDate(row.updated_at),
|
||||||
row.visible,
|
row.visible,
|
||||||
|
true,
|
||||||
row.replenishable,
|
row.replenishable,
|
||||||
row.permalink,
|
row.permalink,
|
||||||
row.moq,
|
Math.max(1, Math.round(row.moq || 1)),
|
||||||
row.rating ? Number(row.rating).toFixed(2) : null,
|
1,
|
||||||
|
row.rating,
|
||||||
row.reviews,
|
row.reviews,
|
||||||
row.weight,
|
row.weight,
|
||||||
row.length,
|
row.length,
|
||||||
@@ -469,25 +470,30 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
row.total_sold,
|
row.total_sold,
|
||||||
row.baskets,
|
row.baskets,
|
||||||
row.notifies,
|
row.notifies,
|
||||||
row.date_last_sold
|
validateDate(row.date_last_sold),
|
||||||
]);
|
imageUrls.image,
|
||||||
|
imageUrls.image_175,
|
||||||
|
imageUrls.image_full,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
await localConnection.query(`
|
await localConnection.query(`
|
||||||
INSERT INTO temp_products (
|
INSERT INTO temp_products (
|
||||||
pid, title, description, SKU, stock_quantity, pending_qty, preorder_count,
|
pid, title, description, SKU, stock_quantity, preorder_count, notions_inv_count,
|
||||||
notions_inv_count, price, regular_price, cost_price, vendor, vendor_reference,
|
price, regular_price, cost_price, vendor, vendor_reference, notions_reference,
|
||||||
notions_reference, brand, line, subline, artist, category_ids, created_at,
|
brand, line, subline, artist, categories, created_at, first_received,
|
||||||
first_received, landing_cost_price, barcode, harmonized_tariff_code,
|
landing_cost_price, barcode, harmonized_tariff_code, updated_at, visible,
|
||||||
updated_at, visible, replenishable, permalink, moq, rating, reviews,
|
managing_stock, replenishable, permalink, moq, uom, rating, reviews,
|
||||||
weight, length, width, height, country_of_origin, location, total_sold,
|
weight, length, width, height, country_of_origin, location, total_sold,
|
||||||
baskets, notifies, date_last_sold
|
baskets, notifies, date_last_sold, image, image_175, image_full, options, tags
|
||||||
) VALUES ${placeholders}
|
) VALUES ${placeholders}
|
||||||
ON CONFLICT (pid) DO UPDATE SET
|
ON CONFLICT (pid) DO UPDATE SET
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
description = EXCLUDED.description,
|
description = EXCLUDED.description,
|
||||||
SKU = EXCLUDED.SKU,
|
SKU = EXCLUDED.SKU,
|
||||||
stock_quantity = EXCLUDED.stock_quantity,
|
stock_quantity = EXCLUDED.stock_quantity,
|
||||||
pending_qty = EXCLUDED.pending_qty,
|
|
||||||
preorder_count = EXCLUDED.preorder_count,
|
preorder_count = EXCLUDED.preorder_count,
|
||||||
notions_inv_count = EXCLUDED.notions_inv_count,
|
notions_inv_count = EXCLUDED.notions_inv_count,
|
||||||
price = EXCLUDED.price,
|
price = EXCLUDED.price,
|
||||||
@@ -500,7 +506,6 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
line = EXCLUDED.line,
|
line = EXCLUDED.line,
|
||||||
subline = EXCLUDED.subline,
|
subline = EXCLUDED.subline,
|
||||||
artist = EXCLUDED.artist,
|
artist = EXCLUDED.artist,
|
||||||
category_ids = EXCLUDED.category_ids,
|
|
||||||
created_at = EXCLUDED.created_at,
|
created_at = EXCLUDED.created_at,
|
||||||
first_received = EXCLUDED.first_received,
|
first_received = EXCLUDED.first_received,
|
||||||
landing_cost_price = EXCLUDED.landing_cost_price,
|
landing_cost_price = EXCLUDED.landing_cost_price,
|
||||||
@@ -508,9 +513,11 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
harmonized_tariff_code = EXCLUDED.harmonized_tariff_code,
|
harmonized_tariff_code = EXCLUDED.harmonized_tariff_code,
|
||||||
updated_at = EXCLUDED.updated_at,
|
updated_at = EXCLUDED.updated_at,
|
||||||
visible = EXCLUDED.visible,
|
visible = EXCLUDED.visible,
|
||||||
|
managing_stock = EXCLUDED.managing_stock,
|
||||||
replenishable = EXCLUDED.replenishable,
|
replenishable = EXCLUDED.replenishable,
|
||||||
permalink = EXCLUDED.permalink,
|
permalink = EXCLUDED.permalink,
|
||||||
moq = EXCLUDED.moq,
|
moq = EXCLUDED.moq,
|
||||||
|
uom = EXCLUDED.uom,
|
||||||
rating = EXCLUDED.rating,
|
rating = EXCLUDED.rating,
|
||||||
reviews = EXCLUDED.reviews,
|
reviews = EXCLUDED.reviews,
|
||||||
weight = EXCLUDED.weight,
|
weight = EXCLUDED.weight,
|
||||||
@@ -522,7 +529,12 @@ async function materializeCalculations(prodConnection, localConnection, incremen
|
|||||||
total_sold = EXCLUDED.total_sold,
|
total_sold = EXCLUDED.total_sold,
|
||||||
baskets = EXCLUDED.baskets,
|
baskets = EXCLUDED.baskets,
|
||||||
notifies = EXCLUDED.notifies,
|
notifies = EXCLUDED.notifies,
|
||||||
date_last_sold = EXCLUDED.date_last_sold
|
date_last_sold = EXCLUDED.date_last_sold,
|
||||||
|
image = EXCLUDED.image,
|
||||||
|
image_175 = EXCLUDED.image_175,
|
||||||
|
image_full = EXCLUDED.image_full,
|
||||||
|
options = EXCLUDED.options,
|
||||||
|
tags = EXCLUDED.tags
|
||||||
`, values);
|
`, values);
|
||||||
}, `Error inserting batch ${i} to ${i + batch.length}`);
|
}, `Error inserting batch ${i} to ${i + batch.length}`);
|
||||||
|
|
||||||
@@ -560,11 +572,15 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start a transaction to ensure temporary tables persist
|
||||||
|
await localConnection.beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
// Setup temporary tables
|
// Setup temporary tables
|
||||||
await setupTemporaryTables(localConnection);
|
await setupTemporaryTables(localConnection);
|
||||||
|
|
||||||
// Materialize calculations into temp table
|
// Materialize calculations into temp table
|
||||||
await materializeCalculations(prodConnection, localConnection, incrementalUpdate, lastSyncTime);
|
await materializeCalculations(prodConnection, localConnection, incrementalUpdate, lastSyncTime, startTime);
|
||||||
|
|
||||||
// Get the list of products that need updating
|
// Get the list of products that need updating
|
||||||
const [products] = await localConnection.query(`
|
const [products] = await localConnection.query(`
|
||||||
@@ -574,7 +590,6 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
t.description,
|
t.description,
|
||||||
t.SKU,
|
t.SKU,
|
||||||
t.stock_quantity,
|
t.stock_quantity,
|
||||||
t.pending_qty,
|
|
||||||
t.preorder_count,
|
t.preorder_count,
|
||||||
t.notions_inv_count,
|
t.notions_inv_count,
|
||||||
t.price,
|
t.price,
|
||||||
@@ -587,7 +602,7 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
t.line,
|
t.line,
|
||||||
t.subline,
|
t.subline,
|
||||||
t.artist,
|
t.artist,
|
||||||
t.category_ids,
|
t.categories,
|
||||||
t.created_at,
|
t.created_at,
|
||||||
t.first_received,
|
t.first_received,
|
||||||
t.landing_cost_price,
|
t.landing_cost_price,
|
||||||
@@ -595,6 +610,7 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
t.harmonized_tariff_code,
|
t.harmonized_tariff_code,
|
||||||
t.updated_at,
|
t.updated_at,
|
||||||
t.visible,
|
t.visible,
|
||||||
|
t.managing_stock,
|
||||||
t.replenishable,
|
t.replenishable,
|
||||||
t.permalink,
|
t.permalink,
|
||||||
t.moq,
|
t.moq,
|
||||||
@@ -609,30 +625,36 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
t.total_sold,
|
t.total_sold,
|
||||||
t.baskets,
|
t.baskets,
|
||||||
t.notifies,
|
t.notifies,
|
||||||
t.date_last_sold
|
t.date_last_sold,
|
||||||
|
t.image,
|
||||||
|
t.image_175,
|
||||||
|
t.image_full,
|
||||||
|
t.options,
|
||||||
|
t.tags
|
||||||
FROM temp_products t
|
FROM temp_products t
|
||||||
|
WHERE t.needs_update = true
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Process products in batches
|
||||||
let recordsAdded = 0;
|
let recordsAdded = 0;
|
||||||
let recordsUpdated = 0;
|
let recordsUpdated = 0;
|
||||||
|
|
||||||
// Process products in batches
|
for (let i = 0; i < products.rows.length; i += BATCH_SIZE) {
|
||||||
for (let i = 0; i < products.length; i += BATCH_SIZE) {
|
const batch = products.rows.slice(i, i + BATCH_SIZE);
|
||||||
const batch = products.slice(i, Math.min(i + BATCH_SIZE, products.length));
|
|
||||||
|
|
||||||
await withRetry(async () => {
|
|
||||||
const placeholders = batch.map((_, idx) => {
|
const placeholders = batch.map((_, idx) => {
|
||||||
const offset = idx * 41; // 41 columns
|
const base = idx * 47; // 47 columns
|
||||||
return `(${Array.from({ length: 41 }, (_, i) => `$${offset + i + 1}`).join(', ')})`;
|
return `(${Array.from({ length: 47 }, (_, i) => `$${base + i + 1}`).join(', ')})`;
|
||||||
}).join(',');
|
}).join(',');
|
||||||
|
|
||||||
const values = batch.flatMap(row => [
|
const values = batch.flatMap(row => {
|
||||||
|
const imageUrls = getImageUrls(row.pid);
|
||||||
|
return [
|
||||||
row.pid,
|
row.pid,
|
||||||
row.title,
|
row.title,
|
||||||
row.description,
|
row.description,
|
||||||
row.SKU,
|
row.SKU || '',
|
||||||
row.stock_quantity,
|
row.stock_quantity > 5000 ? 0 : Math.max(0, row.stock_quantity),
|
||||||
row.pending_qty,
|
|
||||||
row.preorder_count,
|
row.preorder_count,
|
||||||
row.notions_inv_count,
|
row.notions_inv_count,
|
||||||
row.price,
|
row.price,
|
||||||
@@ -645,17 +667,19 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
row.line,
|
row.line,
|
||||||
row.subline,
|
row.subline,
|
||||||
row.artist,
|
row.artist,
|
||||||
row.category_ids,
|
row.categories,
|
||||||
row.created_at,
|
validateDate(row.created_at),
|
||||||
row.first_received,
|
validateDate(row.first_received),
|
||||||
row.landing_cost_price,
|
row.landing_cost_price,
|
||||||
row.barcode,
|
row.barcode,
|
||||||
row.harmonized_tariff_code,
|
row.harmonized_tariff_code,
|
||||||
row.updated_at,
|
validateDate(row.updated_at),
|
||||||
row.visible,
|
row.visible,
|
||||||
|
row.managing_stock,
|
||||||
row.replenishable,
|
row.replenishable,
|
||||||
row.permalink,
|
row.permalink,
|
||||||
row.moq,
|
row.moq,
|
||||||
|
1,
|
||||||
row.rating,
|
row.rating,
|
||||||
row.reviews,
|
row.reviews,
|
||||||
row.weight,
|
row.weight,
|
||||||
@@ -667,26 +691,32 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
row.total_sold,
|
row.total_sold,
|
||||||
row.baskets,
|
row.baskets,
|
||||||
row.notifies,
|
row.notifies,
|
||||||
row.date_last_sold
|
validateDate(row.date_last_sold),
|
||||||
]);
|
imageUrls.image,
|
||||||
|
imageUrls.image_175,
|
||||||
|
imageUrls.image_full,
|
||||||
|
row.options,
|
||||||
|
row.tags
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const result = await localConnection.query(`
|
const [result] = await localConnection.query(`
|
||||||
WITH upserted_products AS (
|
WITH upserted AS (
|
||||||
INSERT INTO products (
|
INSERT INTO products (
|
||||||
pid, title, description, SKU, stock_quantity, pending_qty, preorder_count,
|
pid, title, description, SKU, stock_quantity, preorder_count, notions_inv_count,
|
||||||
notions_inv_count, price, regular_price, cost_price, vendor, vendor_reference,
|
price, regular_price, cost_price, vendor, vendor_reference, notions_reference,
|
||||||
notions_reference, brand, line, subline, artist, category_ids, created_at,
|
brand, line, subline, artist, categories, created_at, first_received,
|
||||||
first_received, landing_cost_price, barcode, harmonized_tariff_code,
|
landing_cost_price, barcode, harmonized_tariff_code, updated_at, visible,
|
||||||
updated_at, visible, replenishable, permalink, moq, rating, reviews,
|
managing_stock, replenishable, permalink, moq, uom, rating, reviews,
|
||||||
weight, length, width, height, country_of_origin, location, total_sold,
|
weight, length, width, height, country_of_origin, location, total_sold,
|
||||||
baskets, notifies, date_last_sold
|
baskets, notifies, date_last_sold, image, image_175, image_full, options, tags
|
||||||
) VALUES ${placeholders}
|
)
|
||||||
|
VALUES ${placeholders}
|
||||||
ON CONFLICT (pid) DO UPDATE SET
|
ON CONFLICT (pid) DO UPDATE SET
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
description = EXCLUDED.description,
|
description = EXCLUDED.description,
|
||||||
SKU = EXCLUDED.SKU,
|
SKU = EXCLUDED.SKU,
|
||||||
stock_quantity = EXCLUDED.stock_quantity,
|
stock_quantity = EXCLUDED.stock_quantity,
|
||||||
pending_qty = EXCLUDED.pending_qty,
|
|
||||||
preorder_count = EXCLUDED.preorder_count,
|
preorder_count = EXCLUDED.preorder_count,
|
||||||
notions_inv_count = EXCLUDED.notions_inv_count,
|
notions_inv_count = EXCLUDED.notions_inv_count,
|
||||||
price = EXCLUDED.price,
|
price = EXCLUDED.price,
|
||||||
@@ -699,7 +729,6 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
line = EXCLUDED.line,
|
line = EXCLUDED.line,
|
||||||
subline = EXCLUDED.subline,
|
subline = EXCLUDED.subline,
|
||||||
artist = EXCLUDED.artist,
|
artist = EXCLUDED.artist,
|
||||||
category_ids = EXCLUDED.category_ids,
|
|
||||||
created_at = EXCLUDED.created_at,
|
created_at = EXCLUDED.created_at,
|
||||||
first_received = EXCLUDED.first_received,
|
first_received = EXCLUDED.first_received,
|
||||||
landing_cost_price = EXCLUDED.landing_cost_price,
|
landing_cost_price = EXCLUDED.landing_cost_price,
|
||||||
@@ -707,9 +736,11 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
harmonized_tariff_code = EXCLUDED.harmonized_tariff_code,
|
harmonized_tariff_code = EXCLUDED.harmonized_tariff_code,
|
||||||
updated_at = EXCLUDED.updated_at,
|
updated_at = EXCLUDED.updated_at,
|
||||||
visible = EXCLUDED.visible,
|
visible = EXCLUDED.visible,
|
||||||
|
managing_stock = EXCLUDED.managing_stock,
|
||||||
replenishable = EXCLUDED.replenishable,
|
replenishable = EXCLUDED.replenishable,
|
||||||
permalink = EXCLUDED.permalink,
|
permalink = EXCLUDED.permalink,
|
||||||
moq = EXCLUDED.moq,
|
moq = EXCLUDED.moq,
|
||||||
|
uom = EXCLUDED.uom,
|
||||||
rating = EXCLUDED.rating,
|
rating = EXCLUDED.rating,
|
||||||
reviews = EXCLUDED.reviews,
|
reviews = EXCLUDED.reviews,
|
||||||
weight = EXCLUDED.weight,
|
weight = EXCLUDED.weight,
|
||||||
@@ -721,58 +752,82 @@ async function importProducts(prodConnection, localConnection, incrementalUpdate
|
|||||||
total_sold = EXCLUDED.total_sold,
|
total_sold = EXCLUDED.total_sold,
|
||||||
baskets = EXCLUDED.baskets,
|
baskets = EXCLUDED.baskets,
|
||||||
notifies = EXCLUDED.notifies,
|
notifies = EXCLUDED.notifies,
|
||||||
date_last_sold = EXCLUDED.date_last_sold
|
date_last_sold = EXCLUDED.date_last_sold,
|
||||||
|
image = EXCLUDED.image,
|
||||||
|
image_175 = EXCLUDED.image_175,
|
||||||
|
image_full = EXCLUDED.image_full,
|
||||||
|
options = EXCLUDED.options,
|
||||||
|
tags = EXCLUDED.tags
|
||||||
RETURNING
|
RETURNING
|
||||||
CASE WHEN xmax::text::int > 0 THEN 0 ELSE 1 END AS inserted,
|
xmax = 0 as inserted
|
||||||
CASE WHEN xmax::text::int > 0 THEN 1 ELSE 0 END AS updated
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) FILTER (WHERE inserted = 1) as inserted,
|
COUNT(*) FILTER (WHERE inserted) as inserted,
|
||||||
COUNT(*) FILTER (WHERE updated = 1) as updated
|
COUNT(*) FILTER (WHERE NOT inserted) as updated
|
||||||
FROM upserted_products
|
FROM upserted
|
||||||
`, values);
|
`, values);
|
||||||
|
|
||||||
recordsAdded += result.rows[0].inserted;
|
recordsAdded += parseInt(result.rows[0].inserted, 10) || 0;
|
||||||
recordsUpdated += result.rows[0].updated;
|
recordsUpdated += parseInt(result.rows[0].updated, 10) || 0;
|
||||||
}, `Error inserting batch ${i} to ${i + batch.length}`);
|
|
||||||
|
// Process category relationships for each product in the batch
|
||||||
|
for (const row of batch) {
|
||||||
|
if (row.categories) {
|
||||||
|
const categoryIds = row.categories.split(',').filter(id => id && id.trim());
|
||||||
|
if (categoryIds.length > 0) {
|
||||||
|
const catPlaceholders = categoryIds.map((_, idx) =>
|
||||||
|
`($${idx * 2 + 1}, $${idx * 2 + 2})`
|
||||||
|
).join(',');
|
||||||
|
const catValues = categoryIds.flatMap(catId => [row.pid, parseInt(catId.trim(), 10)]);
|
||||||
|
|
||||||
|
// First delete existing relationships for this product
|
||||||
|
await localConnection.query(
|
||||||
|
'DELETE FROM product_categories WHERE pid = $1',
|
||||||
|
[row.pid]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then insert the new relationships
|
||||||
|
await localConnection.query(`
|
||||||
|
INSERT INTO product_categories (pid, cat_id)
|
||||||
|
VALUES ${catPlaceholders}
|
||||||
|
ON CONFLICT (pid, cat_id) DO NOTHING
|
||||||
|
`, catValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
outputProgress({
|
outputProgress({
|
||||||
status: "running",
|
status: "running",
|
||||||
operation: "Products import",
|
operation: "Products import",
|
||||||
message: `Imported ${i + batch.length} of ${products.length} products`,
|
message: `Processing products: ${i + batch.length} of ${products.rows.length}`,
|
||||||
current: i + batch.length,
|
current: i + batch.length,
|
||||||
total: products.length,
|
total: products.rows.length,
|
||||||
elapsed: formatElapsedTime((Date.now() - startTime) / 1000),
|
elapsed: formatElapsedTime((Date.now() - startTime) / 1000),
|
||||||
remaining: estimateRemaining(startTime, i + batch.length, products.length),
|
remaining: estimateRemaining(startTime, i + batch.length, products.rows.length),
|
||||||
rate: calculateRate(startTime, i + batch.length)
|
rate: calculateRate(startTime, i + batch.length)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update sync status
|
|
||||||
await localConnection.query(`
|
|
||||||
INSERT INTO sync_status (table_name, last_sync_timestamp)
|
|
||||||
VALUES ('products', NOW())
|
|
||||||
ON CONFLICT (table_name) DO UPDATE SET
|
|
||||||
last_sync_timestamp = NOW()
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Cleanup temporary tables
|
// Cleanup temporary tables
|
||||||
await cleanupTemporaryTables(localConnection);
|
await cleanupTemporaryTables(localConnection);
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
await localConnection.commit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "complete",
|
status: 'complete',
|
||||||
recordsAdded,
|
recordsAdded,
|
||||||
recordsUpdated,
|
recordsUpdated,
|
||||||
totalRecords: products.length
|
totalRecords: products.rows.length,
|
||||||
|
duration: formatElapsedTime(Date.now() - startTime)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in importProducts:', error);
|
// Rollback on error
|
||||||
// Attempt cleanup on error
|
await localConnection.rollback();
|
||||||
try {
|
throw error;
|
||||||
await cleanupTemporaryTables(localConnection);
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.error('Error during cleanup:', cleanupError);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in importProducts:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ async function setupConnections(sshConfig) {
|
|||||||
|
|
||||||
// Create a wrapper for the PostgreSQL pool to match MySQL interface
|
// Create a wrapper for the PostgreSQL pool to match MySQL interface
|
||||||
const localConnection = {
|
const localConnection = {
|
||||||
|
_client: null,
|
||||||
|
_transactionActive: false,
|
||||||
|
|
||||||
query: async (text, params) => {
|
query: async (text, params) => {
|
||||||
|
// If we're not in a transaction, use the pool directly
|
||||||
|
if (!localConnection._transactionActive) {
|
||||||
const client = await localPool.connect();
|
const client = await localPool.connect();
|
||||||
try {
|
try {
|
||||||
const result = await client.query(text, params);
|
const result = await client.query(text, params);
|
||||||
@@ -72,17 +77,55 @@ async function setupConnections(sshConfig) {
|
|||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a transaction, use the dedicated client
|
||||||
|
if (!localConnection._client) {
|
||||||
|
throw new Error('No active transaction client');
|
||||||
|
}
|
||||||
|
const result = await localConnection._client.query(text, params);
|
||||||
|
return [result];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beginTransaction: async () => {
|
||||||
|
if (localConnection._transactionActive) {
|
||||||
|
throw new Error('Transaction already active');
|
||||||
|
}
|
||||||
|
localConnection._client = await localPool.connect();
|
||||||
|
await localConnection._client.query('BEGIN');
|
||||||
|
localConnection._transactionActive = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
commit: async () => {
|
||||||
|
if (!localConnection._transactionActive) {
|
||||||
|
throw new Error('No active transaction to commit');
|
||||||
|
}
|
||||||
|
await localConnection._client.query('COMMIT');
|
||||||
|
localConnection._client.release();
|
||||||
|
localConnection._client = null;
|
||||||
|
localConnection._transactionActive = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
rollback: async () => {
|
||||||
|
if (!localConnection._transactionActive) {
|
||||||
|
throw new Error('No active transaction to rollback');
|
||||||
|
}
|
||||||
|
await localConnection._client.query('ROLLBACK');
|
||||||
|
localConnection._client.release();
|
||||||
|
localConnection._client = null;
|
||||||
|
localConnection._transactionActive = false;
|
||||||
|
},
|
||||||
|
|
||||||
end: async () => {
|
end: async () => {
|
||||||
|
if (localConnection._client) {
|
||||||
|
localConnection._client.release();
|
||||||
|
localConnection._client = null;
|
||||||
|
}
|
||||||
await localPool.end();
|
await localPool.end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return { prodConnection, localConnection, tunnel };
|
||||||
ssh: tunnel.ssh,
|
|
||||||
prodConnection,
|
|
||||||
localConnection
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to close connections
|
// Helper function to close connections
|
||||||
|
|||||||
Reference in New Issue
Block a user