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,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
start.sh
4
start.sh
|
|
@ -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"
|
||||||
Loading…
Reference in a new issue