/** * Script to generate ports seed migration from sea-ports JSON data * * Data source: https://github.com/marchah/sea-ports * License: MIT * * This script: * 1. Reads sea-ports.json from /tmp * 2. Parses and validates port data * 3. Generates SQL INSERT statements * 4. Creates a TypeORM migration file */ import * as fs from 'fs'; import * as path from 'path'; interface SeaPort { name: string; city: string; country: string; coordinates: [number, number]; // [longitude, latitude] province?: string; timezone?: string; unlocs: string[]; code?: string; alias?: string[]; regions?: string[]; } interface SeaPortsData { [locode: string]: SeaPort; } interface ParsedPort { code: string; name: string; city: string; country: string; countryName: string; countryCode: string; latitude: number; longitude: number; timezone: string | null; isActive: boolean; } // Country code to name mapping (ISO 3166-1 alpha-2) const countryNames: { [key: string]: string } = { AE: 'United Arab Emirates', AG: 'Antigua and Barbuda', AL: 'Albania', AM: 'Armenia', AO: 'Angola', AR: 'Argentina', AT: 'Austria', AU: 'Australia', AZ: 'Azerbaijan', BA: 'Bosnia and Herzegovina', BB: 'Barbados', BD: 'Bangladesh', BE: 'Belgium', BG: 'Bulgaria', BH: 'Bahrain', BN: 'Brunei', BR: 'Brazil', BS: 'Bahamas', BZ: 'Belize', CA: 'Canada', CH: 'Switzerland', CI: 'Ivory Coast', CL: 'Chile', CM: 'Cameroon', CN: 'China', CO: 'Colombia', CR: 'Costa Rica', CU: 'Cuba', CY: 'Cyprus', CZ: 'Czech Republic', DE: 'Germany', DJ: 'Djibouti', DK: 'Denmark', DO: 'Dominican Republic', DZ: 'Algeria', EC: 'Ecuador', EE: 'Estonia', EG: 'Egypt', ES: 'Spain', FI: 'Finland', FJ: 'Fiji', FR: 'France', GA: 'Gabon', GB: 'United Kingdom', GE: 'Georgia', GH: 'Ghana', GI: 'Gibraltar', GR: 'Greece', GT: 'Guatemala', GY: 'Guyana', HK: 'Hong Kong', HN: 'Honduras', HR: 'Croatia', HT: 'Haiti', HU: 'Hungary', ID: 'Indonesia', IE: 'Ireland', IL: 'Israel', IN: 'India', IQ: 'Iraq', IR: 'Iran', IS: 'Iceland', IT: 'Italy', JM: 'Jamaica', JO: 'Jordan', JP: 'Japan', KE: 'Kenya', KH: 'Cambodia', KR: 'South Korea', KW: 'Kuwait', KZ: 'Kazakhstan', LB: 'Lebanon', LK: 'Sri Lanka', LR: 'Liberia', LT: 'Lithuania', LV: 'Latvia', LY: 'Libya', MA: 'Morocco', MC: 'Monaco', MD: 'Moldova', ME: 'Montenegro', MG: 'Madagascar', MK: 'North Macedonia', MM: 'Myanmar', MN: 'Mongolia', MO: 'Macau', MR: 'Mauritania', MT: 'Malta', MU: 'Mauritius', MV: 'Maldives', MX: 'Mexico', MY: 'Malaysia', MZ: 'Mozambique', NA: 'Namibia', NG: 'Nigeria', NI: 'Nicaragua', NL: 'Netherlands', NO: 'Norway', NZ: 'New Zealand', OM: 'Oman', PA: 'Panama', PE: 'Peru', PG: 'Papua New Guinea', PH: 'Philippines', PK: 'Pakistan', PL: 'Poland', PR: 'Puerto Rico', PT: 'Portugal', PY: 'Paraguay', QA: 'Qatar', RO: 'Romania', RS: 'Serbia', RU: 'Russia', SA: 'Saudi Arabia', SD: 'Sudan', SE: 'Sweden', SG: 'Singapore', SI: 'Slovenia', SK: 'Slovakia', SN: 'Senegal', SO: 'Somalia', SR: 'Suriname', SY: 'Syria', TH: 'Thailand', TN: 'Tunisia', TR: 'Turkey', TT: 'Trinidad and Tobago', TW: 'Taiwan', TZ: 'Tanzania', UA: 'Ukraine', UG: 'Uganda', US: 'United States', UY: 'Uruguay', VE: 'Venezuela', VN: 'Vietnam', YE: 'Yemen', ZA: 'South Africa', }; function parseSeaPorts(filePath: string): ParsedPort[] { const jsonData = fs.readFileSync(filePath, 'utf-8'); const seaPorts: SeaPortsData = JSON.parse(jsonData); const parsedPorts: ParsedPort[] = []; let skipped = 0; for (const [locode, port] of Object.entries(seaPorts)) { // Validate required fields if (!port.name || !port.coordinates || port.coordinates.length !== 2) { skipped++; continue; } // Extract country code from UN/LOCODE (first 2 characters) const countryCode = locode.substring(0, 2).toUpperCase(); // Skip if invalid country code if (!countryNames[countryCode]) { skipped++; continue; } // Validate coordinates const [longitude, latitude] = port.coordinates; if ( latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 ) { skipped++; continue; } parsedPorts.push({ code: locode.toUpperCase(), name: port.name.trim(), city: port.city?.trim() || port.name.trim(), country: countryCode, countryName: countryNames[countryCode] || port.country, countryCode: countryCode, latitude: Number(latitude.toFixed(6)), longitude: Number(longitude.toFixed(6)), timezone: port.timezone || null, isActive: true, }); } console.log(`āœ… Parsed ${parsedPorts.length} ports`); console.log(`āš ļø Skipped ${skipped} invalid entries`); return parsedPorts; } function generateSQLInserts(ports: ParsedPort[]): string { const batchSize = 100; const batches: string[] = []; for (let i = 0; i < ports.length; i += batchSize) { const batch = ports.slice(i, i + batchSize); const values = batch.map(port => { const name = port.name.replace(/'/g, "''"); const city = port.city.replace(/'/g, "''"); const countryName = port.countryName.replace(/'/g, "''"); const timezone = port.timezone ? `'${port.timezone}'` : 'NULL'; return `( '${port.code}', '${name}', '${city}', '${port.country}', '${countryName}', ${port.latitude}, ${port.longitude}, ${timezone}, ${port.isActive} )`; }).join(',\n '); batches.push(` // Batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(ports.length / batchSize)} (${batch.length} ports) await queryRunner.query(\` INSERT INTO ports (code, name, city, country, country_name, latitude, longitude, timezone, is_active) VALUES ${values} \`); `); } return batches.join('\n'); } function generateMigration(ports: ParsedPort[]): string { const timestamp = Date.now(); const className = `SeedPorts${timestamp}`; const sqlInserts = generateSQLInserts(ports); const migrationContent = `/** * Migration: Seed Ports Table * * Source: sea-ports (https://github.com/marchah/sea-ports) * License: MIT * Generated: ${new Date().toISOString()} * Total ports: ${ports.length} */ import { MigrationInterface, QueryRunner } from 'typeorm'; export class ${className} implements MigrationInterface { name = '${className}'; public async up(queryRunner: QueryRunner): Promise { console.log('Seeding ${ports.length} maritime ports...'); ${sqlInserts} console.log('āœ… Successfully seeded ${ports.length} ports'); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(\`TRUNCATE TABLE ports RESTART IDENTITY CASCADE\`); console.log('šŸ—‘ļø Cleared all ports'); } } `; return migrationContent; } async function main() { const seaPortsPath = '/tmp/sea-ports.json'; console.log('🚢 Generating Ports Seed Migration\n'); // Check if sea-ports.json exists if (!fs.existsSync(seaPortsPath)) { console.error('āŒ Error: /tmp/sea-ports.json not found!'); console.log('Please download it first:'); console.log('curl -o /tmp/sea-ports.json https://raw.githubusercontent.com/marchah/sea-ports/master/lib/ports.json'); process.exit(1); } // Parse ports console.log('šŸ“– Parsing sea-ports.json...'); const ports = parseSeaPorts(seaPortsPath); // Sort by country, then by name ports.sort((a, b) => { if (a.country !== b.country) { return a.country.localeCompare(b.country); } return a.name.localeCompare(b.name); }); // Generate migration console.log('\nšŸ“ Generating migration file...'); const migrationContent = generateMigration(ports); // Write migration file const migrationsDir = path.join(__dirname, '../src/infrastructure/persistence/typeorm/migrations'); const timestamp = Date.now(); const fileName = `${timestamp}-SeedPorts.ts`; const filePath = path.join(migrationsDir, fileName); fs.writeFileSync(filePath, migrationContent, 'utf-8'); console.log(`\nāœ… Migration created: ${fileName}`); console.log(`šŸ“ Location: ${filePath}`); console.log(`\nšŸ“Š Summary:`); console.log(` - Total ports: ${ports.length}`); console.log(` - Countries: ${new Set(ports.map(p => p.country)).size}`); console.log(` - Ports with timezone: ${ports.filter(p => p.timezone).length}`); console.log(`\nšŸš€ Run the migration:`); console.log(` cd apps/backend`); console.log(` npm run migration:run`); } main().catch(console.error);