bulk products
This commit is contained in:
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 |
|
|
@ -9,6 +9,11 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
// Create a new product with multiple images
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
// Check for array
|
||||
let products = [];
|
||||
if (req.body.products) {
|
||||
products = req.body.products;
|
||||
} else {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
|
|
@ -19,6 +24,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
lengthCm,
|
||||
widthCm,
|
||||
heightCm,
|
||||
stockNotification,
|
||||
origin,
|
||||
age,
|
||||
materialType,
|
||||
|
|
@ -27,132 +33,55 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
tags
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !description || !categoryName || !price || !stockQuantity) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory'
|
||||
products.push({
|
||||
name,
|
||||
description,
|
||||
categoryName,
|
||||
price,
|
||||
stockQuantity,
|
||||
weightGrams,
|
||||
lengthCm,
|
||||
widthCm,
|
||||
heightCm,
|
||||
stockNotification,
|
||||
origin,
|
||||
age,
|
||||
materialType,
|
||||
color,
|
||||
images: images || [],
|
||||
tags
|
||||
});
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
const client = await pool.connect();
|
||||
// Prepare promises array for concurrent execution
|
||||
const productPromises = products.map(product => createProduct(product, pool));
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
// Execute all product creation promises concurrently
|
||||
const results = await Promise.all(productPromises);
|
||||
|
||||
// Get category ID by name
|
||||
const categoryResult = await client.query(
|
||||
'SELECT id FROM product_categories WHERE name = $1',
|
||||
[categoryName]
|
||||
);
|
||||
// Separate successes and errors
|
||||
const success = results.filter(result => !result.error).map(result => ({
|
||||
message: 'Product created successfully',
|
||||
product: result.product,
|
||||
code: 201
|
||||
}));
|
||||
|
||||
if (categoryResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: true,
|
||||
message: `Category "${categoryName}" not found`
|
||||
});
|
||||
}
|
||||
|
||||
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]);
|
||||
const errs = results.filter(result => result.error);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Product created successfully',
|
||||
product: product.rows[0]
|
||||
message: errs.length > 0 ?
|
||||
(success.length > 0 ? 'Some Products failed to create, check errors array' : "Failed to create Products") :
|
||||
'Products created successfully',
|
||||
success,
|
||||
errs
|
||||
});
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/stock-notification', async (req, res, next) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { enabled, email, threshold } = req.body;
|
||||
|
|
@ -164,6 +93,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
message: 'Admin access required'
|
||||
});
|
||||
}
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Check if product exists
|
||||
const productCheck = await query(
|
||||
|
|
@ -186,23 +116,22 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
};
|
||||
|
||||
// Update product with notification settings
|
||||
const result = await query(
|
||||
`UPDATE products
|
||||
SET stock_notification = $1
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[JSON.stringify(notificationSettings), id]
|
||||
);
|
||||
const result = await addThresholdNotification(id, enabled, email, threshold, client);
|
||||
await client.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
message: 'Stock notification settings updated successfully',
|
||||
product: result.rows[0]
|
||||
});
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
next(error);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update an existing product
|
||||
router.put('/:id', async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -215,6 +144,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
stockQuantity,
|
||||
weightGrams,
|
||||
lengthCm,
|
||||
stockNotification,
|
||||
widthCm,
|
||||
heightCm,
|
||||
origin,
|
||||
|
|
@ -345,6 +275,10 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
valueIndex++;
|
||||
}
|
||||
|
||||
if (stockNotification) {
|
||||
await addThresholdNotification(id, stockNotification.enabled, stockNotification.email, stockNotification.threshold, client);
|
||||
}
|
||||
|
||||
if (updateFields.length > 0) {
|
||||
const updateQuery = `
|
||||
UPDATE products
|
||||
|
|
@ -486,3 +420,178 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
||||
4
start.sh
4
start.sh
|
|
@ -10,7 +10,7 @@ if [ "$MODE" == "cloud" ]; then
|
|||
sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=cloud/' ./backend/.env
|
||||
|
||||
# Start with cloud profile
|
||||
docker compose --profile cloud up -d
|
||||
docker compose --profile cloud up -d --build
|
||||
else
|
||||
echo "Starting in SELF-HOSTED mode"
|
||||
# Make sure .env has self-hosted settings
|
||||
|
|
@ -18,6 +18,6 @@ else
|
|||
sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=self-hosted/' ./backend/.env
|
||||
|
||||
# Start without extra services
|
||||
docker compose up -d
|
||||
docker compose up -d --build
|
||||
fi
|
||||
echo "Deployment complete in $MODE mode"
|
||||
Loading…
Reference in a new issue