|
@@ -159,7 +159,7 @@ export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * GET /shops/:id - Get detailed shop information with analytics
|
|
|
|
|
|
|
+ * GET /shops/:id - Get detailed shop information with analytics (without full results)
|
|
|
*/
|
|
*/
|
|
|
router.get('/shops/:id', (req: Request, res: Response): void => {
|
|
router.get('/shops/:id', (req: Request, res: Response): void => {
|
|
|
try {
|
|
try {
|
|
@@ -182,21 +182,35 @@ export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
// Get scrape history
|
|
// Get scrape history
|
|
|
const scrapeHistory = db.getScrapeHistory(id, 10);
|
|
const scrapeHistory = db.getScrapeHistory(id, 10);
|
|
|
|
|
|
|
|
- // Get latest content
|
|
|
|
|
- const latestContent = db.getLatestContent(id);
|
|
|
|
|
|
|
+ // Get latest content (without full content, just metadata)
|
|
|
|
|
+ const latestContent = db.getLatestContentByType(id);
|
|
|
|
|
|
|
|
// Get scheduled jobs
|
|
// Get scheduled jobs
|
|
|
const scheduledJobs = db.getScheduledJobs(id);
|
|
const scheduledJobs = db.getScheduledJobs(id);
|
|
|
|
|
|
|
|
- // Build response with content change markers
|
|
|
|
|
- const contentWithChanges: any = {};
|
|
|
|
|
- for (const [type, content] of Object.entries(latestContent)) {
|
|
|
|
|
- contentWithChanges[type] = {
|
|
|
|
|
- url: content.url,
|
|
|
|
|
- changed: content.changed === 1,
|
|
|
|
|
- last_updated: content.updated_at
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Build response with content change markers and URLs only (no content)
|
|
|
|
|
+ const contentMetadata: any = {
|
|
|
|
|
+ shipping_informations: latestContent.shipping.map(c => ({
|
|
|
|
|
+ url: c.url,
|
|
|
|
|
+ changed: c.changed === 1,
|
|
|
|
|
+ last_updated: c.updated_at
|
|
|
|
|
+ })),
|
|
|
|
|
+ contacts: latestContent.contacts.map(c => ({
|
|
|
|
|
+ url: c.url,
|
|
|
|
|
+ changed: c.changed === 1,
|
|
|
|
|
+ last_updated: c.updated_at
|
|
|
|
|
+ })),
|
|
|
|
|
+ terms_of_conditions: latestContent.terms.map(c => ({
|
|
|
|
|
+ url: c.url,
|
|
|
|
|
+ changed: c.changed === 1,
|
|
|
|
|
+ last_updated: c.updated_at
|
|
|
|
|
+ })),
|
|
|
|
|
+ faq: latestContent.faq.map(c => ({
|
|
|
|
|
+ url: c.url,
|
|
|
|
|
+ changed: c.changed === 1,
|
|
|
|
|
+ last_updated: c.updated_at
|
|
|
|
|
+ }))
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
res.json({
|
|
res.json({
|
|
|
shop: {
|
|
shop: {
|
|
@@ -215,7 +229,7 @@ export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
average_scrape_time: analytics?.average_scrape_time || null,
|
|
average_scrape_time: analytics?.average_scrape_time || null,
|
|
|
average_page_scrape_time: analytics?.average_page_scrape_time || null
|
|
average_page_scrape_time: analytics?.average_page_scrape_time || null
|
|
|
},
|
|
},
|
|
|
- content: contentWithChanges,
|
|
|
|
|
|
|
+ content_metadata: contentMetadata,
|
|
|
scrape_history: scrapeHistory.map(h => ({
|
|
scrape_history: scrapeHistory.map(h => ({
|
|
|
id: h.id,
|
|
id: h.id,
|
|
|
job_id: h.job_id,
|
|
job_id: h.job_id,
|
|
@@ -232,6 +246,7 @@ export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
frequency: j.frequency,
|
|
frequency: j.frequency,
|
|
|
last_modified: j.last_modified,
|
|
last_modified: j.last_modified,
|
|
|
status: j.status,
|
|
status: j.status,
|
|
|
|
|
+ enabled: j.enabled,
|
|
|
created_at: j.created_at
|
|
created_at: j.created_at
|
|
|
}))
|
|
}))
|
|
|
});
|
|
});
|
|
@@ -241,6 +256,160 @@ export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * GET /shops/:id/results - Get shop results with full content (supports filters)
|
|
|
|
|
+ */
|
|
|
|
|
+ router.get('/shops/:id/results', (req: Request, res: Response): void => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!db) {
|
|
|
|
|
+ res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { id } = req.params;
|
|
|
|
|
+ const shop = db.getShopById(id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ const limit = req.query.limit ? parseInt(req.query.limit as string) : undefined;
|
|
|
|
|
+ const dateFrom = req.query.date_from as string | undefined;
|
|
|
|
|
+ const dateTo = req.query.date_to as string | undefined;
|
|
|
|
|
+ const contentType = req.query.content_type as string | undefined;
|
|
|
|
|
+
|
|
|
|
|
+ // Get content with filters
|
|
|
|
|
+ const allContent = db.getAllContent(id, {
|
|
|
|
|
+ limit,
|
|
|
|
|
+ dateFrom,
|
|
|
|
|
+ dateTo,
|
|
|
|
|
+ contentType
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Group by type and URL (latest version of each URL)
|
|
|
|
|
+ const grouped: { [key: string]: any[] } = {
|
|
|
|
|
+ shipping_informations: [],
|
|
|
|
|
+ contacts: [],
|
|
|
|
|
+ terms_of_conditions: [],
|
|
|
|
|
+ faq: []
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const urlMap = new Map<string, any>();
|
|
|
|
|
+ for (const content of allContent) {
|
|
|
|
|
+ const key = `${content.content_type}:${content.url}`;
|
|
|
|
|
+ if (!urlMap.has(key)) {
|
|
|
|
|
+ urlMap.set(key, {
|
|
|
|
|
+ url: content.url,
|
|
|
|
|
+ content: content.content,
|
|
|
|
|
+ changed: content.changed === 1,
|
|
|
|
|
+ last_updated: content.updated_at
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Organize by type
|
|
|
|
|
+ for (const content of allContent) {
|
|
|
|
|
+ const key = `${content.content_type}:${content.url}`;
|
|
|
|
|
+ const urlKey = urlMap.get(key);
|
|
|
|
|
+
|
|
|
|
|
+ if (!urlKey) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const typeKey = content.content_type === 'shipping' ? 'shipping_informations' :
|
|
|
|
|
+ content.content_type === 'contacts' ? 'contacts' :
|
|
|
|
|
+ content.content_type === 'terms' ? 'terms_of_conditions' :
|
|
|
|
|
+ 'faq';
|
|
|
|
|
+
|
|
|
|
|
+ // Only add if not already added
|
|
|
|
|
+ if (!grouped[typeKey].find((c: any) => c.url === urlKey.url)) {
|
|
|
|
|
+ grouped[typeKey].push(urlKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.json({
|
|
|
|
|
+ shop_id: id,
|
|
|
|
|
+ filters: {
|
|
|
|
|
+ limit: limit || null,
|
|
|
|
|
+ date_from: dateFrom || null,
|
|
|
|
|
+ date_to: dateTo || null,
|
|
|
|
|
+ content_type: contentType || null
|
|
|
|
|
+ },
|
|
|
|
|
+ results: grouped
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Error fetching shop results', error);
|
|
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * PATCH /shops/:id/schedule - Enable or disable scheduled scraping
|
|
|
|
|
+ */
|
|
|
|
|
+ router.patch('/shops/:id/schedule', (req: Request, res: Response): void => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!db) {
|
|
|
|
|
+ res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { id } = req.params;
|
|
|
|
|
+ const { enabled } = req.body;
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof enabled !== 'boolean') {
|
|
|
|
|
+ res.status(400).json({ error: 'enabled field must be a boolean' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const shop = db.getShopById(id);
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ db.setScheduleEnabled(id, enabled);
|
|
|
|
|
+
|
|
|
|
|
+ res.json({
|
|
|
|
|
+ shop_id: id,
|
|
|
|
|
+ schedule_enabled: enabled,
|
|
|
|
|
+ message: `Schedule ${enabled ? 'enabled' : 'disabled'} successfully`
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Error updating schedule status', error);
|
|
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * DELETE /shops/:id - Delete a shop and all related data
|
|
|
|
|
+ */
|
|
|
|
|
+ router.delete('/shops/:id', (req: Request, res: Response): void => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!db) {
|
|
|
|
|
+ res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { id } = req.params;
|
|
|
|
|
+ const shop = db.getShopById(id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!shop) {
|
|
|
|
|
+ res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ db.deleteShop(id);
|
|
|
|
|
+
|
|
|
|
|
+ res.json({
|
|
|
|
|
+ message: 'Shop and all related data deleted successfully',
|
|
|
|
|
+ shop_id: id
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Error deleting shop', error);
|
|
|
|
|
+ res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* GET /health - Health check endpoint
|
|
* GET /health - Health check endpoint
|
|
|
*/
|
|
*/
|