v1.0
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Sync routes for Formex crawler
|
||||
*/
|
||||
import { sync, getSyncLogs, getMaterials } from '../services/formexCrawler.js';
|
||||
import { getSchedulerStatus } from '../services/scheduler.js';
|
||||
import pool, { run } from '../db/index.js';
|
||||
|
||||
/**
|
||||
* POST /api/sync/formex
|
||||
* Trigger a manual Formex sync (non-streaming)
|
||||
*/
|
||||
export async function triggerFormexSync(req, res) {
|
||||
try {
|
||||
// Run sync (this may take a while)
|
||||
const result = await sync();
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Synchronisation terminée: ${result.itemsCount} produits`,
|
||||
itemsCount: result.itemsCount,
|
||||
duration: result.duration
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
itemsCount: result.itemsCount
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/formex/stream
|
||||
* SSE endpoint for real-time sync progress
|
||||
*/
|
||||
export async function streamFormexSync(req, res) {
|
||||
// Set SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
||||
res.flushHeaders();
|
||||
|
||||
// Emitter function that sends SSE events
|
||||
const emit = (data) => {
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
// Client disconnected, cleanup if needed
|
||||
});
|
||||
|
||||
try {
|
||||
// Run sync with emitter
|
||||
await sync(emit);
|
||||
} catch (err) {
|
||||
emit({ type: 'error', error: err.message });
|
||||
}
|
||||
|
||||
// Close connection
|
||||
res.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/logs
|
||||
* Get sync history
|
||||
*/
|
||||
export async function getLogs(req, res) {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit || '20', 10);
|
||||
const logs = await getSyncLogs(limit);
|
||||
res.json(logs);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/status
|
||||
* Get scheduler status
|
||||
*/
|
||||
export function getStatus(req, res) {
|
||||
const status = getSchedulerStatus();
|
||||
res.json(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/formex/materials
|
||||
* Get Formex materials with filtering and pagination
|
||||
*/
|
||||
export async function getFormexMaterials(req, res) {
|
||||
try {
|
||||
const { search, collection, category, active, page = '1', limit = '25' } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page, 10));
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(limit, 10)));
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
// Build query with optional filters
|
||||
let whereClauses = [];
|
||||
let params = [];
|
||||
|
||||
if (search) {
|
||||
whereClauses.push('(name LIKE ? OR formex_code LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
whereClauses.push('collection = ?');
|
||||
params.push(collection);
|
||||
}
|
||||
|
||||
if (category) {
|
||||
whereClauses.push('category = ?');
|
||||
params.push(category);
|
||||
}
|
||||
|
||||
// For configurator: only active materials
|
||||
if (active === '1' || active === 'true') {
|
||||
whereClauses.push('active = 1');
|
||||
}
|
||||
|
||||
const whereSQL = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
||||
|
||||
// Get total count
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) as total FROM formex_materials ${whereSQL}`,
|
||||
params
|
||||
);
|
||||
const total = countResult.rows[0]?.total || 0;
|
||||
|
||||
// Get paginated results
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM formex_materials ${whereSQL} ORDER BY collection, name LIMIT ? OFFSET ?`,
|
||||
[...params, limitNum, offset]
|
||||
);
|
||||
|
||||
res.json({
|
||||
materials: rows,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limitNum)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/formex/materials/:id
|
||||
* Get a single material by ID with dimensions and variants
|
||||
*/
|
||||
export async function getFormexMaterial(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get main material
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM formex_materials WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Matériau non trouvé' });
|
||||
}
|
||||
|
||||
const material = rows[0];
|
||||
|
||||
// Get dimension variants
|
||||
const dimensions = await pool.query(
|
||||
'SELECT id, width_mm, height_mm, thickness_mm, is_default FROM formex_dimensions WHERE material_id = ? ORDER BY is_default DESC, width_mm',
|
||||
[id]
|
||||
);
|
||||
material.dimensions = dimensions.rows;
|
||||
|
||||
// Get color/decor variants
|
||||
const variants = await pool.query(
|
||||
'SELECT id, variant_code, variant_name, variant_url, image_url FROM formex_variants WHERE material_id = ? ORDER BY variant_name',
|
||||
[id]
|
||||
);
|
||||
material.variants = variants.rows;
|
||||
|
||||
res.json(material);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/formex/materials
|
||||
* Create a new material
|
||||
*/
|
||||
export async function createFormexMaterial(req, res) {
|
||||
try {
|
||||
const { formex_code, name, collection, category, width_mm, height_mm, thickness_mm, image_url, formex_url, price_chf, active } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!formex_code || !name || !collection) {
|
||||
return res.status(400).json({ error: 'formex_code, name et collection sont requis' });
|
||||
}
|
||||
|
||||
// Check for duplicate code
|
||||
const existing = await pool.query(
|
||||
'SELECT id FROM formex_materials WHERE formex_code = ?',
|
||||
[formex_code]
|
||||
);
|
||||
if (existing.rows.length > 0) {
|
||||
return res.status(409).json({ error: 'Un matériau avec ce code existe déjà' });
|
||||
}
|
||||
|
||||
await run(
|
||||
`INSERT INTO formex_materials (formex_code, name, collection, category, width_mm, height_mm, thickness_mm, image_url, formex_url, price_chf, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[formex_code, name, collection, category || null, width_mm || null, height_mm || null, thickness_mm || null,
|
||||
image_url || null, formex_url || null, price_chf || null, active !== undefined ? (active ? 1 : 0) : 1]
|
||||
);
|
||||
|
||||
// Get the created material
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM formex_materials WHERE formex_code = ?',
|
||||
[formex_code]
|
||||
);
|
||||
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/formex/materials/:id
|
||||
* Update a material
|
||||
*/
|
||||
export async function updateFormexMaterial(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { formex_code, name, collection, category, width_mm, height_mm, thickness_mm, image_url, formex_url, price_chf, active } = req.body;
|
||||
|
||||
// Check if material exists
|
||||
const existing = await pool.query(
|
||||
'SELECT * FROM formex_materials WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
if (existing.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Matériau non trouvé' });
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (!formex_code || !name || !collection) {
|
||||
return res.status(400).json({ error: 'formex_code, name et collection sont requis' });
|
||||
}
|
||||
|
||||
// Check for duplicate code (if changed)
|
||||
if (formex_code !== existing.rows[0].formex_code) {
|
||||
const duplicate = await pool.query(
|
||||
'SELECT id FROM formex_materials WHERE formex_code = ? AND id != ?',
|
||||
[formex_code, id]
|
||||
);
|
||||
if (duplicate.rows.length > 0) {
|
||||
return res.status(409).json({ error: 'Un matériau avec ce code existe déjà' });
|
||||
}
|
||||
}
|
||||
|
||||
await run(
|
||||
`UPDATE formex_materials
|
||||
SET formex_code = ?, name = ?, collection = ?, category = ?, width_mm = ?, height_mm = ?, thickness_mm = ?,
|
||||
image_url = ?, formex_url = ?, price_chf = ?, active = ?
|
||||
WHERE id = ?`,
|
||||
[formex_code, name, collection, category || null, width_mm || null, height_mm || null, thickness_mm || null,
|
||||
image_url || null, formex_url || null, price_chf || null, active !== undefined ? (active ? 1 : 0) : 1, id]
|
||||
);
|
||||
|
||||
// Get the updated material
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM formex_materials WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/formex/materials/:id
|
||||
* Delete a material
|
||||
*/
|
||||
export async function deleteFormexMaterial(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Check if material exists
|
||||
const existing = await pool.query(
|
||||
'SELECT * FROM formex_materials WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
if (existing.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Matériau non trouvé' });
|
||||
}
|
||||
|
||||
await run('DELETE FROM formex_materials WHERE id = ?', [id]);
|
||||
|
||||
res.json({ success: true, message: 'Matériau supprimé' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/formex/collections
|
||||
* Get distinct collection names for filter dropdown
|
||||
*/
|
||||
export async function getFormexCollections(req, res) {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT DISTINCT collection FROM formex_materials ORDER BY collection'
|
||||
);
|
||||
res.json(rows.map(r => r.collection));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/formex/categories
|
||||
* Get distinct category names for filter dropdown
|
||||
*/
|
||||
export async function getFormexCategories(req, res) {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT DISTINCT category FROM formex_materials WHERE category IS NOT NULL ORDER BY category'
|
||||
);
|
||||
res.json(rows.map(r => r.category));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user