← 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();