849763896d
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Frontend Tests (push) Successful in 2m12s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Successful in 2m2s
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Backend Tests (push) Successful in 2m8s
397 lines
9.3 KiB
JavaScript
397 lines
9.3 KiB
JavaScript
import { Router } from 'express';
|
|
import Product from '../models/Product.js';
|
|
import { requireServiceKey } from '../middleware/service-auth.js';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/search:
|
|
* get:
|
|
* summary: Search parapharmacy products
|
|
* tags: [Products]
|
|
* parameters:
|
|
* - in: query
|
|
* name: q
|
|
* schema:
|
|
* type: string
|
|
* description: Search query
|
|
* - in: query
|
|
* name: category
|
|
* schema:
|
|
* type: string
|
|
* description: Filter by category
|
|
* - in: query
|
|
* name: brand
|
|
* schema:
|
|
* type: string
|
|
* description: Filter by brand
|
|
* - in: query
|
|
* name: page
|
|
* schema:
|
|
* type: integer
|
|
* default: 1
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* default: 20
|
|
* responses:
|
|
* 200:
|
|
* description: Search results
|
|
*/
|
|
router.get('/search', async (req, res) => {
|
|
try {
|
|
const { q, category, brand, page = 1, limit = 20 } = req.query;
|
|
|
|
const result = await Product.search(q, {
|
|
page: parseInt(page),
|
|
limit: Math.min(parseInt(limit), 50),
|
|
category,
|
|
brand,
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[Products] Search error:', error);
|
|
res.status(500).json({ error: 'Error searching products' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/categories:
|
|
* get:
|
|
* summary: Get all categories
|
|
* tags: [Products]
|
|
* responses:
|
|
* 200:
|
|
* description: List of categories
|
|
*/
|
|
router.get('/categories', async (req, res) => {
|
|
try {
|
|
const categories = await Product.distinct('category');
|
|
res.json(categories.filter(Boolean).sort());
|
|
} catch (error) {
|
|
console.error('[Products] Categories error:', error);
|
|
res.status(500).json({ error: 'Error fetching categories' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/brands:
|
|
* get:
|
|
* summary: Get all brands
|
|
* tags: [Products]
|
|
* responses:
|
|
* 200:
|
|
* description: List of brands
|
|
*/
|
|
router.get('/brands', async (req, res) => {
|
|
try {
|
|
const brands = await Product.distinct('brand');
|
|
res.json(brands.filter(Boolean).sort());
|
|
} catch (error) {
|
|
console.error('[Products] Brands error:', error);
|
|
res.status(500).json({ error: 'Error fetching brands' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products:
|
|
* get:
|
|
* summary: List all products
|
|
* tags: [Products]
|
|
* parameters:
|
|
* - in: query
|
|
* name: page
|
|
* schema:
|
|
* type: integer
|
|
* default: 1
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* default: 20
|
|
* - in: query
|
|
* name: source
|
|
* schema:
|
|
* type: string
|
|
* description: Filter by source
|
|
* responses:
|
|
* 200:
|
|
* description: List of products
|
|
*/
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const { page = 1, limit = 20, source } = req.query;
|
|
|
|
const filter = {};
|
|
if (source) filter.source = source;
|
|
|
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
const [products, total] = await Promise.all([
|
|
Product.find(filter)
|
|
.sort({ scraped_at: -1 })
|
|
.skip(skip)
|
|
.limit(Math.min(parseInt(limit), 50))
|
|
.lean(),
|
|
Product.countDocuments(filter),
|
|
]);
|
|
|
|
res.json({
|
|
results: products,
|
|
total,
|
|
page: parseInt(page),
|
|
pages: Math.ceil(total / parseInt(limit)),
|
|
});
|
|
} catch (error) {
|
|
console.error('[Products] List error:', error);
|
|
res.status(500).json({ error: 'Error fetching products' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/{id}:
|
|
* get:
|
|
* summary: Get product by ID
|
|
* tags: [Products]
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Product details
|
|
* 404:
|
|
* description: Product not found
|
|
*/
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const product = await Product.findById(req.params.id).lean();
|
|
|
|
if (!product) {
|
|
return res.status(404).json({ error: 'Product not found' });
|
|
}
|
|
|
|
res.json(product);
|
|
} catch (error) {
|
|
console.error('[Products] Get error:', error);
|
|
res.status(500).json({ error: 'Error fetching product' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products:
|
|
* post:
|
|
* summary: Create or update a product (for scraper)
|
|
* tags: [Products]
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - name
|
|
* - price
|
|
* - source
|
|
* - source_product_id
|
|
* - source_url
|
|
* responses:
|
|
* 201:
|
|
* description: Product created/updated
|
|
*/
|
|
router.post('/', requireServiceKey('INGEST_API_KEY'), async (req, res) => {
|
|
try {
|
|
const {
|
|
name,
|
|
brand,
|
|
category,
|
|
subcategory,
|
|
description,
|
|
image_url,
|
|
source_url,
|
|
price,
|
|
original_price,
|
|
source,
|
|
source_product_id,
|
|
available,
|
|
rating,
|
|
review_count,
|
|
} = req.body;
|
|
|
|
if (!name || !price || !source || !source_product_id || !source_url) {
|
|
return res.status(400).json({ error: 'Missing required fields' });
|
|
}
|
|
|
|
const product = await Product.upsert({
|
|
name,
|
|
brand,
|
|
category,
|
|
subcategory,
|
|
description,
|
|
image_url,
|
|
source_url,
|
|
price,
|
|
original_price,
|
|
source,
|
|
source_product_id,
|
|
available: available !== false,
|
|
rating,
|
|
review_count,
|
|
scraped_at: new Date(),
|
|
});
|
|
|
|
res.status(201).json(product);
|
|
} catch (error) {
|
|
console.error('[Products] Create error:', error);
|
|
res.status(500).json({ error: 'Error creating product' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/bulk:
|
|
* post:
|
|
* summary: Bulk upsert products (for scraper)
|
|
* tags: [Products]
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - products
|
|
* properties:
|
|
* products:
|
|
* type: array
|
|
* responses:
|
|
* 200:
|
|
* description: Upsert results
|
|
*/
|
|
router.post('/bulk', requireServiceKey('INGEST_API_KEY'), async (req, res) => {
|
|
try {
|
|
const { products } = req.body;
|
|
|
|
if (!Array.isArray(products)) {
|
|
return res.status(400).json({ error: 'products must be an array' });
|
|
}
|
|
if (products.length > 100) {
|
|
return res.status(413).json({ error: 'products exceeds the maximum batch size of 100' });
|
|
}
|
|
|
|
const results = {
|
|
created: 0,
|
|
updated: 0,
|
|
errors: 0,
|
|
};
|
|
|
|
for (const productData of products) {
|
|
try {
|
|
const existing = await Product.findBySource(
|
|
productData.source,
|
|
productData.source_product_id
|
|
);
|
|
|
|
if (existing) {
|
|
Object.assign(existing, productData, { scraped_at: new Date() });
|
|
await existing.save();
|
|
results.updated++;
|
|
} else {
|
|
await Product.create(productData);
|
|
results.created++;
|
|
}
|
|
} catch (err) {
|
|
console.error('[Products] Bulk upsert error:', err.message);
|
|
results.errors++;
|
|
}
|
|
}
|
|
|
|
res.json(results);
|
|
} catch (error) {
|
|
console.error('[Products] Bulk error:', error);
|
|
res.status(500).json({ error: 'Error in bulk upsert' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/{id}:
|
|
* put:
|
|
* summary: Update a product
|
|
* tags: [Products]
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Updated product
|
|
* 404:
|
|
* description: Product not found
|
|
*/
|
|
router.put('/:id', requireServiceKey('ADMIN_API_KEY'), async (req, res) => {
|
|
try {
|
|
const product = await Product.findByIdAndUpdate(
|
|
req.params.id,
|
|
{ ...req.body, updated_at: new Date() },
|
|
{ new: true }
|
|
);
|
|
|
|
if (!product) {
|
|
return res.status(404).json({ error: 'Product not found' });
|
|
}
|
|
|
|
res.json(product);
|
|
} catch (error) {
|
|
console.error('[Products] Update error:', error);
|
|
res.status(500).json({ error: 'Error updating product' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/products/{id}:
|
|
* delete:
|
|
* summary: Delete a product
|
|
* tags: [Products]
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 204:
|
|
* description: Product deleted
|
|
* 404:
|
|
* description: Product not found
|
|
*/
|
|
router.delete('/:id', requireServiceKey('ADMIN_API_KEY'), async (req, res) => {
|
|
try {
|
|
const product = await Product.findByIdAndDelete(req.params.id);
|
|
|
|
if (!product) {
|
|
return res.status(404).json({ error: 'Product not found' });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error('[Products] Delete error:', error);
|
|
res.status(500).json({ error: 'Error deleting product' });
|
|
}
|
|
});
|
|
|
|
export default router;
|