364 lines
8.6 KiB
TypeScript
364 lines
8.6 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
console.log('Seeding ${ports.length} maritime ports...');
|
|
|
|
${sqlInserts}
|
|
|
|
console.log('✅ Successfully seeded ${ports.length} ports');
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
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);
|