← back to Jill Website

scripts/check-links.ts

234 lines

#!/usr/bin/env ts-node

/**
 * Automated Link Validation Script
 *
 * This script checks all external links in the system and stores results in the database.
 * Designed to be run daily via cron job.
 *
 * Usage: ts-node scripts/check-links.ts
 */

import { PropertyModel } from '../src/models/Property';
import { ActivityModel } from '../src/models/Activity';
import { LinkCheckModel } from '../src/models/LinkCheck';
import { linkValidationService } from '../src/services/linkValidationService';
import { getPool } from '../src/config/database';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

interface LinkCheckSummary {
  total: number;
  valid: number;
  invalid: number;
  bySource: {
    [key: string]: {
      total: number;
      valid: number;
      invalid: number;
    };
  };
}

async function checkAllLinks(): Promise<LinkCheckSummary> {
  const linkChecks = new LinkCheckModel();
  const properties = new PropertyModel();
  const activities = new ActivityModel();

  const summary: LinkCheckSummary = {
    total: 0,
    valid: 0,
    invalid: 0,
    bySource: {},
  };

  console.log('🔍 Starting link validation...\n');

  // Helper function to update summary
  const updateSummary = (source: string, isValid: boolean) => {
    summary.total++;
    if (isValid) {
      summary.valid++;
    } else {
      summary.invalid++;
    }

    if (!summary.bySource[source]) {
      summary.bySource[source] = { total: 0, valid: 0, invalid: 0 };
    }
    summary.bySource[source].total++;
    if (isValid) {
      summary.bySource[source].valid++;
    } else {
      summary.bySource[source].invalid++;
    }
  };

  // Check all property links
  console.log('📦 Checking property links...');
  const allProperties = await properties.findAll(1000, 0);

  for (const property of allProperties) {
    console.log(`  - Property: ${property.name} (ID: ${property.id})`);

    // Check Airbnb URL
    if (property.airbnb_url) {
      const checkResult = await linkValidationService.checkLink(property.airbnb_url);
      await linkChecks.upsert({
        url: property.airbnb_url,
        source_type: 'property',
        source_id: property.id!,
        http_status: checkResult.httpStatus,
        is_valid: checkResult.isValid,
        error_message: checkResult.errorMessage,
        response_time_ms: checkResult.responseTimeMs,
      });
      updateSummary('property', checkResult.isValid);

      if (!checkResult.isValid) {
        console.log(`    ❌ Airbnb URL failed: ${checkResult.errorMessage}`);
      } else {
        console.log(`    ✅ Airbnb URL valid (${checkResult.httpStatus})`);
      }
    }

    // Check image URLs
    if (property.images && property.images.length > 0) {
      for (let i = 0; i < property.images.length; i++) {
        const imageUrl = property.images[i];
        const checkResult = await linkValidationService.checkLink(imageUrl);
        await linkChecks.upsert({
          url: imageUrl,
          source_type: 'property_image',
          source_id: property.id!,
          http_status: checkResult.httpStatus,
          is_valid: checkResult.isValid,
          error_message: checkResult.errorMessage,
          response_time_ms: checkResult.responseTimeMs,
        });
        updateSummary('property_image', checkResult.isValid);

        if (!checkResult.isValid) {
          console.log(`    ❌ Image ${i + 1} failed: ${checkResult.errorMessage}`);
        }
      }
    }
  }

  // Check all activity links
  console.log('\n🎯 Checking activity links...');
  const allActivities = await activities.findAll(1000, 0);

  for (const activity of allActivities) {
    console.log(`  - Activity: ${activity.name} (ID: ${activity.id})`);

    // Check website URL
    if (activity.website) {
      const checkResult = await linkValidationService.checkLink(activity.website);
      await linkChecks.upsert({
        url: activity.website,
        source_type: 'activity_website',
        source_id: activity.id!,
        http_status: checkResult.httpStatus,
        is_valid: checkResult.isValid,
        error_message: checkResult.errorMessage,
        response_time_ms: checkResult.responseTimeMs,
      });
      updateSummary('activity_website', checkResult.isValid);

      if (!checkResult.isValid) {
        console.log(`    ❌ Website failed: ${checkResult.errorMessage}`);
      } else {
        console.log(`    ✅ Website valid (${checkResult.httpStatus})`);
      }
    }

    // Check maps link
    if (activity.mapsLink) {
      const checkResult = await linkValidationService.checkLink(activity.mapsLink);
      await linkChecks.upsert({
        url: activity.mapsLink,
        source_type: 'activity_maps',
        source_id: activity.id!,
        http_status: checkResult.httpStatus,
        is_valid: checkResult.isValid,
        error_message: checkResult.errorMessage,
        response_time_ms: checkResult.responseTimeMs,
      });
      updateSummary('activity_maps', checkResult.isValid);

      if (!checkResult.isValid) {
        console.log(`    ❌ Maps link failed: ${checkResult.errorMessage}`);
      }
    }

    // Check review link
    if (activity.reviewLink) {
      const checkResult = await linkValidationService.checkLink(activity.reviewLink);
      await linkChecks.upsert({
        url: activity.reviewLink,
        source_type: 'activity_review',
        source_id: activity.id!,
        http_status: checkResult.httpStatus,
        is_valid: checkResult.isValid,
        error_message: checkResult.errorMessage,
        response_time_ms: checkResult.responseTimeMs,
      });
      updateSummary('activity_review', checkResult.isValid);

      if (!checkResult.isValid) {
        console.log(`    ❌ Review link failed: ${checkResult.errorMessage}`);
      }
    }
  }

  return summary;
}

async function main() {
  try {
    const startTime = Date.now();
    const summary = await checkAllLinks();
    const duration = Date.now() - startTime;

    console.log('\n' + '='.repeat(60));
    console.log('📊 LINK VALIDATION SUMMARY');
    console.log('='.repeat(60));
    console.log(`Total links checked: ${summary.total}`);
    console.log(`Valid links: ${summary.valid} (${((summary.valid / summary.total) * 100).toFixed(1)}%)`);
    console.log(`Invalid links: ${summary.invalid} (${((summary.invalid / summary.total) * 100).toFixed(1)}%)`);
    console.log(`Duration: ${(duration / 1000).toFixed(1)}s`);
    console.log('\nBreakdown by source:');

    for (const [source, stats] of Object.entries(summary.bySource)) {
      console.log(`  ${source}:`);
      console.log(`    Total: ${stats.total}`);
      console.log(`    Valid: ${stats.valid}`);
      console.log(`    Invalid: ${stats.invalid}`);
    }

    console.log('='.repeat(60));

    // Close database connection
    await getPool().end();

    // Exit with error code if any links are broken
    if (summary.invalid > 0) {
      console.log('\n⚠️  Warning: Some links are broken!');
      process.exit(1);
    } else {
      console.log('\n✅ All links are valid!');
      process.exit(0);
    }
  } catch (error) {
    console.error('\n❌ Error running link validation:', error);
    await getPool().end();
    process.exit(1);
  }
}

// Run the script
main();