Files
FarmaFinder/apps/parapharmacy-api/src/routes/products.js
T
Antoni Nuñez Romeu 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
security: harden production configuration and routes
2026-07-22 17:24:54 +02:00

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;