bulk products

This commit is contained in:
2ManyProjects 2025-05-05 15:39:21 -05:00
parent 96334a595f
commit 5d0ce42592
4 changed files with 254 additions and 145 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

View file

@ -9,150 +9,79 @@ module.exports = (pool, query, authMiddleware) => {
// Create a new product with multiple images // Create a new product with multiple images
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { // Check for array
name, let products = [];
description, if (req.body.products) {
categoryName, products = req.body.products;
price, } else {
stockQuantity, const {
weightGrams, name,
lengthCm, description,
widthCm, categoryName,
heightCm, price,
origin, stockQuantity,
age, weightGrams,
materialType, lengthCm,
color, widthCm,
images, heightCm,
tags stockNotification,
} = req.body; origin,
age,
// Validate required fields materialType,
if (!name || !description || !categoryName || !price || !stockQuantity) { color,
return res.status(400).json({ images,
error: true, tags
message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory' } = req.body;
products.push({
name,
description,
categoryName,
price,
stockQuantity,
weightGrams,
lengthCm,
widthCm,
heightCm,
stockNotification,
origin,
age,
materialType,
color,
images: images || [],
tags
}); });
} }
// Prepare promises array for concurrent execution
const productPromises = products.map(product => createProduct(product, pool));
// Begin transaction // Execute all product creation promises concurrently
const client = await pool.connect(); const results = await Promise.all(productPromises);
try { // Separate successes and errors
await client.query('BEGIN'); const success = results.filter(result => !result.error).map(result => ({
message: 'Product created successfully',
// Get category ID by name product: result.product,
const categoryResult = await client.query( code: 201
'SELECT id FROM product_categories WHERE name = $1', }));
[categoryName]
); const errs = results.filter(result => result.error);
if (categoryResult.rows.length === 0) { res.status(201).json({
return res.status(404).json({ message: errs.length > 0 ?
error: true, (success.length > 0 ? 'Some Products failed to create, check errors array' : "Failed to create Products") :
message: `Category "${categoryName}" not found` 'Products created successfully',
}); success,
} errs
});
const categoryId = categoryResult.rows[0].id;
// Create product
const productId = uuidv4();
await client.query(
`INSERT INTO products (
id, name, description, category_id, price, stock_quantity,
weight_grams, length_cm, width_cm, height_cm,
origin, age, material_type, color
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
[
productId, name, description, categoryId, price, stockQuantity,
weightGrams || null, lengthCm || null, widthCm || null, heightCm || null,
origin || null, age || null, materialType || null, color || null
]
);
// Add images if provided
if (images && images.length > 0) {
for (let i = 0; i < images.length; i++) {
const { path, isPrimary = (i === 0) } = images[i];
await client.query(
'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)',
[productId, path, i, isPrimary]
);
}
}
// Add tags if provided
if (tags && tags.length > 0) {
for (const tagName of tags) {
// Get tag ID
let tagResult = await client.query(
'SELECT id FROM tags WHERE name = $1',
[tagName]
);
let tagId;
// If tag doesn't exist, create it
if (tagResult.rows.length === 0) {
const newTagResult = await client.query(
'INSERT INTO tags (name) VALUES ($1) RETURNING id',
[tagName]
);
tagId = newTagResult.rows[0].id;
} else {
tagId = tagResult.rows[0].id;
}
// Add tag to product
await client.query(
'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)',
[productId, tagId]
);
}
}
await client.query('COMMIT');
// Get complete product with images and tags
const productQuery = `
SELECT p.*,
pc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags,
json_agg(json_build_object(
'id', pi.id,
'path', pi.image_path,
'isPrimary', pi.is_primary,
'displayOrder', pi.display_order
)) FILTER (WHERE pi.id IS NOT NULL) AS images
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
LEFT JOIN product_images pi ON p.id = pi.product_id
WHERE p.id = $1
GROUP BY p.id, pc.name
`;
const product = await query(productQuery, [productId]);
res.status(201).json({
message: 'Product created successfully',
product: product.rows[0]
});
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} catch (error) { } catch (error) {
next(error); next(error);
} }
}); });
router.post('/:id/stock-notification', async (req, res, next) => { router.post('/:id/stock-notification', async (req, res, next) => {
const client = await pool.connect();
try { try {
const { id } = req.params; const { id } = req.params;
const { enabled, email, threshold } = req.body; const { enabled, email, threshold } = req.body;
@ -164,6 +93,7 @@ module.exports = (pool, query, authMiddleware) => {
message: 'Admin access required' message: 'Admin access required'
}); });
} }
await client.query('BEGIN');
// Check if product exists // Check if product exists
const productCheck = await query( const productCheck = await query(
@ -186,22 +116,21 @@ module.exports = (pool, query, authMiddleware) => {
}; };
// Update product with notification settings // Update product with notification settings
const result = await query( const result = await addThresholdNotification(id, enabled, email, threshold, client);
`UPDATE products await client.query('COMMIT');
SET stock_notification = $1
WHERE id = $2
RETURNING *`,
[JSON.stringify(notificationSettings), id]
);
res.json({ res.json({
message: 'Stock notification settings updated successfully', message: 'Stock notification settings updated successfully',
product: result.rows[0] product: result.rows[0]
}); });
} catch (error) { } catch (error) {
await client.query('ROLLBACK');
next(error); next(error);
} finally {
client.release();
} }
}); });
// Update an existing product // Update an existing product
router.put('/:id', async (req, res, next) => { router.put('/:id', async (req, res, next) => {
@ -215,6 +144,7 @@ module.exports = (pool, query, authMiddleware) => {
stockQuantity, stockQuantity,
weightGrams, weightGrams,
lengthCm, lengthCm,
stockNotification,
widthCm, widthCm,
heightCm, heightCm,
origin, origin,
@ -344,6 +274,10 @@ module.exports = (pool, query, authMiddleware) => {
updateValues.push(color); updateValues.push(color);
valueIndex++; valueIndex++;
} }
if (stockNotification) {
await addThresholdNotification(id, stockNotification.enabled, stockNotification.email, stockNotification.threshold, client);
}
if (updateFields.length > 0) { if (updateFields.length > 0) {
const updateQuery = ` const updateQuery = `
@ -485,4 +419,179 @@ module.exports = (pool, query, authMiddleware) => {
}); });
return router; return router;
}; };
async function addThresholdNotification(id, enabled, email, threshold ,client) {
// Store notification settings as JSONB
const notificationSettings = {
enabled,
email: email || null,
threshold: threshold || 0
};
// Update product with notification settings
const result = await client.query(
`UPDATE products
SET stock_notification = $1
WHERE id = $2
RETURNING *`,
[JSON.stringify(notificationSettings), id]
);
return result;
}
async function createProduct(product, pool) {
const {
name,
description,
categoryName,
price,
stockQuantity,
weightGrams,
lengthCm,
widthCm,
heightCm,
stockNotification,
origin,
age,
materialType,
color,
images,
tags
} = product;
// Validate required fields
if (!name || !description || !categoryName || !price || !stockQuantity) {
return {
error: true,
message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory',
code: 400
};
}
// Get a client from the pool for this specific product
const client = await pool.connect();
try {
await client.query('BEGIN');
// Get category ID by name
const categoryResult = await client.query(
'SELECT id FROM product_categories WHERE name = $1',
[categoryName]
);
if (categoryResult.rows.length === 0) {
return {
error: true,
message: `Category "${categoryName}" not found`,
code: 404
};
}
const categoryId = categoryResult.rows[0].id;
// Create product
const productId = uuidv4();
await client.query(
`INSERT INTO products (
id, name, description, category_id, price, stock_quantity,
weight_grams, length_cm, width_cm, height_cm,
origin, age, material_type, color
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
[
productId, name, description, categoryId, price, stockQuantity,
weightGrams || null, lengthCm || null, widthCm || null, heightCm || null,
origin || null, age || null, materialType || null, color || null
]
);
// Add images if provided
if (images && images.length > 0) {
const imagePromises = images.map((image, i) => {
const { path, isPrimary = (i === 0) } = image;
return client.query(
'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)',
[productId, path, i, isPrimary]
);
});
await Promise.all(imagePromises);
}
// Add tags if provided
if (tags && tags.length > 0) {
for (const tagName of tags) {
// Get tag ID
let tagResult = await client.query(
'SELECT id FROM tags WHERE name = $1',
[tagName]
);
let tagId;
// If tag doesn't exist, create it
if (tagResult.rows.length === 0) {
const newTagResult = await client.query(
'INSERT INTO tags (name) VALUES ($1) RETURNING id',
[tagName]
);
tagId = newTagResult.rows[0].id;
} else {
tagId = tagResult.rows[0].id;
}
// Add tag to product
await client.query(
'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)',
[productId, tagId]
);
}
}
// Get complete product with images and tags
const productQuery = `
SELECT p.*,
pc.name as category_name,
ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags,
json_agg(json_build_object(
'id', pi.id,
'path', pi.image_path,
'isPrimary', pi.is_primary,
'displayOrder', pi.display_order
)) FILTER (WHERE pi.id IS NOT NULL) AS images
FROM products p
JOIN product_categories pc ON p.category_id = pc.id
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
LEFT JOIN product_images pi ON p.id = pi.product_id
WHERE p.id = $1
GROUP BY p.id, pc.name
`;
const productResult = await client.query(productQuery, [productId]);
// Add threshold notification if provided
if (stockNotification) {
await addThresholdNotification(productId, stockNotification.enabled, stockNotification.email, stockNotification.threshold, client);
}
await client.query('COMMIT');
return {
error: false,
product: productResult.rows[0]
};
} catch (error) {
await client.query('ROLLBACK');
return {
error: true,
message: `Error creating product: ${error.message}`,
code: 500
};
} finally {
client.release();
}
}

View file

@ -10,7 +10,7 @@ if [ "$MODE" == "cloud" ]; then
sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=cloud/' ./backend/.env sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=cloud/' ./backend/.env
# Start with cloud profile # Start with cloud profile
docker compose --profile cloud up -d docker compose --profile cloud up -d --build
else else
echo "Starting in SELF-HOSTED mode" echo "Starting in SELF-HOSTED mode"
# Make sure .env has self-hosted settings # Make sure .env has self-hosted settings
@ -18,6 +18,6 @@ else
sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=self-hosted/' ./backend/.env sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=self-hosted/' ./backend/.env
# Start without extra services # Start without extra services
docker compose up -d docker compose up -d --build
fi fi
echo "Deployment complete in $MODE mode" echo "Deployment complete in $MODE mode"