index.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { loadConfig } from './config';
  2. import { createServer } from './api/server';
  3. import { JobQueue } from './queue/JobQueue';
  4. import { WebshopScraper } from './scraper/WebshopScraper';
  5. import { logger } from './utils/logger';
  6. import { ShopDatabase } from './database/Database';
  7. import { ScrapeScheduler } from './scheduler/ScrapeScheduler';
  8. import { WebhookManager } from './webhooks/WebhookManager';
  9. async function main() {
  10. try {
  11. logger.info('Starting Webshop Scraper application...');
  12. // Load configuration
  13. const config = loadConfig();
  14. // Initialize database
  15. const db = new ShopDatabase();
  16. // Initialize job queue
  17. const jobQueue = new JobQueue(config.maxConcurrentJobs);
  18. // Initialize scraper with database
  19. const scraper = new WebshopScraper(db);
  20. // Initialize scheduler
  21. const scheduler = new ScrapeScheduler(db, jobQueue);
  22. // Initialize webhook manager
  23. const webhookManager = new WebhookManager(db);
  24. // Handle job processing
  25. jobQueue.on('process-job', async (job) => {
  26. logger.info(`Processing job ${job.id}`);
  27. const startTime = Date.now();
  28. let scrapeHistoryId: string | undefined;
  29. try {
  30. // Create scrape history record if shopId is available
  31. if (job.shopId) {
  32. scrapeHistoryId = db.createScrapeHistory(job.shopId, job.id);
  33. db.updateScrapeHistory(scrapeHistoryId, { status: 'processing' });
  34. // Trigger webhook: scrape_started
  35. await webhookManager.notifyScrapeStarted(
  36. job.shopId,
  37. job.id,
  38. job.sitemapUrl
  39. );
  40. }
  41. const result = await scraper.scrape(job.sitemapUrl, job.shopId);
  42. jobQueue.updateJob(job.id, {
  43. status: 'completed',
  44. result
  45. });
  46. // Update scrape history
  47. if (scrapeHistoryId) {
  48. const scrapeTimeMs = Date.now() - startTime;
  49. db.updateScrapeHistory(scrapeHistoryId, {
  50. status: 'completed',
  51. completed_at: new Date().toISOString(),
  52. scrape_time_ms: scrapeTimeMs
  53. });
  54. }
  55. // Trigger webhook: scrape_completed
  56. if (job.shopId) {
  57. const totalUrls = (result.shipping_informations?.length || 0) +
  58. (result.contacts?.length || 0) +
  59. (result.terms_of_conditions?.length || 0) +
  60. (result.faq?.length || 0);
  61. const totalPages = totalUrls;
  62. await webhookManager.notifyScrapeCompleted(
  63. job.shopId,
  64. job.id,
  65. {
  66. scrape_time_ms: Date.now() - startTime,
  67. urls_found: totalUrls,
  68. pages_scraped: totalPages
  69. }
  70. );
  71. }
  72. // Schedule next scrape if this is a new shop or first scrape
  73. if (job.shopId) {
  74. const existingSchedules = db.getScheduledJobs(job.shopId);
  75. const hasQueuedJobs = existingSchedules.some(s => s.status === 'queued');
  76. if (!hasQueuedJobs) {
  77. // Create initial schedule for this shop
  78. scheduler.createInitialSchedule(job.shopId, 'weekly', null);
  79. logger.info(`Created initial schedule for shop ${job.shopId}`);
  80. }
  81. }
  82. logger.info(`Job ${job.id} completed successfully`);
  83. } catch (error) {
  84. const errorMessage = error instanceof Error ? error.message : 'Unknown error';
  85. jobQueue.updateJob(job.id, {
  86. status: 'failed',
  87. error: errorMessage
  88. });
  89. // Update scrape history with error
  90. if (scrapeHistoryId) {
  91. db.updateScrapeHistory(scrapeHistoryId, {
  92. status: 'failed',
  93. completed_at: new Date().toISOString(),
  94. error: errorMessage
  95. });
  96. }
  97. // Trigger webhook: scrape_failed
  98. if (job.shopId) {
  99. await webhookManager.notifyScrapeFailed(
  100. job.shopId,
  101. job.id,
  102. errorMessage
  103. );
  104. }
  105. logger.error(`Job ${job.id} failed`, error);
  106. }
  107. });
  108. // Create and start HTTP server
  109. const app = createServer(config.apiKey, jobQueue, db);
  110. const server = app.listen(config.port, () => {
  111. logger.info(`Server listening on port ${config.port}`);
  112. logger.info('API endpoints:');
  113. logger.info(` POST /api/jobs - Create new scraping job`);
  114. logger.info(` GET /api/jobs/:id - Get job status and result`);
  115. logger.info(` GET /api/jobs - List all jobs`);
  116. logger.info(` GET /api/shops - List all shops`);
  117. logger.info(` GET /api/shops/:id - Get shop details and analytics`);
  118. logger.info(` GET /health - Health check`);
  119. logger.info('');
  120. logger.info('Authentication: Bearer token required for /api/* endpoints');
  121. });
  122. // Start scheduler
  123. scheduler.start();
  124. // Graceful shutdown
  125. const shutdown = async () => {
  126. logger.info('Shutting down gracefully...');
  127. // Stop scheduler
  128. scheduler.stop();
  129. // Close database
  130. db.close();
  131. server.close(() => {
  132. logger.info('Server closed');
  133. process.exit(0);
  134. });
  135. // Force shutdown after 10 seconds
  136. setTimeout(() => {
  137. logger.error('Forced shutdown after timeout');
  138. process.exit(1);
  139. }, 10000);
  140. };
  141. process.on('SIGTERM', shutdown);
  142. process.on('SIGINT', shutdown);
  143. } catch (error) {
  144. logger.error('Failed to start application', error);
  145. process.exit(1);
  146. }
  147. }
  148. main();